| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- import { zodResolver } from '@hookform/resolvers/zod'
- import dayjs from 'dayjs'
- import { ExternalLink } from 'lucide-react'
- import { useState } from 'react'
- import { useForm, type SubmitHandler } from 'react-hook-form'
- import { toast } from 'sonner'
- import {
- Button,
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogSection,
- DialogSectionSeparator,
- DialogTitle,
- Form,
- FormControl,
- FormField,
- Input,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- WarningIcon,
- } from 'ui'
- import { Admonition } from 'ui-patterns'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import { z } from 'zod'
- import {
- CUSTOM_EXPIRY_VALUE,
- EXPIRES_AT_OPTIONS,
- NON_EXPIRING_TOKEN_VALUE,
- } from '../AccessToken.constants'
- import { getExpirationDate } from '../AccessToken.utils'
- import { DatePicker } from '@/components/ui/DatePicker'
- import {
- useAccessTokenCreateMutation,
- type NewAccessToken,
- } from '@/data/access-tokens/access-tokens-create-mutation'
- import { useTrack } from '@/lib/telemetry/track'
- const formId = 'new-access-token-form'
- const TokenSchema = z.object({
- tokenName: z.string().min(1, 'Please enter a name for the token'),
- expiresAt: z.preprocess(
- (val) => (val === NON_EXPIRING_TOKEN_VALUE ? undefined : val),
- z.string().optional()
- ),
- })
- export interface NewAccessTokenDialogProps {
- open: boolean
- tokenScope: 'V0' | undefined
- onOpenChange: (open: boolean) => void
- onCreateToken: (token: NewAccessToken) => void
- }
- export const NewTokenDialog = ({
- open,
- tokenScope,
- onOpenChange,
- onCreateToken,
- }: NewAccessTokenDialogProps) => {
- const [customExpiryDate, setCustomExpiryDate] = useState<{ date: string } | undefined>(undefined)
- const [isCustomExpiry, setIsCustomExpiry] = useState(false)
- const form = useForm<z.infer<typeof TokenSchema>>({
- resolver: zodResolver(TokenSchema as any),
- defaultValues: { tokenName: '', expiresAt: EXPIRES_AT_OPTIONS['month'].value },
- mode: 'onChange',
- })
- const track = useTrack()
- const { mutate: createAccessToken, isPending } = useAccessTokenCreateMutation()
- const onSubmit: SubmitHandler<z.infer<typeof TokenSchema>> = async (values) => {
- let expiresAt: string | undefined
- if (isCustomExpiry && customExpiryDate) {
- expiresAt = customExpiryDate.date
- } else {
- expiresAt = getExpirationDate(values.expiresAt || '')
- }
- createAccessToken(
- { name: values.tokenName, scope: tokenScope, expires_at: expiresAt },
- {
- onSuccess: (data) => {
- track('access_token_created', {
- tokenType: 'classic',
- expiryPreset: values.expiresAt || 'never',
- })
- toast.success('Access token created successfully')
- onCreateToken(data)
- handleClose()
- },
- }
- )
- }
- const handleClose = () => {
- form.reset({ tokenName: '' })
- setCustomExpiryDate(undefined)
- setIsCustomExpiry(false)
- onOpenChange(false)
- }
- const handleExpiryChange = (value: string) => {
- if (value === CUSTOM_EXPIRY_VALUE) {
- setIsCustomExpiry(true)
- // Set a default custom date (today at 23:59:59)
- const defaultCustomDate = {
- date: dayjs().endOf('day').toISOString(),
- }
- setCustomExpiryDate(defaultCustomDate)
- form.setValue('expiresAt', value)
- } else {
- setIsCustomExpiry(false)
- setCustomExpiryDate(undefined)
- form.setValue('expiresAt', value)
- }
- }
- const handleCustomDateChange = (value: { date: string }) => {
- setCustomExpiryDate(value)
- }
- return (
- <Dialog
- open={open}
- onOpenChange={(open) => {
- if (!open) {
- form.reset()
- setCustomExpiryDate(undefined)
- setIsCustomExpiry(false)
- }
- onOpenChange(open)
- }}
- >
- <DialogContent>
- <DialogHeader>
- <DialogTitle>
- {tokenScope === 'V0' ? 'Generate token for experimental API' : 'Generate New Token'}
- </DialogTitle>
- </DialogHeader>
- <DialogSectionSeparator />
- {tokenScope === 'V0' ? (
- <Admonition
- type="warning"
- className="rounded-none border-t-0 border-x-0"
- title="The experimental API provides additional endpoints which allows you to manage your organizations and projects."
- description={
- <>
- <p>
- These include deleting organizations and projects which cannot be undone. As such,
- be very careful when using this API.
- </p>
- <div className="mt-4">
- <Button asChild type="default" icon={<ExternalLink />}>
- <a href="https://api.supabase.com/api/v0" target="_blank" rel="noreferrer">
- Experimental API documentation
- </a>
- </Button>
- </div>
- </>
- }
- />
- ) : (
- <Admonition
- type="warning"
- className="rounded-none border-t-0 border-x-0"
- title="Access tokens can be used to control your whole account"
- description="Be careful when sharing your tokens"
- />
- )}
- <DialogSection className="flex flex-col gap-4">
- <Form {...form}>
- <form
- id={formId}
- className="flex flex-col gap-4"
- onSubmit={form.handleSubmit(onSubmit)}
- >
- <FormField
- key="tokenName"
- name="tokenName"
- control={form.control}
- render={({ field }) => (
- <FormItemLayout name="tokenName" label="Name">
- <FormControl>
- <Input
- id="tokenName"
- {...field}
- placeholder="Provide a name for your token"
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- <FormField
- key="expiresAt"
- name="expiresAt"
- control={form.control}
- render={({ field }) => (
- <FormItemLayout name="expiresAt" label="Expires in">
- <div className="flex gap-2">
- <FormControl className="grow">
- <Select value={field.value} onValueChange={handleExpiryChange}>
- <SelectTrigger>
- <SelectValue placeholder="Expires at" />
- </SelectTrigger>
- <SelectContent>
- {Object.values(EXPIRES_AT_OPTIONS).map(
- (option: { value: string; label: string }) => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- )
- )}
- </SelectContent>
- </Select>
- </FormControl>
- {isCustomExpiry && (
- <DatePicker
- selectsRange={false}
- triggerButtonSize="small"
- contentSide="top"
- to={customExpiryDate?.date}
- minDate={new Date()}
- maxDate={dayjs().add(1, 'year').toDate()}
- onChange={(date) => {
- if (date.to) handleCustomDateChange({ date: date.to })
- }}
- />
- )}
- </div>
- {field.value === NON_EXPIRING_TOKEN_VALUE && (
- <div className="w-full flex gap-x-2 items-center mt-3 mx-0.5">
- <WarningIcon />
- <span className="text-xs text-left text-foreground-lighter">
- Make sure to keep your non-expiring token safe and secure.
- </span>
- </div>
- )}
- </FormItemLayout>
- )}
- />
- </form>
- </Form>
- </DialogSection>
- <DialogFooter>
- <Button
- type="default"
- disabled={isPending}
- onClick={() => {
- form.reset()
- setCustomExpiryDate(undefined)
- setIsCustomExpiry(false)
- onOpenChange(false)
- }}
- >
- Cancel
- </Button>
- <Button form={formId} htmlType="submit" loading={isPending}>
- Generate token
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
- }
|