import dayjs from 'dayjs' import { useMemo, useState } from 'react' import { toast } from 'sonner' import { Badge, Button, Checkbox, DialogFooter, DialogHeader, DialogSection, DialogSectionSeparator, DialogTitle, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from 'ui' import { useJWTSigningKeyCreateMutation } from '@/data/jwt-signing-keys/jwt-signing-key-create-mutation' import { JWTAlgorithm } from '@/data/jwt-signing-keys/jwt-signing-keys-query' import { stringToBase64URL } from '@/lib/base64url' const RSA_JWK_REQUIRED_PROPERTIES = ['kty', 'n', 'e', 'p', 'q', 'd', 'dq', 'dp', 'qi'] const EC_JWK_REQUIRED_PROPERTIES = ['kty', 'crv', 'x', 'y', 'd'] export const CreateKeyDialog = ({ projectRef, onClose, }: { projectRef: string onClose: () => void }) => { const [newKeyAlgorithm, setNewKeyAlgorithm] = useState('ES256') const [isBYOK, setBYOK] = useState(false) const [privateKey, setPrivateKey] = useState('') const [isBase64, setBase64] = useState(false) const privateKeyMessage = useMemo(() => { const plain = privateKey.replace(/\s+/g, '') if (!plain) { return null } if (newKeyAlgorithm === 'HS256') { if (privateKey.length < 16) { return 'Secret must be at least 16 letters long' } return null } let jwk try { jwk = JSON.parse(privateKey) } catch (e: any) { return 'Private key is not valid JSON' } if (typeof jwk !== 'object' || !jwk) { return 'Private key must be a JSON object' } if (typeof jwk.kty !== 'string' || !jwk.kty) { return 'Private key must have a kty property' } if (newKeyAlgorithm === 'RS256') { if (jwk.kty !== 'RSA') { return 'Private key must be of RSA type' } if (jwk.e !== 'AQAB') { return 'RSA private keys must use the 65537 (AQAB) public exponent' } for (let prop of RSA_JWK_REQUIRED_PROPERTIES) { if (typeof jwk[prop] !== 'string' || !jwk[prop]) { return `Incomplete RSA private key, required properties are: ${RSA_JWK_REQUIRED_PROPERTIES.join(', ')}` } } } else if (newKeyAlgorithm === 'ES256') { if (jwk.kty !== 'EC') { return 'Private key must be of EC type' } if (jwk.crv !== 'P-256') { return 'EC private keys must use P-256 curve' } for (let prop of EC_JWK_REQUIRED_PROPERTIES) { if (typeof jwk[prop] !== 'string' || !jwk[prop]) { return `Incomplete EC private key, required properties are: ${EC_JWK_REQUIRED_PROPERTIES.join(', ')}` } } } return null }, [privateKey, newKeyAlgorithm]) const { mutate, isPending: isPendingMutation } = useJWTSigningKeyCreateMutation({ onSuccess: () => { toast.success('Standby key created successfully') onClose() }, onError: (error) => { let errorMessage = error.message if (errorMessage.includes('Please wait until')) { const dateString = errorMessage .replace('Please wait until ', '') .replace('before attempting this request again.', '') .trim() const date = dayjs(dateString) if (date.isValid()) { errorMessage = `Please wait for ${date.fromNow(true)} before attempting this request again.` } } toast.error(`Failed to add new standby key: ${errorMessage}`) }, }) const handleAddNewStandbyKey = async () => { mutate({ projectRef: projectRef!, algorithm: newKeyAlgorithm, status: 'standby', private_jwk: isBYOK ? newKeyAlgorithm === 'HS256' ? { kty: 'oct', k: isBase64 ? privateKey .replace(/\s+/g, '') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, '') : stringToBase64URL(privateKey), } : JSON.parse(privateKey) : null, }) } return ( <> Create a new Standby Key

Adds a new JSON Web Token signing key. Once all of your application's components have picked it up you can rotate the current key with it.

This action does not invalidate existing tokens, so your users remain signed in.

{isBYOK && (