Ctrl K
ComunidadGuías…

src/features/account/security-dialogs.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { Fingerprint } from 'lucide-react'
import { useState } from 'react'
import { Field, TextInput } from '@/components/product-form'
import { Button } from '@/components/ui/button'
import { disableTotp, startTotpEnrollment, verifyTotp } from '@/lib/api/account'
import { tt } from '@/lib/i18n'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './account-drawer'
import { accountQueryKeys } from './queries'
import type { MutationHandler, SecurityState } from './types'
import { enrollPasskey, readTotpEnrollment, type TotpEnrollmentDisplay, withTotpQrCode } from './utils'

export function TotpDialogs({
  code,
  dialog,
  mfaRequired,
  mutate,
  password,
  profileEmail,
  setCode,
  setDialog,
  setPassword,
  setTotpEnrollment,
  totpEnrollment,
}: {
  code: string
  dialog: 'mfa-enroll' | 'mfa-verify' | 'mfa-disable' | 'passkey' | null
  mfaRequired: boolean
  mutate: MutationHandler
  password: string
  profileEmail: string
  setCode: (value: string) => void
  setDialog: (dialog: 'mfa-enroll' | 'mfa-verify' | 'mfa-disable' | 'passkey' | null) => void
  setPassword: (value: string) => void
  setTotpEnrollment: (value: TotpEnrollmentDisplay | null) => void
  totpEnrollment: TotpEnrollmentDisplay | null
}) {
  return (
    <>
      <TotpEnrollDialog
        code={code}
        dialog={dialog}
        mutate={mutate}
        password={password}
        profileEmail={profileEmail}
        setCode={setCode}
        setDialog={setDialog}
        setPassword={setPassword}
        setTotpEnrollment={setTotpEnrollment}
        totpEnrollment={totpEnrollment}
      />
      <TotpVerifyDialog code={code} dialog={dialog} mutate={mutate} setCode={setCode} setDialog={setDialog} />
      <TotpDisableDialog
        dialog={dialog}
        mfaRequired={mfaRequired}
        mutate={mutate}
        password={password}
        profileEmail={profileEmail}
        setDialog={setDialog}
        setPassword={setPassword}
      />
    </>
  )
}

function TotpEnrollDialog({
  code,
  dialog,
  mutate,
  password,
  profileEmail,
  setCode,
  setDialog,
  setPassword,
  setTotpEnrollment,
  totpEnrollment,
}: {
  code: string
  dialog: string | null
  mutate: MutationHandler
  password: string
  profileEmail: string
  setCode: (value: string) => void
  setDialog: (dialog: null) => void
  setPassword: (value: string) => void
  setTotpEnrollment: (value: TotpEnrollmentDisplay | null) => void
  totpEnrollment: TotpEnrollmentDisplay | null
}) {
  const [verificationComplete, setVerificationComplete] = useState(false)

  function reset() {
    setCode('')
    setPassword('')
    setTotpEnrollment(null)
    setVerificationComplete(false)
    setDialog(null)
  }

  return (
    <Dialog
      onOpenChange={(open) => {
        if (!open) reset()
      }}
      open={dialog === 'mfa-enroll'}
    >
      <DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-md">
        <form
          onSubmit={async (event) => {
            event.preventDefault()
            if (verificationComplete) {
              reset()
              return
            }
            if (totpEnrollment) {
              const result = await mutate('MFA enabled.', () => verifyTotp({ code, trustDevice: true }), {
                invalidate: [accountQueryKeys.security],
              })
              if (result) {
                setCode('')
                setPassword('')
                setVerificationComplete(true)
              }
              return
            }
            await mutate('TOTP enrollment started.', async () => {
              const enrollment = await startTotpEnrollment({ password })
              setTotpEnrollment(await withTotpQrCode(readTotpEnrollment(enrollment)))
              return enrollment
            })
          }}
        >
          <DialogHeader>
            <DialogTitle>{tt(verificationComplete ? 'Save backup codes' : 'Enroll authenticator app')}</DialogTitle>
            <DialogDescription>
              {tt(
                verificationComplete
                  ? 'Keep these recovery codes somewhere safe before finishing setup.'
                  : totpEnrollment
                    ? 'Scan the QR code, then enter the current code from your authenticator app.'
                    : 'Confirm your password to begin authenticator app setup.',
              )}
            </DialogDescription>
          </DialogHeader>
          <div className="dialogFormBody formStack">
            {verificationComplete && totpEnrollment ? (
              <TotpBackupCodes backupCodes={totpEnrollment.backupCodes} />
            ) : totpEnrollment ? (
              <>
                <TotpEnrollmentDetails enrollment={totpEnrollment} />
                <Field label={tt('Authenticator code')}>
                  <TextInput
                    autoComplete="one-time-code"
                    inputMode="numeric"
                    name="totp-code"
                    onChange={(event) => setCode(event.target.value)}
                    value={code}
                  />
                </Field>
              </>
            ) : (
              <>
                <input autoComplete="username" hidden name="username" readOnly type="text" value={profileEmail} />
                <Field label={tt('Password')}>
                  <TextInput
                    autoComplete="current-password"
                    name="password"
                    onChange={(event) => setPassword(event.target.value)}
                    type="password"
                    value={password}
                  />
                </Field>
              </>
            )}
          </div>
          <DialogFooter>
            {verificationComplete ? (
              <Button type="submit">{tt('Done')}</Button>
            ) : (
              <>
                <Button onClick={reset} type="button" variant="secondary">
                  {tt('Cancel')}
                </Button>
                <Button type="submit" variant="secondary">
                  {totpEnrollment ? tt('Verify code') : tt('Enroll authenticator app')}
                </Button>
              </>
            )}
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  )
}

