Sécurité

Sessions et sécurité pour un produit d’IA. Appareils actifs avec déconnexion qui fait disparaître les lignes en animation, changement de mot de passe avec jauge de robustesse en direct, configuration de l’authentification à deux facteurs avec un vrai code QR, une vérification à 6 chiffres et des codes de récupération téléchargeables, des passkeys via WebAuthn, et la suppression du compte derrière une confirmation saisie.

Chaque produit d’IA finit par avoir la même page de sécurité : où vous êtes connecté, comment vous vous connectez et comment partir. Security est cette page, construite sur les groupes et lignes du block Settings, avec chaque flux relié à des callbacks asynchrones que vous possédez.

Active sessions liste chaque appareil avec son navigateur, son système, son lieu et sa dernière activité, en temps relatif lisible. Cet appareil est marqué et ne peut pas être déconnecté d’ici. Déconnecter un appareil affiche la progression dans son bouton, puis la ligne se replie et le focus passe à la ligne suivante. Se déconnecter partout ailleurs demande d’abord confirmation et nomme les appareils concernés.

Change password vérifie pendant la saisie : une jauge de robustesse en quatre niveaux et une liste de ce qui manque encore, sans rien bloquer de ce que vous tapez. Si votre serveur indique que le mot de passe actuel est erroné, le message s’affiche sous ce champ et le focus y retourne, avec tout ce que vous avez saisi conservé. Vous pouvez déconnecter les autres sessions dans la même étape.

La configuration de l’authentification à deux facteurs dessine un vrai code QR scannable pour le lien otpauth renvoyé par votre serveur, affiche la clé de configuration avec un bouton de copie quand le scan n’est pas possible, et propose sur mobile d’ouvrir le lien dans une application d’authentification. Le code à 6 chiffres se vérifie dès qu’il est complet, vibre et s’efface en cas d’erreur, puis affiche des codes de récupération à copier ou à télécharger en fichier texte. La désactivation demande d’abord confirmation.

Passkeys appelle votre flux WebAuthn et gère les réponses du navigateur : fermer l’invite n’ajoute rien et n’affiche aucune erreur, et un appareil qui a déjà une passkey le signale. Supprimer le compte exige la phrase exacte, liste ce qui disparaît avec lui, et garde la boîte de dialogue ouverte avec votre message si le serveur refuse.

  1. Ajouter le registre Pro à components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Ajouter votre token

    Créez un token sur votre page de compte et placez-le dans .env.local sous le nom HEXTAUI_PRO_TOKEN.

  3. Ajouter le block

    pnpm dlx shadcn@latest add @hextaui-pro/security

Branchez-le à votre API

Passez ce que le compte a actuellement et un callback asynchrone pour chaque action. Résolvez quand le serveur a terminé et actualisez les données, et chaque partie affiche sa propre progression. Levez une erreur pour afficher votre message d’erreur sur place, et la saisie de la personne est conservée.

"use client"

import * as React from "react"
import { IconShieldLock } from "@tabler/icons-react"

import { SettingsSection, SettingsShell, type SettingsSectionItem } from "../settings/settings"
import { SecuritySettings, type SecurityPasskey, type SecuritySession } from "@/components/blocks/security/security"

const sections: SettingsSectionItem[] = [
  { id: "security", label: "Security", icon: <IconShieldLock />, keywords: ["password", "2fa", "sessions"] },
]

type Account = {
  email: string
  sessions: SecuritySession[]
  passkeys: SecurityPasskey[]
  passwordChangedAt: string | null
  twoFactor: boolean
}

async function call<T = void>(path: string, init?: RequestInit): Promise<T> {
  const response = await fetch(path, { headers: { "content-type": "application/json" }, ...init })
  if (!response.ok) {
    const problem = await response.json().catch(() => null)
    throw new Error(problem?.detail ?? "Check your connection and try again.")
  }
  return response.status === 204 ? (undefined as T) : response.json()
}

