import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import dayjs from 'dayjs' import { Ban, Check, Copy, Mail, ShieldOff, Trash, X } from 'lucide-react' import Link from 'next/link' import { ComponentProps, ReactNode, useEffect, useState } from 'react' import { toast } from 'sonner' import { Button, cn, Separator } from 'ui' import { Admonition } from 'ui-patterns/admonition' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { PROVIDERS_SCHEMAS } from '../AuthProvidersFormValidation' import { BanUserModal } from './BanUserModal' import { DeleteUserModal } from './DeleteUserModal' import { UserHeader } from './UserHeader' import { PANEL_PADDING } from './Users.constants' import { providerIconMap } from './Users.utils' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import CopyButton from '@/components/ui/CopyButton' import { useAuthConfigQuery } from '@/data/auth/auth-config-query' import { useUserDeleteMFAFactorsMutation } from '@/data/auth/user-delete-mfa-factors-mutation' import { useUserResetPasswordMutation } from '@/data/auth/user-reset-password-mutation' import { useUserSendMagicLinkMutation } from '@/data/auth/user-send-magic-link-mutation' import { useUserSendOTPMutation } from '@/data/auth/user-send-otp-mutation' import { useUserUpdateMutation } from '@/data/auth/user-update-mutation' import { User } from '@/data/auth/users-infinite-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { BASE_PATH } from '@/lib/constants' import { timeout } from '@/lib/helpers' const DATE_FORMAT = 'DD MMM, YYYY HH:mm' const CONTAINER_CLASS = cn( 'bg-surface-100 border-default text-foreground flex items-center justify-between', 'gap-x-4 border px-5 py-4 text-sm first:rounded-tr first:rounded-tl last:rounded-br last:rounded-bl' ) interface UserOverviewProps { user: User onDeleteSuccess: () => void } export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => { const { ref: projectRef } = useParams() const isEmailAuth = user.email !== null const isPhoneAuth = user.phone !== null const isBanned = user.banned_until !== null const isVerified = user.confirmed_at != null const { authenticationSignInProviders } = useIsFeatureEnabled([ 'authentication:sign_in_providers', ]) const providers = ((user.raw_app_meta_data?.providers as string[]) ?? []).map( (provider: string) => { return { name: provider.startsWith('sso') ? 'SAML' : provider, icon: provider === 'email' ? `${BASE_PATH}/img/icons/email-icon2.svg` : providerIconMap[provider] ? `${BASE_PATH}/img/icons/${providerIconMap[provider]}.svg` : undefined, } } ) const { can: canUpdateUser } = useAsyncCheckPermissions(PermissionAction.AUTH_EXECUTE, '*') const { can: canSendMagicLink } = useAsyncCheckPermissions( PermissionAction.AUTH_EXECUTE, 'send_magic_link' ) const { can: canSendRecovery } = useAsyncCheckPermissions( PermissionAction.AUTH_EXECUTE, 'send_recovery' ) const { can: canSendOtp } = useAsyncCheckPermissions(PermissionAction.AUTH_EXECUTE, 'send_otp') const { can: canRemoveUser } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_DELETE, 'auth.users' ) const { can: canRemoveMFAFactors } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_DELETE, 'auth.mfa_factors' ) const [successAction, setSuccessAction] = useState< 'send_magic_link' | 'send_recovery' | 'send_otp' >() const [isBanModalOpen, setIsBanModalOpen] = useState(false) const [isUnbanModalOpen, setIsUnbanModalOpen] = useState(false) const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) const [isDeleteFactorsModalOpen, setIsDeleteFactorsModalOpen] = useState(false) const { data } = useAuthConfigQuery({ projectRef }) const mailerOtpExpiry = data?.MAILER_OTP_EXP ?? 0 const minutes = Math.floor(mailerOtpExpiry / 60) const seconds = Math.floor(mailerOtpExpiry % 60) const formattedExpiry = `${mailerOtpExpiry > 60 ? `${minutes} minute${minutes > 1 ? 's' : ''} ${seconds > 0 ? 'and' : ''} ` : ''}${seconds > 0 ? `${seconds} second${seconds > 1 ? 's' : ''}` : ''}` const { mutate: resetPassword, isPending: isResettingPassword } = useUserResetPasswordMutation({ onSuccess: (_, vars) => { setSuccessAction('send_recovery') toast.success(`Sent password recovery to ${vars.user.email}`) }, onError: (err) => { toast.error(`Failed to send password recovery: ${err.message}`) }, }) const { mutate: sendMagicLink, isPending: isSendingMagicLink } = useUserSendMagicLinkMutation({ onSuccess: (_, vars) => { setSuccessAction('send_magic_link') toast.success( isVerified ? `Sent magic link to ${vars.user.email}` : `Sent confirmation email to ${vars.user.email}` ) }, onError: (err) => { toast.error( isVerified ? `Failed to send magic link: ${err.message}` : `Failed to send confirmation email: ${err.message}` ) }, }) const { mutate: sendOTP, isPending: isSendingOTP } = useUserSendOTPMutation({ onSuccess: (_, vars) => { setSuccessAction('send_otp') toast.success(`Sent OTP to ${vars.user.phone}`) }, onError: (err) => { toast.error(`Failed to send OTP: ${err.message}`) }, }) const { mutate: deleteUserMFAFactors } = useUserDeleteMFAFactorsMutation({ onSuccess: () => { toast.success("Successfully deleted the user's factors") setIsDeleteFactorsModalOpen(false) }, }) const { mutate: updateUser, isPending: isUpdatingUser } = useUserUpdateMutation({ onSuccess: () => { toast.success('Successfully unbanned user') setIsUnbanModalOpen(false) }, }) const handleDeleteFactors = async () => { await timeout(200) if (!projectRef) return console.error('Project ref is required') deleteUserMFAFactors({ projectRef, userId: user.id as string }) } const handleUnban = () => { if (projectRef === undefined) return console.error('Project ref is required') if (user.id === undefined) { return toast.error(`Failed to ban user: User ID not found`) } updateUser({ projectRef, userId: user.id, banDuration: 'none', }) } useEffect(() => { if (successAction !== undefined) { const timer = setTimeout(() => setSuccessAction(undefined), 5000) return () => clearTimeout(timer) } }, [successAction]) return ( <>
{isBanned ? ( ) : ( )}