function TotpVerifyDialog({
  code,
  dialog,
  mutate,
  setCode,
  setDialog,
}: {
  code: string
  dialog: string | null
  mutate: MutationHandler
  setCode: (value: string) => void
  setDialog: (dialog: null) => void
}) {
  return (
    <Dialog
      onOpenChange={(open) => {
        if (!open) {
          setCode('')
          setDialog(null)
        }
      }}
      open={dialog === 'mfa-verify'}
    >
      <DialogContent>
        <form
          onSubmit={async (event) => {
            event.preventDefault()
            const result = await mutate('MFA challenge verified.', () => verifyTotp({ code, trustDevice: true }))
            if (result) setDialog(null)
          }}
        >
          <DialogHeader>
            <DialogTitle>{tt('Verify authenticator code')}</DialogTitle>
            <DialogDescription>{tt('Enter the current code from your authenticator app.')}</DialogDescription>
          </DialogHeader>
          <div className="dialogFormBody formStack">
            <Field label={tt('Authenticator code')}>
              <TextInput
                autoComplete="one-time-code"
                inputMode="numeric"
                name="totp-code"
                onChange={(event) => setCode(event.target.value)}
                value={code}
              />
            </Field>
          </div>
          <DialogFooter>
            <Button onClick={() => setDialog(null)} type="button" variant="secondary">
              {tt('Cancel')}
            </Button>
            <Button type="submit" variant="secondary">
              {tt('Verify code')}
            </Button>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  )
}

