From 532ff4cea9565b9c16d84b402c1ac9b70cc4b776 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Fri, 3 Nov 2023 11:32:47 +0530 Subject: [PATCH] feat: api key dashboard --- apps/dashboard/app/api-keys/page.tsx | 2 - apps/dashboard/app/api-keys/server-actions.ts | 88 +++++++++ apps/dashboard/app/api/api-keys/route.ts | 59 ------ .../dashboard/app/security/email/add/page.tsx | 2 +- apps/dashboard/app/security/server-actions.ts | 1 - apps/dashboard/components/api-keys/create.tsx | 186 ++++++++++++------ apps/dashboard/components/api-keys/list.tsx | 15 +- apps/dashboard/components/api-keys/revoke.tsx | 52 +++-- apps/dashboard/services/graphql/generated.ts | 28 +-- .../services/graphql/mutations/api-keys.ts | 6 + .../services/graphql/queries/api-keys.ts | 16 +- 11 files changed, 287 insertions(+), 168 deletions(-) create mode 100644 apps/dashboard/app/api-keys/server-actions.ts delete mode 100644 apps/dashboard/app/api/api-keys/route.ts diff --git a/apps/dashboard/app/api-keys/page.tsx b/apps/dashboard/app/api-keys/page.tsx index 378858c2da..16ff4a30f8 100644 --- a/apps/dashboard/app/api-keys/page.tsx +++ b/apps/dashboard/app/api-keys/page.tsx @@ -1,7 +1,5 @@ -import { getServerSession } from "next-auth" import { Box, Link, Typography } from "@mui/joy" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" import ContentContainer from "@/components/content-container" import ApiKeysList from "@/components/api-keys/list" import ApiKeyCreate from "@/components/api-keys/create" diff --git a/apps/dashboard/app/api-keys/server-actions.ts b/apps/dashboard/app/api-keys/server-actions.ts new file mode 100644 index 0000000000..5bba52aeea --- /dev/null +++ b/apps/dashboard/app/api-keys/server-actions.ts @@ -0,0 +1,88 @@ +"use server" +import { getServerSession } from "next-auth" +import { revalidatePath } from "next/cache" + +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { createApiKey, revokeApiKey } from "@/services/graphql/mutations/api-keys" + +export const revokeApiKeyServerAction = async (id: string) => { + if (!id || typeof id !== "string") { + return { + error: true, + message: "API Key ID to revoke is not present", + data: null, + } + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + return { + error: true, + message: "Token is not present", + data: null, + } + } + + try { + await revokeApiKey(token, id) + } catch (err) { + console.log("error in revokeApiKey ", err) + return { + error: true, + message: + "Something went wrong Please try again and if error persist contact support", + data: null, + } + } + + revalidatePath("/api-keys") + + return { + error: false, + message: "API Key revoked successfully", + data: { ok: true }, + } +} + +export const createApiKeyServerAction = async (_prevState: unknown, form: FormData) => { + const apiKeyName = form.get("apiKeyName") + if (!apiKeyName || typeof apiKeyName !== "string") { + return { + error: true, + message: "API Key name to create is not present", + data: null, + } + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + return { + error: true, + message: "Token is not present", + data: null, + } + } + + let data + try { + data = await createApiKey(token, apiKeyName) + } catch (err) { + console.log("error in createApiKey ", err) + return { + error: true, + message: + "Something went wrong Please try again and if error persist contact support", + data: null, + } + } + + revalidatePath("/api-keys") + + return { + error: false, + message: "API Key created successfully", + data: { apiKeySecret: data?.apiKeyCreate.apiKeySecret }, + } +} diff --git a/apps/dashboard/app/api/api-keys/route.ts b/apps/dashboard/app/api/api-keys/route.ts deleted file mode 100644 index 4fefed697a..0000000000 --- a/apps/dashboard/app/api/api-keys/route.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { getServerSession } from "next-auth" - -import { NextRequest, NextResponse } from "next/server" - -import { authOptions } from "../auth/[...nextauth]/route" - -import { createApiKey, revokeApiKey } from "@/services/graphql/mutations/api-keys" - -export async function DELETE(request: NextRequest) { - const { id }: { id: string } = await request.json() - - const session = await getServerSession(authOptions) - const token = session?.accessToken - - if (!id) { - return new NextResponse( - JSON.stringify({ error: "API Key ID to revoke is not present" }), - { status: 400 }, - ) - } - - if (!token) { - return new NextResponse(JSON.stringify({ error: "Token is not present" }), { - status: 400, - }) - } - - await revokeApiKey(token, id) - - return new NextResponse(JSON.stringify({ ok: true }), { - status: 200, - }) -} - -export async function POST(request: NextRequest) { - const { name }: { name: string } = await request.json() - - const session = await getServerSession(authOptions) - const token = session?.accessToken - - if (!name) { - return new NextResponse( - JSON.stringify({ error: "API Key name to create is not present" }), - { status: 400 }, - ) - } - - if (!token) { - return new NextResponse(JSON.stringify({ error: "Token is not present" }), { - status: 400, - }) - } - - const data = await createApiKey(token, name) - - return new NextResponse(JSON.stringify({ secret: data?.apiKeyCreate.apiKeySecret }), { - status: 200, - }) -} diff --git a/apps/dashboard/app/security/email/add/page.tsx b/apps/dashboard/app/security/email/add/page.tsx index a742f4ec8c..3d77619465 100644 --- a/apps/dashboard/app/security/email/add/page.tsx +++ b/apps/dashboard/app/security/email/add/page.tsx @@ -27,7 +27,7 @@ export default function AddEmail() { const [state, formAction] = useFormState(emailRegisterInitiateServerAction, { error: null, message: null, - responsePayload: {}, + data: null, }) return ( diff --git a/apps/dashboard/app/security/server-actions.ts b/apps/dashboard/app/security/server-actions.ts index 95d7417167..987ee0cd45 100644 --- a/apps/dashboard/app/security/server-actions.ts +++ b/apps/dashboard/app/security/server-actions.ts @@ -53,7 +53,6 @@ export const emailRegisterInitiateServerAction = async ( } if (data?.userEmailRegistrationInitiate.errors.length) { - console.log() return { error: true, message: data?.userEmailRegistrationInitiate.errors[0].message, diff --git a/apps/dashboard/components/api-keys/create.tsx b/apps/dashboard/components/api-keys/create.tsx index a75f7ee671..e7c67747c6 100644 --- a/apps/dashboard/components/api-keys/create.tsx +++ b/apps/dashboard/components/api-keys/create.tsx @@ -3,28 +3,47 @@ import { Box, Button, - FormLabel, + FormControl, + FormHelperText, Input, Modal, ModalClose, Sheet, Typography, + Tooltip, } from "@mui/joy" + +import InfoOutlined from "@mui/icons-material/InfoOutlined" + import AddIcon from "@mui/icons-material/Add" import CopyIcon from "@mui/icons-material/CopyAll" import { useState } from "react" +import { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-next-line no-implicit-any error + experimental_useFormState as useFormState, +} from "react-dom" + +import FormSubmitButton from "../form-submit-button" + +import { createApiKeyServerAction } from "@/app/api-keys/server-actions" + const ApiKeyCreate = () => { const [open, setOpen] = useState(false) - const [name, setName] = useState("") - const [apiKeySecret, setApiKeySecret] = useState("") - const [error, setError] = useState("") + const [copied, setCopied] = useState(false) + const [state, formAction] = useFormState(createApiKeyServerAction, { + error: null, + message: null, + data: null, + }) - const close = () => { + const handleModalClose = () => { setOpen(false) - setName("") - setApiKeySecret("") - setError("") + state.error = null + state.message = null + state.data = null + console.log("Modal has been closed") } return ( @@ -34,7 +53,7 @@ const ApiKeyCreate = () => { { display: "flex", flexDirection: "column", gap: 2, - alignItems: "flex-start", + alignItems: "center", }} > - { textColor="inherit" fontWeight="lg" > - Create API Key + Create API key - Talk to the Blink Servers using this token. - {apiKeySecret ? ( + {state?.data?.apiKeySecret ? ( <> -

