| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416 |
- import { zodResolver } from '@hookform/resolvers/zod'
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useParams } from 'common'
- import { useEffect, useState } from 'react'
- import { useForm } from 'react-hook-form'
- import { toast } from 'sonner'
- import {
- Button,
- Card,
- CardContent,
- CardFooter,
- Form,
- FormControl,
- FormField,
- FormInputGroupInput,
- InputGroup,
- InputGroupAddon,
- InputGroupText,
- Switch,
- } from 'ui'
- import { GenericSkeletonLoader } from 'ui-patterns'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import {
- PageSection,
- PageSectionContent,
- PageSectionMeta,
- PageSectionSummary,
- PageSectionTitle,
- } from 'ui-patterns/PageSection'
- import * as z from 'zod'
- import AlertError from '@/components/ui/AlertError'
- import NoPermission from '@/components/ui/NoPermission'
- import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
- import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
- import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
- import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { IS_PLATFORM } from '@/lib/constants'
- function HoursOrNeverText({ value }: { value: number }) {
- if (value === 0) {
- return 'never'
- } else if (value === 1) {
- return 'hour'
- } else {
- return 'hours'
- }
- }
- const RefreshTokenSchema = z.object({
- REFRESH_TOKEN_ROTATION_ENABLED: z.boolean(),
- SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: z.coerce.number().min(0, 'Must be a value more than 0'),
- })
- const UserSessionsSchema = z.object({
- SESSIONS_TIMEBOX: z.coerce.number().min(0, 'Must be a positive number'),
- SESSIONS_INACTIVITY_TIMEOUT: z.coerce
- .number()
- .multipleOf(0.1)
- .min(0, 'Must be a positive number'),
- SESSIONS_SINGLE_PER_USER: z.boolean(),
- })
- export const SessionsAuthSettingsForm = () => {
- const { ref: projectRef } = useParams()
- const {
- data: authConfig,
- error: authConfigError,
- isError,
- isPending: isLoading,
- } = useAuthConfigQuery({ projectRef })
- const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation()
- // Separate loading states for each form
- const [isUpdatingRefreshTokens, setIsUpdatingRefreshTokens] = useState(false)
- const [isUpdatingUserSessions, setIsUpdatingUserSessions] = useState(false)
- const { can: canReadConfig } = useAsyncCheckPermissions(
- PermissionAction.READ,
- 'custom_config_gotrue'
- )
- const { can: canUpdateConfig } = useAsyncCheckPermissions(
- PermissionAction.UPDATE,
- 'custom_config_gotrue'
- )
- const { hasAccess: hasUserSessionsEntitlement, isLoading: isLoadingEntitlements } =
- useCheckEntitlements('auth.user_sessions')
- const promptProPlanUpgrade = IS_PLATFORM && !hasUserSessionsEntitlement
- const refreshTokenForm = useForm<z.infer<typeof RefreshTokenSchema>>({
- resolver: zodResolver(RefreshTokenSchema as any),
- defaultValues: {
- REFRESH_TOKEN_ROTATION_ENABLED: false,
- SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: 0,
- },
- })
- const userSessionsForm = useForm({
- resolver: zodResolver(UserSessionsSchema as any),
- defaultValues: {
- SESSIONS_TIMEBOX: 0,
- SESSIONS_INACTIVITY_TIMEOUT: 0,
- SESSIONS_SINGLE_PER_USER: false,
- },
- })
- useEffect(() => {
- if (authConfig) {
- // Only reset forms if they're not currently being updated
- if (!isUpdatingRefreshTokens) {
- refreshTokenForm.reset({
- REFRESH_TOKEN_ROTATION_ENABLED: authConfig.REFRESH_TOKEN_ROTATION_ENABLED || false,
- SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: authConfig.SECURITY_REFRESH_TOKEN_REUSE_INTERVAL,
- })
- }
- if (!isUpdatingUserSessions) {
- userSessionsForm.reset({
- SESSIONS_TIMEBOX: authConfig.SESSIONS_TIMEBOX || 0,
- SESSIONS_INACTIVITY_TIMEOUT: authConfig.SESSIONS_INACTIVITY_TIMEOUT || 0,
- SESSIONS_SINGLE_PER_USER: authConfig.SESSIONS_SINGLE_PER_USER || false,
- })
- }
- }
- }, [authConfig, isUpdatingRefreshTokens, isUpdatingUserSessions])
- const onSubmitRefreshTokens = (values: any) => {
- const payload = { ...values }
- setIsUpdatingRefreshTokens(true)
- updateAuthConfig(
- { projectRef: projectRef!, config: payload },
- {
- onError: (error) => {
- toast.error(`Failed to update refresh token settings: ${error?.message}`)
- setIsUpdatingRefreshTokens(false)
- },
- onSuccess: () => {
- toast.success('Successfully updated refresh token settings')
- setIsUpdatingRefreshTokens(false)
- },
- }
- )
- }
- const onSubmitUserSessions = (values: any) => {
- const payload = { ...values }
- setIsUpdatingUserSessions(true)
- updateAuthConfig(
- { projectRef: projectRef!, config: payload },
- {
- onError: (error) => {
- toast.error(`Failed to update user session settings: ${error?.message}`)
- setIsUpdatingUserSessions(false)
- },
- onSuccess: () => {
- toast.success('Successfully updated user session settings')
- setIsUpdatingUserSessions(false)
- },
- }
- )
- }
- if (isError) {
- return (
- <PageSection>
- <PageSectionContent>
- <AlertError error={authConfigError} subject="Failed to retrieve auth configuration" />
- </PageSectionContent>
- </PageSection>
- )
- }
- if (!canReadConfig) {
- return (
- <PageSection>
- <PageSectionContent>
- <NoPermission resourceText="view auth configuration settings" />
- </PageSectionContent>
- </PageSection>
- )
- }
- if (isLoading || isLoadingEntitlements) {
- return (
- <PageSection>
- <PageSectionContent>
- <GenericSkeletonLoader />
- </PageSectionContent>
- </PageSection>
- )
- }
- return (
- <>
- <PageSection>
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Refresh Tokens</PageSectionTitle>
- </PageSectionSummary>
- </PageSectionMeta>
- <PageSectionContent>
- <Form {...refreshTokenForm}>
- <form
- onSubmit={refreshTokenForm.handleSubmit(onSubmitRefreshTokens)}
- className="space-y-4"
- >
- <Card>
- <CardContent>
- <FormField
- control={refreshTokenForm.control}
- name="REFRESH_TOKEN_ROTATION_ENABLED"
- render={({ field }) => (
- <FormItemLayout
- layout="flex-row-reverse"
- label="Detect and revoke potentially compromised refresh tokens"
- description="Prevent replay attacks from potentially compromised refresh tokens."
- >
- <FormControl>
- <Switch
- checked={field.value}
- onCheckedChange={field.onChange}
- disabled={!canUpdateConfig}
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- </CardContent>
- <CardContent>
- <FormField
- control={refreshTokenForm.control}
- name="SECURITY_REFRESH_TOKEN_REUSE_INTERVAL"
- render={({ field }) => (
- <FormItemLayout
- layout="flex-row-reverse"
- label="Refresh token reuse interval"
- description="Time interval where the same refresh token can be used multiple times to request for an access token. Recommendation: 10 seconds."
- >
- <FormControl className="w-full">
- <InputGroup>
- <FormInputGroupInput
- type="number"
- min={0}
- {...field}
- disabled={!canUpdateConfig}
- />
- <InputGroupAddon align="inline-end">
- <InputGroupText>seconds</InputGroupText>
- </InputGroupAddon>
- </InputGroup>
- </FormControl>
- </FormItemLayout>
- )}
- />
- </CardContent>
- <CardFooter className="justify-end space-x-2">
- {refreshTokenForm.formState.isDirty && (
- <Button type="default" onClick={() => refreshTokenForm.reset()}>
- Cancel
- </Button>
- )}
- <Button
- type="primary"
- htmlType="submit"
- disabled={
- !canUpdateConfig ||
- isUpdatingRefreshTokens ||
- !refreshTokenForm.formState.isDirty
- }
- loading={isUpdatingRefreshTokens}
- >
- Save changes
- </Button>
- </CardFooter>
- </Card>
- </form>
- </Form>
- </PageSectionContent>
- </PageSection>
- <PageSection>
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>User Sessions</PageSectionTitle>
- </PageSectionSummary>
- </PageSectionMeta>
- <PageSectionContent>
- <Form {...userSessionsForm}>
- <form
- onSubmit={userSessionsForm.handleSubmit(onSubmitUserSessions)}
- className="space-y-4"
- >
- <Card>
- <CardContent>
- <FormField
- control={userSessionsForm.control}
- name="SESSIONS_SINGLE_PER_USER"
- render={({ field }) => (
- <FormItemLayout
- layout="flex-row-reverse"
- label="Enforce single session per user"
- description="If enabled, all but a user's most recently active session will be terminated."
- >
- <FormControl>
- <Switch
- checked={field.value}
- onCheckedChange={field.onChange}
- disabled={!canUpdateConfig || !hasUserSessionsEntitlement}
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- </CardContent>
- <CardContent>
- <FormField
- control={userSessionsForm.control}
- name="SESSIONS_TIMEBOX"
- render={({ field }) => (
- <FormItemLayout
- layout="flex-row-reverse"
- label="Time-box user sessions"
- description="The amount of time before a user is forced to sign in again. Use 0 for never."
- >
- <FormControl className="w-full">
- <InputGroup>
- <FormInputGroupInput
- type="number"
- min={0}
- {...field}
- disabled={!canUpdateConfig || !hasUserSessionsEntitlement}
- />
- <InputGroupAddon align="inline-end">
- <InputGroupText>
- <HoursOrNeverText value={field.value || 0} />
- </InputGroupText>
- </InputGroupAddon>
- </InputGroup>
- </FormControl>
- </FormItemLayout>
- )}
- />
- </CardContent>
- <CardContent>
- <FormField
- control={userSessionsForm.control}
- name="SESSIONS_INACTIVITY_TIMEOUT"
- render={({ field }) => (
- <FormItemLayout
- layout="flex-row-reverse"
- label="Inactivity timeout"
- description="The amount of time a user needs to be inactive to be forced to sign in again. Use 0 for never."
- >
- <FormControl className="w-full">
- <InputGroup>
- <FormInputGroupInput
- type="number"
- {...field}
- className="flex-1"
- disabled={!canUpdateConfig || !hasUserSessionsEntitlement}
- />
- <InputGroupAddon align="inline-end">
- <InputGroupText>
- <HoursOrNeverText value={field.value || 0} />
- </InputGroupText>
- </InputGroupAddon>
- </InputGroup>
- </FormControl>
- </FormItemLayout>
- )}
- />
- </CardContent>
- {promptProPlanUpgrade && (
- <UpgradeToPro
- fullWidth
- source="authSessions"
- featureProposition="configure user sessions"
- primaryText="Configuring user sessions is only available on the Pro Plan and above"
- secondaryText="Upgrade to Pro Plan to configure settings for user sessions."
- />
- )}
- <CardFooter className="justify-end space-x-2">
- {userSessionsForm.formState.isDirty && (
- <Button type="default" onClick={() => userSessionsForm.reset()}>
- Cancel
- </Button>
- )}
- <Button
- type={promptProPlanUpgrade ? 'default' : 'primary'}
- htmlType="submit"
- disabled={
- !canUpdateConfig ||
- isUpdatingUserSessions ||
- !userSessionsForm.formState.isDirty
- }
- loading={isUpdatingUserSessions}
- >
- Save changes
- </Button>
- </CardFooter>
- </Card>
- </form>
- </Form>
- </PageSectionContent>
- </PageSection>
- </>
- )
- }
|