From 689a897fc304fa8ba6279abffd3f32179d28ba89 Mon Sep 17 00:00:00 2001 From: Mish Ushakov Date: Sat, 23 Nov 2024 00:02:26 +0100 Subject: [PATCH 1/7] updated api key management --- .../src/app/(dashboard)/dashboard/page.tsx | 2 +- apps/web/src/components/Dashboard/Keys.tsx | 430 ++++++++++++++---- 2 files changed, 335 insertions(+), 97 deletions(-) diff --git a/apps/web/src/app/(dashboard)/dashboard/page.tsx b/apps/web/src/app/(dashboard)/dashboard/page.tsx index cefe1d8de..8931908e6 100644 --- a/apps/web/src/app/(dashboard)/dashboard/page.tsx +++ b/apps/web/src/app/(dashboard)/dashboard/page.tsx @@ -250,7 +250,7 @@ function MainContent({ case 'personal': return case 'keys': - return + return case 'sandboxes': return case 'templates': diff --git a/apps/web/src/components/Dashboard/Keys.tsx b/apps/web/src/components/Dashboard/Keys.tsx index de8ed38c8..fcfd086b0 100644 --- a/apps/web/src/components/Dashboard/Keys.tsx +++ b/apps/web/src/components/Dashboard/Keys.tsx @@ -1,8 +1,7 @@ import { Button } from '../Button' - +import { Button as AlertDialogAction } from '../ui/button' import { AlertDialog, - AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, @@ -11,32 +10,82 @@ import { AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' -import { Copy } from 'lucide-react' +import { Edit, MoreVertical, Trash } from 'lucide-react' import { useEffect, useState } from 'react' import { useToast } from '../ui/use-toast' -import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs' -import { Team } from '@/utils/useUser' +import { E2BUser, Team } from '@/utils/useUser' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '../ui/table' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '../ui/dropdown-menu' -export const KeysContent = ({ currentTeam }: { currentTeam: Team }) => { - const supabase = createPagesBrowserClient() +type TeamApiKey = { + id: string + value: string | null + maskedValue: string + name: string + createdBy: { + email: string + id: string + } | null + createdAt: string + lastUsed: string | null + updatedAt: string | null +} +export const KeysContent = ({ + currentTeam, + user, +}: { + currentTeam: Team + user: E2BUser +}) => { const { toast } = useToast() - const [isDialogOpen, setIsDialogOpen] = useState(false) - const [currentKey, setCurrentKey] = useState(null) - const [hoveredKey, setHoveredKey] = useState(null) - const [apiKeys, setApiKeys] = useState([]) + const [isKeyDialogOpen, setIsKeyDialogOpen] = useState(false) + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) + const [isKeyPreviewDialogOpen, setIsKeyPreviewDialogOpen] = useState(false) + const [currentKey, setCurrentKey] = useState(null) + const [newApiKeyInput, setNewApiKeyInput] = useState('') + const [apiKeys, setApiKeys] = useState([]) useEffect(() => { - setApiKeys(currentTeam.apiKeys) - }, [currentTeam]) + async function fetchApiKeys() { + const res = await fetch( + `${process.env.NEXT_PUBLIC_BILLING_API_URL}/teams/${currentTeam.id}/api-keys`, + { + headers: { + 'X-USER-ACCESS-TOKEN': user.accessToken, + }, + } + ) - const closeDialog = () => setIsDialogOpen(false) - const openDialog = (key: string) => { - setCurrentKey(key) - setIsDialogOpen(true) - } + if (!res.ok) { + toast({ + title: 'An error occurred', + description: 'We were unable to fetch the team API keys', + }) + console.log(res.statusText) + return + } + + const keys = await res.json() + setApiKeys(keys) + } + + fetchApiKeys() + }, [currentTeam]) - const deleteApiKey = async () => { + async function deleteApiKey() { if (apiKeys.length === 1) { toast({ title: 'Cannot delete the last API key', @@ -45,130 +94,319 @@ export const KeysContent = ({ currentTeam }: { currentTeam: Team }) => { return } - const { error } = await supabase - .from('team_api_keys') - .delete() - .eq('api_key', currentKey) - - if (error) { - // TODO: Add sentry event here - toast( - { - title: 'An error occurred', - description: 'We were unable to delete the API key', - + const res = await fetch( + `${process.env.NEXT_PUBLIC_BILLING_API_URL}/teams/${currentTeam.id}/api-keys/${currentKey?.id}`, + { + method: 'DELETE', + headers: { + 'X-USER-ACCESS-TOKEN': user.accessToken, }, - ) - console.log(error) - return - } + } + ) - setApiKeys(apiKeys.filter(apiKey => apiKey !== currentKey)) - closeDialog() - } - - const addApiKey = async () => { - if (currentTeam === null) { + if (!res.ok) { toast({ title: 'An error occurred', - description: 'We were unable to create the API key', + description: 'We were unable to delete the API key', }) } - const res = await fetch(`${process.env.NEXT_PUBLIC_BILLING_API_URL}/teams/${currentTeam.id}/api-keys`, { - method: 'POST', - headers: { - 'X-Team-API-Key': currentTeam.apiKeys[0], - }, - }) + setApiKeys(apiKeys.filter((apiKey) => apiKey.id !== currentKey?.id)) + setCurrentKey(null) + setIsDeleteDialogOpen(false) + } + + async function createApiKey() { + const res = await fetch( + `${process.env.NEXT_PUBLIC_BILLING_API_URL}/teams/${currentTeam.id}/api-keys`, + { + method: 'POST', + headers: { + 'X-USER-ACCESS-TOKEN': user.accessToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: newApiKeyInput, + }), + } + ) if (!res.ok) { toast({ title: 'An error occurred', description: 'We were unable to create the API key', }) - console.log(res.status, res.statusText) - // TODO: Add sentry event here + return } const newKey = await res.json() + setApiKeys([...apiKeys, newKey]) + setNewApiKeyInput('') + setCurrentKey(newKey) + setIsKeyPreviewDialogOpen(true) + } + + async function updateApiKey() { + const res = await fetch( + `${process.env.NEXT_PUBLIC_BILLING_API_URL}/teams/${currentTeam.id}/api-keys/${currentKey?.id}`, + { + method: 'PATCH', + headers: { + 'X-USER-ACCESS-TOKEN': user.accessToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: newApiKeyInput, + }), + } + ) + + if (!res.ok) { + toast({ + title: 'An error occurred', + description: 'We were unable to update the API key', + }) + } + toast({ - title: 'API key created', + title: 'API key updated', }) - setApiKeys([...apiKeys, newKey.apiKey]) + setApiKeys( + apiKeys.map((apiKey) => + apiKey.id === currentKey?.id + ? { + ...apiKey, + name: newApiKeyInput, + } + : apiKey + ) + ) + + setNewApiKeyInput('') + setCurrentKey(null) + setIsKeyDialogOpen(false) } - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text) - toast({ - title: 'API Key copied to clipboard', + function sortApiKeys(apiKeys: TeamApiKey[]) { + return apiKeys.sort((a, b) => { + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() }) } - const maskApiKey = (key: string) => { - const firstFour = key.slice(0, 4) - const lastFour = key.slice(-4) - const stars = '*'.repeat(key.length - 8) // use fixed-width character - return `${firstFour}${stars}${lastFour}` + function copyApiKey() { + navigator.clipboard.writeText( + currentKey?.value || currentKey?.maskedValue || '' + ) + + toast({ + title: 'Copied API key to clipboard', + }) } return (
-
- -
- -
+
+ +
-

Active keys

+ + + + Name + Key + Created by + Created at + + + + + {sortApiKeys(apiKeys).map((apiKey, index) => ( + + {apiKey.name} + {apiKey.maskedValue} + {apiKey.createdBy?.email} + + {new Date(apiKey.createdAt).toLocaleString('en-UK', { + timeZoneName: 'short', + })} + + + + + + + + { + setNewApiKeyInput(apiKey.name) + setCurrentKey(apiKey) + setIsKeyDialogOpen(true) + }} + > + + Edit + + { + setCurrentKey(apiKey) + setIsDeleteDialogOpen(true) + }} + > + + Delete + + + + + + ))} + +
- {apiKeys.map((apiKey, index) => ( -
+ + + + You are about to {currentKey ? 'edit' : 'create'} an API key + + +
{ + e.preventDefault() + currentKey ? updateApiKey() : createApiKey() + setIsKeyDialogOpen(false) + }} > -
setHoveredKey(apiKey)} - onMouseLeave={() => setHoveredKey(null)} - > - {hoveredKey === apiKey ? apiKey : maskApiKey(apiKey)} -
+ { + setNewApiKeyInput(e.target.value) + }} + autoFocus + required + /> -
- copyToClipboard(apiKey)} /> - -
-
- ))} + + { + setIsKeyDialogOpen(false) + setCurrentKey(null) + setNewApiKeyInput('') + }} + > + Cancel + + + {currentKey ? 'Update' : 'Create'} + + + + + + + + + Your API key + + You will only see the API key once. Make sure to copy it now. + + +
+ -
+ + { + setIsKeyPreviewDialogOpen(false) + setCurrentKey(null) + }} + > + Cancel + + { + copyApiKey() + setIsKeyPreviewDialogOpen(false) + }} + > + Copy + + + + + - + - + - You are about to delete an API key - - This action cannot be undone. This will permanently delete the - API key with immediate effect. + + You are about to delete an API key + + + This action cannot be undone. This will permanently delete the API + key with immediate effect. - Cancel - Continue + { + setIsDeleteDialogOpen(false) + setCurrentKey(null) + }} + > + Cancel + + + Delete + -
) } From 1afb64a5ab10fbaf3737b402db87163a45d538d1 Mon Sep 17 00:00:00 2001 From: Mish Ushakov Date: Sat, 23 Nov 2024 00:42:42 +0100 Subject: [PATCH 2/7] sort out the Default api key (legacy) --- apps/web/src/components/Dashboard/Keys.tsx | 104 ++++++++++++--------- 1 file changed, 58 insertions(+), 46 deletions(-) diff --git a/apps/web/src/components/Dashboard/Keys.tsx b/apps/web/src/components/Dashboard/Keys.tsx index fcfd086b0..88c37d76e 100644 --- a/apps/web/src/components/Dashboard/Keys.tsx +++ b/apps/web/src/components/Dashboard/Keys.tsx @@ -11,7 +11,7 @@ import { AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { Edit, MoreVertical, Trash } from 'lucide-react' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useToast } from '../ui/use-toast' import { E2BUser, Team } from '@/utils/useUser' import { @@ -190,11 +190,12 @@ export const KeysContent = ({ setIsKeyDialogOpen(false) } - function sortApiKeys(apiKeys: TeamApiKey[]) { - return apiKeys.sort((a, b) => { + const sortedApiKeys = useMemo(() => { + const [first, ...rest] = apiKeys + return rest.sort((a, b) => { return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() }) - } + }, [apiKeys]) function copyApiKey() { navigator.clipboard.writeText( @@ -232,50 +233,61 @@ export const KeysContent = ({ - {sortApiKeys(apiKeys).map((apiKey, index) => ( - - {apiKey.name} - {apiKey.maskedValue} - {apiKey.createdBy?.email} - - {new Date(apiKey.createdAt).toLocaleString('en-UK', { - timeZoneName: 'short', - })} - - - - - - - - { - setNewApiKeyInput(apiKey.name) - setCurrentKey(apiKey) - setIsKeyDialogOpen(true) - }} - > - - Edit - - { - setCurrentKey(apiKey) - setIsDeleteDialogOpen(true) - }} - > - - Delete - - - + {sortedApiKeys.length === 0 ? ( + + + Click on 'Add API Key' button above to create your first API + key. - ))} + ) : ( + sortedApiKeys.map((apiKey, index) => ( + + {apiKey.name} + + {apiKey.maskedValue} + + {apiKey.createdBy?.email} + + {new Date(apiKey.createdAt).toLocaleString('en-UK', { + timeZoneName: 'short', + })} + + + + + + + + { + setNewApiKeyInput(apiKey.name) + setCurrentKey(apiKey) + setIsKeyDialogOpen(true) + }} + > + + Edit + + { + setCurrentKey(apiKey) + setIsDeleteDialogOpen(true) + }} + > + + Delete + + + + + + )) + )} From 051119676a30ac70a26e3c340f1f0f43f8e06487 Mon Sep 17 00:00:00 2001 From: Mish Ushakov Date: Sat, 23 Nov 2024 00:46:17 +0100 Subject: [PATCH 3/7] added comment and resolved build error --- apps/web/src/components/Dashboard/Keys.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/Dashboard/Keys.tsx b/apps/web/src/components/Dashboard/Keys.tsx index 88c37d76e..54bec2451 100644 --- a/apps/web/src/components/Dashboard/Keys.tsx +++ b/apps/web/src/components/Dashboard/Keys.tsx @@ -190,8 +190,9 @@ export const KeysContent = ({ setIsKeyDialogOpen(false) } + // remove the first API key from the list (for legacy reasons) and sort the rest by creation date const sortedApiKeys = useMemo(() => { - const [first, ...rest] = apiKeys + const [_first, ...rest] = apiKeys return rest.sort((a, b) => { return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() }) From 647cd44916c48b9865a13d082f23ee6aff414888 Mon Sep 17 00:00:00 2001 From: Mish Ushakov Date: Sat, 23 Nov 2024 00:58:53 +0100 Subject: [PATCH 4/7] fixes build, sorry --- apps/web/src/components/Dashboard/Keys.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/Dashboard/Keys.tsx b/apps/web/src/components/Dashboard/Keys.tsx index 54bec2451..4c1c9d522 100644 --- a/apps/web/src/components/Dashboard/Keys.tsx +++ b/apps/web/src/components/Dashboard/Keys.tsx @@ -192,7 +192,7 @@ export const KeysContent = ({ // remove the first API key from the list (for legacy reasons) and sort the rest by creation date const sortedApiKeys = useMemo(() => { - const [_first, ...rest] = apiKeys + const rest = apiKeys.slice(1) return rest.sort((a, b) => { return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() }) From a7dfc893fe065cb7eb64de6e9de58cbe4bf4123b Mon Sep 17 00:00:00 2001 From: Mish Ushakov Date: Sat, 23 Nov 2024 01:03:14 +0100 Subject: [PATCH 5/7] escaped quotes --- apps/web/src/components/Dashboard/Keys.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/Dashboard/Keys.tsx b/apps/web/src/components/Dashboard/Keys.tsx index 4c1c9d522..4bcd51cbe 100644 --- a/apps/web/src/components/Dashboard/Keys.tsx +++ b/apps/web/src/components/Dashboard/Keys.tsx @@ -237,8 +237,8 @@ export const KeysContent = ({ {sortedApiKeys.length === 0 ? ( - Click on 'Add API Key' button above to create your first API - key. + Click on "Add API Key" button above to create your + first API key. ) : ( From b5e6894c128eca3d218f8caad97d2d26ed09a2cf Mon Sep 17 00:00:00 2001 From: Mish Ushakov Date: Mon, 25 Nov 2024 13:55:28 +0100 Subject: [PATCH 6/7] undo last change to api keys to avoid confusion --- apps/web/src/components/Dashboard/Keys.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web/src/components/Dashboard/Keys.tsx b/apps/web/src/components/Dashboard/Keys.tsx index 4bcd51cbe..8db103ca0 100644 --- a/apps/web/src/components/Dashboard/Keys.tsx +++ b/apps/web/src/components/Dashboard/Keys.tsx @@ -190,10 +190,8 @@ export const KeysContent = ({ setIsKeyDialogOpen(false) } - // remove the first API key from the list (for legacy reasons) and sort the rest by creation date const sortedApiKeys = useMemo(() => { - const rest = apiKeys.slice(1) - return rest.sort((a, b) => { + return apiKeys.sort((a, b) => { return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() }) }, [apiKeys]) From fa84b8278ed13d0f9ba29e148d0d62485195e9fb Mon Sep 17 00:00:00 2001 From: Mish Ushakov Date: Mon, 25 Nov 2024 20:48:09 +0100 Subject: [PATCH 7/7] changed date formatting --- apps/web/src/components/Dashboard/Keys.tsx | 4 +--- apps/web/src/components/Dashboard/Sandboxes.tsx | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/Dashboard/Keys.tsx b/apps/web/src/components/Dashboard/Keys.tsx index 8db103ca0..95fecbcbd 100644 --- a/apps/web/src/components/Dashboard/Keys.tsx +++ b/apps/web/src/components/Dashboard/Keys.tsx @@ -251,9 +251,7 @@ export const KeysContent = ({ {apiKey.createdBy?.email} - {new Date(apiKey.createdAt).toLocaleString('en-UK', { - timeZoneName: 'short', - })} + {new Date(apiKey.createdAt).toLocaleString()} diff --git a/apps/web/src/components/Dashboard/Sandboxes.tsx b/apps/web/src/components/Dashboard/Sandboxes.tsx index 4ba410f9a..b57f5a23b 100644 --- a/apps/web/src/components/Dashboard/Sandboxes.tsx +++ b/apps/web/src/components/Dashboard/Sandboxes.tsx @@ -78,14 +78,10 @@ export function SandboxesContent({ team }: { team: Team }) { {sandbox.templateID} {sandbox.alias} - {new Date(sandbox.startedAt).toLocaleString('en-UK', { - timeZoneName: 'short', - })} + {new Date(sandbox.startedAt).toLocaleString()} - {new Date(sandbox.endAt).toLocaleString('en-UK', { - timeZoneName: 'short', - })} + {new Date(sandbox.endAt).toLocaleString()} {sandbox.cpuCount} {sandbox.memoryMB}