jwt-settings.tsx 29 KB

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