added post key route

This commit is contained in:
daniel31x13 2024-01-13 01:20:06 -05:00
parent 834d25a99e
commit d91ebb3fa2
13 changed files with 417 additions and 83 deletions

View File

@ -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 (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">Create an Access Token</p>
<div className="divider mb-3 mt-1"></div>
<div className="flex gap-2 items-center">
<div className="w-full">
<p className="mb-2">Name</p>
<TextInput
value={key.name}
onChange={(e) => setKey({ ...key, name: e.target.value })}
placeholder="e.g. For the Mobile App"
className="bg-base-200"
/>
</div>
<div>
<p className="mb-2">Date of Expiry</p>
<div className="dropdown dropdown-bottom dropdown-end">
<div
tabIndex={0}
role="button"
className="btn btn-outline w-36 flex items-center btn-sm h-10"
>
{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"}
</div>
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-xl w-52 mt-1">
<li>
<label
className="label cursor-pointer flex justify-start"
tabIndex={0}
role="button"
>
<input
type="radio"
name="sort-radio"
className="radio checked:bg-primary"
checked={key.expires === KeyExpiry.sevenDays}
onChange={() => {
(document?.activeElement as HTMLElement)?.blur();
setKey({ ...key, expires: KeyExpiry.sevenDays });
}}
/>
<span className="label-text">7 Days</span>
</label>
</li>
<li>
<label
className="label cursor-pointer flex justify-start"
tabIndex={0}
role="button"
>
<input
type="radio"
name="sort-radio"
className="radio checked:bg-primary"
checked={key.expires === KeyExpiry.oneMonth}
onChange={() => {
(document?.activeElement as HTMLElement)?.blur();
setKey({ ...key, expires: KeyExpiry.oneMonth });
}}
/>
<span className="label-text">30 Days</span>
</label>
</li>
<li>
<label
className="label cursor-pointer flex justify-start"
tabIndex={0}
role="button"
>
<input
type="radio"
name="sort-radio"
className="radio checked:bg-primary"
checked={key.expires === KeyExpiry.twoMonths}
onChange={() => {
(document?.activeElement as HTMLElement)?.blur();
setKey({ ...key, expires: KeyExpiry.twoMonths });
}}
/>
<span className="label-text">60 Days</span>
</label>
</li>
<li>
<label
className="label cursor-pointer flex justify-start"
tabIndex={0}
role="button"
>
<input
type="radio"
name="sort-radio"
className="radio checked:bg-primary"
checked={key.expires === KeyExpiry.threeMonths}
onChange={() => {
(document?.activeElement as HTMLElement)?.blur();
setKey({ ...key, expires: KeyExpiry.threeMonths });
}}
/>
<span className="label-text">90 Days</span>
</label>
</li>
<li>
<label
className="label cursor-pointer flex justify-start"
tabIndex={0}
role="button"
>
<input
type="radio"
name="sort-radio"
className="radio checked:bg-primary"
checked={key.expires === KeyExpiry.never}
onChange={() => {
(document?.activeElement as HTMLElement)?.blur();
setKey({ ...key, expires: KeyExpiry.never });
}}
/>
<span className="label-text">No Expiration</span>
</label>
</li>
</ul>
</div>
</div>
</div>
<div className="flex justify-between items-center mt-5">
<button
className="btn btn-accent dark:border-violet-400 text-white"
onClick={submit}
>
Create Access Token
</button>
</div>
</Modal>
);
}

View File

@ -64,16 +64,16 @@ export default function SettingsSidebar({ className }: { className?: string }) {
</div> </div>
</Link> </Link>
<Link href="/settings/api"> <Link href="/settings/access-tokens">
<div <div
className={`${ className={`${
active === `/settings/api` active === `/settings/access-tokens`
? "bg-primary/20" ? "bg-primary/20"
: "hover:bg-neutral/20" : "hover:bg-neutral/20"
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`} } duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
> >
<i className="bi-key text-primary text-2xl"></i> <i className="bi-key text-primary text-2xl"></i>
<p className="truncate w-full pr-7">API Keys</p> <p className="truncate w-full pr-7">Access Token</p>
</div> </div>
</Link> </Link>

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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 };
}

View File

@ -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 });
}
}

View File

@ -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 });
}
}

View File

@ -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 (
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">API Keys</p>
<div className="divider my-3"></div>
<div className="flex flex-col gap-3">
<p>
Access Tokens can be used to access Linkwarden from other apps and
services without giving away your Username and Password.
</p>
<button
className={`btn btn-accent dark:border-violet-400 text-white tracking-wider w-fit flex items-center gap-2`}
onClick={() => {
setNewKeyModal(true);
}}
>
New Access Token
</button>
</div>
{newKeyModal ? (
<NewKeyModal onClose={() => setNewKeyModal(false)} />
) : undefined}
</SettingsLayout>
);
}

View File

@ -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<AccountSettings>(account);
const [archiveAsScreenshot, setArchiveAsScreenshot] =
useState<boolean>(false);
const [archiveAsPDF, setArchiveAsPDF] = useState<boolean>(false);
const [archiveAsWaybackMachine, setArchiveAsWaybackMachine] =
useState<boolean>(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 (
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">API Keys (Soon)</p>
<div className="divider my-3"></div>
<div className="flex flex-col gap-3">
<div className="badge badge-warning rounded-md w-fit">
Status: Under Development
</div>
<p>This page will be for creating and managing your API keys.</p>
<p>
For now, you can <i>temporarily</i> use your{" "}
<code className="text-xs whitespace-nowrap bg-black/40 rounded-md px-2 py-1">
next-auth.session-token
</code>{" "}
in your browser cookies as the API key for your integrations.
</p>
</div>
</SettingsLayout>
);
}

View File

@ -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");

View File

@ -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");

View File

@ -144,7 +144,7 @@ model Subscription {
model ApiKey { model ApiKey {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String name String
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
userId Int userId Int
token String @unique token String @unique
@ -153,5 +153,5 @@ model ApiKey {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
@@unique([token, userId]) @@unique([name, userId])
} }

View File

@ -134,3 +134,11 @@ export enum LinkType {
pdf, pdf,
image, image,
} }
export enum KeyExpiry {
sevenDays,
oneMonth,
twoMonths,
threeMonths,
never,
}