From d91ebb3fa2aced31d256448786f5e88912b90f46 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Sat, 13 Jan 2024 01:20:06 -0500 Subject: [PATCH] added post key route --- components/ModalContent/NewKeyModal.tsx | 199 ++++++++++++++++++ components/SettingsSidebar.tsx | 6 +- lib/api/controllers/tokens/getTokens.ts | 22 ++ lib/api/controllers/tokens/postToken.ts | 77 +++++++ .../tokens/tokenId/deleteTokenById.ts | 16 ++ pages/api/v1/tokens/[id].ts | 13 ++ pages/api/v1/tokens/index.ts | 20 ++ pages/settings/access-tokens.tsx | 35 +++ pages/settings/api.tsx | 78 ------- .../migration.sql | 8 + .../20240113060555_minor_fix/migration.sql | 14 ++ prisma/schema.prisma | 4 +- types/global.ts | 8 + 13 files changed, 417 insertions(+), 83 deletions(-) create mode 100644 components/ModalContent/NewKeyModal.tsx create mode 100644 lib/api/controllers/tokens/getTokens.ts create mode 100644 lib/api/controllers/tokens/postToken.ts create mode 100644 lib/api/controllers/tokens/tokenId/deleteTokenById.ts create mode 100644 pages/api/v1/tokens/[id].ts create mode 100644 pages/api/v1/tokens/index.ts create mode 100644 pages/settings/access-tokens.tsx delete mode 100644 pages/settings/api.tsx create mode 100644 prisma/migrations/20240113051701_make_key_names_unique/migration.sql create mode 100644 prisma/migrations/20240113060555_minor_fix/migration.sql diff --git a/components/ModalContent/NewKeyModal.tsx b/components/ModalContent/NewKeyModal.tsx new file mode 100644 index 0000000..e36f724 --- /dev/null +++ b/components/ModalContent/NewKeyModal.tsx @@ -0,0 +1,199 @@ +import React, { useState } from "react"; +import TextInput from "@/components/TextInput"; +import { KeyExpiry } from "@/types/global"; +import { useSession } from "next-auth/react"; +import toast from "react-hot-toast"; +import Modal from "../Modal"; + +type Props = { + onClose: Function; +}; + +export default function NewKeyModal({ onClose }: Props) { + const { data } = useSession(); + + const initial = { + name: "", + expires: 0 as KeyExpiry, + }; + + const [key, setKey] = useState(initial as any); + + const [submitLoader, setSubmitLoader] = useState(false); + + const submit = async () => { + if (!submitLoader) { + setSubmitLoader(true); + + let response; + + const load = toast.loading("Creating..."); + + response = await fetch("/api/v1/tokens", { + method: "POST", + body: JSON.stringify({ + name: key.name, + expires: key.expires, + }), + }); + + const data = await response.json(); + + toast.dismiss(load); + + if (response.ok) { + toast.success(`Created!`); + onClose(); + } else toast.error(data.response as string); + + setSubmitLoader(false); + + return response; + } + }; + + return ( + +

Create an Access Token

+ +
+ +
+
+

Name

+ + setKey({ ...key, name: e.target.value })} + placeholder="e.g. For the Mobile App" + className="bg-base-200" + /> +
+ +
+

Date of Expiry

+ +
+
+ {key.expires === KeyExpiry.sevenDays && "7 Days"} + {key.expires === KeyExpiry.oneMonth && "30 Days"} + {key.expires === KeyExpiry.twoMonths && "60 Days"} + {key.expires === KeyExpiry.threeMonths && "90 Days"} + {key.expires === KeyExpiry.never && "No Expiration"} +
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
+ +
+ +
+
+ ); +} diff --git a/components/SettingsSidebar.tsx b/components/SettingsSidebar.tsx index d4558f8..3ffbc5f 100644 --- a/components/SettingsSidebar.tsx +++ b/components/SettingsSidebar.tsx @@ -64,16 +64,16 @@ export default function SettingsSidebar({ className }: { className?: string }) { - +
-

API Keys

+

Access Token

diff --git a/lib/api/controllers/tokens/getTokens.ts b/lib/api/controllers/tokens/getTokens.ts new file mode 100644 index 0000000..d525ac7 --- /dev/null +++ b/lib/api/controllers/tokens/getTokens.ts @@ -0,0 +1,22 @@ +import { prisma } from "@/lib/api/db"; +import { KeyExpiry } from "@/types/global"; +import bcrypt from "bcrypt"; +import crypto from "crypto"; + +export default async function getToken(userId: number) { + const getTokens = await prisma.apiKey.findMany({ + where: { + userId, + }, + select: { + name: true, + expires: true, + createdAt: true, + }, + }); + + return { + response: getTokens, + status: 200, + }; +} diff --git a/lib/api/controllers/tokens/postToken.ts b/lib/api/controllers/tokens/postToken.ts new file mode 100644 index 0000000..24a8cba --- /dev/null +++ b/lib/api/controllers/tokens/postToken.ts @@ -0,0 +1,77 @@ +import { prisma } from "@/lib/api/db"; +import { KeyExpiry } from "@/types/global"; +import bcrypt from "bcrypt"; +import crypto from "crypto"; + +export default async function postToken( + body: { + name: string; + expires: KeyExpiry; + }, + userId: number +) { + console.log(body); + + const checkHasEmptyFields = !body.name || body.expires === undefined; + + if (checkHasEmptyFields) + return { + response: "Please fill out all the fields.", + status: 400, + }; + + const checkIfTokenExists = await prisma.apiKey.findFirst({ + where: { + name: body.name, + userId, + }, + }); + + if (checkIfTokenExists) { + return { + response: "Token with that name already exists.", + status: 400, + }; + } + + let expiryDate = new Date(); + + switch (body.expires) { + case KeyExpiry.sevenDays: + expiryDate.setDate(expiryDate.getDate() + 7); + break; + case KeyExpiry.oneMonth: + expiryDate.setDate(expiryDate.getDate() + 30); + break; + case KeyExpiry.twoMonths: + expiryDate.setDate(expiryDate.getDate() + 60); + break; + case KeyExpiry.threeMonths: + expiryDate.setDate(expiryDate.getDate() + 90); + break; + case KeyExpiry.never: + expiryDate.setDate(expiryDate.getDate() + 73000); // 200 years (not really never) + break; + default: + expiryDate.setDate(expiryDate.getDate() + 7); + break; + } + + const saltRounds = 10; + + const hashedKey = bcrypt.hashSync(crypto.randomUUID(), saltRounds); + + const createToken = await prisma.apiKey.create({ + data: { + name: body.name, + userId, + token: hashedKey, + expires: expiryDate, + }, + }); + + return { + response: createToken.token, + status: 200, + }; +} diff --git a/lib/api/controllers/tokens/tokenId/deleteTokenById.ts b/lib/api/controllers/tokens/tokenId/deleteTokenById.ts new file mode 100644 index 0000000..742016f --- /dev/null +++ b/lib/api/controllers/tokens/tokenId/deleteTokenById.ts @@ -0,0 +1,16 @@ +import { prisma } from "@/lib/api/db"; +import { KeyExpiry } from "@/types/global"; + +export default async function deleteToken(userId: number, tokenId: number) { + if (!tokenId) + return { response: "Please choose a valid token.", status: 401 }; + + const deletedToken = await prisma.apiKey.delete({ + where: { + id: tokenId, + userId, + }, + }); + + return { response: deletedToken, status: 200 }; +} diff --git a/pages/api/v1/tokens/[id].ts b/pages/api/v1/tokens/[id].ts new file mode 100644 index 0000000..6c2e51b --- /dev/null +++ b/pages/api/v1/tokens/[id].ts @@ -0,0 +1,13 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import verifyUser from "@/lib/api/verifyUser"; +import deleteToken from "@/lib/api/controllers/tokens/tokenId/deleteTokenById"; + +export default async function token(req: NextApiRequest, res: NextApiResponse) { + const user = await verifyUser({ req, res }); + if (!user) return; + + if (req.method === "DELETE") { + const deleted = await deleteToken(user.id, Number(req.query.id) as number); + return res.status(deleted.status).json({ response: deleted.response }); + } +} diff --git a/pages/api/v1/tokens/index.ts b/pages/api/v1/tokens/index.ts new file mode 100644 index 0000000..8af63fb --- /dev/null +++ b/pages/api/v1/tokens/index.ts @@ -0,0 +1,20 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import verifyUser from "@/lib/api/verifyUser"; +import postToken from "@/lib/api/controllers/tokens/postToken"; +import getTokens from "@/lib/api/controllers/tokens/getTokens"; + +export default async function tokens( + req: NextApiRequest, + res: NextApiResponse +) { + const user = await verifyUser({ req, res }); + if (!user) return; + + if (req.method === "POST") { + const token = await postToken(JSON.parse(req.body), user.id); + return res.status(token.status).json({ response: token.response }); + } else if (req.method === "GET") { + const token = await getTokens(user.id); + return res.status(token.status).json({ response: token.response }); + } +} diff --git a/pages/settings/access-tokens.tsx b/pages/settings/access-tokens.tsx new file mode 100644 index 0000000..14912a2 --- /dev/null +++ b/pages/settings/access-tokens.tsx @@ -0,0 +1,35 @@ +import SettingsLayout from "@/layouts/SettingsLayout"; +import React, { useState } from "react"; +import NewKeyModal from "@/components/ModalContent/NewKeyModal"; + +export default function Api() { + const [newKeyModal, setNewKeyModal] = useState(false); + + return ( + +

API Keys

+ +
+ +
+

+ Access Tokens can be used to access Linkwarden from other apps and + services without giving away your Username and Password. +

+ + +
+ + {newKeyModal ? ( + setNewKeyModal(false)} /> + ) : undefined} +
+ ); +} diff --git a/pages/settings/api.tsx b/pages/settings/api.tsx deleted file mode 100644 index dc4bb9a..0000000 --- a/pages/settings/api.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import Checkbox from "@/components/Checkbox"; -import SubmitButton from "@/components/SubmitButton"; -import SettingsLayout from "@/layouts/SettingsLayout"; -import React, { useEffect, useState } from "react"; -import useAccountStore from "@/store/account"; -import { toast } from "react-hot-toast"; -import { AccountSettings } from "@/types/global"; -import TextInput from "@/components/TextInput"; - -export default function Api() { - const [submitLoader, setSubmitLoader] = useState(false); - const { account, updateAccount } = useAccountStore(); - const [user, setUser] = useState(account); - - const [archiveAsScreenshot, setArchiveAsScreenshot] = - useState(false); - const [archiveAsPDF, setArchiveAsPDF] = useState(false); - const [archiveAsWaybackMachine, setArchiveAsWaybackMachine] = - useState(false); - - useEffect(() => { - setUser({ - ...account, - archiveAsScreenshot, - archiveAsPDF, - archiveAsWaybackMachine, - }); - }, [account, archiveAsScreenshot, archiveAsPDF, archiveAsWaybackMachine]); - - function objectIsEmpty(obj: object) { - return Object.keys(obj).length === 0; - } - - useEffect(() => { - if (!objectIsEmpty(account)) { - setArchiveAsScreenshot(account.archiveAsScreenshot); - setArchiveAsPDF(account.archiveAsPDF); - setArchiveAsWaybackMachine(account.archiveAsWaybackMachine); - } - }, [account]); - - const submit = async () => { - // setSubmitLoader(true); - // const load = toast.loading("Applying..."); - // const response = await updateAccount({ - // ...user, - // }); - // toast.dismiss(load); - // if (response.ok) { - // toast.success("Settings Applied!"); - // } else toast.error(response.data as string); - // setSubmitLoader(false); - }; - - return ( - -

API Keys (Soon)

- -
- -
-
- Status: Under Development -
- -

This page will be for creating and managing your API keys.

- -

- For now, you can temporarily use your{" "} - - next-auth.session-token - {" "} - in your browser cookies as the API key for your integrations. -

-
-
- ); -} diff --git a/prisma/migrations/20240113051701_make_key_names_unique/migration.sql b/prisma/migrations/20240113051701_make_key_names_unique/migration.sql new file mode 100644 index 0000000..55efb95 --- /dev/null +++ b/prisma/migrations/20240113051701_make_key_names_unique/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[name]` on the table `ApiKey` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "ApiKey_name_key" ON "ApiKey"("name"); diff --git a/prisma/migrations/20240113060555_minor_fix/migration.sql b/prisma/migrations/20240113060555_minor_fix/migration.sql new file mode 100644 index 0000000..d3999b6 --- /dev/null +++ b/prisma/migrations/20240113060555_minor_fix/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - A unique constraint covering the columns `[name,userId]` on the table `ApiKey` will be added. If there are existing duplicate values, this will fail. + +*/ +-- DropIndex +DROP INDEX "ApiKey_name_key"; + +-- DropIndex +DROP INDEX "ApiKey_token_userId_key"; + +-- CreateIndex +CREATE UNIQUE INDEX "ApiKey_name_userId_key" ON "ApiKey"("name", "userId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3f10539..60a03e1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -144,7 +144,7 @@ model Subscription { model ApiKey { id Int @id @default(autoincrement()) - name String + name String user User @relation(fields: [userId], references: [id]) userId Int token String @unique @@ -153,5 +153,5 @@ model ApiKey { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - @@unique([token, userId]) + @@unique([name, userId]) } diff --git a/types/global.ts b/types/global.ts index 8b3efcf..36e38c1 100644 --- a/types/global.ts +++ b/types/global.ts @@ -134,3 +134,11 @@ export enum LinkType { pdf, image, } + +export enum KeyExpiry { + sevenDays, + oneMonth, + twoMonths, + threeMonths, + never, +}