CreateSecretAPIKeyDialog.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { useParams } from 'common'
  3. import { Plus, ShieldCheck } from 'lucide-react'
  4. import { parseAsString, useQueryState } from 'nuqs'
  5. import { useForm, type SubmitHandler } from 'react-hook-form'
  6. import { toast } from 'sonner'
  7. import {
  8. Alert,
  9. AlertDescription,
  10. AlertTitle,
  11. Button,
  12. Dialog,
  13. DialogContent,
  14. DialogDescription,
  15. DialogFooter,
  16. DialogHeader,
  17. DialogSection,
  18. DialogSectionSeparator,
  19. DialogTitle,
  20. DialogTrigger,
  21. Form,
  22. FormControl,
  23. FormField,
  24. Input,
  25. } from 'ui'
  26. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  27. import * as z from 'zod'
  28. import { useAPIKeyCreateMutation } from '@/data/api-keys/api-key-create-mutation'
  29. const NAME_SCHEMA = z
  30. .string()
  31. .min(4, 'Name must be at least 4 characters')
  32. .max(64, "Name can't be more than 64 characters long")
  33. .regex(/^[a-z0-9_]+$/, 'Name can only contain lowercased letters, digits and underscore')
  34. .refine((val: string) => !val.match(/^[0-9].+$/), 'Name must not start with a digit')
  35. .refine(
  36. (val: string) => val !== 'anon' && val !== 'service_role',
  37. 'Using "anon" or "service_role" for API key name is not possible'
  38. )
  39. const FORM_ID = 'create-secret-api-key'
  40. const SCHEMA = z.object({
  41. name: NAME_SCHEMA,
  42. description: z.string().max(256, "Description shouldn't be too long").trim(),
  43. })
  44. export const CreateSecretAPIKeyDialog = () => {
  45. const { ref: projectRef } = useParams()
  46. const [visible, setVisible] = useQueryState('new', parseAsString)
  47. const onOpenChange = (value: boolean) => {
  48. if (value) setVisible('secret')
  49. else setVisible('')
  50. }
  51. const defaultValues = { name: '', description: '' }
  52. const form = useForm<z.infer<typeof SCHEMA>>({
  53. resolver: zodResolver(SCHEMA as any),
  54. defaultValues,
  55. })
  56. const { mutate: createAPIKey, isPending: isCreatingAPIKey } = useAPIKeyCreateMutation()
  57. const onSubmit: SubmitHandler<z.infer<typeof SCHEMA>> = async (values) => {
  58. createAPIKey(
  59. {
  60. projectRef,
  61. type: 'secret',
  62. name: values.name,
  63. description: values.description,
  64. },
  65. {
  66. onSuccess: (data) => {
  67. toast.success(`Your secret API key ${data.prefix}... is ready.`)
  68. form.reset(defaultValues)
  69. onOpenChange(false)
  70. },
  71. }
  72. )
  73. }
  74. return (
  75. <Dialog open={visible === 'secret'} onOpenChange={onOpenChange}>
  76. <DialogTrigger asChild>
  77. <Button type="default" className="mt-2" icon={<Plus />}>
  78. New secret key
  79. </Button>
  80. </DialogTrigger>
  81. <DialogContent>
  82. <DialogHeader>
  83. <DialogTitle>Create new secret API key</DialogTitle>
  84. <DialogDescription className="grid gap-y-2">
  85. <p>
  86. Secret API keys allow elevated access to your project's data, bypassing Row-Level
  87. security.
  88. </p>
  89. </DialogDescription>
  90. </DialogHeader>
  91. <DialogSectionSeparator />
  92. <DialogSection className="flex flex-col gap-4">
  93. <Form {...form}>
  94. <form
  95. className="flex flex-col gap-4"
  96. id={FORM_ID}
  97. onSubmit={form.handleSubmit(onSubmit)}
  98. >
  99. <FormField
  100. key="name"
  101. name="name"
  102. control={form.control}
  103. render={({ field }) => (
  104. <FormItemLayout
  105. label="Name"
  106. description="A short, unique name of lowercased letters, digits and underscore"
  107. >
  108. <FormControl>
  109. <Input {...field} placeholder="Example: my_super_secret_key_123" />
  110. </FormControl>
  111. </FormItemLayout>
  112. )}
  113. />
  114. <FormField
  115. key="description"
  116. name="description"
  117. control={form.control}
  118. render={({ field }) => (
  119. <FormItemLayout label="Description" labelOptional="Optional">
  120. <FormControl>
  121. <Input
  122. {...field}
  123. placeholder="Short notes on how or where this key will be used"
  124. />
  125. </FormControl>
  126. </FormItemLayout>
  127. )}
  128. />
  129. </form>
  130. </Form>
  131. <Alert variant="warning">
  132. <ShieldCheck />
  133. <AlertTitle>Securing your API key</AlertTitle>
  134. <AlertDescription className="">
  135. <ul className="list-disc">
  136. <li>Keep this key secret.</li>
  137. <li>Do not use on the web, in mobile or desktop apps.</li>
  138. <li>Don't post it publicly or commit in source control.</li>
  139. <li>
  140. This key provides elevated access to your data, bypassing Row-Level Security.
  141. </li>
  142. <li>
  143. If it leaks or is revealed, swap it with a new secret API key and then delete it.
  144. </li>
  145. <li>
  146. If used in a browser, it will always return HTTP 401 Unauthorized. Delete
  147. immediately.
  148. </li>
  149. </ul>
  150. </AlertDescription>
  151. </Alert>
  152. </DialogSection>
  153. <DialogFooter>
  154. <Button form={FORM_ID} htmlType="submit" loading={isCreatingAPIKey}>
  155. Create API key
  156. </Button>
  157. </DialogFooter>
  158. </DialogContent>
  159. </Dialog>
  160. )
  161. }