export function SecurityPage({ initial }: { initial: Account }) {
  const [account, setAccount] = React.useState(initial)
  const refresh = async () => setAccount(await call<Account>("/api/account/security"))

  return (
    <SettingsShell sections={sections} className="h-svh">
      <SettingsSection id="security">
        <SecuritySettings
          email={account.email}
          sessions={account.sessions}
          onSignOutSession={async (id) => {
            await call(`/api/sessions/${id}`, { method: "DELETE" })
            await refresh()
          }}
          onSignOutOtherSessions={async () => {
            await call("/api/sessions/others", { method: "DELETE" })
            await refresh()
          }}
          passwordChangedAt={account.passwordChangedAt}
          onChangePassword={async (change) => {
            const response = await fetch("/api/password", {
              method: "PUT",
              body: JSON.stringify(change),
            })
            if (response.status === 403) return { currentPassword: "That password is incorrect." }
            if (!response.ok) throw new Error("Couldn’t change your password. Try again.")
            await refresh()
          }}
          twoFactorEnabled={account.twoFactor}
          onStartTwoFactor={() => call<{ uri: string }>("/api/2fa/setup", { method: "POST" })}
          onVerifyTwoFactor={async (code) => {
            const { recoveryCodes } = await call<{ recoveryCodes: string[] }>("/api/2fa/verify", {
              method: "POST",
              body: JSON.stringify({ code }),
            })
            await refresh()
            return recoveryCodes
          }}
          onDisableTwoFactor={async () => {
            await call("/api/2fa", { method: "DELETE" })
            await refresh()
          }}
          passkeys={account.passkeys}
          onAddPasskey={async () => {
            await registerPasskey()
            await refresh()
          }}
          onRemovePasskey={async (id) => {
            await call(`/api/passkeys/${id}`, { method: "DELETE" })
            await refresh()
          }}
          onDeleteAccount={async () => {
            await call("/api/account", { method: "DELETE" })
            window.location.assign("/")
          }}
        />
      </SettingsSection>
    </SettingsShell>
  )
}

async function registerPasskey() {
  const options = await call<PublicKeyCredentialCreationOptionsJSON>("/api/passkeys/options", {
    method: "POST",
  })
  const credential = (await navigator.credentials.create({
    publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(options),
  })) as PublicKeyCredential
  await call("/api/passkeys", { method: "POST", body: JSON.stringify(credential.toJSON()) })
}

Utiliser les parties séparément

SecuritySessions, SecurityPasswordRow, SecurityTwoFactorRow, SecurityPasskeys et SecurityDeleteAccount fonctionnent chacun seul, vous pouvez donc les placer dans n’importe quelle section. Retournez des erreurs de champ depuis onChangePassword pour les afficher sous le champ.

"use client"

import { SettingsGroup, SettingsSection } from "../settings/settings"
import {
  SecurityDeleteAccount,
  SecurityPasswordRow,
  SecuritySessions,
  SecurityTwoFactorRow,
  type SecuritySession,
} from "@/components/blocks/security/security"

export function AccountSection({
  email,
  sessions,
  twoFactor,
  refresh,
}: {
  email: string
  sessions: SecuritySession[]
  twoFactor: boolean
  refresh: () => Promise<void>
}) {
  return (
    <SettingsSection id="account">
      <SettingsGroup title="Sign-in">
        <SecurityPasswordRow
          email={email}
          onChangePassword={async ({ currentPassword, newPassword, signOutOthers }) => {
            const response = await fetch("/api/password", {
              method: "PUT",
              body: JSON.stringify({ currentPassword, newPassword, signOutOthers }),
            })
            if (response.status === 403) return { currentPassword: "That password is incorrect." }
            if (response.status === 422) return { newPassword: "That password showed up in a data breach." }
            if (!response.ok) throw new Error("Couldn’t change your password. Try again.")
          }}
        />
        <SecurityTwoFactorRow
          email={email}
          enabled={twoFactor}
          onStart={() => fetch("/api/2fa/setup", { method: "POST" }).then((response) => response.json())}
          onVerify={async (code) => {
            const response = await fetch("/api/2fa/verify", { method: "POST", body: JSON.stringify({ code }) })
            if (!response.ok) throw new Error("That code didn’t work. Try the newest one.")
            const { recoveryCodes } = await response.json()
            await refresh()
            return recoveryCodes
          }}
          onDisable={() => fetch("/api/2fa", { method: "DELETE" }).then(refresh)}
        />
      </SettingsGroup>
      <SecuritySessions
        title="Where you’re signed in"
        sessions={sessions}
        onSignOut={(id) => fetch(`/api/sessions/${id}`, { method: "DELETE" }).then(refresh)}
        onSignOutOthers={() => fetch("/api/sessions/others", { method: "DELETE" }).then(refresh)}
      />
      <SecurityDeleteAccount
        email={email}
        consequences={["Your workspace and its 3 projects", "Your Team plan, canceled right away"]}
        onDelete={async () => {
          const response = await fetch("/api/account", { method: "DELETE" })
          if (!response.ok) throw new Error("Couldn’t delete your account. Nothing was deleted.")
          window.location.assign("/")
        }}
      />
    </SettingsSection>
  )
}

