From 515359c3bcdee513b7cf0941644f69cda0a65aaa Mon Sep 17 00:00:00 2001 From: Stojan Dimitrovski Date: Tue, 10 Jun 2025 16:36:34 +0200 Subject: [PATCH] fix: more api key ui fixes (#36300) --- .../interfaces/APIKeys/APIKeyDeleteDialog.tsx | 19 ++--- .../interfaces/APIKeys/APIKeyRow.tsx | 5 +- .../interfaces/APIKeys/ApiKeyPill.tsx | 2 +- .../APIKeys/CreateSecretAPIKeyDialog.tsx | 70 ++++++++++++++----- .../interfaces/APIKeys/SecretAPIKeys.tsx | 66 +++++++++++++---- .../data/api-keys/api-key-create-mutation.ts | 2 +- .../data/api-keys/api-key-delete-mutation.ts | 2 +- 7 files changed, 121 insertions(+), 45 deletions(-) diff --git a/apps/studio/components/interfaces/APIKeys/APIKeyDeleteDialog.tsx b/apps/studio/components/interfaces/APIKeys/APIKeyDeleteDialog.tsx index 5b27b54556d..90ed55186ba 100644 --- a/apps/studio/components/interfaces/APIKeys/APIKeyDeleteDialog.tsx +++ b/apps/studio/components/interfaces/APIKeys/APIKeyDeleteDialog.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import { toast } from 'sonner' +import { Trash2 } from 'lucide-react' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common/hooks' @@ -11,9 +12,10 @@ import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' interface APIKeyDeleteDialogProps { apiKey: Extract + lastSeen?: { timestamp: string } } -export const APIKeyDeleteDialog = ({ apiKey }: APIKeyDeleteDialogProps) => { +export const APIKeyDeleteDialog = ({ apiKey, lastSeen }: APIKeyDeleteDialogProps) => { const { ref: projectRef } = useParams() const [isOpen, setIsOpen] = useState(false) @@ -42,22 +44,23 @@ export const APIKeyDeleteDialog = ({ apiKey }: APIKeyDeleteDialogProps) => { } }} > - Delete API key + Delete API key setIsOpen(false)} onConfirm={onDeleteAPIKey} - title={`Delete ${apiKey.description ?? ''} API secret key`} - confirmString={apiKey.description || 'Delete API secret key'} - confirmLabel="Delete API secret key" - confirmPlaceholder="Type API key description to confirm" + title={`Delete ${apiKey.type} API key: ${apiKey.name}`} + confirmString={apiKey.name} + confirmLabel="Yes, irreversibly delete this API key" + confirmPlaceholder="Type the name of the API key to confirm" loading={isDeletingAPIKey} variant="destructive" alert={{ title: 'This cannot be undone', - description: - 'Deleting this API key will invalidate it immediately. Any applications using this key will no longer be able to access this project.', + description: lastSeen + ? `This API key was used ${lastSeen.timestamp}. Make sure all backend components using it have been updated. Deletion will cause them to receive HTTP 401 Unauthorized status codes on all Supabase APIs.` + : `This API key has not been used in the past 24 hours. Make sure you've updated all backend components using it before deletion.`, }} /> diff --git a/apps/studio/components/interfaces/APIKeys/APIKeyRow.tsx b/apps/studio/components/interfaces/APIKeys/APIKeyRow.tsx index fb2bde73338..2800dcda2d8 100644 --- a/apps/studio/components/interfaces/APIKeys/APIKeyRow.tsx +++ b/apps/studio/components/interfaces/APIKeys/APIKeyRow.tsx @@ -14,8 +14,10 @@ import { APIKeysData } from 'data/api-keys/api-keys-query' export const APIKeyRow = ({ apiKey, + lastSeen, }: { apiKey: Extract + lastSeen?: { timestamp: string } }) => { const MotionTableRow = motion(TableRow) @@ -39,6 +41,7 @@ export const APIKeyRow = ({ {apiKey.description || '/'} + {lastSeen?.timestamp ?? '/'} @@ -52,7 +55,7 @@ export const APIKeyRow = ({ /> - + diff --git a/apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx b/apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx index 9bf5647a460..a7f16a5c889 100644 --- a/apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx +++ b/apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx @@ -116,7 +116,7 @@ export function ApiKeyPill({ isSecret ? 'overflow-hidden' : '', show ? 'ring-1 ring-foreground-lighter ring-opacity-50' : 'ring-0 ring-opacity-0', 'transition-all', - 'max-w-[380px]', + 'max-w-[340px]', 'cursor-text', 'relative' )} diff --git a/apps/studio/components/interfaces/APIKeys/CreateSecretAPIKeyDialog.tsx b/apps/studio/components/interfaces/APIKeys/CreateSecretAPIKeyDialog.tsx index c7a00c370e1..73a41e8833e 100644 --- a/apps/studio/components/interfaces/APIKeys/CreateSecretAPIKeyDialog.tsx +++ b/apps/studio/components/interfaces/APIKeys/CreateSecretAPIKeyDialog.tsx @@ -16,18 +16,34 @@ import { FormField_Shadcn_, Form_Shadcn_, Input_Shadcn_, + Alert, + Alert_Shadcn_, + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import * as z from 'zod' +import { toast } from 'sonner' import { useParams } from 'common' import { useAPIKeyCreateMutation } from 'data/api-keys/api-key-create-mutation' -import { Plus } from 'lucide-react' +import { Plus, ShieldCheck } from 'lucide-react' + +const NAME_SCHEMA = z + .string() + .min(4, 'Name must be at least 4 characters') + .max(64, "Name can't be more than 64 characters long") + .regex(/^[a-z0-9_]+$/, 'Name can only contain lowercased letters, digits and underscore') + .refine((val: string) => !val.match(/^[0-9].+$/), 'Name must not start with a digit') + .refine( + (val: string) => val !== 'anon' && val !== 'service_role', + 'Using "anon" or "service_role" for API key name is not possible' + ) const FORM_ID = 'create-secret-api-key' const SCHEMA = z.object({ - name: z.string(), - description: z.string().trim(), + name: NAME_SCHEMA, + description: z.string().max(256, "Description shouldn't be too long").trim(), }) const CreateSecretAPIKeyDialog = () => { @@ -57,7 +73,8 @@ const CreateSecretAPIKeyDialog = () => { description: values.description, }, { - onSuccess: () => { + onSuccess: (data) => { + toast.success(`Your secret API key ${data.prefix}... is ready.`) onClose(false) }, } @@ -68,7 +85,7 @@ const CreateSecretAPIKeyDialog = () => { @@ -76,12 +93,8 @@ const CreateSecretAPIKeyDialog = () => { Create new secret API key

- Secret API keys are used to authorize requests to your project from servers, - functions, workers or other backend components of your application.{' '} -

- -

- Keep these keys private. Don't publish them online or commit them to source control. + Secret API keys allow elevated access to your project's data, bypassing Row-Level + security.

@@ -100,10 +113,10 @@ const CreateSecretAPIKeyDialog = () => { render={({ field }) => ( - + )} @@ -113,18 +126,39 @@ const CreateSecretAPIKeyDialog = () => { name="description" control={form.control} render={({ field }) => ( - + - + )} /> + + + Securing your API key + +
    +
  • Keep this key secret.
  • +
  • Do not use on the web, in mobile or desktop apps.
  • +
  • Don't post it publicly or commit in source control.
  • +
  • + This key provides elevated access to your data, bypassing Row-Level Security. +
  • +
  • + If it leaks or is revealed, swap it with a new secret API key and then delete it. +
  • +
  • + If used in a browser, it will always return HTTP 401 Unauthorized. Delete + immediately. +
  • +
+
+