diff --git a/components/ModalContent/NewKeyModal.tsx b/components/ModalContent/NewTokenModal.tsx
similarity index 75%
rename from components/ModalContent/NewKeyModal.tsx
rename to components/ModalContent/NewTokenModal.tsx
index e058561..15887d8 100644
--- a/components/ModalContent/NewKeyModal.tsx
+++ b/components/ModalContent/NewTokenModal.tsx
@@ -1,25 +1,25 @@
import React, { useState } from "react";
import TextInput from "@/components/TextInput";
-import { KeyExpiry } from "@/types/global";
-import { useSession } from "next-auth/react";
+import { TokenExpiry } from "@/types/global";
import toast from "react-hot-toast";
import Modal from "../Modal";
+import useTokenStore from "@/store/tokens";
type Props = {
onClose: Function;
};
-export default function NewKeyModal({ onClose }: Props) {
- const { data } = useSession();
-
+export default function NewTokenModal({ onClose }: Props) {
const [newToken, setNewToken] = useState("");
+ const { addToken } = useTokenStore();
+
const initial = {
name: "",
- expires: 0 as KeyExpiry,
+ expires: 0 as TokenExpiry,
};
- const [key, setKey] = useState(initial as any);
+ const [token, setToken] = useState(initial as any);
const [submitLoader, setSubmitLoader] = useState(false);
@@ -27,30 +27,18 @@ export default function NewKeyModal({ onClose }: Props) {
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();
+ const { ok, data } = await addToken(token);
toast.dismiss(load);
- if (response.ok) {
+ if (ok) {
toast.success(`Created!`);
- setNewToken(data.response);
- } else toast.error(data.response as string);
+ setNewToken((data as any).secretKey);
+ } else toast.error(data as string);
setSubmitLoader(false);
-
- return response;
}
};
@@ -90,8 +78,8 @@ export default function NewKeyModal({ onClose }: Props) {
Name
setKey({ ...key, name: e.target.value })}
+ value={token.name}
+ onChange={(e) => setToken({ ...token, name: e.target.value })}
placeholder="e.g. For the iOS shortcut"
className="bg-base-200"
/>
@@ -106,11 +94,11 @@ export default function NewKeyModal({ onClose }: Props) {
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"}
+ {token.expires === TokenExpiry.sevenDays && "7 Days"}
+ {token.expires === TokenExpiry.oneMonth && "30 Days"}
+ {token.expires === TokenExpiry.twoMonths && "60 Days"}
+ {token.expires === TokenExpiry.threeMonths && "90 Days"}
+ {token.expires === TokenExpiry.never && "No Expiration"}
-
@@ -123,10 +111,13 @@ export default function NewKeyModal({ onClose }: Props) {
type="radio"
name="sort-radio"
className="radio checked:bg-primary"
- checked={key.expires === KeyExpiry.sevenDays}
+ checked={token.expires === TokenExpiry.sevenDays}
onChange={() => {
(document?.activeElement as HTMLElement)?.blur();
- setKey({ ...key, expires: KeyExpiry.sevenDays });
+ setToken({
+ ...token,
+ expires: TokenExpiry.sevenDays,
+ });
}}
/>
7 Days
@@ -142,10 +133,10 @@ export default function NewKeyModal({ onClose }: Props) {
type="radio"
name="sort-radio"
className="radio checked:bg-primary"
- checked={key.expires === KeyExpiry.oneMonth}
+ checked={token.expires === TokenExpiry.oneMonth}
onChange={() => {
(document?.activeElement as HTMLElement)?.blur();
- setKey({ ...key, expires: KeyExpiry.oneMonth });
+ setToken({ ...token, expires: TokenExpiry.oneMonth });
}}
/>
30 Days
@@ -161,10 +152,13 @@ export default function NewKeyModal({ onClose }: Props) {
type="radio"
name="sort-radio"
className="radio checked:bg-primary"
- checked={key.expires === KeyExpiry.twoMonths}
+ checked={token.expires === TokenExpiry.twoMonths}
onChange={() => {
(document?.activeElement as HTMLElement)?.blur();
- setKey({ ...key, expires: KeyExpiry.twoMonths });
+ setToken({
+ ...token,
+ expires: TokenExpiry.twoMonths,
+ });
}}
/>
60 Days
@@ -180,10 +174,13 @@ export default function NewKeyModal({ onClose }: Props) {
type="radio"
name="sort-radio"
className="radio checked:bg-primary"
- checked={key.expires === KeyExpiry.threeMonths}
+ checked={token.expires === TokenExpiry.threeMonths}
onChange={() => {
(document?.activeElement as HTMLElement)?.blur();
- setKey({ ...key, expires: KeyExpiry.threeMonths });
+ setToken({
+ ...token,
+ expires: TokenExpiry.threeMonths,
+ });
}}
/>
90 Days
@@ -199,10 +196,10 @@ export default function NewKeyModal({ onClose }: Props) {
type="radio"
name="sort-radio"
className="radio checked:bg-primary"
- checked={key.expires === KeyExpiry.never}
+ checked={token.expires === TokenExpiry.never}
onChange={() => {
(document?.activeElement as HTMLElement)?.blur();
- setKey({ ...key, expires: KeyExpiry.never });
+ setToken({ ...token, expires: TokenExpiry.never });
}}
/>
No Expiration
diff --git a/components/ModalContent/RevokeTokenModal.tsx b/components/ModalContent/RevokeTokenModal.tsx
new file mode 100644
index 0000000..9aef2d9
--- /dev/null
+++ b/components/ModalContent/RevokeTokenModal.tsx
@@ -0,0 +1,62 @@
+import React, { useEffect, useState } from "react";
+import useLinkStore from "@/store/links";
+import toast from "react-hot-toast";
+import Modal from "../Modal";
+import { useRouter } from "next/router";
+import { AccessToken } from "@prisma/client";
+import useTokenStore from "@/store/tokens";
+
+type Props = {
+ onClose: Function;
+ activeToken: AccessToken;
+};
+
+export default function DeleteTokenModal({ onClose, activeToken }: Props) {
+ const [token, setToken] = useState(activeToken);
+
+ const { revokeToken } = useTokenStore();
+ const [submitLoader, setSubmitLoader] = useState(false);
+
+ const router = useRouter();
+
+ useEffect(() => {
+ setToken(activeToken);
+ }, []);
+
+ const deleteLink = async () => {
+ console.log(token);
+ const load = toast.loading("Deleting...");
+
+ const response = await revokeToken(token.id as number);
+
+ toast.dismiss(load);
+
+ response.ok && toast.success(`Token Revoked.`);
+
+ onClose();
+ };
+
+ return (
+
+
Revoke Token
+
+
+
+
+
+ Are you sure you want to revoke this Access Token? Any apps or
+ services using this token will no longer be able to access Linkwarden
+ using it.
+
+
+
+
+
+ );
+}
diff --git a/components/SettingsSidebar.tsx b/components/SettingsSidebar.tsx
index 3ffbc5f..bcd1644 100644
--- a/components/SettingsSidebar.tsx
+++ b/components/SettingsSidebar.tsx
@@ -73,7 +73,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
>
- Access Token
+ Access Tokens
diff --git a/lib/api/controllers/tokens/getTokens.ts b/lib/api/controllers/tokens/getTokens.ts
index d525ac7..a5db351 100644
--- a/lib/api/controllers/tokens/getTokens.ts
+++ b/lib/api/controllers/tokens/getTokens.ts
@@ -1,14 +1,13 @@
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({
+ const getTokens = await prisma.accessToken.findMany({
where: {
userId,
+ revoked: false,
},
select: {
+ id: true,
name: true,
expires: true,
createdAt: true,
diff --git a/lib/api/controllers/tokens/postToken.ts b/lib/api/controllers/tokens/postToken.ts
index b6fb1c1..f88030d 100644
--- a/lib/api/controllers/tokens/postToken.ts
+++ b/lib/api/controllers/tokens/postToken.ts
@@ -1,12 +1,12 @@
import { prisma } from "@/lib/api/db";
-import { KeyExpiry } from "@/types/global";
+import { TokenExpiry } from "@/types/global";
import crypto from "crypto";
-import { decode, encode, getToken } from "next-auth/jwt";
+import { decode, encode } from "next-auth/jwt";
export default async function postToken(
body: {
name: string;
- expires: KeyExpiry;
+ expires: TokenExpiry;
},
userId: number
) {
@@ -20,9 +20,10 @@ export default async function postToken(
status: 400,
};
- const checkIfTokenExists = await prisma.apiKey.findFirst({
+ const checkIfTokenExists = await prisma.accessToken.findFirst({
where: {
name: body.name,
+ revoked: false,
userId,
},
});
@@ -39,16 +40,16 @@ export default async function postToken(
const oneDayInSeconds = 86400;
let expiryDateSecond = 7 * oneDayInSeconds;
- if (body.expires === KeyExpiry.oneMonth) {
+ if (body.expires === TokenExpiry.oneMonth) {
expiryDate.setDate(expiryDate.getDate() + 30);
expiryDateSecond = 30 * oneDayInSeconds;
- } else if (body.expires === KeyExpiry.twoMonths) {
+ } else if (body.expires === TokenExpiry.twoMonths) {
expiryDate.setDate(expiryDate.getDate() + 60);
expiryDateSecond = 60 * oneDayInSeconds;
- } else if (body.expires === KeyExpiry.threeMonths) {
+ } else if (body.expires === TokenExpiry.threeMonths) {
expiryDate.setDate(expiryDate.getDate() + 90);
expiryDateSecond = 90 * oneDayInSeconds;
- } else if (body.expires === KeyExpiry.never) {
+ } else if (body.expires === TokenExpiry.never) {
expiryDate.setDate(expiryDate.getDate() + 73000); // 200 years (not really never)
expiryDateSecond = 73050 * oneDayInSeconds;
} else {
@@ -72,7 +73,7 @@ export default async function postToken(
secret: process.env.NEXTAUTH_SECRET,
});
- const createToken = await prisma.apiKey.create({
+ const createToken = await prisma.accessToken.create({
data: {
name: body.name,
userId,
@@ -82,7 +83,10 @@ export default async function postToken(
});
return {
- response: token,
+ response: {
+ secretKey: token,
+ token: createToken,
+ },
status: 200,
};
}
diff --git a/lib/api/controllers/tokens/tokenId/deleteTokenById.ts b/lib/api/controllers/tokens/tokenId/deleteTokenById.ts
index 742016f..ea17f1f 100644
--- a/lib/api/controllers/tokens/tokenId/deleteTokenById.ts
+++ b/lib/api/controllers/tokens/tokenId/deleteTokenById.ts
@@ -1,16 +1,24 @@
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({
+ const tokenExists = await prisma.accessToken.findFirst({
where: {
id: tokenId,
userId,
},
});
- return { response: deletedToken, status: 200 };
+ const revokedToken = await prisma.accessToken.update({
+ where: {
+ id: tokenExists?.id,
+ },
+ data: {
+ revoked: true,
+ },
+ });
+
+ return { response: revokedToken, status: 200 };
}
diff --git a/lib/api/verifyToken.ts b/lib/api/verifyToken.ts
new file mode 100644
index 0000000..1c1abfc
--- /dev/null
+++ b/lib/api/verifyToken.ts
@@ -0,0 +1,36 @@
+import { NextApiRequest } from "next";
+import { JWT, getToken } from "next-auth/jwt";
+import { prisma } from "./db";
+
+type Props = {
+ req: NextApiRequest;
+};
+
+export default async function verifyToken({
+ req,
+}: Props): Promise {
+ const token = await getToken({ req });
+ const userId = token?.id;
+
+ if (!userId) {
+ return "You must be logged in.";
+ }
+
+ if (token.exp < Date.now() / 1000) {
+ return "Your session has expired, please log in again.";
+ }
+
+ // check if token is revoked
+ const revoked = await prisma.accessToken.findFirst({
+ where: {
+ token: token.jti,
+ revoked: true,
+ },
+ });
+
+ if (revoked) {
+ return "Your session has expired, please log in again.";
+ }
+
+ return token;
+}
diff --git a/lib/api/verifyUser.ts b/lib/api/verifyUser.ts
index 8c58143..847bdaf 100644
--- a/lib/api/verifyUser.ts
+++ b/lib/api/verifyUser.ts
@@ -1,8 +1,8 @@
import { NextApiRequest, NextApiResponse } from "next";
-import { getToken } from "next-auth/jwt";
import { prisma } from "./db";
import { User } from "@prisma/client";
import verifySubscription from "./verifySubscription";
+import verifyToken from "./verifyToken";
type Props = {
req: NextApiRequest;
@@ -15,21 +15,15 @@ export default async function verifyUser({
req,
res,
}: Props): Promise {
- const token = await getToken({ req });
+ const token = await verifyToken({ req });
+
+ if (typeof token === "string") {
+ res.status(401).json({ response: token });
+ return null;
+ }
+
const userId = token?.id;
- if (!userId) {
- res.status(401).json({ response: "You must be logged in." });
- 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({
where: {
id: userId,
diff --git a/pages/api/v1/archives/[linkId].ts b/pages/api/v1/archives/[linkId].ts
index 3734692..b13e690 100644
--- a/pages/api/v1/archives/[linkId].ts
+++ b/pages/api/v1/archives/[linkId].ts
@@ -1,6 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import readFile from "@/lib/api/storage/readFile";
-import { getToken } from "next-auth/jwt";
import { prisma } from "@/lib/api/db";
import { ArchivedFormat } from "@/types/global";
import verifyUser from "@/lib/api/verifyUser";
@@ -9,6 +8,7 @@ import { UsersAndCollections } from "@prisma/client";
import formidable from "formidable";
import createFile from "@/lib/api/storage/createFile";
import fs from "fs";
+import verifyToken from "@/lib/api/verifyToken";
export const config = {
api: {
@@ -33,8 +33,8 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
return res.status(401).json({ response: "Invalid parameters." });
if (req.method === "GET") {
- const token = await getToken({ req });
- const userId = token?.id;
+ const token = await verifyToken({ req });
+ const userId = typeof token === "string" ? undefined : token?.id;
const collectionIsAccessible = await prisma.collection.findFirst({
where: {
diff --git a/pages/api/v1/avatar/[id].ts b/pages/api/v1/avatar/[id].ts
index 8c591ff..c39f420 100644
--- a/pages/api/v1/avatar/[id].ts
+++ b/pages/api/v1/avatar/[id].ts
@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "@/lib/api/db";
import readFile from "@/lib/api/storage/readFile";
-import { getToken } from "next-auth/jwt";
+import verifyToken from "@/lib/api/verifyToken";
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
const queryId = Number(req.query.id);
@@ -12,8 +12,8 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
.status(401)
.send("Invalid parameters.");
- const token = await getToken({ req });
- const userId = token?.id;
+ const token = await verifyToken({ req });
+ const userId = typeof token === "string" ? undefined : token?.id;
if (req.method === "GET") {
const targetUser = await prisma.user.findUnique({
diff --git a/pages/api/v1/public/users/[id].ts b/pages/api/v1/public/users/[id].ts
index f5b66ed..f34671d 100644
--- a/pages/api/v1/public/users/[id].ts
+++ b/pages/api/v1/public/users/[id].ts
@@ -1,10 +1,10 @@
import type { NextApiRequest, NextApiResponse } from "next";
import getPublicUser from "@/lib/api/controllers/public/users/getPublicUser";
-import { getToken } from "next-auth/jwt";
+import verifyToken from "@/lib/api/verifyToken";
export default async function users(req: NextApiRequest, res: NextApiResponse) {
- const token = await getToken({ req });
- const requestingId = token?.id;
+ const token = await verifyToken({ req });
+ const requestingId = typeof token === "string" ? undefined : token?.id;
const lookupId = req.query.id as string;
diff --git a/pages/api/v1/users/[id].ts b/pages/api/v1/users/[id].ts
index 2dd808a..cf75e16 100644
--- a/pages/api/v1/users/[id].ts
+++ b/pages/api/v1/users/[id].ts
@@ -2,26 +2,22 @@ import type { NextApiRequest, NextApiResponse } from "next";
import getUserById from "@/lib/api/controllers/users/userId/getUserById";
import updateUserById from "@/lib/api/controllers/users/userId/updateUserById";
import deleteUserById from "@/lib/api/controllers/users/userId/deleteUserById";
-import { getToken } from "next-auth/jwt";
import { prisma } from "@/lib/api/db";
import verifySubscription from "@/lib/api/verifySubscription";
+import verifyToken from "@/lib/api/verifyToken";
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
export default async function users(req: NextApiRequest, res: NextApiResponse) {
- const token = await getToken({ req });
+ const token = await verifyToken({ req });
+
+ if (typeof token === "string") {
+ res.status(401).json({ response: token });
+ return null;
+ }
+
const userId = token?.id;
- if (!userId) {
- 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))
return res.status(401).json({ response: "Permission denied." });
diff --git a/pages/settings/access-tokens.tsx b/pages/settings/access-tokens.tsx
index 14912a2..f43a459 100644
--- a/pages/settings/access-tokens.tsx
+++ b/pages/settings/access-tokens.tsx
@@ -1,13 +1,33 @@
import SettingsLayout from "@/layouts/SettingsLayout";
-import React, { useState } from "react";
-import NewKeyModal from "@/components/ModalContent/NewKeyModal";
+import React, { useEffect, useState } from "react";
+import NewTokenModal from "@/components/ModalContent/NewTokenModal";
+import RevokeTokenModal from "@/components/ModalContent/RevokeTokenModal";
+import { AccessToken } from "@prisma/client";
+import useTokenStore from "@/store/tokens";
-export default function Api() {
- const [newKeyModal, setNewKeyModal] = useState(false);
+export default function AccessTokens() {
+ const [newTokenModal, setNewTokenModal] = useState(false);
+ const [revokeTokenModal, setRevokeTokenModal] = useState(false);
+ const [selectedToken, setSelectedToken] = useState(null);
+
+ const openRevokeModal = (token: AccessToken) => {
+ setSelectedToken(token);
+ setRevokeTokenModal(true);
+ };
+
+ const { setTokens, tokens } = useTokenStore();
+
+ useEffect(() => {
+ fetch("/api/v1/tokens")
+ .then((res) => res.json())
+ .then((data) => {
+ if (data.response) setTokens(data.response as AccessToken[]);
+ });
+ }, []);
return (
- API Keys
+ Access Tokens
@@ -20,16 +40,68 @@ export default function Api() {
+
+ {tokens.length > 0 ? (
+ <>
+
+
+
+ {/* head */}
+
+
+ |
+ Name |
+ Created |
+ Expires |
+ |
+
+
+
+ {tokens.map((token, i) => (
+
+
+ {i + 1} |
+ {token.name} |
+
+ {new Date(token.createdAt || "").toLocaleDateString()}
+ |
+
+ {new Date(token.expires || "").toLocaleDateString()}
+ |
+
+
+ |
+
+
+ ))}
+
+
+ >
+ ) : undefined}
- {newKeyModal ? (
- setNewKeyModal(false)} />
+ {newTokenModal ? (
+ setNewTokenModal(false)} />
) : undefined}
+ {revokeTokenModal && selectedToken && (
+ {
+ setRevokeTokenModal(false);
+ setSelectedToken(null);
+ }}
+ activeToken={selectedToken}
+ />
+ )}
);
}
diff --git a/prisma/migrations/20240124192212_added_revoke_field/migration.sql b/prisma/migrations/20240124192212_added_revoke_field/migration.sql
new file mode 100644
index 0000000..b9802a0
--- /dev/null
+++ b/prisma/migrations/20240124192212_added_revoke_field/migration.sql
@@ -0,0 +1,35 @@
+/*
+ Warnings:
+
+ - You are about to drop the `ApiKey` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "ApiKey" DROP CONSTRAINT "ApiKey_userId_fkey";
+
+-- DropTable
+DROP TABLE "ApiKey";
+
+-- CreateTable
+CREATE TABLE "AccessToken" (
+ "id" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+ "userId" INTEGER NOT NULL,
+ "token" TEXT NOT NULL,
+ "revoked" BOOLEAN NOT NULL DEFAULT false,
+ "expires" TIMESTAMP(3) NOT NULL,
+ "lastUsedAt" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "AccessToken_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "AccessToken_token_key" ON "AccessToken"("token");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "AccessToken_name_userId_key" ON "AccessToken"("name", "userId");
+
+-- AddForeignKey
+ALTER TABLE "AccessToken" ADD CONSTRAINT "AccessToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20240124201018_removed_name_unique_constraint/migration.sql b/prisma/migrations/20240124201018_removed_name_unique_constraint/migration.sql
new file mode 100644
index 0000000..4a570db
--- /dev/null
+++ b/prisma/migrations/20240124201018_removed_name_unique_constraint/migration.sql
@@ -0,0 +1,2 @@
+-- DropIndex
+DROP INDEX "AccessToken_name_userId_key";
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 60a03e1..aebc98f 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -39,7 +39,7 @@ model User {
pinnedLinks Link[]
collectionsJoined UsersAndCollections[]
whitelistedUsers WhitelistedUser[]
- apiKeys ApiKey[]
+ accessTokens AccessToken[]
subscriptions Subscription?
archiveAsScreenshot Boolean @default(true)
archiveAsPDF Boolean @default(true)
@@ -142,16 +142,15 @@ model Subscription {
updatedAt DateTime @default(now()) @updatedAt
}
-model ApiKey {
+model AccessToken {
id Int @id @default(autoincrement())
name String
user User @relation(fields: [userId], references: [id])
userId Int
token String @unique
+ revoked Boolean @default(false)
expires DateTime
lastUsedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
-
- @@unique([name, userId])
}
diff --git a/store/tokens.ts b/store/tokens.ts
new file mode 100644
index 0000000..eff1100
--- /dev/null
+++ b/store/tokens.ts
@@ -0,0 +1,56 @@
+import { AccessToken } from "@prisma/client";
+import { create } from "zustand";
+
+// Token store
+
+type ResponseObject = {
+ ok: boolean;
+ data: object | string;
+};
+
+type TokenStore = {
+ tokens: Partial[];
+ setTokens: (data: Partial[]) => void;
+ addToken: (body: Partial[]) => Promise;
+ revokeToken: (tokenId: number) => Promise;
+};
+
+const useTokenStore = create((set) => ({
+ tokens: [],
+ setTokens: async (data) => {
+ set(() => ({
+ tokens: data,
+ }));
+ },
+ addToken: async (body) => {
+ const response = await fetch("/api/v1/tokens", {
+ body: JSON.stringify(body),
+ method: "POST",
+ });
+
+ const data = await response.json();
+
+ if (response.ok)
+ set((state) => ({
+ tokens: [...state.tokens, data.response.token],
+ }));
+
+ return { ok: response.ok, data: data.response };
+ },
+ revokeToken: async (tokenId) => {
+ const response = await fetch(`/api/v1/tokens/${tokenId}`, {
+ method: "DELETE",
+ });
+
+ const data = await response.json();
+
+ if (response.ok)
+ set((state) => ({
+ tokens: state.tokens.filter((token) => token.id !== tokenId),
+ }));
+
+ return { ok: response.ok, data: data.response };
+ },
+}));
+
+export default useTokenStore;
diff --git a/types/global.ts b/types/global.ts
index 36e38c1..3c8de79 100644
--- a/types/global.ts
+++ b/types/global.ts
@@ -135,7 +135,7 @@ export enum LinkType {
image,
}
-export enum KeyExpiry {
+export enum TokenExpiry {
sevenDays,
oneMonth,
twoMonths,