import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { useEffect, useMemo, 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 { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { PageSection, PageSectionContent, PageSectionMeta, PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { AccessTokenSchema, createRefreshTokenSchema, createUserSessionsSchema, MAX_REFRESH_TOKEN_REUSE_INTERVAL_SECONDS, MAX_SESSIONS_INACTIVITY_TIMEOUT_HOURS, MAX_SESSIONS_TIMEBOX_HOURS, type AccessTokenFormValues, } from './SessionsAuthSettingsForm.utils' 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' } } 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 [isUpdatingAccessToken, setIsUpdatingAccessToken] = useState(false) 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 // NOTE(fm): The maximums below were introduced after these settings were unbounded, // so they are validated against the currently saved value: a project already above a // maximum can still save the section, but can only move the value into range. // Normalized exactly as the reset() calls below, so an untouched field compares equal. const savedRefreshTokenReuseInterval = authConfig?.SECURITY_REFRESH_TOKEN_REUSE_INTERVAL ?? 0 const savedSessionsTimebox = authConfig?.SESSIONS_TIMEBOX || 0 const savedSessionsInactivityTimeout = authConfig?.SESSIONS_INACTIVITY_TIMEOUT || 0 const refreshTokenResolver = useMemo( () => zodResolver(createRefreshTokenSchema({ savedReuseInterval: savedRefreshTokenReuseInterval })), [savedRefreshTokenReuseInterval] ) const userSessionsResolver = useMemo( () => zodResolver( createUserSessionsSchema({ savedTimebox: savedSessionsTimebox, savedInactivityTimeout: savedSessionsInactivityTimeout, }) ), [savedSessionsTimebox, savedSessionsInactivityTimeout] ) const accessTokenForm = useForm({ resolver: zodResolver(AccessTokenSchema), defaultValues: { JWT_EXP: 3600, }, }) const refreshTokenForm = useForm({ resolver: refreshTokenResolver, defaultValues: { REFRESH_TOKEN_ROTATION_ENABLED: false, SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: 0, }, }) const userSessionsForm = useForm({ resolver: userSessionsResolver, 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 (!isUpdatingAccessToken) { accessTokenForm.reset({ JWT_EXP: authConfig.JWT_EXP ?? 3600, }) } if (!isUpdatingRefreshTokens) { refreshTokenForm.reset({ REFRESH_TOKEN_ROTATION_ENABLED: authConfig.REFRESH_TOKEN_ROTATION_ENABLED || false, SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: savedRefreshTokenReuseInterval, }) } if (!isUpdatingUserSessions) { userSessionsForm.reset({ SESSIONS_TIMEBOX: savedSessionsTimebox, SESSIONS_INACTIVITY_TIMEOUT: savedSessionsInactivityTimeout, SESSIONS_SINGLE_PER_USER: authConfig.SESSIONS_SINGLE_PER_USER || false, }) } } }, [ authConfig, isUpdatingAccessToken, isUpdatingRefreshTokens, isUpdatingUserSessions, savedRefreshTokenReuseInterval, savedSessionsTimebox, savedSessionsInactivityTimeout, ]) const onSubmitAccessToken = (values: AccessTokenFormValues) => { const payload = { ...values } setIsUpdatingAccessToken(true) updateAuthConfig( { projectRef: projectRef!, config: payload }, { onError: (error) => { toast.error(`Failed to update access token settings: ${error?.message}`) setIsUpdatingAccessToken(false) }, onSuccess: () => { toast.success('Successfully updated access token settings') setIsUpdatingAccessToken(false) }, } ) } 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 ( ) } if (!canReadConfig) { return ( ) } if (isLoading || isLoadingEntitlements) { return ( ) } return ( <> User Sessions
( )} /> ( )} /> ( )} /> {promptProPlanUpgrade && ( )} {userSessionsForm.formState.isDirty && ( )}
Access Tokens
( seconds )} /> {accessTokenForm.formState.isDirty && ( )}
Refresh Tokens
( )} /> ( seconds )} /> {refreshTokenForm.formState.isDirty && ( )}
) }