Anatomie

Les parties à composer, de l’extérieur vers l’intérieur.

PartieDescription
SecuritySettingsTout ce qui suit en un seul appel. Chaque partie apparaît quand vous passez les données et callbacks dont elle a besoin.
SecuritySessionsUn groupe qui liste les appareils connectés, avec déconnexion par appareil et pour toutes les autres sessions.
SecurityPasswordRowUne ligne qui ouvre la boîte de dialogue de changement de mot de passe.
SecurityTwoFactorRowUne ligne avec l’état de l’authentification à deux facteurs, la boîte de configuration et la confirmation de désactivation.
SecurityPasskeysUn groupe qui liste les passkeys, avec ajout et suppression.
SecurityDeleteAccountUn groupe avec la ligne de suppression du compte et sa confirmation saisie.
measurePasswordLa vérification de robustesse utilisée par la boîte de dialogue de mot de passe, pour vos propres formulaires.
PropTypePar défaut
emailL’e-mail du compte. Utilisé dans la phrase de suppression, l’indication pour le gestionnaire de mots de passe et le fichier des codes de récupération.
string–
sessions{ id, browser, os, device?, location?, lastActive, current? }. device vaut "desktop", "laptop", "phone" ou "tablet".
SecuritySession[]–
onSignOutSessionDéconnecte une session. Retirez-la de sessions quand la promesse se résout et la ligne se replie.
(id: string) => Promise<void>–
onSignOutOtherSessionsDéconnecte toutes les sessions sauf la session courante. S’exécute après la confirmation de la personne.
() => Promise<void>–
passwordChangedAtAffiché sous la forme Last changed sous Password.
Date | string | number | null–
onChangePasswordRetournez { currentPassword } ou { newPassword } pour afficher un message sous ce champ, ou levez une erreur pour l’afficher au-dessus des boutons.
({ currentPassword, newPassword, signOutOthers }) => Promise<void | errors>–
twoFactorEnabledIndique si l’authentification à deux facteurs est activée.
booleanfalse
onStartTwoFactorCrée un secret en attente à l’ouverture de la boîte de configuration. uri est le lien otpauth dessiné en code QR ; secret vaut par défaut celui de uri.
() => Promise<{ uri, secret?, recoveryCodes? }>–
onVerifyTwoFactorVérifie le code à 6 chiffres et active l’authentification à deux facteurs. Retournez les codes de récupération pour les afficher, ou levez une erreur si le code est erroné.
(code: string) => Promise<void | string[]>–
onDisableTwoFactorDésactive l’authentification à deux facteurs. S’exécute après la confirmation de la personne.
() => Promise<void>–
passkeys{ id, name, createdAt, lastUsed? }.
SecurityPasskey[]–
onAddPasskeyExécute votre enregistrement WebAuthn. Une NotAllowedError ou AbortError compte comme une annulation et n’affiche rien.
() => Promise<void>–
onRemovePasskeySupprime une passkey. S’exécute après la confirmation de la personne.
(id: string) => Promise<void>–
passkeysSupportedRemplace la vérification de la prise en charge de WebAuthn.
booleandetected
onDeleteAccountSupprime le compte. S’exécute après que la personne a saisi la phrase. Levez une erreur pour garder la boîte de dialogue ouverte avec votre message.
() => Promise<void>–
deleteConsequencesCe qui est supprimé, listé dans la confirmation.
ReactNode[]–

