mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
feat(security): let an operator require a second factor
Wire the previously-unused mfaEnforced flag into a real policy. An admin can require a second factor instance-wide (an AppSettings toggle in the General section) or per user (a toggle in user management); the effective requirement is the OR of the two. When required and the account has no active factor — no confirmed TOTP, no registered security key, and no primary passkey — the user is walled behind a forced-enrollment interstitial after sign-in, with only the security settings and sign-out reachable until a factor is enrolled. The gate is surfaced like the onboarding gate: a hint cookie the proxy reads without a DB round-trip, recomputed from the database on every sign-in and on /api/auth/me so a tampered cookie self-corrects. Nobody is locked out — the enrollment surface stays reachable, and toggling the policy off (or the disable-mfa CLI) is the operator's recovery path.
This commit is contained in:
1 parent
135bb04f0d
commit
c8bef61344
22 files changed
+431
-8
No files matched your search
@@ -26,6 +26,8 @@ export interface AdminUser {
|
||||
email: string | null;
|
||||
role: string;
|
||||
createdAt: string;
|
||||
// v1.23 — per-user "require a second factor" override.
|
||||
mfaEnforced: boolean;
|
||||
passkeyCount: number;
|
||||
}
|
||||
|
||||
@@ -65,6 +67,8 @@ export interface SystemStatus {
|
||||
|
||||
export interface AdminSettings {
|
||||
registrationEnabled: boolean;
|
||||
// v1.23 — instance-wide "require a second factor" policy.
|
||||
mfaRequired: boolean;
|
||||
defaultLocale: string;
|
||||
telegramGlobal: boolean;
|
||||
ntfyGlobal: boolean;
|
||||
|
||||
@@ -43,6 +43,19 @@ export function GeneralSettingsSection() {
|
||||
disabled={updateSettings.isPending}
|
||||
/>
|
||||
|
||||
{/* v1.23 — instance-wide "require a second factor" policy. Every
|
||||
account without an active factor is sent to forced enrollment after
|
||||
sign-in. */}
|
||||
<SettingsToggle
|
||||
label={t("admin.mfaRequired")}
|
||||
description={t("admin.mfaRequiredDescription")}
|
||||
checked={settings?.mfaRequired ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings.mutate({ mfaRequired: checked })
|
||||
}
|
||||
disabled={updateSettings.isPending}
|
||||
/>
|
||||
|
||||
{/* v1.4.27 MB7 / CF-56 — match the SettingsToggle stacking
|
||||
contract: stack on `<sm`, side-by-side on `sm+`. */}
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { KeyRound, Loader2, LogOut, Pencil, Shield, Users } from "lucide-react";
|
||||
import {
|
||||
KeyRound,
|
||||
Loader2,
|
||||
LogOut,
|
||||
Pencil,
|
||||
Shield,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -177,6 +185,31 @@ export function UserManagementSection() {
|
||||
<Shield className="h-3 w-3" aria-hidden="true" />
|
||||
{u.role === "ADMIN" ? t("admin.toUser") : t("admin.toAdmin")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`min-h-11 min-w-11 px-3 text-xs ${u.mfaEnforced ? "text-dracula-green" : ""}`}
|
||||
onClick={() =>
|
||||
updateUser.mutate({
|
||||
id: u.id,
|
||||
data: { mfaEnforced: !u.mfaEnforced },
|
||||
})
|
||||
}
|
||||
title={
|
||||
u.mfaEnforced
|
||||
? t("admin.mfaEnforcedOnHint")
|
||||
: t("admin.mfaEnforcedOffHint")
|
||||
}
|
||||
aria-label={
|
||||
u.mfaEnforced
|
||||
? t("admin.mfaEnforcedOnHint")
|
||||
: t("admin.mfaEnforcedOffHint")
|
||||
}
|
||||
aria-pressed={u.mfaEnforced}
|
||||
>
|
||||
<ShieldCheck className="h-3 w-3" aria-hidden="true" />
|
||||
{u.mfaEnforced ? t("admin.mfaEnforcedOn") : t("admin.mfaEnforcedOff")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
Reference in new issue
Block a user