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:
+2
-1
@@ -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
@@ -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() {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user