function TotpDisableDialog({
  dialog,
  mfaRequired,
  mutate,
  password,
  profileEmail,
  setDialog,
  setPassword,
}: {
  dialog: string | null
  mfaRequired: boolean
  mutate: MutationHandler
  password: string
  profileEmail: string
  setDialog: (dialog: null) => void
  setPassword: (value: string) => void
}) {
  return (
    <Dialog
      onOpenChange={(open) => {
        if (!open) {
          setPassword('')
          setDialog(null)
        }
      }}
      open={dialog === 'mfa-disable'}
    >
      <DialogContent>
        <form
          onSubmit={async (event) => {
            event.preventDefault()
            const result = await mutate('MFA disabled.', () => disableTotp({ password }), {
              invalidate: [accountQueryKeys.security],
            })
            if (result) {
              setPassword('')
              setDialog(null)
            }
          }}
        >
          <DialogHeader>
            <DialogTitle>{tt('Disable MFA')}</DialogTitle>
            <DialogDescription>{tt('Confirm your password to remove authenticator app protection.')}</DialogDescription>
          </DialogHeader>
          <div className="dialogFormBody formStack">
            <input autoComplete="username" hidden name="username" readOnly type="text" value={profileEmail} />
            <Field label={tt('Password')}>
              <TextInput
                autoComplete="current-password"
                name="password"
                onChange={(event) => setPassword(event.target.value)}
                type="password"
                value={password}
              />
            </Field>
          </div>
          <DialogFooter>
            <Button onClick={() => setDialog(null)} type="button" variant="secondary">
              {tt('Cancel')}
            </Button>
            <Button disabled={mfaRequired} type="submit" variant="destructive">
              {tt('Disable authenticator app')}
            </Button>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  )
}

export function PasskeyDialog({
  dialog,
  mutate,
  passkeyName,
  security,
  setDialog,
  setPasskeyName,
}: {
  dialog: string | null
  mutate: MutationHandler
  passkeyName: string
  security: SecurityState | null
  setDialog: (dialog: null) => void
  setPasskeyName: (value: string) => void
}) {
  return (
    <Dialog
      onOpenChange={(open) => {
        if (!open) {
          setPasskeyName('')
          setDialog(null)
        }
      }}
      open={dialog === 'passkey'}
    >
      <DialogContent>
        <form
          onSubmit={async (event) => {
            event.preventDefault()
            const result = await mutate('Passkey enrolled.', () => enrollPasskey(passkeyName), {
              invalidate: [accountQueryKeys.passkeys, accountQueryKeys.security],
            })
            if (result) {
              setPasskeyName('')
              setDialog(null)
            }
          }}
        >
          <DialogHeader>
            <DialogTitle>{tt('Add passkey')}</DialogTitle>
            <DialogDescription>{tt('Create a hardware-backed passkey for this account.')}</DialogDescription>
          </DialogHeader>
          <div className="dialogFormBody formStack">
            <Field label={tt('Passkey name')}>
              <TextInput
                autoComplete="webauthn"
                name="passkey-name"
                onChange={(event) => setPasskeyName(event.target.value)}
                value={passkeyName}
              />
            </Field>
          </div>
          <DialogFooter>
            <Button onClick={() => setDialog(null)} type="button" variant="secondary">
              {tt('Cancel')}
            </Button>
            <Button disabled={!security?.policy.passkeys.enabled} type="submit" variant="secondary">
              <Fingerprint size={18} /> {tt('Add passkey')}
            </Button>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  )
}

function TotpEnrollmentDetails({ enrollment }: { enrollment: TotpEnrollmentDisplay }) {
  return (
    <div className="setupPanel">
      <h3>{tt('Authenticator setup')}</h3>
      {enrollment.qrCode ? (
        <img
          className="setupQr mx-auto"
          src={enrollment.qrCode}
          alt="Authenticator app QR code"
          width="168"
          height="168"
        />
      ) : null}
      {enrollment.secret ? (
        <p>
          <strong>{tt('Manual setup key')}</strong>
          <code>{enrollment.secret}</code>
        </p>
      ) : enrollment.otpAuthUri ? (
        <p>
          <strong>{tt('Enrollment URI')}</strong>
          <code>{enrollment.otpAuthUri}</code>
        </p>
      ) : null}
    </div>
  )
}

function TotpBackupCodes({ backupCodes }: { backupCodes: string[] }) {
  return (
    <div className="setupPanel mt-0">
      <strong>{tt('Backup codes')}</strong>
      <p>{tt('Each code can be used once if you lose access to your authenticator app.')}</p>
      <div className="backupCodeGrid">
        {backupCodes.map((code) => (
          <code key={code}>{code}</code>
        ))}
      </div>
    </div>
  )
}