| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- 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<JWTAlgorithm>('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 (
- <>
- <DialogHeader>
- <DialogTitle>Create a new Standby Key</DialogTitle>
- </DialogHeader>
- <DialogSectionSeparator />
- <DialogSection className="space-y-4">
- <p className="text-sm text-foreground-light">
- 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.
- <br />
- <br />
- This action does not invalidate existing tokens, so your users remain signed in.
- </p>
- </DialogSection>
- <DialogSectionSeparator />
- <DialogSection className="flex flex-col gap-4">
- <div className="flex flex-col gap-4">
- <Label htmlFor="algorithm">Choose signing algorithm:</Label>
- <Select
- name="algorithm"
- value={newKeyAlgorithm}
- onValueChange={(value: JWTAlgorithm) => setNewKeyAlgorithm(value)}
- >
- <SelectTrigger id="algorithm">
- <SelectValue placeholder="Select algorithm" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="ES256">
- <span>ES256 (ECC)</span>
- <Badge variant="success" className="ml-2">
- Recommended
- </Badge>
- </SelectItem>
- <SelectItem value="RS256">RS256 (RSA)</SelectItem>
- <SelectItem value="HS256">HS256 (Shared Secret)</SelectItem>
- </SelectContent>
- </Select>
- </div>
- <div className="flex flex-col gap-4">
- <Label htmlFor="byok" className="flex items-center gap-x-2">
- <Checkbox id="byok" checked={isBYOK} onCheckedChange={(value) => setBYOK(!!value)} />
- {newKeyAlgorithm === 'HS256'
- ? 'Import an existing secret'
- : 'Import an existing private key'}
- </Label>
- {isBYOK && (
- <div className="flex flex-col gap-2">
- <Textarea
- className="font-mono"
- placeholder={
- newKeyAlgorithm === 'HS256'
- ? 'Type in your JWT secret'
- : 'Add a private key in JWK (JSON Web Key) format'
- }
- value={privateKey}
- onChange={(e: any) => {
- setPrivateKey(e.target.value)
- }}
- autoComplete="off"
- autoCorrect="off"
- autoCapitalize="off"
- spellCheck="false"
- />
- {privateKeyMessage && <p className="text-red-900 text-sm">{privateKeyMessage}</p>}
- </div>
- )}
- {isBYOK && newKeyAlgorithm === 'HS256' && (
- <>
- <Label htmlFor="base64" className="flex items-center gap-x-2">
- <Checkbox
- id="base64"
- checked={isBase64}
- onCheckedChange={(value) => setBase64(!!value)}
- />
- Secret is already Base64 encoded
- </Label>
- </>
- )}
- </div>
- </DialogSection>
- <DialogFooter>
- <Button
- onClick={() => handleAddNewStandbyKey()}
- disabled={isPendingMutation || !!privateKeyMessage}
- loading={isPendingMutation}
- >
- Create standby key
- </Button>
- </DialogFooter>
- </>
- )
- }
|