mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 03:15:06 +03:00
322 lines
13 KiB
TypeScript
322 lines
13 KiB
TypeScript
import { FC, useState, Dispatch, SetStateAction } from 'react'
|
|
import { useRouter } from 'next/router'
|
|
import * as Tooltip from '@radix-ui/react-tooltip'
|
|
import {
|
|
Alert,
|
|
Button,
|
|
Dropdown,
|
|
Input,
|
|
Modal,
|
|
IconEye,
|
|
IconEyeOff,
|
|
IconKey,
|
|
IconLoader,
|
|
IconChevronDown,
|
|
IconRefreshCw,
|
|
IconPenTool,
|
|
IconAlertCircle,
|
|
} from 'ui'
|
|
import {
|
|
JwtSecretUpdateError,
|
|
JwtSecretUpdateProgress,
|
|
JwtSecretUpdateStatus,
|
|
} from '@supabase/shared-types/out/events'
|
|
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
|
|
import { uuidv4 } from 'lib/helpers'
|
|
import {
|
|
useStore,
|
|
useJwtSecretUpdateStatus,
|
|
useProjectPostgrestConfig,
|
|
checkPermissions,
|
|
} from 'hooks'
|
|
import { API_URL } from 'lib/constants'
|
|
import {
|
|
JWT_SECRET_UPDATE_ERROR_MESSAGES,
|
|
JWT_SECRET_UPDATE_PROGRESS_MESSAGES,
|
|
} from './API.constants'
|
|
import Panel from 'components/ui/Panel'
|
|
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
|
import { patch } from 'lib/common/fetch'
|
|
|
|
interface Props {}
|
|
|
|
const JWTSettings: FC<Props> = ({}) => {
|
|
const { ui } = useStore()
|
|
const router = useRouter()
|
|
const { ref } = router.query
|
|
|
|
const [customToken, setCustomToken] = useState<string>('')
|
|
const [showCustomTokenInput, setShowCustomTokenInput] = useState(false)
|
|
const [isCreatingKey, setIsCreatingKey] = useState<boolean>(false)
|
|
const [isRegeneratingKey, setIsGeneratingKey] = useState<boolean>(false)
|
|
const [isSubmittingJwtSecretUpdateRequest, setIsSubmittingJwtSecretUpdateRequest] =
|
|
useState<boolean>(false)
|
|
|
|
const canReadJWTSecret = checkPermissions(PermissionAction.READ, 'field.jwt_secret')
|
|
const canGenerateNewJWTSecret = checkPermissions(
|
|
PermissionAction.INFRA_EXECUTE,
|
|
'queue_job.projects.update_jwt'
|
|
)
|
|
|
|
const {
|
|
changeTrackingId,
|
|
jwtSecretUpdateError,
|
|
jwtSecretUpdateProgress,
|
|
jwtSecretUpdateStatus,
|
|
mutateJwtSecretUpdateStatus,
|
|
}: any = useJwtSecretUpdateStatus(ref)
|
|
|
|
const { config, isError } = useProjectPostgrestConfig(ref as string | undefined)
|
|
|
|
const { Failed, Updated, Updating } = JwtSecretUpdateStatus
|
|
|
|
const isJwtSecretUpdateFailed = jwtSecretUpdateStatus === Failed
|
|
const isNotUpdatingJwtSecret =
|
|
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === Updated
|
|
const isUpdatingJwtSecret = jwtSecretUpdateStatus === Updating
|
|
const jwtSecretUpdateErrorMessage =
|
|
JWT_SECRET_UPDATE_ERROR_MESSAGES[jwtSecretUpdateError as JwtSecretUpdateError]
|
|
const jwtSecretUpdateProgressMessage =
|
|
JWT_SECRET_UPDATE_PROGRESS_MESSAGES[jwtSecretUpdateProgress as JwtSecretUpdateProgress]
|
|
|
|
async function handleJwtSecretUpdate(
|
|
jwt_secret: string,
|
|
setModalVisibility: Dispatch<SetStateAction<boolean>>
|
|
) {
|
|
setIsSubmittingJwtSecretUpdateRequest(true)
|
|
try {
|
|
const trackingId = uuidv4()
|
|
const res = await patch(`${API_URL}/projects/${ref}/config/secrets`, {
|
|
jwt_secret,
|
|
change_tracking_id: trackingId,
|
|
})
|
|
if (res.error) throw res.error
|
|
setModalVisibility(false)
|
|
mutateJwtSecretUpdateStatus()
|
|
ui.setNotification({
|
|
category: 'info',
|
|
message:
|
|
'Successfully submitted JWT secret update request. Please wait while your project is updated.',
|
|
})
|
|
} catch (error: any) {
|
|
ui.setNotification({ category: 'error', message: error.message })
|
|
} finally {
|
|
setIsSubmittingJwtSecretUpdateRequest(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Panel title={<h5 className="mb-0">JWT Settings</h5>}>
|
|
<Panel.Content className="space-y-6 border-t border-panel-border-interior-light dark:border-panel-border-interior-dark">
|
|
{isError ? (
|
|
<div className="flex items-center justify-center space-x-2 py-8">
|
|
<IconAlertCircle size={16} strokeWidth={1.5} />
|
|
<p className="text-sm text-scale-1100">Failed to retrieve JWT settings</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<Input
|
|
label="JWT Secret"
|
|
readOnly
|
|
copy={canReadJWTSecret && isNotUpdatingJwtSecret}
|
|
reveal={canReadJWTSecret && isNotUpdatingJwtSecret}
|
|
disabled
|
|
value={
|
|
!canReadJWTSecret
|
|
? 'You need additional permissions to view the JWT secret'
|
|
: isJwtSecretUpdateFailed
|
|
? 'JWT secret update failed'
|
|
: isUpdatingJwtSecret
|
|
? 'Updating JWT secret...'
|
|
: config?.jwt_secret || ''
|
|
}
|
|
className="input-mono"
|
|
descriptionText={
|
|
'Used to decode your JWTs. You can also use this to mint your own JWTs.'
|
|
}
|
|
layout="horizontal"
|
|
/>
|
|
<div className="space-y-3">
|
|
<div className="rounded-md border bg-bg-alt-light p-3 px-6 shadow-sm dark:border-dark dark:bg-bg-alt-dark">
|
|
{isUpdatingJwtSecret ? (
|
|
<div className="flex items-center space-x-2">
|
|
<IconLoader className="animate-spin" size={14} />
|
|
<p className="text-sm">
|
|
Updating JWT secret: {jwtSecretUpdateProgressMessage}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="w-full space-y-2">
|
|
<div className="flex w-full items-center justify-between">
|
|
<div className="flex flex-col space-y-1">
|
|
<p className="text-sm">Generate a new JWT secret</p>
|
|
<p className="text-sm opacity-50">
|
|
A random secret will be created, or you can create your own.
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-col items-end">
|
|
{isUpdatingJwtSecret ? (
|
|
<Button loading type="secondary">
|
|
Updating JWT secret...
|
|
</Button>
|
|
) : !canGenerateNewJWTSecret ? (
|
|
<Tooltip.Root delayDuration={0}>
|
|
<Tooltip.Trigger>
|
|
<Button
|
|
disabled
|
|
as="span"
|
|
type="default"
|
|
iconRight={<IconChevronDown />}
|
|
>
|
|
Generate a new secret
|
|
</Button>
|
|
</Tooltip.Trigger>
|
|
<Tooltip.Content side="bottom">
|
|
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
|
<div
|
|
className={[
|
|
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
|
'border border-scale-200',
|
|
].join(' ')}
|
|
>
|
|
<span className="text-xs text-scale-1200">
|
|
You need additional permissions to generate a new JWT secret
|
|
</span>
|
|
</div>
|
|
</Tooltip.Content>
|
|
</Tooltip.Root>
|
|
) : (
|
|
<Dropdown
|
|
align="end"
|
|
side="bottom"
|
|
overlay={
|
|
<>
|
|
<Dropdown.Item
|
|
onClick={() => setIsGeneratingKey(true)}
|
|
icon={<IconRefreshCw size={16} />}
|
|
>
|
|
Generate a random secret
|
|
</Dropdown.Item>
|
|
<Dropdown.Separator />
|
|
<Dropdown.Item
|
|
onClick={() => setIsCreatingKey(true)}
|
|
icon={<IconPenTool size={16} />}
|
|
>
|
|
Create my own secret
|
|
</Dropdown.Item>
|
|
</>
|
|
}
|
|
>
|
|
<Button as="span" type="default" iconRight={<IconChevronDown />}>
|
|
Generate a new secret
|
|
</Button>
|
|
</Dropdown>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{isJwtSecretUpdateFailed ? (
|
|
<Alert withIcon variant="warning" title="Failed to update JWT secret">
|
|
Please try again. If the failures persist, please contact Supabase support with
|
|
the following details: <br />
|
|
Change tracking ID: {changeTrackingId} <br />
|
|
Error message: {jwtSecretUpdateErrorMessage}
|
|
</Alert>
|
|
) : canGenerateNewJWTSecret ? (
|
|
<Alert
|
|
withIcon
|
|
variant="warning"
|
|
title="This will invalidate all existing API keys!"
|
|
>
|
|
Generating a new JWT secret will invalidate <u>all</u> of your API keys,
|
|
including your <code>service_role</code> and <code>anon</code> keys. Your
|
|
project will also be restarted during this process, which will terminate any
|
|
existing connections.
|
|
</Alert>
|
|
) : (
|
|
<></>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</Panel.Content>
|
|
</Panel>
|
|
<ConfirmModal
|
|
danger
|
|
visible={isRegeneratingKey}
|
|
title="Are you absolutely sure?"
|
|
description="This action cannot be undone and the old JWT secret will be lost. All existing API keys
|
|
will be invalidated, and any open connections will be terminated."
|
|
buttonLabel="Generate new secret"
|
|
buttonLoadingLabel="Generating"
|
|
onSelectCancel={() => setIsGeneratingKey(false)}
|
|
onSelectConfirm={() => handleJwtSecretUpdate('ROLL', setIsGeneratingKey)}
|
|
/>
|
|
|
|
<Modal
|
|
closable
|
|
header="Create a custom JWT secret"
|
|
visible={isCreatingKey}
|
|
size="medium"
|
|
variant="danger"
|
|
loading={isSubmittingJwtSecretUpdateRequest}
|
|
customFooter={
|
|
<div className="space-x-2">
|
|
<Button type="default" onClick={() => setIsCreatingKey(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
disabled={
|
|
customToken.length < 32 || customToken.includes('@') || customToken.includes('$')
|
|
}
|
|
loading={isSubmittingJwtSecretUpdateRequest}
|
|
onClick={() => handleJwtSecretUpdate(customToken, setIsCreatingKey)}
|
|
>
|
|
Apply new JWT secret
|
|
</Button>
|
|
</div>
|
|
}
|
|
>
|
|
<Modal.Content>
|
|
<div className="space-y-2 py-4">
|
|
<p className="text-sm text-scale-1100">
|
|
Create a custom JWT secret. Make sure it is a strong combination of characters that
|
|
cannot be guessed easily.
|
|
</p>
|
|
<Alert
|
|
withIcon
|
|
variant="warning"
|
|
title="All existing API keys will be invalidated, and any open connections will be terminated."
|
|
/>
|
|
<Input
|
|
onChange={(e: any) => setCustomToken(e.target.value)}
|
|
value={customToken}
|
|
icon={<IconKey />}
|
|
type={showCustomTokenInput ? 'text' : 'password'}
|
|
className="w-full text-left"
|
|
label="Custom JWT secret"
|
|
descriptionText="Minimally 32 characters long, '@' and '$' are not allowed."
|
|
actions={
|
|
<div className="mr-1 flex items-center justify-center">
|
|
<Button
|
|
type="default"
|
|
icon={showCustomTokenInput ? <IconEye /> : <IconEyeOff />}
|
|
onClick={() => setShowCustomTokenInput(!showCustomTokenInput)}
|
|
/>
|
|
</div>
|
|
}
|
|
/>
|
|
</div>
|
|
</Modal.Content>
|
|
</Modal>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default JWTSettings
|