import { zodResolver } from '@hookform/resolvers/zod' import dayjs from 'dayjs' import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { useCallback, useState } from 'react' import { useForm, type SubmitHandler } from 'react-hook-form' import { toast } from 'sonner' import { Button, Form, ScrollArea, Separator, Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from 'ui' import { Admonition } from 'ui-patterns' import { CUSTOM_EXPIRY_VALUE, EXPIRES_AT_OPTIONS, type ScopedAccessTokenPermission, } from '../AccessToken.constants' import { TokenSchema, type TokenFormValues } from '../AccessToken.schemas' import { getExpirationDate, mapPermissionToFGA } from '../AccessToken.utils' import { useOrgAndProjectData } from '../hooks/useOrgAndProjectData' import { BasicInfo } from './Form/BasicInfo' import { Permissions } from './Form/Permissions/Permissions' import { ResourceAccess } from './Form/ResourceAccess/ResourceAccess' import { useAccessTokenCreateMutation, type NewScopedAccessToken, type ScopedAccessTokenCreateVariables, } from '@/data/scoped-access-tokens/scoped-access-token-create-mutation' import { useTrack } from '@/lib/telemetry/track' export interface NewScopedTokenSheetProps { visible: boolean onOpenChange: (open: boolean) => void tokenScope: 'V0' | undefined onCreateToken: (token: NewScopedAccessToken) => void } export const NewScopedTokenSheet = ({ visible, onOpenChange, tokenScope, onCreateToken, }: NewScopedTokenSheetProps) => { const [resourceSearchOpen, setResourceSearchOpen] = useState(false) const { organizations, projects } = useOrgAndProjectData() const form = useForm({ resolver: zodResolver(TokenSchema as any), defaultValues: { tokenName: '', expiresAt: EXPIRES_AT_OPTIONS['month'].value, customExpiryDate: undefined, resourceAccess: 'all-orgs', selectedOrganizations: [], selectedProjects: [], permissionRows: [], }, mode: 'onChange', }) const track = useTrack() const { mutate: createAccessToken, isPending } = useAccessTokenCreateMutation() const resourceAccess = form.watch('resourceAccess') const expiresAt = form.watch('expiresAt') const permissionRows = form.watch('permissionRows') || [] const onSubmit: SubmitHandler = async (values) => { if (!permissionRows || permissionRows.length === 0) { toast.error('Please configure at least one permission.') return } const hasValidPermissions = permissionRows.every( (row) => row.resource && row.actions && row.actions.length > 0 ) if (!hasValidPermissions) { toast.error('Please ensure all permissions have both resource and action selected.') return } if (values.resourceAccess === 'selected-orgs') { const selectedOrgs = values.selectedOrganizations || [] if (selectedOrgs.length === 0) { toast.error('Please select at least one organization.') return } const availableOrgSlugs = organizations.map((org) => org.slug) const invalidOrgs = selectedOrgs.filter((slug) => !availableOrgSlugs.includes(slug)) if (invalidOrgs.length > 0) { toast.error( `You don't have access to the following organization(s): ${invalidOrgs.join(', ')}` ) return } } if (values.resourceAccess === 'selected-projects') { const selectedProjects = values.selectedProjects || [] if (selectedProjects.length === 0) { toast.error('Please select at least one project.') return } const availableProjectRefs = projects.map((project) => project.ref) const invalidProjects = selectedProjects.filter((ref) => !availableProjectRefs.includes(ref)) if (invalidProjects.length > 0) { toast.error( `You don't have access to the following project(s): ${invalidProjects.join(', ')}` ) return } } const finalExpiresAt = values.expiresAt === CUSTOM_EXPIRY_VALUE ? values.customExpiryDate : getExpirationDate(values.expiresAt || '') const permissions = permissionRows .flatMap((row) => { const { resource, actions } = row return actions.flatMap((action) => mapPermissionToFGA(resource, action)) }) .filter(Boolean) as ScopedAccessTokenPermission[] if (!permissions || permissions.length === 0) { toast.error('Please configure at least one valid permission.') return } const finalPayload: ScopedAccessTokenCreateVariables = { name: values.tokenName, permissions, } if (finalExpiresAt) { finalPayload.expires_at = finalExpiresAt } if ( values.resourceAccess === 'selected-orgs' && values.selectedOrganizations && values.selectedOrganizations.length > 0 ) { finalPayload.organization_slugs = values.selectedOrganizations } else if ( values.resourceAccess === 'selected-projects' && values.selectedProjects && values.selectedProjects.length > 0 ) { finalPayload.project_refs = values.selectedProjects } if (!finalPayload.name || finalPayload.name.trim() === '') { toast.error('Please enter a token name.') return } if (!finalPayload.permissions || finalPayload.permissions.length === 0) { toast.error('Please configure at least one permission.') return } createAccessToken(finalPayload, { onSuccess: (data) => { track('access_token_created', { tokenType: 'scoped', expiryPreset: values.expiresAt || 'never', resourceAccess: values.resourceAccess, permissionCount: permissions.length, }) toast.success('Access token created successfully') onCreateToken(data) handleClose() }, onError: (error) => { if (error.message && error.message.includes("don't have access")) { toast.error( `Access Error: ${error.message}. Please verify you have access to the selected resources.` ) } else { toast.error(`Failed to create access token: ${error.message}`) } }, }) } const handleClose = () => { form.reset({ tokenName: '', expiresAt: EXPIRES_AT_OPTIONS['month'].value, customExpiryDate: undefined, resourceAccess: 'all-orgs', selectedOrganizations: [], selectedProjects: [], permissionRows: [], }) onOpenChange(false) } const handleCustomDateChange = useCallback( (date: { date: string } | undefined) => { form.setValue('customExpiryDate', date?.date, { shouldValidate: true }) }, [form] ) const handleCustomExpiryChange = useCallback( (isCustom: boolean) => { if (isCustom && !form.getValues('customExpiryDate')) { form.setValue('customExpiryDate', dayjs().endOf('day').toISOString(), { shouldValidate: true, }) } if (!isCustom) { form.setValue('customExpiryDate', undefined, { shouldValidate: true }) } }, [form] ) return ( { if (!open) { handleClose() } else { onOpenChange(open) } }} > {tokenScope === 'V0' ? 'Generate token for experimental API' : 'Generate New Token'} A form to generate a new scoped access token.
{tokenScope === 'V0' && (

These include deleting organizations and projects which cannot be undone. As such, be very careful when using this API.

} />
)}
) }