API Secret:

- {apiKeySecret} - + - + The API Key Secret will be shown only once here.
Please save it somewhere safely!
+ ) : ( <> - Name - - setName(t.target.value)} - /> + +
+ + {state.error ? ( + + + {state.message} + + ) : null} - {error && {error}} + + + Create + + +
+
)} - -
diff --git a/apps/dashboard/components/api-keys/list.tsx b/apps/dashboard/components/api-keys/list.tsx index 999f80857d..752a1e6acc 100644 --- a/apps/dashboard/components/api-keys/list.tsx +++ b/apps/dashboard/components/api-keys/list.tsx @@ -11,6 +11,15 @@ import RevokeKey from "./revoke" import { apiKeys } from "@/services/graphql/queries/api-keys" import { authOptions } from "@/app/api/auth/[...nextauth]/route" +const formatDate = (dateStr: string): string => { + const options: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "long", + day: "2-digit", + } + return new Date(dateStr).toLocaleDateString(undefined, options) +} + const ApiKeysList = async () => { const session = await getServerSession(authOptions) const token = session?.accessToken @@ -35,11 +44,11 @@ const ApiKeysList = async () => { {keys.map(({ id, name, createdAt, expiration }) => ( - + {id} {name} - {createdAt} - {expiration} + {formatDate(createdAt)} + {formatDate(expiration)} diff --git a/apps/dashboard/components/api-keys/revoke.tsx b/apps/dashboard/components/api-keys/revoke.tsx index 3df32ca0ff..0d09686fcc 100644 --- a/apps/dashboard/components/api-keys/revoke.tsx +++ b/apps/dashboard/components/api-keys/revoke.tsx @@ -1,14 +1,17 @@ "use client" - +import ReportProblemRoundedIcon from "@mui/icons-material/ReportProblemRounded" import { Box, Button, Modal, ModalClose, Sheet, Typography } from "@mui/joy" import React, { useState } from "react" +import { revokeApiKeyServerAction } from "@/app/api-keys/server-actions" + type Props = { id: string } const RevokeKey = ({ id }: Props) => { - const [open, setOpen] = useState(false) + const [open, setOpen] = useState(false) + const [loading, setLoading] = useState(false) return ( <> @@ -23,14 +26,14 @@ const RevokeKey = ({ id }: Props) => { @@ -41,6 +44,11 @@ const RevokeKey = ({ id }: Props) => { alignItems: "center", }} > + { - - WARNING! You will no longer be able to authenticate with this API Key. - - - - API Key ID: {id} + + You will no longer be able to authenticate with this API Key. + + + API Key ID + + + {id} + +