Konto: Passwort-Anmeldung pro Nutzer deaktivierbar

Schalter im eigenen Konto, erzwingt vorher mindestens einen
registrierten Passkey (sonst Aussperrung). Durchsetzung beim Login:
nach erfolgreicher Passwort-Prüfung wird bei deaktiviertem Flag sofort
wieder abgemeldet; Passkey-Anmeldung bleibt davon unberührt.
This commit is contained in:
2026-07-18 00:46:40 +02:00
parent 00745ae48b
commit 72459d8da5
3 changed files with 94 additions and 3 deletions
+2 -1
View File
@@ -12,7 +12,7 @@ export default async function KontoPage() {
const { data: profile } = await supabase const { data: profile } = await supabase
.from("profiles") .from("profiles")
.select("role, full_name") .select("role, full_name, password_auth_enabled")
.eq("id", user.id) .eq("id", user.id)
.single(); .single();
if (!profile || !["staff", "admin"].includes(profile.role)) redirect("/"); if (!profile || !["staff", "admin"].includes(profile.role)) redirect("/");
@@ -28,6 +28,7 @@ export default async function KontoPage() {
role={profile.role} role={profile.role}
initialFullName={profile.full_name ?? ""} initialFullName={profile.full_name ?? ""}
initialEmail={user.email ?? ""} initialEmail={user.email ?? ""}
initialPasswordAuthEnabled={profile.password_auth_enabled}
/> />
</main> </main>
</div> </div>
+16 -1
View File
@@ -46,7 +46,22 @@ export default function LoginPage() {
setBusy(false); setBusy(false);
return; return;
} }
await redirectByRole(data.user.id);
const { data: profile } = await supabase
.from("profiles")
.select("role, password_auth_enabled")
.eq("id", data.user.id)
.single();
if (profile?.password_auth_enabled === false) {
await supabase.auth.signOut();
setError(
"Die Anmeldung mit Passwort ist für dieses Konto deaktiviert. Bitte mit Passkey anmelden."
);
setBusy(false);
return;
}
router.replace(profile?.role === "admin" ? "/admin" : "/intern");
router.refresh();
} }
async function signInWithPasskey() { async function signInWithPasskey() {
+76 -1
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { createClient } from "@/lib/supabase/client"; import { createClient } from "@/lib/supabase/client";
import PasskeyManager from "@/components/PasskeyManager"; import PasskeyManager from "@/components/PasskeyManager";
@@ -24,10 +24,12 @@ export default function AccountManager({
role, role,
initialFullName, initialFullName,
initialEmail, initialEmail,
initialPasswordAuthEnabled,
}: { }: {
role: string; role: string;
initialFullName: string; initialFullName: string;
initialEmail: string; initialEmail: string;
initialPasswordAuthEnabled: boolean;
}) { }) {
const supabase = useMemo(() => createClient(), []); const supabase = useMemo(() => createClient(), []);
const router = useRouter(); const router = useRouter();
@@ -52,6 +54,21 @@ export default function AccountManager({
const [pwInfo, setPwInfo] = useState<string | null>(null); const [pwInfo, setPwInfo] = useState<string | null>(null);
const [pwBusy, setPwBusy] = useState(false); const [pwBusy, setPwBusy] = useState(false);
// Passwort-Anmeldung an/aus
const [passwordAuthEnabled, setPasswordAuthEnabled] = useState(
initialPasswordAuthEnabled
);
const [passkeyCount, setPasskeyCount] = useState<number | null>(null);
const [authError, setAuthError] = useState<string | null>(null);
const [authInfo, setAuthInfo] = useState<string | null>(null);
const [authBusy, setAuthBusy] = useState(false);
useEffect(() => {
supabase.auth.passkey
.list()
.then(({ data }) => setPasskeyCount((data ?? []).length));
}, [supabase]);
async function saveName(e: React.FormEvent) { async function saveName(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
setNameError(null); setNameError(null);
@@ -129,6 +146,40 @@ export default function AccountManager({
setPwBusy(false); setPwBusy(false);
} }
async function togglePasswordAuth() {
setAuthError(null);
setAuthInfo(null);
const next = !passwordAuthEnabled;
if (!next && (passkeyCount ?? 0) === 0) {
setAuthError(
"Ohne registrierten Passkey kannst du die Passwort-Anmeldung nicht deaktivieren – sonst wärst du ausgesperrt. Registriere zuerst einen Passkey weiter unten."
);
return;
}
if (
!next &&
!window.confirm(
"Anmeldung mit E-Mail und Passwort wirklich deaktivieren? Du kannst dich danach nur noch per Passkey anmelden."
)
)
return;
setAuthBusy(true);
const { error } = await supabase
.from("profiles")
.update({ password_auth_enabled: next })
.eq("id", (await supabase.auth.getUser()).data.user?.id ?? "");
if (error) {
setAuthError(`Speichern fehlgeschlagen: ${error.message}`);
} else {
setPasswordAuthEnabled(next);
setAuthInfo(
next ? "Passwort-Anmeldung aktiviert." : "Passwort-Anmeldung deaktiviert."
);
}
setAuthBusy(false);
}
return ( return (
<div className="max-w-3xl space-y-6"> <div className="max-w-3xl space-y-6">
<div> <div>
@@ -256,6 +307,30 @@ export default function AccountManager({
</form> </form>
</section> </section>
<section className="card">
<h2 className="font-semibold mb-1">Anmeldung mit Passwort</h2>
<p className="text-sm text-gray-600 mb-3">
Du kannst die Anmeldung mit E-Mail und Passwort für dein Konto
deaktivieren und dich künftig ausschließlich per Passkey anmelden.
Dafür muss mindestens ein Passkey registriert sein.
</p>
{authError && (
<p className="mb-2 text-sm text-[var(--c-del)]" role="alert">
{authError}
</p>
)}
{authInfo && <p className="mb-2 text-sm text-green-700">{authInfo}</p>}
<label className="flex items-center gap-2 cursor-pointer text-sm w-fit">
<input
type="checkbox"
checked={passwordAuthEnabled}
onChange={togglePasswordAuth}
disabled={authBusy || passkeyCount === null}
/>
Anmeldung mit E-Mail und Passwort erlaubt
</label>
</section>
<PasskeyManager /> <PasskeyManager />
</div> </div>
); );