From fe320d418d8b2931c2b934ba287985ccb7487a36 Mon Sep 17 00:00:00 2001 From: poswalsameer Date: Mon, 10 Feb 2025 00:48:48 +0530 Subject: [PATCH] feat: create, show, and delete API Keys --- .../(settings)/settings/@profile/page.tsx | 67 +++++- apps/platform/src/components/common/slug.tsx | 2 +- .../apiKeys/addApiKeyDialog/index.tsx | 192 ++++++++++++++++++ .../userProfile/apiKeys/apiKeyCard/index.tsx | 80 ++++++++ .../apiKeys/confirmDeleteApiKey/index.tsx | 140 +++++++++++++ apps/platform/src/lib/controller-instance.ts | 11 +- apps/platform/src/store/index.ts | 7 + packages/api-client/src/index.ts | 4 +- 8 files changed, 495 insertions(+), 8 deletions(-) create mode 100644 apps/platform/src/components/userProfile/apiKeys/addApiKeyDialog/index.tsx create mode 100644 apps/platform/src/components/userProfile/apiKeys/apiKeyCard/index.tsx create mode 100644 apps/platform/src/components/userProfile/apiKeys/confirmDeleteApiKey/index.tsx diff --git a/apps/platform/src/app/(main)/(settings)/settings/@profile/page.tsx b/apps/platform/src/app/(main)/(settings)/settings/@profile/page.tsx index ffcf3af8..133c2956 100644 --- a/apps/platform/src/app/(main)/(settings)/settings/@profile/page.tsx +++ b/apps/platform/src/app/(main)/(settings)/settings/@profile/page.tsx @@ -1,16 +1,28 @@ 'use client' -import React, { useCallback, useState } from 'react' +import React, { useCallback, useState, useEffect } from 'react' import { toast } from 'sonner' -import { useAtom } from 'jotai' +import { useAtom, useAtomValue } from 'jotai' import InputLoading from './loading' import { Input } from '@/components/ui/input' import { Separator } from '@/components/ui/separator' import ControllerInstance from '@/lib/controller-instance' import { Button } from '@/components/ui/button' -import { userAtom } from '@/store' +import { + userAtom, + apiKeysOfProjectAtom, + deleteApiKeyOpenAtom, + selectedApiKeyAtom +} from '@/store' +import { useSearchParams } from 'next/navigation' +import AddApiKeyDialog from '@/components/userProfile/apiKeys/addApiKeyDialog' +import ApiKeyCard from '@/components/userProfile/apiKeys/apiKeyCard' +import ConfirmDeleteApiKey from '@/components/userProfile/apiKeys/confirmDeleteApiKey' function ProfilePage(): React.JSX.Element { const [user, setUser] = useAtom(userAtom) + const [apiKeys, setApiKeys] = useAtom(apiKeysOfProjectAtom) + const isDeleteApiKeyOpen = useAtomValue(deleteApiKeyOpenAtom) + const selectedApiKey = useAtomValue(selectedApiKeyAtom) const [isLoading, setIsLoading] = useState(true) const [userData, setUserData] = useState({ @@ -20,6 +32,9 @@ function ProfilePage(): React.JSX.Element { }) const [isModified, setIsModified] = useState(false) + const searchParams = useSearchParams() + const tab = searchParams.get('profile') ?? 'profile' + const updateSelf = useCallback(async () => { toast.loading('Updating profile...') setIsLoading(true) @@ -63,6 +78,33 @@ function ProfilePage(): React.JSX.Element { setIsLoading(false) }, [userData.name, userData.email, user?.name, user?.email, setUser]) + useEffect(() => { + const getAllApiKeys = async () => { + const { success, error, data } = await ControllerInstance.getInstance().apiKeyController.getApiKeysOfUser( + {}, + {} + ) + + if (success && data) { + setApiKeys(data.items) + } + if (error) { + toast.error('Something went wrong!', { + description: ( +

+ Something went wrong while fetching API Keys. Check console for + more info. +

+ ) + }) + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) + } + } + + getAllApiKeys() + }, [setApiKeys]) + return (
{/* Avatar */} @@ -124,15 +166,30 @@ function ProfilePage(): React.JSX.Element { Save Changes - -
+ +
API Keys
Generate new API keys to use with the Keyshade CLI.
+
+ {tab === 'profile' && } +
+ {apiKeys.length !== 0 && +
+ {apiKeys.map((apiKey) => ( + + ))} + + {/* Delete API Key alert dialog */} + {isDeleteApiKeyOpen && selectedApiKey ? ( + + ) : null} +
+ } diff --git a/apps/platform/src/components/common/slug.tsx b/apps/platform/src/components/common/slug.tsx index db2b080f..a63b0b48 100644 --- a/apps/platform/src/components/common/slug.tsx +++ b/apps/platform/src/components/common/slug.tsx @@ -23,7 +23,7 @@ export default function Slug({ text }: SlugProps): React.JSX.Element { return ( + + + + + Add a new API Key + + + Add a new API key to the project + + + +
+
+
+ + + setNewApiKeyData({ + ...newApiKeyData, + apiKeyName: e.target.value + }) + } + placeholder="Enter the API key" + value={newApiKeyData.apiKeyName} + /> +
+ +
+ + +
+ +
+ +
+
+
+
+ + ) +} diff --git a/apps/platform/src/components/userProfile/apiKeys/apiKeyCard/index.tsx b/apps/platform/src/components/userProfile/apiKeys/apiKeyCard/index.tsx new file mode 100644 index 00000000..d9a62f00 --- /dev/null +++ b/apps/platform/src/components/userProfile/apiKeys/apiKeyCard/index.tsx @@ -0,0 +1,80 @@ +'use client' + +import type { ApiKey } from '@keyshade/schema' +import dayjs from 'dayjs' +import { useSetAtom } from 'jotai' +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger +} from '@/components/ui/context-menu' +import { + selectedApiKeyAtom, + editApiKeyOpenAtom, + deleteApiKeyOpenAtom +} from '@/store' +import Slug from '@/components/common/slug' + +const formatDate = (date: string): string => { + return dayjs(date).format('D MMMM, YYYY') +} + +export default function ApiKeyCard({ + apiKey +}: { + apiKey: ApiKey +}): React.JSX.Element { + const setSelectedApiKey = useSetAtom(selectedApiKeyAtom) + const setIsEditApiKeyOpen = useSetAtom(editApiKeyOpenAtom) + const setIsDeleteApiKeyOpen = useSetAtom(deleteApiKeyOpenAtom) + + const handleDeleteClick = () => { + setSelectedApiKey(apiKey) + setIsDeleteApiKeyOpen(true) + } + + return ( + + +
+
+
+
{apiKey.name}
+
+
+
+
+ +
+
+
Expiring in
+
+ {apiKey.expiresAt ? ( + dayjs(apiKey.expiresAt).diff(dayjs(), "day") >= 1 ? ( + `${dayjs(apiKey.expiresAt).diff(dayjs(), "day")} days` + ) : ( + `${dayjs(apiKey.expiresAt).diff(dayjs(), "hour")} hours` + ) + ) : "Never"} +
+
+
+
+
+ + + Edit + + + Delete + + +
+ ) +} diff --git a/apps/platform/src/components/userProfile/apiKeys/confirmDeleteApiKey/index.tsx b/apps/platform/src/components/userProfile/apiKeys/confirmDeleteApiKey/index.tsx new file mode 100644 index 00000000..2fc7e1f6 --- /dev/null +++ b/apps/platform/src/components/userProfile/apiKeys/confirmDeleteApiKey/index.tsx @@ -0,0 +1,140 @@ +'use client' + +import React, { useState, useCallback, useEffect } from 'react' +import { TrashSVG } from '@public/svg/shared' +import { toast } from 'sonner' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from '@/components/ui/alert-dialog' +import ControllerInstance from '@/lib/controller-instance' +import { + deleteApiKeyOpenAtom, + apiKeysOfProjectAtom, + selectedApiKeyAtom +} from '@/store' + +export default function ConfirmDeleteApiKey(): React.JSX.Element { + const [isLoading, setIsLoading] = useState(false) + const selectedApiKey = useAtomValue(selectedApiKeyAtom) + const [isDeleteApiKeyOpen, setIsDeleteApiKeyOpen] = useAtom(deleteApiKeyOpenAtom) + const setApiKeys = useSetAtom(apiKeysOfProjectAtom) + + const handleClose = useCallback(() => { + setIsDeleteApiKeyOpen(false) + }, [setIsDeleteApiKeyOpen]) + + const deleteApiKey = useCallback(async () => { + setIsLoading(true) + + if (selectedApiKey === null) { + toast.error('No API Key selected', { + description: ( +

+ No API Key selected. Please select an API Key. +

+ ) + }) + return + } + + const apiKeySlug = selectedApiKey.slug + + toast.loading("Deleting your API Key...") + const { success, error } = + await ControllerInstance.getInstance().apiKeyController.deleteApiKey( + { apiKeySlug: apiKeySlug }, + {} + ) + + toast.dismiss() + if (success) { + toast.success('API Key deleted successfully', { + description: ( +

+ The API Key has been deleted. +

+ ) + }) + + // Remove the API Key from the store + setApiKeys((prevApiKeys) => + prevApiKeys.filter( + (apiKey) => apiKey.slug !== apiKeySlug + ) + ) + } + if (error) { + toast.dismiss() + toast.error('Something went wrong!', { + description: ( +

+ Something went wrong while deleting the API Key. Check console for more info. +

+ ) + }) + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) + } + + handleClose() + setIsLoading(false) + }, [setApiKeys, selectedApiKey, handleClose]) + + //Cleaning the pointer events for the context menu after closing the alert dialog + const cleanup = useCallback(() => { + document.body.style.pointerEvents = '' + document.documentElement.style.pointerEvents = '' + }, []) + + useEffect(() => { + if (!isDeleteApiKeyOpen) { + cleanup() + } + return () => cleanup() + }, [isDeleteApiKeyOpen, cleanup]) + + return ( + + + +
+ + + Do you want to delete this API? + +
+ + This action cannot be undone. This will permanently delete your API and remove your API key data from our servers. + +
+ + + Cancel + + + Yes, delete {selectedApiKey ? selectedApiKey.name : "this API Key" } + + +
+
+ ) +} diff --git a/apps/platform/src/lib/controller-instance.ts b/apps/platform/src/lib/controller-instance.ts index a766038f..640eab0d 100644 --- a/apps/platform/src/lib/controller-instance.ts +++ b/apps/platform/src/lib/controller-instance.ts @@ -7,7 +7,8 @@ import { VariableController, WorkspaceController, WorkspaceMembershipController, - WorkspaceRoleController + WorkspaceRoleController, + ApiKeyController } from '@keyshade/api-client' export default class ControllerInstance { @@ -22,6 +23,7 @@ export default class ControllerInstance { private _environmentController: EnvironmentController private _secretController: SecretController private _variableController: VariableController + private _apiKeyController: ApiKeyController get authController(): AuthController { return this._authController @@ -59,6 +61,10 @@ export default class ControllerInstance { return this._userController } + get apiKeyController(): ApiKeyController { + return this._apiKeyController + } + static getInstance(): ControllerInstance { if (!ControllerInstance.instance) { ControllerInstance.instance = new ControllerInstance() @@ -85,6 +91,9 @@ export default class ControllerInstance { ControllerInstance.instance._variableController = new VariableController( process.env.NEXT_PUBLIC_BACKEND_URL ) + ControllerInstance.instance._apiKeyController = new ApiKeyController( + process.env.NEXT_PUBLIC_BACKEND_URL + ) } return ControllerInstance.instance } diff --git a/apps/platform/src/store/index.ts b/apps/platform/src/store/index.ts index e4d6c0c2..0ee01ef9 100644 --- a/apps/platform/src/store/index.ts +++ b/apps/platform/src/store/index.ts @@ -1,6 +1,7 @@ import { atom } from 'jotai' import { atomWithStorage } from 'jotai/utils' import type { + ApiKey, GetAllEnvironmentsOfProjectResponse, Project, ProjectWithCount, @@ -21,12 +22,14 @@ export const selectedSecretAtom = atom(null) export const selectedEnvironmentAtom = atom< GetAllEnvironmentsOfProjectResponse['items'][number] | null >(null) +export const selectedApiKeyAtom = atom(null) export const projectsOfWorkspaceAtom = atom([]) export const environmentsOfProjectAtom = atom< GetAllEnvironmentsOfProjectResponse['items'] >([]) export const variablesOfProjectAtom = atom([]) export const secretsOfProjectAtom = atom([]) +export const apiKeysOfProjectAtom = atom([]) export const createProjectOpenAtom = atom(false) export const editProjectOpenAtom = atom(false) @@ -43,3 +46,7 @@ export const deleteSecretOpenAtom = atom(false) export const createEnvironmentOpenAtom = atom(false) export const editEnvironmentOpenAtom = atom(false) export const deleteEnvironmentOpenAtom = atom(false) + +export const createApiKeyOpenAtom = atom(false) +export const editApiKeyOpenAtom = atom(false) +export const deleteApiKeyOpenAtom = atom(false) diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts index 1390c3f6..2bfd7463 100644 --- a/packages/api-client/src/index.ts +++ b/packages/api-client/src/index.ts @@ -10,6 +10,7 @@ import WorkspaceRoleController from '@api-client/controllers/workspace-role' import WorkspaceMembershipController from '@api-client/controllers/workspace-membership' import AuthController from '@api-client/controllers/auth' import UserController from '@api-client/controllers/user' +import ApiKeyController from './controllers/api-key' export { AppController, EnvironmentController, @@ -22,5 +23,6 @@ export { WorkspaceRoleController, WorkspaceMembershipController, AuthController, - UserController + UserController, + ApiKeyController }