Skip to content

Commit

Permalink
feat: add login functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
theodorusclarence committed Jan 20, 2022
1 parent a939335 commit 852651f
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 25 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Expand Up @@ -16,6 +16,7 @@ module.exports = {
'no-unused-vars': 'off',
'no-console': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',

'react/display-name': 'off',

Expand Down
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -23,6 +23,7 @@
"@notionhq/client": "^0.4.12",
"axios": "^0.24.0",
"clsx": "^1.1.1",
"jsonwebtoken": "^8.5.1",
"next": "^12.0.8",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.0.4",
Expand All @@ -41,6 +42,7 @@
"@tailwindcss/forms": "^0.4.0",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@types/jsonwebtoken": "^8.5.8",
"@types/react": "^17.0.38",
"@types/react-copy-to-clipboard": "^5.0.2",
"@types/tailwindcss": "^2.2.4",
Expand Down
7 changes: 7 additions & 0 deletions src/lib/helper.ts
Expand Up @@ -26,3 +26,10 @@ export function openGraph({
export function trimHttps(url: string) {
return url.replace(/^https?:\/\//, '');
}

export function getFromLocalStorage(key: string) {
if (typeof localStorage !== 'undefined') {
return localStorage.getItem(key);
}
return null;
}
10 changes: 1 addition & 9 deletions src/pages/_middleware.ts
Expand Up @@ -4,15 +4,7 @@ import { getUrlBySlug } from '@/lib/notion';

export default async function middleware(req: NextRequest) {
const path = req.nextUrl.pathname.split('/')[1];
const whitelist = [
'favicons',
'fonts',
'images',
'svg',
'',
'testing',
'new',
];
const whitelist = ['favicons', 'fonts', 'images', 'svg', '', 'login', 'new'];
if (whitelist.includes(path)) {
return;
}
Expand Down
29 changes: 29 additions & 0 deletions src/pages/api/login.ts
@@ -0,0 +1,29 @@
import jwt from 'jsonwebtoken';
import { NextApiRequest, NextApiResponse } from 'next';

export default async function LoginHandler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === 'POST') {
const { password } = req.body as { password: string };

if (!password) {
return res.status(400).json({
message: 'Password are required',
});
}

if (password !== process.env.NEXT_PUBLIC_APP_PASSWORD) {
return res.status(401).json({
message: 'Incorrect password',
});
}

return res
.status(200)
.json({ token: jwt.sign({}, process.env.NEXT_PUBLIC_APP_SECRET!) });
} else {
res.status(405).json({ message: 'Method Not Allowed' });
}
}
14 changes: 13 additions & 1 deletion src/pages/api/new.ts
@@ -1,3 +1,4 @@
import jwt from 'jsonwebtoken';
import { NextApiRequest, NextApiResponse } from 'next';

import { addLink, checkSlugIsTaken } from '@/lib/notion';
Expand All @@ -8,13 +9,24 @@ export default async function NewLinkHandler(
) {
if (req.method === 'POST') {
const url = req.body as { link: string; slug: string };

if (!url.link || !url.slug) {
return res.status(400).json({
message: 'Link and slug are required',
});
}

let APP_TOKEN = req.headers['authorization'] as string | undefined;
if (!APP_TOKEN) {
return res.status(401).send({ message: 'Unauthorized' });
}

APP_TOKEN = APP_TOKEN.replace(/^Bearer\s+/, '');
try {
jwt.verify(APP_TOKEN, process.env.NEXT_PUBLIC_APP_SECRET!);
} catch (error) {
return res.status(401).send({ message: 'Unauthorized' });
}

const taken = await checkSlugIsTaken(url.slug);
if (taken) {
return res.status(409).json({
Expand Down
92 changes: 92 additions & 0 deletions src/pages/login.tsx
@@ -0,0 +1,92 @@
import axios from 'axios';
import router from 'next/router';
import * as React from 'react';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import toast from 'react-hot-toast';

import useLoadingToast from '@/hooks/toast/useLoadingToast';

import Accent from '@/components/Accent';
import Button from '@/components/buttons/Button';
import Input from '@/components/forms/Input';
import Layout from '@/components/layout/Layout';
import Seo from '@/components/Seo';

import { DEFAULT_TOAST_MESSAGE } from '@/constant/toast';

type NewLinkFormData = {
slug: string;
link: string;
};

export default function NewLinkPage() {
const isLoading = useLoadingToast();

//#region //*=========== Form ===========
const methods = useForm<NewLinkFormData>({
mode: 'onTouched',
});
const { handleSubmit } = methods;
//#endregion //*======== Form ===========

//#region //*=========== Form Submit ===========
const onSubmit: SubmitHandler<NewLinkFormData> = (data) => {
toast.promise(
axios.post<{ token: string }>('/api/login', data).then((res) => {
localStorage.setItem('@notiolink/app_token', res.data.token);
router.replace(`/new`);
}),
{
...DEFAULT_TOAST_MESSAGE,
success: 'Logged in, you can now add new link',
}
);
};
//#endregion //*======== Form Submit ===========

return (
<Layout>
<Seo templateTitle='Login' />

<main>
<section>
<div className='layout flex flex-col justify-center items-center py-20 min-h-screen'>
<h1 className='h0'>
<Accent>Login to the account</Accent>
</h1>

<FormProvider {...methods}>
<form
onSubmit={handleSubmit(onSubmit)}
className='mt-8 w-full max-w-sm'
>
<div className='space-y-4'>
<Input
id='password'
label='Password'
type='password'
validation={{
required: 'Password must be filled',
}}
/>
</div>

<div className='flex flex-col mt-5'>
<Button
isLoading={isLoading}
className='justify-center w-full md:ml-auto md:w-auto'
variant='outline'
isDarkBg
type='submit'
>
Login!
</Button>
</div>
</form>
</FormProvider>
</div>
</section>
</main>
</Layout>
);
}
59 changes: 50 additions & 9 deletions src/pages/new.tsx
Expand Up @@ -4,6 +4,9 @@ import * as React from 'react';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import toast from 'react-hot-toast';

import { getFromLocalStorage } from '@/lib/helper';
import useLoadingToast from '@/hooks/toast/useLoadingToast';

import Accent from '@/components/Accent';
import Button from '@/components/buttons/Button';
import Input from '@/components/forms/Input';
Expand All @@ -19,6 +22,17 @@ type NewLinkFormData = {

export default function NewLinkPage() {
const router = useRouter();
const isLoading = useLoadingToast();

//#region //*=========== Check Auth ===========
const token = getFromLocalStorage('@notiolink/app_token');
React.useEffect(() => {
if (!token) {
toast.error('Missing token, please login first');
router.replace('/login');
}
}, [router, token]);
//#endregion //*======== Check Auth ===========

//#region //*=========== Form ===========
const methods = useForm<NewLinkFormData>({
Expand All @@ -29,15 +43,28 @@ export default function NewLinkPage() {

//#region //*=========== Form Submit ===========
const onSubmit: SubmitHandler<NewLinkFormData> = (data) => {
toast.promise(
axios.post('/api/new', data).then(() => {
router.replace(`/${data.slug}/detail`);
}),
{
...DEFAULT_TOAST_MESSAGE,
success: 'Link successfully shortened',
}
);
toast
.promise(
axios
.post('/api/new', data, {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then(() => {
router.replace(`/${data.slug}/detail`);
}),
{
...DEFAULT_TOAST_MESSAGE,
success: 'Link successfully shortened',
}
)
.catch((err: { response: { status: number } }) => {
if (err.response.status === 401) {
toast.error('Token expired, please login again');
router.replace('/login');
}
});
};
//#endregion //*======== Form Submit ===========

Expand All @@ -63,6 +90,18 @@ export default function NewLinkPage() {
<Accent>Shorten New Link</Accent>
</h1>

<Button
className='absolute top-8 right-8'
onClick={() => {
localStorage.removeItem('@notiolink/app_token');
router.push('/');
}}
variant='outline'
isDarkBg
>
Logout
</Button>

<FormProvider {...methods}>
<form
onSubmit={handleSubmit(onSubmit)}
Expand All @@ -73,6 +112,7 @@ export default function NewLinkPage() {
id='slug'
label='Slug'
placeholder='slug'
autoFocus
validation={{
required: 'Slug must be filled',
pattern: {
Expand Down Expand Up @@ -103,6 +143,7 @@ export default function NewLinkPage() {
variant='outline'
isDarkBg
type='submit'
isLoading={isLoading}
>
Shorten!
</Button>
Expand Down

1 comment on commit 852651f

@vercel
Copy link

@vercel vercel bot commented on 852651f Jan 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.