Provider Information

The user has the following providers

{providers.map((provider) => { const providerMeta = PROVIDERS_SCHEMAS.find( (x) => ('key' in x && x.key === provider.name) || x.title.toLowerCase() === provider.name ) const enabledProperty = provider.name.toLowerCase() === 'web3' ? ( { solana: 'EXTERNAL_WEB3_SOLANA_ENABLED', ethereum: 'EXTERNAL_WEB3_ETHEREUM_ENABLED', } as const )[ ( (user.raw_user_meta_data?.custom_claims as { chain?: string } | undefined) ?.chain ?? '' ).toLowerCase() as 'solana' | 'ethereum' ] : Object.keys(providerMeta?.properties ?? {}).find((x) => x.toLowerCase().endsWith('_enabled') ) const providerName = provider.name === 'email' ? provider.name.toLowerCase() : (providerMeta?.title ?? provider.name) const isActive = data?.[enabledProperty as keyof typeof data] ?? false return (
{provider.icon && ( {`${provider.name} )}

{providerName}

Signed in with a {providerName} account via{' '} {providerName === 'SAML' ? 'SSO' : 'OAuth'}

{authenticationSignInProviders && ( )}
{isActive ? (
Enabled
) : (
Disabled
)}
) })}
{isEmailAuth && ( <> , text: 'Send password recovery', isLoading: isResettingPassword, disabled: !canSendRecovery, onClick: () => { if (projectRef) resetPassword({ projectRef, user }) }, }} success={ successAction === 'send_recovery' ? { title: 'Password recovery sent', description: `The link in the email is valid for ${formattedExpiry}`, } : undefined } /> , text: isVerified ? 'Send magic link' : 'Send confirmation email', isLoading: isSendingMagicLink, disabled: !canSendMagicLink, onClick: () => { if (projectRef) sendMagicLink({ projectRef, user }) }, }} success={ successAction === 'send_magic_link' ? { title: isVerified ? 'Magic link sent' : 'Confirmation email sent', description: isVerified ? `The link in the email is valid for ${formattedExpiry}` : 'The confirmation email has been sent to the user', } : undefined } /> )} {isPhoneAuth && ( , text: 'Send OTP', isLoading: isSendingOTP, disabled: !canSendOtp, onClick: () => { if (projectRef) sendOTP({ projectRef, user }) }, }} success={ successAction === 'send_otp' ? { title: 'OTP sent', description: `The link in the OTP SMS is valid for ${formattedExpiry}`, } : undefined } /> )}

Danger zone

Be wary of the following features as they cannot be undone.

, text: 'Remove MFA factors', disabled: !canRemoveMFAFactors, onClick: () => setIsDeleteFactorsModalOpen(true), }} className="!bg border-destructive-400" /> , text: isBanned ? 'Unban user' : 'Ban user', disabled: !canUpdateUser, onClick: () => { if (isBanned) { setIsUnbanModalOpen(true) } else { setIsBanModalOpen(true) } }, }} className="!bg border-destructive-400" /> , type: 'danger', text: 'Delete user', disabled: !canRemoveUser, onClick: () => setIsDeleteModalOpen(true), }} className="!bg border-destructive-400" />
setIsDeleteModalOpen(false)} onDeleteSuccess={() => { setIsDeleteModalOpen(false) onDeleteSuccess() }} /> setIsDeleteFactorsModalOpen(false)} onConfirm={() => handleDeleteFactors()} alert={{ base: { variant: 'warning' }, title: "Removing MFA factors will drop the user's authentication assurance level (AAL) to AAL1", description: 'Note that this does not sign the user out', }} >

Are you sure you want to remove the MFA factors for the user{' '} {user.email ?? user.phone ?? 'this user'}?

setIsBanModalOpen(false)} /> setIsUnbanModalOpen(false)} onConfirm={() => handleUnban()} >

The user will have access to your project again once unbanned. Are you sure you want to unban this user?

) } export const RowData = ({ property, value }: { property: string; value?: string | boolean }) => { return ( <>

{property}

{typeof value === 'boolean' ? (
{value ? (
) : (
)}
) : (

{!value ? '-' : value}

{!!value && ( } className="transition opacity-0 group-hover:opacity-100 px-1" text={value} /> )}
)}
) } export const RowAction = ({ title, description, button, success, className, }: { title: string description: string button: { icon: ReactNode type?: ComponentProps['type'] text: string disabled?: boolean isLoading?: boolean onClick: () => void } success?: { title: string description: string } className?: string }) => { const disabled = button?.disabled ?? false return (

{success ? success.title : title}

{success ? success.description : description}

: button.icon} loading={button.isLoading ?? false} onClick={button.onClick} disabled={disabled} tooltip={{ content: { side: 'bottom', text: disabled ? `You need additional permissions to ${button.text.toLowerCase()}` : undefined, }, }} > {button.text}
) }