SecretRow.tsx 5.1 KB

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