SecretRow.tsx133 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { useParams } from 'common'
3import dayjs from 'dayjs'
4import { Check, Key, Trash } from 'lucide-react'
5import { useState } from 'react'
6import { toast } from 'sonner'
7import { cn } from 'ui'
8import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
9
10import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
11import CopyButton from '@/components/ui/CopyButton'
12import { useClientSecretDeleteMutation } from '@/data/oauth-secrets/client-secret-delete-mutation'
13import { Secret, useClientSecretsQuery } from '@/data/oauth-secrets/client-secrets-query'
14import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query'
15import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
16
17export interface SecretRowProps {
18 secret: Secret
19 appId?: string
20}
21
22export const SecretRow = ({ secret, appId }: SecretRowProps) => {
23 const { slug } = useParams()
24 const [showDeleteModal, setShowDeleteModal] = useState(false)
25 const { can: canManageSecrets } = useAsyncCheckPermissions(PermissionAction.UPDATE, 'oauth_apps')
26
27 const { data } = useClientSecretsQuery({ slug, appId })
28 const secrets = data?.client_secrets ?? []
29 const isLast = secrets.length === 1
30
31 const { data: members = [] } = useOrganizationMembersQuery({ slug })
32 const generatedBy = members.find((x) => x.gotrue_id === secret.created_by)
33 const generatedByName = generatedBy?.username ?? generatedBy?.primary_email ?? secret.created_by
34
35 const { mutate: deleteSecret, isPending: isDeleting } = useClientSecretDeleteMutation({
36 onSuccess: () => {
37 // Show success toast and close modal after successful deletion
38 toast.success('Successfully deleted client secret')
39 setShowDeleteModal(false)
40 },
41 })
42
43 const handleDelete = () => {
44 if (!slug) return console.error('Slug is required')
45 if (!appId) return console.error('App ID is required')
46 deleteSecret({ slug, appId, secretId: secret.id })
47 }
48
49 const isNew = secret.client_secret !== undefined
50
51 return (
52 <>
53 <div
54 className={cn(
55 'flex items-center justify-between p-4 border first:rounded-t last:rounded-b',
56 isNew && 'bg-background-alternative'
57 )}
58 >
59 <div className="flex flex-row gap-6 items-center">
60 <Key size={16} strokeWidth={2} />
61 <div className="flex flex-col gap-1">
62 <div className="flex items-center gap-2">
63 <div className="flex items-center gap-2">
64 {isNew && <Check size={14} className="text-brand" strokeWidth={3} />}
65 <p className="font-mono text-sm">
66 {isNew ? secret.client_secret : `${secret.client_secret_alias}${'*'.repeat(36)}`}
67 </p>
68 {isNew && secret.client_secret && (
69 <CopyButton text={secret.client_secret} type="default" iconOnly />
70 )}
71 </div>
72 </div>
73 <div className="flex flex-col gap-0">
74 <p className="text-sm text-foreground-lighter">
75 Added {isNew ? 'now' : dayjs(secret.created_at).fromNow()} by {generatedByName}
76 </p>
77 {secret.last_used_at && (
78 <p className="text-sm text-foreground-lighter">
79 Last used {dayjs(secret.last_used_at).fromNow()}
80 </p>
81 )}
82 {!secret.last_used_at && !isNew && (
83 <p className="text-sm text-foreground-lighter">Never used</p>
84 )}
85 </div>
86 </div>
87 </div>
88 <div className="flex items-center gap-2">
89 <ButtonTooltip
90 type="default"
91 className="w-7"
92 icon={<Trash />}
93 disabled={!appId || !canManageSecrets || isLast}
94 onClick={() => setShowDeleteModal(true)}
95 tooltip={{
96 content: {
97 className: 'w-64 text-center',
98 side: 'bottom',
99 text: !canManageSecrets
100 ? 'You need additional permissions to delete client secrets'
101 : isLast
102 ? 'You must have at least one client secret for the OAuth application to function.'
103 : undefined,
104 },
105 }}
106 />
107 </div>
108 </div>
109
110 <ConfirmationModal
111 visible={showDeleteModal}
112 variant="destructive"
113 title="Confirm to delete client secret"
114 confirmLabel="Delete"
115 confirmLabelLoading="Deleting"
116 loading={isDeleting}
117 onCancel={() => setShowDeleteModal(false)}
118 onConfirm={handleDelete}
119 disabled={isLast}
120 alert={{
121 title: 'This action cannot be undone',
122 description: 'The client secret will be permanently removed and cannot be recovered.',
123 }}
124 >
125 <p className="text-sm text-foreground-light">
126 {isLast
127 ? 'The last client secret cannot be deleted. Please generate a new secret before deleting this one.'
128 : 'Are you sure you want to delete this client secret?'}
129 </p>
130 </ConfirmationModal>
131 </>
132 )
133}