SecuritySessions

Accepte aussi toutes les props de SettingsGroup.

PropTypePar défaut
sessionsLes appareils connectés.
SecuritySession[]–
onSignOutDéconnecte une session.
(id: string) => Promise<void>–
onSignOutOthersDéconnecte toutes les autres sessions.
() => Promise<void>–
titleLe titre du groupe.
ReactNode"Active sessions"
PropTypePar défaut
emailRempli dans un champ de nom d’utilisateur masqué pour que les gestionnaires de mots de passe mettent à jour le bon identifiant.
string–
changedAtDate du dernier changement du mot de passe.
Date | string | number | null–
onChangePasswordIdentique à SecuritySettings.
(change) => Promise<void | errors>–
PropTypePar défaut
emailNommé dans la confirmation et le fichier des codes de récupération.
string–
enabledIndique si l’authentification à deux facteurs est activée.
boolean–
onStartIdentique à onStartTwoFactor.
() => Promise<{ uri, secret?, recoveryCodes? }>–
onVerifyIdentique à onVerifyTwoFactor.
(code: string) => Promise<void | string[]>–
onDisableIdentique à onDisableTwoFactor.
() => Promise<void>–

SecurityPasskeys

Accepte aussi toutes les props de SettingsGroup.

PropTypePar défaut
passkeysLes passkeys enregistrées.
SecurityPasskey[]–
onAddExécute votre enregistrement WebAuthn.
() => Promise<void>–
onRemoveSupprime une passkey.
(id: string) => Promise<void>–
supportedIndique si ce navigateur peut créer des passkeys.
booleandetected

SecurityDeleteAccount

Accepte aussi toutes les props de SettingsGroup.

PropTypePar défaut
emailLa phrase de confirmation est delete suivi de cette valeur.
string–
onDeleteSupprime le compte.
() => Promise<void>–
consequencesCe qui est supprimé avec lui.
ReactNode[]–
titleLe titre du groupe.
ReactNode"Danger zone"
ToucheAction
TabParcourt les lignes et leurs boutons, puis chaque boîte de dialogue.
EnterSoumet le formulaire de mot de passe depuis n’importe quel champ, et supprime le compte depuis le champ de la phrase dès qu’elle correspond.
EscapeFerme une boîte de dialogue, sauf pendant un enregistrement.
0–9Remplit le code de vérification. Il se vérifie tout seul dès que les six chiffres sont saisis, et coller un code fonctionne aussi.
  • Les actions destructrices ne reçoivent jamais le focus en premier. Les confirmations nomment ce qu’elles affectent, comme les appareils déconnectés ou l’e-mail supprimé, et c’est le champ de la phrase de suppression qui reçoit le focus plutôt que le bouton.
  • Quand une ligne se replie, le focus passe au bouton de la ligne suivante, ou de la précédente, donc il ne retombe jamais sur la page. Fermer une boîte de dialogue rend le focus au bouton qui l’a ouverte, ou à son remplaçant quand ce bouton a disparu.
  • Les déconnexions, les passkeys ajoutées et retirées et la robustesse du mot de passe sont annoncées poliment. Les erreurs de vos callbacks sont annoncées comme des alertes et restent à côté du champ ou du bouton auquel elles appartiennent.
  • Chaque exigence de mot de passe indique si elle est satisfaite, le code QR a un libellé textuel, et la clé de configuration et les codes de récupération sont du texte lisible que l’on peut sélectionner.
  • Avec réduction des animations, les lignes s’estompent au lieu de se replier et les changements d’étape ne glissent pas. En mode contraste élevé, le code QR et la jauge de robustesse gardent leurs formes.

Construit avec

Les composants HextaUI gratuits dont Security est constitué. Chacun s’installe séparément.

Code

12 fichiers, ajoutés à components/blocks/security.