diff --git a/.eslintrc.js b/.eslintrc.js index ecfeed0..8403513 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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', diff --git a/package.json b/package.json index c342a3d..8cf4664 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 8436ef2..5ff1299 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -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; +} diff --git a/src/pages/_middleware.ts b/src/pages/_middleware.ts index d4c56c7..016470b 100644 --- a/src/pages/_middleware.ts +++ b/src/pages/_middleware.ts @@ -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; } diff --git a/src/pages/api/login.ts b/src/pages/api/login.ts new file mode 100644 index 0000000..e84848f --- /dev/null +++ b/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' }); + } +} diff --git a/src/pages/api/new.ts b/src/pages/api/new.ts index 48d3967..3d41af1 100644 --- a/src/pages/api/new.ts +++ b/src/pages/api/new.ts @@ -1,3 +1,4 @@ +import jwt from 'jsonwebtoken'; import { NextApiRequest, NextApiResponse } from 'next'; import { addLink, checkSlugIsTaken } from '@/lib/notion'; @@ -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({ diff --git a/src/pages/login.tsx b/src/pages/login.tsx new file mode 100644 index 0000000..2c0b25f --- /dev/null +++ b/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({ + mode: 'onTouched', + }); + const { handleSubmit } = methods; + //#endregion //*======== Form =========== + + //#region //*=========== Form Submit =========== + const onSubmit: SubmitHandler = (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 ( + + + +
+
+
+

+ Login to the account +

+ + +
+
+ +
+ +
+ +
+
+
+
+
+
+
+ ); +} diff --git a/src/pages/new.tsx b/src/pages/new.tsx index def7872..65b43c8 100644 --- a/src/pages/new.tsx +++ b/src/pages/new.tsx @@ -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'; @@ -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({ @@ -29,15 +43,28 @@ export default function NewLinkPage() { //#region //*=========== Form Submit =========== const onSubmit: SubmitHandler = (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 =========== @@ -63,6 +90,18 @@ export default function NewLinkPage() { Shorten New Link + +
Shorten! diff --git a/yarn.lock b/yarn.lock index a59beae..e2d6e92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2017,6 +2017,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/jsonwebtoken@^8.5.8": + version "8.5.8" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz#01b39711eb844777b7af1d1f2b4cf22fda1c0c44" + integrity sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A== + dependencies: + "@types/node" "*" + "@types/minimist@^1.2.0": version "1.2.1" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" @@ -2662,6 +2669,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -3476,6 +3488,13 @@ dotgitignore@^2.1.0: find-up "^3.0.0" minimatch "^3.0.4" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + electron-to-chromium@^1.3.878: version "1.3.885" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.885.tgz#c8cec32fbc61364127849ae00f2395a1bae7c454" @@ -5165,6 +5184,22 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= +jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82" @@ -5173,6 +5208,23 @@ jsonparse@^1.2.0: array-includes "^3.1.2" object.assign "^4.1.2" +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -5323,16 +5375,51 @@ lodash.get@^4: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" @@ -6467,16 +6554,16 @@ rxjs@^6.6.7: dependencies: tslib "^1.9.0" +safe-buffer@^5.0.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -6502,7 +6589,7 @@ semver-compare@^1.0.0: resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= -"semver@2 || 3 || 4 || 5": +"semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==