| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695 |
- import { zodResolver } from '@hookform/resolvers/zod'
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import {
- JwtSecretUpdateError,
- JwtSecretUpdateProgress,
- JwtSecretUpdateStatus,
- } from '@supabase/shared-types/out/events'
- import { useFlag, useParams } from 'common'
- import {
- AlertCircle,
- ChevronDown,
- CloudOff,
- ExternalLink,
- Hourglass,
- Key,
- Lightbulb,
- Loader2,
- PenTool,
- Power,
- RefreshCw,
- TriangleAlert,
- } from 'lucide-react'
- import Link from 'next/link'
- import { useEffect, useMemo, useState, type Dispatch, type SetStateAction } from 'react'
- import { useForm, type SubmitHandler } from 'react-hook-form'
- import { toast } from 'sonner'
- import {
- Button,
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
- Form,
- FormControl,
- FormField,
- FormInputGroupInput,
- InputGroup,
- InputGroupAddon,
- InputGroupText,
- Modal,
- } from 'ui'
- import { Admonition } from 'ui-patterns/admonition'
- import { Input } from 'ui-patterns/DataInputs/Input'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import * as z from 'zod'
- import {
- JWT_SECRET_UPDATE_ERROR_MESSAGES,
- JWT_SECRET_UPDATE_PROGRESS_MESSAGES,
- } from './jwt.constants'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { FormActions } from '@/components/ui/Forms/FormActions'
- import { InlineLink } from '@/components/ui/InlineLink'
- import Panel from '@/components/ui/Panel'
- import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
- import { useLegacyAPIKeysStatusQuery } from '@/data/api-keys/legacy-api-keys-status-query'
- import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
- import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
- import { useJwtSecretUpdateMutation } from '@/data/config/jwt-secret-update-mutation'
- import { useJwtSecretUpdatingStatusQuery } from '@/data/config/jwt-secret-updating-status-query'
- import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query'
- import { useLegacyJWTSigningKeyQuery } from '@/data/jwt-signing-keys/legacy-jwt-signing-key-query'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { uuidv4 } from '@/lib/helpers'
- const MAX_JWT_EXP = 604800
- const formSchema = z.object({
- JWT_EXP: z.preprocess(
- (val) => (val === '' || val === null || val === undefined ? undefined : val),
- z.coerce
- .number({
- required_error: 'Must have a JWT expiry value',
- invalid_type_error: 'Must have a JWT expiry value',
- })
- .positive('Must be greater than 0')
- .max(MAX_JWT_EXP, `Must be less than ${MAX_JWT_EXP}`)
- ),
- })
- const formId = 'jwt-exp-form'
- const customJwtSecretFormSchema = z.object({
- customToken: z
- .string()
- .min(32, 'Must be at least 32 characters')
- .regex(/^(?!.*[@$]).*$/, '@ and $ are not allowed'),
- })
- const customJwtSecretFormId = 'custom-jwt-secret-form'
- export const JWTSettings = () => {
- const { ref: projectRef } = useParams()
- const disableLegacyJwtSecretRotation = useFlag('disableLegacyJwtSecretRotation')
- const [customToken, setCustomToken] = useState<string>('')
- const [isCreatingKey, setIsCreatingKey] = useState<boolean>(false)
- const [isRegeneratingKey, setIsGeneratingKey] = useState<boolean>(false)
- const { can: canReadJWTSecret } = useAsyncCheckPermissions(
- PermissionAction.READ,
- 'field.jwt_secret'
- )
- const { can: canGenerateNewJWTSecret } = useAsyncCheckPermissions(
- PermissionAction.INFRA_EXECUTE,
- 'queue_job.projects.update_jwt'
- )
- const { can: canUpdateConfig } = useAsyncCheckPermissions(
- PermissionAction.UPDATE,
- 'custom_config_gotrue'
- )
- const { data } = useJwtSecretUpdatingStatusQuery({ projectRef })
- const { data: config, isError } = useProjectPostgrestConfigQuery({ projectRef })
- const { mutateAsync: updateJwt, isPending: isSubmittingJwtSecretUpdateRequest } =
- useJwtSecretUpdateMutation()
- const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
- const { data: legacyKey, isPending } = useLegacyJWTSigningKeyQuery(
- { projectRef },
- { enabled: canReadAPIKeys, retry: false }
- )
- const { data: legacyAPIKeysStatus } = useLegacyAPIKeysStatusQuery(
- { projectRef },
- { enabled: canReadAPIKeys }
- )
- const { data: authConfig, isPending: isLoadingAuthConfig } = useAuthConfigQuery({ projectRef })
- const { mutate: updateAuthConfig, isPending: isUpdatingAuthConfig } =
- useAuthConfigUpdateMutation()
- const { Failed, Updated, Updating } = JwtSecretUpdateStatus
- const isJwtSecretUpdateFailed = data?.jwtSecretUpdateStatus === Failed
- const isNotUpdatingJwtSecret =
- data?.jwtSecretUpdateStatus === undefined || data?.jwtSecretUpdateStatus === Updated
- const isUpdatingJwtSecret = data?.jwtSecretUpdateStatus === Updating
- const jwtSecretUpdateErrorMessage =
- JWT_SECRET_UPDATE_ERROR_MESSAGES[data?.jwtSecretUpdateError as JwtSecretUpdateError]
- const jwtSecretUpdateProgressMessage =
- JWT_SECRET_UPDATE_PROGRESS_MESSAGES[data?.jwtSecretUpdateProgress as JwtSecretUpdateProgress]
- const INITIAL_VALUES = useMemo(
- () => ({
- JWT_EXP: authConfig?.JWT_EXP ?? 3600,
- }),
- [authConfig]
- )
- const form = useForm<z.infer<typeof formSchema>>({
- defaultValues: INITIAL_VALUES,
- resolver: zodResolver(formSchema as any),
- })
- const customJwtSecretForm = useForm<z.infer<typeof customJwtSecretFormSchema>>({
- defaultValues: { customToken: '' },
- resolver: zodResolver(customJwtSecretFormSchema as any),
- })
- const { reset, formState } = form
- const { isDirty } = formState
- useEffect(() => {
- reset(INITIAL_VALUES)
- }, [INITIAL_VALUES, reset])
- const onUpdateJwtExp: SubmitHandler<z.infer<typeof formSchema>> = async (values) => {
- if (!projectRef) return console.error('Project ref is required')
- updateAuthConfig(
- { projectRef, config: values },
- {
- onError: (error) => {
- toast.error(`Failed to update JWT expiry: ${error?.message}`)
- },
- onSuccess: (newValues) => {
- toast.success('Successfully updated JWT expiry')
- reset({ JWT_EXP: newValues.JWT_EXP ?? values.JWT_EXP })
- },
- }
- )
- }
- async function handleJwtSecretUpdate(
- jwt_secret: string,
- setModalVisibility: Dispatch<SetStateAction<boolean>>
- ) {
- if (!projectRef) return console.error('Project ref is required')
- const trackingId = uuidv4()
- try {
- await updateJwt({ projectRef, jwtSecret: jwt_secret, changeTrackingId: trackingId })
- setModalVisibility(false)
- toast(
- 'Successfully submitted JWT secret update request. Please wait while your project is updated.'
- )
- } catch (error: any) {
- toast.error(`Failed to update JWT secret: ${error.message}`)
- }
- }
- return (
- <>
- <Panel
- footer={
- <div className="flex py-4 w-full">
- <FormActions
- form={formId}
- isSubmitting={isUpdatingAuthConfig}
- hasChanges={isDirty}
- handleReset={reset}
- disabled={!canUpdateConfig}
- helper={
- !canUpdateConfig
- ? 'You need additional permissions to update JWT settings'
- : undefined
- }
- />
- </div>
- }
- >
- <Panel.Content className="border-t border-panel-border-interior-light in-data-[theme*=dark]:border-panel-border-interior-dark">
- <Form {...form}>
- <form
- id={formId}
- onSubmit={form.handleSubmit(onUpdateJwtExp)}
- className="space-y-6"
- noValidate
- >
- {isError ? (
- <div className="flex items-center justify-center py-8 space-x-2">
- <AlertCircle size={16} strokeWidth={1.5} />
- <p className="text-sm text-foreground-light">Failed to retrieve JWT settings</p>
- </div>
- ) : (
- <>
- {legacyKey && legacyKey.status !== 'revoked' && (
- <Admonition
- type="warning"
- title="Legacy JWT secret has been migrated to new JWT Signing Keys"
- >
- <p className="leading-normal!">
- Legacy JWT secret can only be changed by rotating to a standby key and then
- revoking it. It is used to{' '}
- <em className="text-foreground not-italic">
- {legacyKey.status === 'in_use' ? 'sign and verify' : 'only verify'}
- </em>{' '}
- JSON Web Tokens by Briven products.
- </p>
- {legacyAPIKeysStatus && legacyAPIKeysStatus.enabled && (
- <p className="leading-normal!">
- <em className="text-warning not-italic">
- This includes the <code className="text-code-inline">anon</code> and{' '}
- <code className="text-code-inline">service_role</code> JWT based API
- keys.
- </em>{' '}
- Consider switching to publishable and secret API keys to disable them.
- </p>
- )}
- <Button asChild type="default" icon={<ExternalLink />} className="mt-2">
- <Link href={`/project/${projectRef}/settings/api-keys`}>
- Go to API keys
- </Link>
- </Button>
- </Admonition>
- )}
- {legacyKey && legacyKey.status === 'revoked' && (
- <Admonition
- type="note"
- title="Your project has revoked the legacy JWT secret"
- description="No new JSON Web Tokens are issued nor verified with it by Briven products."
- />
- )}
- <FormItemLayout
- layout="flex-row-reverse"
- id="JWT_SECRET"
- label={
- legacyKey?.status === 'revoked'
- ? 'Revoked legacy JWT secret'
- : legacyKey
- ? 'Legacy JWT secret (still used)'
- : 'Legacy JWT secret'
- }
- description={
- legacyKey?.status === 'revoked'
- ? 'No longer used to sign JWTs by Briven Auth.'
- : !legacyKey || legacyKey.status === 'in_use'
- ? 'Used to sign and verify JWTs issued by Briven Auth.'
- : 'Used only to verify JWTs.'
- }
- >
- <Input
- id="JWT_SECRET"
- copy={canReadJWTSecret && isNotUpdatingJwtSecret}
- reveal={canReadJWTSecret && isNotUpdatingJwtSecret}
- readOnly
- value={
- !canReadJWTSecret
- ? 'You need additional permissions to view the JWT secret'
- : isJwtSecretUpdateFailed
- ? 'JWT secret update failed'
- : isUpdatingJwtSecret
- ? 'Updating JWT secret...'
- : config?.jwt_secret || ''
- }
- />
- </FormItemLayout>
- <FormField
- control={form.control}
- name="JWT_EXP"
- disabled={!canUpdateConfig || isLoadingAuthConfig}
- render={({ field }) => (
- <FormItemLayout
- name="JWT_EXP"
- layout="flex-row-reverse"
- label="Access token expiry time"
- description={
- <>
- <p>
- How long access tokens are valid for before a refresh token has to be
- used.
- </p>
- <p>Recommendation: 3600 (1 hour).</p>
- </>
- }
- >
- <FormControl>
- <InputGroup>
- <FormInputGroupInput
- {...field}
- id="JWT_EXP"
- type="number"
- min={0}
- max={MAX_JWT_EXP}
- onChange={(e) =>
- field.onChange(
- isNaN(e.target.valueAsNumber) ? '' : e.target.valueAsNumber
- )
- }
- />
- <InputGroupAddon align="inline-end">
- <InputGroupText>seconds</InputGroupText>
- </InputGroupAddon>
- </InputGroup>
- </FormControl>
- </FormItemLayout>
- )}
- />
- </>
- )}
- </form>
- </Form>
- {!isPending && !legacyKey && (
- <>
- {isUpdatingJwtSecret && (
- <div className="flex items-center space-x-2">
- <Loader2 className="animate-spin" size={14} />
- <p className="text-sm">Updating JWT secret: {jwtSecretUpdateProgressMessage}</p>
- </div>
- )}
- {isJwtSecretUpdateFailed && (
- <Admonition type="warning" title="Failed to update JWT secret">
- Please try again. If the failures persist, please contact Briven support with
- the following details: <br />
- Change tracking ID: {data?.changeTrackingId} <br />
- Error message: {jwtSecretUpdateErrorMessage}
- </Admonition>
- )}
- <Collapsible className="bg border rounded-md mt-4">
- <CollapsibleTrigger className="p-4 w-full flex items-center justify-between [&[data-state=open]>svg]:-rotate-180!">
- <p className="text-sm">
- {disableLegacyJwtSecretRotation
- ? 'How to migrate to the new API keys?'
- : 'How to change your JWT secret?'}
- </p>
- <ChevronDown size={14} className="transition-transform duration-200" />
- </CollapsibleTrigger>
- <CollapsibleContent className="border-t p-4">
- <p className="text-sm text-foreground-light text-balance mb-2">
- {disableLegacyJwtSecretRotation
- ? '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.'
- : 'Instead of changing the legacy JWT secret use a combination of the JWT Signing Keys and API keys features. Consider these advantages:'}
- </p>
- {disableLegacyJwtSecretRotation ? (
- <ol className="text-sm text-foreground-light list-decimal list-outside pl-7 space-y-2">
- <li>
- <p className="text-foreground">
- Click "Migrate JWT secret" in{' '}
- <InlineLink href={`/project/${projectRef}/settings/jwt`}>
- JWT Signing Keys
- </InlineLink>
- .
- </p>
- <p className="text-foreground-lighter">
- This imports your legacy secret into the new system and generates a
- standby asymmetric key.
- </p>
- </li>
- <li>
- <p className="text-foreground">Create and roll out new API keys.</p>
- <p className="text-foreground-lighter">
- In{' '}
- <InlineLink href={`/project/${projectRef}/settings/api-keys`}>
- API Keys
- </InlineLink>
- , create a publishable key and secret key, then swap them into your apps
- in place of <code className="text-code-inline">anon</code> and{' '}
- <code className="text-code-inline break-keep!">service_role</code>{' '}
- respectively. Watch the "Last used" indicators to confirm no traffic still
- depends on the legacy keys.
- </p>
- </li>
- <li>
- <p className="text-foreground">
- Click "Rotate keys" in{' '}
- <InlineLink href={`/project/${projectRef}/settings/jwt`}>
- JWT Signing Keys
- </InlineLink>{' '}
- to start signing new JWTs with the standby key.
- </p>
- <p className="text-foreground-lighter">
- Existing <code className="text-code-inline">anon</code>,{' '}
- <code className="text-code-inline">service_role</code>, and active user
- JWTs stay valid. Before rotating, switch any code that verifies JWTs
- directly against the legacy secret (e.g.{' '}
- <code className="text-code-inline">jose</code>,{' '}
- <code className="text-code-inline">jsonwebtoken</code>) to{' '}
- <code className="text-code-inline">briven.auth.getClaims()</code> or a
- JWKS-based verifier, and disable the "Verify JWT" setting on any affected
- Edge Functions.
- </p>
- </li>
- <li>
- <p className="text-foreground">
- Optionally, revoke the legacy JWT secret in{' '}
- <InlineLink href={`/project/${projectRef}/settings/jwt`}>
- JWT Signing Keys
- </InlineLink>{' '}
- once you're sure it's no longer in use.
- </p>
- </li>
- </ol>
- ) : (
- <ul className="text-sm text-foreground-light list-disc list-inside">
- <li>Zero-downtime, reversible change.</li>
- <li>Users remain signed in and bad actors out.</li>
- <li>
- Create multiple secret API keys that are immediately revocable and fully
- covered by audit logs.
- </li>
- <li>
- Private keys and shared secrets are no longer visible by organization
- members, so they can't leak.
- </li>
- <li>
- Maintain tighter alignment with SOC2 and other security compliance
- frameworks.
- </li>
- <li>
- Improve app's performance by using public keys to verify JWTs instead of
- calling <code className="text-code-inline">getUser()</code>.
- </li>
- </ul>
- )}
- <div className="flex flex-row gap-x-2 mt-4">
- {disableLegacyJwtSecretRotation ? (
- <Button type="default" icon={<ExternalLink className="size-4" />} asChild>
- <Link
- href="https://supabase.com/docs/guides/auth/signing-keys#getting-started"
- target="_blank"
- rel="noreferrer"
- >
- Read the full migration guide
- </Link>
- </Button>
- ) : (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <ButtonTooltip
- disabled={!canGenerateNewJWTSecret}
- type="default"
- iconRight={<ChevronDown size={14} />}
- loading={isUpdatingJwtSecret}
- tooltip={{
- content: {
- side: 'bottom',
- text: !canGenerateNewJWTSecret
- ? 'You need additional permissions to generate a new JWT secret'
- : undefined,
- },
- }}
- >
- Change legacy JWT secret
- </ButtonTooltip>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="start" side="bottom">
- <DropdownMenuItem
- className="space-x-2"
- onClick={() => setIsGeneratingKey(true)}
- >
- <RefreshCw size={16} />
- <p>Generate a random secret</p>
- </DropdownMenuItem>
- <DropdownMenuSeparator />
- <DropdownMenuItem
- className="space-x-2"
- onClick={() => setIsCreatingKey(true)}
- >
- <PenTool size={16} />
- <p>Create my own secret</p>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- )}
- </div>
- </CollapsibleContent>
- </Collapsible>
- </>
- )}
- </Panel.Content>
- </Panel>
- <TextConfirmModal
- variant="destructive"
- size="large"
- visible={isRegeneratingKey && !disableLegacyJwtSecretRotation}
- title="Confirm legacy JWT secret change"
- confirmString="I understand and wish to proceed"
- confirmLabel={customToken ? 'Apply custom secret' : 'Generate random secret'}
- confirmPlaceholder=""
- loading={isSubmittingJwtSecretUpdateRequest}
- onCancel={() => {
- setIsGeneratingKey(false)
- setCustomToken('')
- }}
- onConfirm={() => handleJwtSecretUpdate(customToken || 'ROLL', setIsGeneratingKey)}
- >
- <ul className="space-y-4 text-sm">
- <li className="flex gap-2 bg border rounded-md p-4">
- <Lightbulb size={24} className="shrink-0 text-brand" />
- <div className="flex flex-col gap-2">
- <p>Use new JWT Signing Keys and API Keys instead</p>
- <p className="text-foreground-light">
- Consider using a combination of the JWT Signing Keys and API Keys features to
- achieve the same effect.{' '}
- <em className="text-brand not-italic">
- Some or all of the warnings listed below might not apply when using these features
- </em>
- .
- </p>
- </div>
- </li>
- <li className="flex gap-2 px-4">
- <CloudOff size={24} className="text-foreground-light shrink-0" />
- <div className="flex flex-col gap-2">
- <p>Your application will experience significant downtime</p>
- <p className="text-foreground-light">
- As new <code>anon</code> and <code>service_role</code> keys will be created and the
- existing ones permanently destroyed, your application will stop functioning for the
- duration it takes you to swap them.{' '}
- <em className="text-warning not-italic">
- If you have a mobile, desktop, CLI or any offline-capable application the downtime
- may be more significant and dependent on app store reviews or user-initiated
- upgrades or downloads!
- </em>
- </p>
- <p className="text-foreground-light">
- Currently active users will be forcefully signed out (inactive users will keep their
- sessions).
- </p>
- <p className="text-foreground-light">
- All long-lived Storage pre-signed URLs will be permanently invalidated.
- </p>
- </div>
- </li>
- <li className="flex gap-2 px-4">
- <Power size={24} className="text-foreground-light shrink-0" />
- <div className="flex flex-col gap-2">
- <p>Your project and database will be restarted</p>
- <p className="text-foreground-light">
- This process restarts your project, terminating existing connections to your
- database. You may see API or other unusual errors for{' '}
- <em className="text-warning not-italic">up to 2 minutes</em> while the new secret is
- deployed.
- </p>
- </div>
- </li>
- <li className="flex gap-2 px-4">
- <Hourglass size={24} className="text-foreground-light shrink-0" />
- <div className="flex flex-col gap-2">
- <p>20-minute cooldown period</p>
- <p className="text-foreground-light">
- Should you need to revert or repeat this operation, it will take at least 20 minutes
- before you're able to do so again.
- </p>
- </div>
- </li>
- <li className="flex gap-2 px-4">
- <TriangleAlert size={24} className="text-foreground-light shrink-0" />
- <div className="flex flex-col gap-2">
- <p>Irreversible change! This cannot be undone!</p>
- <p className="text-foreground-light">
- The old JWT secret will be permanently lost (unless you've saved it prior). Even if
- you use it again the <code>anon</code> and <code>service_role</code> API keys{' '}
- <em className="text-warning not-italic">will not be restorable</em> to their exact
- values.
- </p>
- </div>
- </li>
- </ul>
- </TextConfirmModal>
- <Modal
- header="Pick a new JWT secret"
- visible={isCreatingKey && !disableLegacyJwtSecretRotation}
- size="medium"
- variant="danger"
- onCancel={() => {
- setIsCreatingKey(false)
- setCustomToken('')
- customJwtSecretForm.reset({ customToken: '' })
- }}
- loading={isSubmittingJwtSecretUpdateRequest}
- customFooter={
- <div className="space-x-2">
- <Button
- type="default"
- onClick={() => {
- setIsCreatingKey(false)
- setCustomToken('')
- customJwtSecretForm.reset({ customToken: '' })
- }}
- >
- Cancel
- </Button>
- <Button
- type="primary"
- htmlType="submit"
- form={customJwtSecretFormId}
- loading={isSubmittingJwtSecretUpdateRequest}
- >
- Proceed to final confirmation
- </Button>
- </div>
- }
- >
- <Modal.Content>
- <Form {...customJwtSecretForm}>
- <form
- id={customJwtSecretFormId}
- onSubmit={customJwtSecretForm.handleSubmit((values) => {
- setIsGeneratingKey(true)
- setIsCreatingKey(false)
- setCustomToken(values.customToken)
- })}
- className="flex flex-col space-y-2"
- noValidate
- >
- <p className="text-sm text-foreground-light">
- Pick a new custom JWT secret. Make sure it is a strong combination of characters
- that cannot be guessed easily.
- </p>
- <FormField
- control={customJwtSecretForm.control}
- name="customToken"
- render={({ field }) => (
- <FormItemLayout
- layout="vertical"
- label="Custom JWT secret"
- description="Minimally 32 characters long, '@' and '$' are not allowed."
- >
- <FormControl>
- <Input copy reveal icon={<Key />} className="w-full text-left" {...field} />
- </FormControl>
- </FormItemLayout>
- )}
- />
- </form>
- </Form>
- </Modal.Content>
- </Modal>
- </>
- )
- }
|