finished access token creation feature

This commit is contained in:
daniel31x13 2024-01-24 12:51:16 -05:00
parent d91ebb3fa2
commit 05563134b4
6 changed files with 265 additions and 208 deletions

View File

@ -12,6 +12,8 @@ type Props = {
export default function NewKeyModal({ onClose }: Props) { export default function NewKeyModal({ onClose }: Props) {
const { data } = useSession(); const { data } = useSession();
const [newToken, setNewToken] = useState("");
const initial = { const initial = {
name: "", name: "",
expires: 0 as KeyExpiry, expires: 0 as KeyExpiry,
@ -43,7 +45,7 @@ export default function NewKeyModal({ onClose }: Props) {
if (response.ok) { if (response.ok) {
toast.success(`Created!`); toast.success(`Created!`);
onClose(); setNewToken(data.response);
} else toast.error(data.response as string); } else toast.error(data.response as string);
setSubmitLoader(false); setSubmitLoader(false);
@ -54,146 +56,173 @@ export default function NewKeyModal({ onClose }: Props) {
return ( return (
<Modal toggleModal={onClose}> <Modal toggleModal={onClose}>
<p className="text-xl font-thin">Create an Access Token</p> {newToken ? (
<div className="flex flex-col justify-center space-y-4">
<div className="divider mb-3 mt-1"></div> <p className="text-xl font-thin">Access Token Created</p>
<p>
<div className="flex gap-2 items-center"> Your new token has been created. Please copy it and store it
<div className="w-full"> somewhere safe. You will not be able to see it again.
<p className="mb-2">Name</p> </p>
<TextInput <TextInput
value={key.name} spellCheck={false}
onChange={(e) => setKey({ ...key, name: e.target.value })} value={newToken}
placeholder="e.g. For the Mobile App" onChange={() => {}}
className="bg-base-200" className="w-full"
/> />
<button
onClick={() => {
navigator.clipboard.writeText(newToken);
toast.success("Copied to clipboard!");
}}
className="btn btn-primary w-fit mx-auto"
>
Copy to Clipboard
</button>
</div> </div>
) : (
<>
<p className="text-xl font-thin">Create an Access Token</p>
<div> <div className="divider mb-3 mt-1"></div>
<p className="mb-2">Date of Expiry</p>
<div className="dropdown dropdown-bottom dropdown-end"> <div className="flex gap-2 items-center">
<div <div className="w-full">
tabIndex={0} <p className="mb-2">Name</p>
role="button"
className="btn btn-outline w-36 flex items-center btn-sm h-10" <TextInput
> value={key.name}
{key.expires === KeyExpiry.sevenDays && "7 Days"} onChange={(e) => setKey({ ...key, name: e.target.value })}
{key.expires === KeyExpiry.oneMonth && "30 Days"} placeholder="e.g. For the iOS shortcut"
{key.expires === KeyExpiry.twoMonths && "60 Days"} className="bg-base-200"
{key.expires === KeyExpiry.threeMonths && "90 Days"} />
{key.expires === KeyExpiry.never && "No Expiration"}
</div> </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"> <div>
<button <p className="mb-2">Expires in</p>
className="btn btn-accent dark:border-violet-400 text-white"
onClick={submit} <div className="dropdown dropdown-bottom dropdown-end">
> <div
Create Access Token tabIndex={0}
</button> role="button"
</div> 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> </Modal>
); );
} }

View File

@ -8,6 +8,7 @@ type Props = {
onChange: ChangeEventHandler<HTMLInputElement>; onChange: ChangeEventHandler<HTMLInputElement>;
onKeyDown?: KeyboardEventHandler<HTMLInputElement> | undefined; onKeyDown?: KeyboardEventHandler<HTMLInputElement> | undefined;
className?: string; className?: string;
spellCheck?: boolean;
}; };
export default function TextInput({ export default function TextInput({
@ -18,9 +19,11 @@ export default function TextInput({
onChange, onChange,
onKeyDown, onKeyDown,
className, className,
spellCheck,
}: Props) { }: Props) {
return ( return (
<input <input
spellCheck={spellCheck}
autoFocus={autoFocus} autoFocus={autoFocus}
type={type ? type : "text"} type={type ? type : "text"}
placeholder={placeholder} placeholder={placeholder}

View File

@ -1,7 +1,7 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import { KeyExpiry } from "@/types/global"; import { KeyExpiry } from "@/types/global";
import bcrypt from "bcrypt";
import crypto from "crypto"; import crypto from "crypto";
import { decode, encode, getToken } from "next-auth/jwt";
export default async function postToken( export default async function postToken(
body: { body: {
@ -34,44 +34,55 @@ export default async function postToken(
}; };
} }
const now = Date.now();
let expiryDate = new Date(); let expiryDate = new Date();
const oneDayInSeconds = 86400;
let expiryDateSecond = 7 * oneDayInSeconds;
switch (body.expires) { if (body.expires === KeyExpiry.oneMonth) {
case KeyExpiry.sevenDays: expiryDate.setDate(expiryDate.getDate() + 30);
expiryDate.setDate(expiryDate.getDate() + 7); expiryDateSecond = 30 * oneDayInSeconds;
break; } else if (body.expires === KeyExpiry.twoMonths) {
case KeyExpiry.oneMonth: expiryDate.setDate(expiryDate.getDate() + 60);
expiryDate.setDate(expiryDate.getDate() + 30); expiryDateSecond = 60 * oneDayInSeconds;
break; } else if (body.expires === KeyExpiry.threeMonths) {
case KeyExpiry.twoMonths: expiryDate.setDate(expiryDate.getDate() + 90);
expiryDate.setDate(expiryDate.getDate() + 60); expiryDateSecond = 90 * oneDayInSeconds;
break; } else if (body.expires === KeyExpiry.never) {
case KeyExpiry.threeMonths: expiryDate.setDate(expiryDate.getDate() + 73000); // 200 years (not really never)
expiryDate.setDate(expiryDate.getDate() + 90); expiryDateSecond = 73050 * oneDayInSeconds;
break; } else {
case KeyExpiry.never: expiryDate.setDate(expiryDate.getDate() + 7);
expiryDate.setDate(expiryDate.getDate() + 73000); // 200 years (not really never) expiryDateSecond = 7 * oneDayInSeconds;
break;
default:
expiryDate.setDate(expiryDate.getDate() + 7);
break;
} }
const saltRounds = 10; const token = await encode({
token: {
id: userId,
iat: now / 1000,
exp: (expiryDate as any) / 1000,
jti: crypto.randomUUID(),
},
maxAge: expiryDateSecond || 604800,
secret: process.env.NEXTAUTH_SECRET,
});
const hashedKey = bcrypt.hashSync(crypto.randomUUID(), saltRounds); const tokenBody = await decode({
token,
secret: process.env.NEXTAUTH_SECRET,
});
const createToken = await prisma.apiKey.create({ const createToken = await prisma.apiKey.create({
data: { data: {
name: body.name, name: body.name,
userId, userId,
token: hashedKey, token: tokenBody?.jti as string,
expires: expiryDate, expires: expiryDate,
}, },
}); });
return { return {
response: createToken.token, response: token,
status: 200, status: 200,
}; };
} }

View File

@ -23,6 +23,13 @@ export default async function verifyUser({
return null; return null;
} }
if (token.exp < Date.now() / 1000) {
res
.status(401)
.json({ response: "Your session has expired, please log in again." });
return null;
}
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
id: userId, id: userId,

View File

@ -65,6 +65,7 @@ import ZitadelProvider from "next-auth/providers/zitadel";
import ZohoProvider from "next-auth/providers/zoho"; import ZohoProvider from "next-auth/providers/zoho";
import ZoomProvider from "next-auth/providers/zoom"; import ZoomProvider from "next-auth/providers/zoom";
import * as process from "process"; import * as process from "process";
import type { NextApiRequest, NextApiResponse } from "next";
const emailEnabled = const emailEnabled =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false; process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
@ -1059,60 +1060,60 @@ if (process.env.NEXT_PUBLIC_ZOOM_ENABLED_ENABLED === "true") {
}; };
} }
export const authOptions: AuthOptions = { export default async function auth(req: NextApiRequest, res: NextApiResponse) {
adapter: adapter as Adapter, return await NextAuth(req, res, {
session: { adapter: adapter as Adapter,
strategy: "jwt", session: {
maxAge: 30 * 24 * 60 * 60, // 30 days strategy: "jwt",
}, maxAge: 30 * 24 * 60 * 60, // 30 days
providers, },
pages: { providers,
signIn: "/login", pages: {
verifyRequest: "/confirmation", signIn: "/login",
}, verifyRequest: "/confirmation",
callbacks: { },
async signIn({ user, account, profile, email, credentials }) { callbacks: {
if (account?.provider !== "credentials") { async signIn({ user, account, profile, email, credentials }) {
// registration via SSO can be separately disabled if (account?.provider !== "credentials") {
const existingUser = await prisma.account.findFirst({ // registration via SSO can be separately disabled
where: { const existingUser = await prisma.account.findFirst({
providerAccountId: account?.providerAccountId, where: {
}, providerAccountId: account?.providerAccountId,
}); },
if (existingUser && newSsoUsersDisabled) { });
return false; if (existingUser && newSsoUsersDisabled) {
return false;
}
} }
} return true;
return true; },
}, async jwt({ token, trigger, user }) {
async jwt({ token, trigger, user }) { token.sub = token.sub ? Number(token.sub) : undefined;
token.sub = token.sub ? Number(token.sub) : undefined; if (trigger === "signIn" || trigger === "signUp")
if (trigger === "signIn" || trigger === "signUp") token.id = user?.id as number;
token.id = user?.id as number;
return token; return token;
}, },
async session({ session, token }) { async session({ session, token }) {
session.user.id = token.id; session.user.id = token.id;
if (STRIPE_SECRET_KEY) { if (STRIPE_SECRET_KEY) {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
id: token.id, id: token.id,
}, },
include: { include: {
subscriptions: true, subscriptions: true,
}, },
}); });
if (user) { if (user) {
const subscribedUser = await verifySubscription(user); const subscribedUser = await verifySubscription(user);
}
} }
}
return session; return session;
},
}, },
}, });
}; }
export default NextAuth(authOptions);

View File

@ -16,6 +16,12 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
return res.status(401).json({ response: "You must be logged in." }); return res.status(401).json({ response: "You must be logged in." });
} }
if (token.exp < Date.now() / 1000) {
return res
.status(401)
.json({ response: "Your session has expired, please log in again." });
}
if (userId !== Number(req.query.id)) if (userId !== Number(req.query.id))
return res.status(401).json({ response: "Permission denied." }); return res.status(401).json({ response: "Permission denied." });