jwt-settings.tsx695 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { PermissionAction } from '@supabase/shared-types/out/constants'
3import {
4 JwtSecretUpdateError,
5 JwtSecretUpdateProgress,
6 JwtSecretUpdateStatus,
7} from '@supabase/shared-types/out/events'
8import { useFlag, useParams } from 'common'
9import {
10 AlertCircle,
11 ChevronDown,
12 CloudOff,
13 ExternalLink,
14 Hourglass,
15 Key,
16 Lightbulb,
17 Loader2,
18 PenTool,
19 Power,
20 RefreshCw,
21 TriangleAlert,
22} from 'lucide-react'
23import Link from 'next/link'
24import { useEffect, useMemo, useState, type Dispatch, type SetStateAction } from 'react'
25import { useForm, type SubmitHandler } from 'react-hook-form'
26import { toast } from 'sonner'
27import {
28 Button,
29 Collapsible,
30 CollapsibleContent,
31 CollapsibleTrigger,
32 DropdownMenu,
33 DropdownMenuContent,
34 DropdownMenuItem,
35 DropdownMenuSeparator,
36 DropdownMenuTrigger,
37 Form,
38 FormControl,
39 FormField,
40 FormInputGroupInput,
41 InputGroup,
42 InputGroupAddon,
43 InputGroupText,
44 Modal,
45} from 'ui'
46import { Admonition } from 'ui-patterns/admonition'
47import { Input } from 'ui-patterns/DataInputs/Input'
48import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
49import * as z from 'zod'
50
51import {
52 JWT_SECRET_UPDATE_ERROR_MESSAGES,
53 JWT_SECRET_UPDATE_PROGRESS_MESSAGES,
54} from './jwt.constants'
55import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
56import { FormActions } from '@/components/ui/Forms/FormActions'
57import { InlineLink } from '@/components/ui/InlineLink'
58import Panel from '@/components/ui/Panel'
59import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
60import { useLegacyAPIKeysStatusQuery } from '@/data/api-keys/legacy-api-keys-status-query'
61import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
62import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
63import { useJwtSecretUpdateMutation } from '@/data/config/jwt-secret-update-mutation'
64import { useJwtSecretUpdatingStatusQuery } from '@/data/config/jwt-secret-updating-status-query'
65import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query'
66import { useLegacyJWTSigningKeyQuery } from '@/data/jwt-signing-keys/legacy-jwt-signing-key-query'
67import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
68import { uuidv4 } from '@/lib/helpers'
69
70const MAX_JWT_EXP = 604800
71const formSchema = z.object({
72 JWT_EXP: z.preprocess(
73 (val) => (val === '' || val === null || val === undefined ? undefined : val),
74 z.coerce
75 .number({
76 required_error: 'Must have a JWT expiry value',
77 invalid_type_error: 'Must have a JWT expiry value',
78 })
79 .positive('Must be greater than 0')
80 .max(MAX_JWT_EXP, `Must be less than ${MAX_JWT_EXP}`)
81 ),
82})
83const formId = 'jwt-exp-form'
84
85const customJwtSecretFormSchema = z.object({
86 customToken: z
87 .string()
88 .min(32, 'Must be at least 32 characters')
89 .regex(/^(?!.*[@$]).*$/, '@ and $ are not allowed'),
90})
91const customJwtSecretFormId = 'custom-jwt-secret-form'
92
93export const JWTSettings = () => {
94 const { ref: projectRef } = useParams()
95
96 const disableLegacyJwtSecretRotation = useFlag('disableLegacyJwtSecretRotation')
97
98 const [customToken, setCustomToken] = useState<string>('')
99 const [isCreatingKey, setIsCreatingKey] = useState<boolean>(false)
100 const [isRegeneratingKey, setIsGeneratingKey] = useState<boolean>(false)
101
102 const { can: canReadJWTSecret } = useAsyncCheckPermissions(
103 PermissionAction.READ,
104 'field.jwt_secret'
105 )
106 const { can: canGenerateNewJWTSecret } = useAsyncCheckPermissions(
107 PermissionAction.INFRA_EXECUTE,
108 'queue_job.projects.update_jwt'
109 )
110 const { can: canUpdateConfig } = useAsyncCheckPermissions(
111 PermissionAction.UPDATE,
112 'custom_config_gotrue'
113 )
114
115 const { data } = useJwtSecretUpdatingStatusQuery({ projectRef })
116 const { data: config, isError } = useProjectPostgrestConfigQuery({ projectRef })
117 const { mutateAsync: updateJwt, isPending: isSubmittingJwtSecretUpdateRequest } =
118 useJwtSecretUpdateMutation()
119
120 const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
121 const { data: legacyKey, isPending } = useLegacyJWTSigningKeyQuery(
122 { projectRef },
123 { enabled: canReadAPIKeys, retry: false }
124 )
125 const { data: legacyAPIKeysStatus } = useLegacyAPIKeysStatusQuery(
126 { projectRef },
127 { enabled: canReadAPIKeys }
128 )
129
130 const { data: authConfig, isPending: isLoadingAuthConfig } = useAuthConfigQuery({ projectRef })
131 const { mutate: updateAuthConfig, isPending: isUpdatingAuthConfig } =
132 useAuthConfigUpdateMutation()
133
134 const { Failed, Updated, Updating } = JwtSecretUpdateStatus
135
136 const isJwtSecretUpdateFailed = data?.jwtSecretUpdateStatus === Failed
137 const isNotUpdatingJwtSecret =
138 data?.jwtSecretUpdateStatus === undefined || data?.jwtSecretUpdateStatus === Updated
139 const isUpdatingJwtSecret = data?.jwtSecretUpdateStatus === Updating
140 const jwtSecretUpdateErrorMessage =
141 JWT_SECRET_UPDATE_ERROR_MESSAGES[data?.jwtSecretUpdateError as JwtSecretUpdateError]
142 const jwtSecretUpdateProgressMessage =
143 JWT_SECRET_UPDATE_PROGRESS_MESSAGES[data?.jwtSecretUpdateProgress as JwtSecretUpdateProgress]
144
145 const INITIAL_VALUES = useMemo(
146 () => ({
147 JWT_EXP: authConfig?.JWT_EXP ?? 3600,
148 }),
149 [authConfig]
150 )
151
152 const form = useForm<z.infer<typeof formSchema>>({
153 defaultValues: INITIAL_VALUES,
154 resolver: zodResolver(formSchema as any),
155 })
156
157 const customJwtSecretForm = useForm<z.infer<typeof customJwtSecretFormSchema>>({
158 defaultValues: { customToken: '' },
159 resolver: zodResolver(customJwtSecretFormSchema as any),
160 })
161
162 const { reset, formState } = form
163 const { isDirty } = formState
164
165 useEffect(() => {
166 reset(INITIAL_VALUES)
167 }, [INITIAL_VALUES, reset])
168
169 const onUpdateJwtExp: SubmitHandler<z.infer<typeof formSchema>> = async (values) => {
170 if (!projectRef) return console.error('Project ref is required')
171
172 updateAuthConfig(
173 { projectRef, config: values },
174 {
175 onError: (error) => {
176 toast.error(`Failed to update JWT expiry: ${error?.message}`)
177 },
178 onSuccess: (newValues) => {
179 toast.success('Successfully updated JWT expiry')
180 reset({ JWT_EXP: newValues.JWT_EXP ?? values.JWT_EXP })
181 },
182 }
183 )
184 }
185
186 async function handleJwtSecretUpdate(
187 jwt_secret: string,
188 setModalVisibility: Dispatch<SetStateAction<boolean>>
189 ) {
190 if (!projectRef) return console.error('Project ref is required')
191 const trackingId = uuidv4()
192 try {
193 await updateJwt({ projectRef, jwtSecret: jwt_secret, changeTrackingId: trackingId })
194 setModalVisibility(false)
195 toast(
196 'Successfully submitted JWT secret update request. Please wait while your project is updated.'
197 )
198 } catch (error: any) {
199 toast.error(`Failed to update JWT secret: ${error.message}`)
200 }
201 }
202 return (
203 <>
204 <Panel
205 footer={
206 <div className="flex py-4 w-full">
207 <FormActions
208 form={formId}
209 isSubmitting={isUpdatingAuthConfig}
210 hasChanges={isDirty}
211 handleReset={reset}
212 disabled={!canUpdateConfig}
213 helper={
214 !canUpdateConfig
215 ? 'You need additional permissions to update JWT settings'
216 : undefined
217 }
218 />
219 </div>
220 }
221 >
222 <Panel.Content className="border-t border-panel-border-interior-light in-data-[theme*=dark]:border-panel-border-interior-dark">
223 <Form {...form}>
224 <form
225 id={formId}
226 onSubmit={form.handleSubmit(onUpdateJwtExp)}
227 className="space-y-6"
228 noValidate
229 >
230 {isError ? (
231 <div className="flex items-center justify-center py-8 space-x-2">
232 <AlertCircle size={16} strokeWidth={1.5} />
233 <p className="text-sm text-foreground-light">Failed to retrieve JWT settings</p>
234 </div>
235 ) : (
236 <>
237 {legacyKey && legacyKey.status !== 'revoked' && (
238 <Admonition
239 type="warning"
240 title="Legacy JWT secret has been migrated to new JWT Signing Keys"
241 >
242 <p className="leading-normal!">
243 Legacy JWT secret can only be changed by rotating to a standby key and then
244 revoking it. It is used to{' '}
245 <em className="text-foreground not-italic">
246 {legacyKey.status === 'in_use' ? 'sign and verify' : 'only verify'}
247 </em>{' '}
248 JSON Web Tokens by Briven products.
249 </p>
250
251 {legacyAPIKeysStatus && legacyAPIKeysStatus.enabled && (
252 <p className="leading-normal!">
253 <em className="text-warning not-italic">
254 This includes the <code className="text-code-inline">anon</code> and{' '}
255 <code className="text-code-inline">service_role</code> JWT based API
256 keys.
257 </em>{' '}
258 Consider switching to publishable and secret API keys to disable them.
259 </p>
260 )}
261
262 <Button asChild type="default" icon={<ExternalLink />} className="mt-2">
263 <Link href={`/project/${projectRef}/settings/api-keys`}>
264 Go to API keys
265 </Link>
266 </Button>
267 </Admonition>
268 )}
269 {legacyKey && legacyKey.status === 'revoked' && (
270 <Admonition
271 type="note"
272 title="Your project has revoked the legacy JWT secret"
273 description="No new JSON Web Tokens are issued nor verified with it by Briven products."
274 />
275 )}
276 <FormItemLayout
277 layout="flex-row-reverse"
278 id="JWT_SECRET"
279 label={
280 legacyKey?.status === 'revoked'
281 ? 'Revoked legacy JWT secret'
282 : legacyKey
283 ? 'Legacy JWT secret (still used)'
284 : 'Legacy JWT secret'
285 }
286 description={
287 legacyKey?.status === 'revoked'
288 ? 'No longer used to sign JWTs by Briven Auth.'
289 : !legacyKey || legacyKey.status === 'in_use'
290 ? 'Used to sign and verify JWTs issued by Briven Auth.'
291 : 'Used only to verify JWTs.'
292 }
293 >
294 <Input
295 id="JWT_SECRET"
296 copy={canReadJWTSecret && isNotUpdatingJwtSecret}
297 reveal={canReadJWTSecret && isNotUpdatingJwtSecret}
298 readOnly
299 value={
300 !canReadJWTSecret
301 ? 'You need additional permissions to view the JWT secret'
302 : isJwtSecretUpdateFailed
303 ? 'JWT secret update failed'
304 : isUpdatingJwtSecret
305 ? 'Updating JWT secret...'
306 : config?.jwt_secret || ''
307 }
308 />
309 </FormItemLayout>
310
311 <FormField
312 control={form.control}
313 name="JWT_EXP"
314 disabled={!canUpdateConfig || isLoadingAuthConfig}
315 render={({ field }) => (
316 <FormItemLayout
317 name="JWT_EXP"
318 layout="flex-row-reverse"
319 label="Access token expiry time"
320 description={
321 <>
322 <p>
323 How long access tokens are valid for before a refresh token has to be
324 used.
325 </p>
326 <p>Recommendation: 3600 (1 hour).</p>
327 </>
328 }
329 >
330 <FormControl>
331 <InputGroup>
332 <FormInputGroupInput
333 {...field}
334 id="JWT_EXP"
335 type="number"
336 min={0}
337 max={MAX_JWT_EXP}
338 onChange={(e) =>
339 field.onChange(
340 isNaN(e.target.valueAsNumber) ? '' : e.target.valueAsNumber
341 )
342 }
343 />
344 <InputGroupAddon align="inline-end">
345 <InputGroupText>seconds</InputGroupText>
346 </InputGroupAddon>
347 </InputGroup>
348 </FormControl>
349 </FormItemLayout>
350 )}
351 />
352 </>
353 )}
354 </form>
355 </Form>
356
357 {!isPending && !legacyKey && (
358 <>
359 {isUpdatingJwtSecret && (
360 <div className="flex items-center space-x-2">
361 <Loader2 className="animate-spin" size={14} />
362 <p className="text-sm">Updating JWT secret: {jwtSecretUpdateProgressMessage}</p>
363 </div>
364 )}
365
366 {isJwtSecretUpdateFailed && (
367 <Admonition type="warning" title="Failed to update JWT secret">
368 Please try again. If the failures persist, please contact Briven support with
369 the following details: <br />
370 Change tracking ID: {data?.changeTrackingId} <br />
371 Error message: {jwtSecretUpdateErrorMessage}
372 </Admonition>
373 )}
374
375 <Collapsible className="bg border rounded-md mt-4">
376 <CollapsibleTrigger className="p-4 w-full flex items-center justify-between [&[data-state=open]>svg]:-rotate-180!">
377 <p className="text-sm">
378 {disableLegacyJwtSecretRotation
379 ? 'How to migrate to the new API keys?'
380 : 'How to change your JWT secret?'}
381 </p>
382 <ChevronDown size={14} className="transition-transform duration-200" />
383 </CollapsibleTrigger>
384 <CollapsibleContent className="border-t p-4">
385 <p className="text-sm text-foreground-light text-balance mb-2">
386 {disableLegacyJwtSecretRotation
387 ? 'Migrate to the new publishable and secret API keys to enable rotation with zero downtime and without signing users out. The change is reversible until you revoke the legacy secret.'
388 : 'Instead of changing the legacy JWT secret use a combination of the JWT Signing Keys and API keys features. Consider these advantages:'}
389 </p>
390
391 {disableLegacyJwtSecretRotation ? (
392 <ol className="text-sm text-foreground-light list-decimal list-outside pl-7 space-y-2">
393 <li>
394 <p className="text-foreground">
395 Click "Migrate JWT secret" in{' '}
396 <InlineLink href={`/project/${projectRef}/settings/jwt`}>
397 JWT Signing Keys
398 </InlineLink>
399 .
400 </p>
401 <p className="text-foreground-lighter">
402 This imports your legacy secret into the new system and generates a
403 standby asymmetric key.
404 </p>
405 </li>
406 <li>
407 <p className="text-foreground">Create and roll out new API keys.</p>
408 <p className="text-foreground-lighter">
409 In{' '}
410 <InlineLink href={`/project/${projectRef}/settings/api-keys`}>
411 API Keys
412 </InlineLink>
413 , create a publishable key and secret key, then swap them into your apps
414 in place of <code className="text-code-inline">anon</code> and{' '}
415 <code className="text-code-inline break-keep!">service_role</code>{' '}
416 respectively. Watch the "Last used" indicators to confirm no traffic still
417 depends on the legacy keys.
418 </p>
419 </li>
420 <li>
421 <p className="text-foreground">
422 Click "Rotate keys" in{' '}
423 <InlineLink href={`/project/${projectRef}/settings/jwt`}>
424 JWT Signing Keys
425 </InlineLink>{' '}
426 to start signing new JWTs with the standby key.
427 </p>
428 <p className="text-foreground-lighter">
429 Existing <code className="text-code-inline">anon</code>,{' '}
430 <code className="text-code-inline">service_role</code>, and active user
431 JWTs stay valid. Before rotating, switch any code that verifies JWTs
432 directly against the legacy secret (e.g.{' '}
433 <code className="text-code-inline">jose</code>,{' '}
434 <code className="text-code-inline">jsonwebtoken</code>) to{' '}
435 <code className="text-code-inline">briven.auth.getClaims()</code> or a
436 JWKS-based verifier, and disable the "Verify JWT" setting on any affected
437 Edge Functions.
438 </p>
439 </li>
440 <li>
441 <p className="text-foreground">
442 Optionally, revoke the legacy JWT secret in{' '}
443 <InlineLink href={`/project/${projectRef}/settings/jwt`}>
444 JWT Signing Keys
445 </InlineLink>{' '}
446 once you're sure it's no longer in use.
447 </p>
448 </li>
449 </ol>
450 ) : (
451 <ul className="text-sm text-foreground-light list-disc list-inside">
452 <li>Zero-downtime, reversible change.</li>
453 <li>Users remain signed in and bad actors out.</li>
454 <li>
455 Create multiple secret API keys that are immediately revocable and fully
456 covered by audit logs.
457 </li>
458 <li>
459 Private keys and shared secrets are no longer visible by organization
460 members, so they can't leak.
461 </li>
462 <li>
463 Maintain tighter alignment with SOC2 and other security compliance
464 frameworks.
465 </li>
466 <li>
467 Improve app's performance by using public keys to verify JWTs instead of
468 calling <code className="text-code-inline">getUser()</code>.
469 </li>
470 </ul>
471 )}
472
473 <div className="flex flex-row gap-x-2 mt-4">
474 {disableLegacyJwtSecretRotation ? (
475 <Button type="default" icon={<ExternalLink className="size-4" />} asChild>
476 <Link
477 href="https://supabase.com/docs/guides/auth/signing-keys#getting-started"
478 target="_blank"
479 rel="noreferrer"
480 >
481 Read the full migration guide
482 </Link>
483 </Button>
484 ) : (
485 <DropdownMenu>
486 <DropdownMenuTrigger asChild>
487 <ButtonTooltip
488 disabled={!canGenerateNewJWTSecret}
489 type="default"
490 iconRight={<ChevronDown size={14} />}
491 loading={isUpdatingJwtSecret}
492 tooltip={{
493 content: {
494 side: 'bottom',
495 text: !canGenerateNewJWTSecret
496 ? 'You need additional permissions to generate a new JWT secret'
497 : undefined,
498 },
499 }}
500 >
501 Change legacy JWT secret
502 </ButtonTooltip>
503 </DropdownMenuTrigger>
504 <DropdownMenuContent align="start" side="bottom">
505 <DropdownMenuItem
506 className="space-x-2"
507 onClick={() => setIsGeneratingKey(true)}
508 >
509 <RefreshCw size={16} />
510 <p>Generate a random secret</p>
511 </DropdownMenuItem>
512 <DropdownMenuSeparator />
513 <DropdownMenuItem
514 className="space-x-2"
515 onClick={() => setIsCreatingKey(true)}
516 >
517 <PenTool size={16} />
518 <p>Create my own secret</p>
519 </DropdownMenuItem>
520 </DropdownMenuContent>
521 </DropdownMenu>
522 )}
523 </div>
524 </CollapsibleContent>
525 </Collapsible>
526 </>
527 )}
528 </Panel.Content>
529 </Panel>
530
531 <TextConfirmModal
532 variant="destructive"
533 size="large"
534 visible={isRegeneratingKey && !disableLegacyJwtSecretRotation}
535 title="Confirm legacy JWT secret change"
536 confirmString="I understand and wish to proceed"
537 confirmLabel={customToken ? 'Apply custom secret' : 'Generate random secret'}
538 confirmPlaceholder=""
539 loading={isSubmittingJwtSecretUpdateRequest}
540 onCancel={() => {
541 setIsGeneratingKey(false)
542 setCustomToken('')
543 }}
544 onConfirm={() => handleJwtSecretUpdate(customToken || 'ROLL', setIsGeneratingKey)}
545 >
546 <ul className="space-y-4 text-sm">
547 <li className="flex gap-2 bg border rounded-md p-4">
548 <Lightbulb size={24} className="shrink-0 text-brand" />
549
550 <div className="flex flex-col gap-2">
551 <p>Use new JWT Signing Keys and API Keys instead</p>
552 <p className="text-foreground-light">
553 Consider using a combination of the JWT Signing Keys and API Keys features to
554 achieve the same effect.{' '}
555 <em className="text-brand not-italic">
556 Some or all of the warnings listed below might not apply when using these features
557 </em>
558 .
559 </p>
560 </div>
561 </li>
562 <li className="flex gap-2 px-4">
563 <CloudOff size={24} className="text-foreground-light shrink-0" />
564
565 <div className="flex flex-col gap-2">
566 <p>Your application will experience significant downtime</p>
567 <p className="text-foreground-light">
568 As new <code>anon</code> and <code>service_role</code> keys will be created and the
569 existing ones permanently destroyed, your application will stop functioning for the
570 duration it takes you to swap them.{' '}
571 <em className="text-warning not-italic">
572 If you have a mobile, desktop, CLI or any offline-capable application the downtime
573 may be more significant and dependent on app store reviews or user-initiated
574 upgrades or downloads!
575 </em>
576 </p>
577 <p className="text-foreground-light">
578 Currently active users will be forcefully signed out (inactive users will keep their
579 sessions).
580 </p>
581 <p className="text-foreground-light">
582 All long-lived Storage pre-signed URLs will be permanently invalidated.
583 </p>
584 </div>
585 </li>
586
587 <li className="flex gap-2 px-4">
588 <Power size={24} className="text-foreground-light shrink-0" />
589 <div className="flex flex-col gap-2">
590 <p>Your project and database will be restarted</p>
591 <p className="text-foreground-light">
592 This process restarts your project, terminating existing connections to your
593 database. You may see API or other unusual errors for{' '}
594 <em className="text-warning not-italic">up to 2 minutes</em> while the new secret is
595 deployed.
596 </p>
597 </div>
598 </li>
599 <li className="flex gap-2 px-4">
600 <Hourglass size={24} className="text-foreground-light shrink-0" />
601 <div className="flex flex-col gap-2">
602 <p>20-minute cooldown period</p>
603 <p className="text-foreground-light">
604 Should you need to revert or repeat this operation, it will take at least 20 minutes
605 before you're able to do so again.
606 </p>
607 </div>
608 </li>
609 <li className="flex gap-2 px-4">
610 <TriangleAlert size={24} className="text-foreground-light shrink-0" />
611 <div className="flex flex-col gap-2">
612 <p>Irreversible change! This cannot be undone!</p>
613 <p className="text-foreground-light">
614 The old JWT secret will be permanently lost (unless you've saved it prior). Even if
615 you use it again the <code>anon</code> and <code>service_role</code> API keys{' '}
616 <em className="text-warning not-italic">will not be restorable</em> to their exact
617 values.
618 </p>
619 </div>
620 </li>
621 </ul>
622 </TextConfirmModal>
623
624 <Modal
625 header="Pick a new JWT secret"
626 visible={isCreatingKey && !disableLegacyJwtSecretRotation}
627 size="medium"
628 variant="danger"
629 onCancel={() => {
630 setIsCreatingKey(false)
631 setCustomToken('')
632 customJwtSecretForm.reset({ customToken: '' })
633 }}
634 loading={isSubmittingJwtSecretUpdateRequest}
635 customFooter={
636 <div className="space-x-2">
637 <Button
638 type="default"
639 onClick={() => {
640 setIsCreatingKey(false)
641 setCustomToken('')
642 customJwtSecretForm.reset({ customToken: '' })
643 }}
644 >
645 Cancel
646 </Button>
647 <Button
648 type="primary"
649 htmlType="submit"
650 form={customJwtSecretFormId}
651 loading={isSubmittingJwtSecretUpdateRequest}
652 >
653 Proceed to final confirmation
654 </Button>
655 </div>
656 }
657 >
658 <Modal.Content>
659 <Form {...customJwtSecretForm}>
660 <form
661 id={customJwtSecretFormId}
662 onSubmit={customJwtSecretForm.handleSubmit((values) => {
663 setIsGeneratingKey(true)
664 setIsCreatingKey(false)
665 setCustomToken(values.customToken)
666 })}
667 className="flex flex-col space-y-2"
668 noValidate
669 >
670 <p className="text-sm text-foreground-light">
671 Pick a new custom JWT secret. Make sure it is a strong combination of characters
672 that cannot be guessed easily.
673 </p>
674 <FormField
675 control={customJwtSecretForm.control}
676 name="customToken"
677 render={({ field }) => (
678 <FormItemLayout
679 layout="vertical"
680 label="Custom JWT secret"
681 description="Minimally 32 characters long, '@' and '$' are not allowed."
682 >
683 <FormControl>
684 <Input copy reveal icon={<Key />} className="w-full text-left" {...field} />
685 </FormControl>
686 </FormItemLayout>
687 )}
688 />
689 </form>
690 </Form>
691 </Modal.Content>
692 </Modal>
693 </>
694 )
695}