Login-/Session-/Passkey-Logik vollständig entfernen
Die App läuft künftig nur noch lokal, daher ist Anmeldung nicht mehr nötig: Proxy-Middleware, Login-Seite, Session-Handling, Passkey/WebAuthn (inkl. DB-Tabelle tabletten_passkeys), Logout-Button und der Einstellungen-Tab (enthielt ausschließlich Passkey-Verwaltung) entfallen. Der eigenständig per CHECK_SECRET geschützte Cron-Endpoint /api/check sowie SMTP- und MongoDB-Auth (lib/mailService.ts, lib/mongodb.ts) sind davon unabhängig und bleiben unverändert. Nicht mehr benötigte Abhängigkeiten (@simplewebauthn/*, bcryptjs, jose) sowie die zugehörigen Env-Variablen (AUTH_USERS, AUTH_SECRET, TABLETTEN_RP_*) wurden aus package.json, compose.yml und docker-compose.yml gestrichen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import LogoutButton from '@/components/LogoutButton';
|
||||
import packageJson from '@/package.json';
|
||||
|
||||
interface AppLayoutProps {
|
||||
@@ -11,7 +8,6 @@ interface AppLayoutProps {
|
||||
}
|
||||
|
||||
export default function AppLayout({ children }: AppLayoutProps) {
|
||||
const pathname = usePathname();
|
||||
const version = packageJson.version;
|
||||
const [buildDate] = useState(() =>
|
||||
process.env.NEXT_PUBLIC_BUILD_DATE || new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
@@ -26,16 +22,6 @@ export default function AppLayout({ children }: AppLayoutProps) {
|
||||
|
||||
<div className="app-inner">
|
||||
|
||||
{/* Einstellungen und Logout oben rechts */}
|
||||
<div className="app-logout-bar">
|
||||
{pathname !== '/einstellungen' && (
|
||||
<Link href="/einstellungen" className="btn-settings">
|
||||
Einstellungen
|
||||
</Link>
|
||||
)}
|
||||
<LogoutButton className="btn-logout" />
|
||||
</div>
|
||||
|
||||
{/* Inhaltsbereich */}
|
||||
<main className="app-main">
|
||||
{children}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { logout } from '@/app/login/actions';
|
||||
|
||||
interface LogoutButtonProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function LogoutButton({ className, children }: LogoutButtonProps) {
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={className || 'btn-logout'}
|
||||
>
|
||||
{children || 'Abmelden'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { startRegistration } from '@simplewebauthn/browser';
|
||||
|
||||
interface Passkey {
|
||||
credentialId: string;
|
||||
label: string;
|
||||
createdAt: string | null;
|
||||
lastUsedAt: string | null;
|
||||
}
|
||||
|
||||
function formatDate(iso: string | null): string {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return '—';
|
||||
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
}
|
||||
|
||||
export default function Passkeys() {
|
||||
const [passkeys, setPasskeys] = useState<Passkey[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [label, setLabel] = useState('');
|
||||
|
||||
function load() {
|
||||
return fetch('/api/passkey')
|
||||
.then((r) => (r.ok ? r.json() : Promise.reject(new Error())))
|
||||
.then((data) => setPasskeys(data.passkeys ?? []))
|
||||
.catch(() => setError('Passkeys konnten nicht geladen werden.'))
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
|
||||
async function handleRegister() {
|
||||
setError(null);
|
||||
setBusy(true);
|
||||
try {
|
||||
const optRes = await fetch('/api/passkey/register');
|
||||
if (!optRes.ok) throw new Error('Optionen konnten nicht geladen werden.');
|
||||
const optionsJSON = await optRes.json();
|
||||
|
||||
const response = await startRegistration({ optionsJSON });
|
||||
|
||||
const verifyRes = await fetch('/api/passkey/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ response, label }),
|
||||
});
|
||||
if (!verifyRes.ok) {
|
||||
const data = await verifyRes.json().catch(() => null);
|
||||
throw new Error(data?.error ?? 'Passkey konnte nicht registriert werden.');
|
||||
}
|
||||
setLabel('');
|
||||
await load();
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'NotAllowedError') {
|
||||
setError(null); // Abbruch durch Nutzer
|
||||
} else {
|
||||
setError(err instanceof Error ? err.message : 'Passkey konnte nicht registriert werden.');
|
||||
}
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(credentialId: string) {
|
||||
if (!confirm('Diesen Passkey wirklich entfernen?')) return;
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(`/api/passkey?id=${encodeURIComponent(credentialId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
await load();
|
||||
} catch {
|
||||
setError('Passkey konnte nicht entfernt werden.');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="passkeys">
|
||||
<h3 className="passkeys-title">Passkeys</h3>
|
||||
<p className="passkeys-hint">
|
||||
Melde dich künftig per Fingerabdruck, Gesichtserkennung oder Geräte-PIN an. Das Passwort
|
||||
bleibt als Alternative bestehen.
|
||||
</p>
|
||||
|
||||
{loading ? (
|
||||
<p className="passkeys-empty">Lädt…</p>
|
||||
) : passkeys.length === 0 ? (
|
||||
<p className="passkeys-empty">Noch keine Passkeys registriert.</p>
|
||||
) : (
|
||||
<ul className="passkeys-list">
|
||||
{passkeys.map((pk) => (
|
||||
<li key={pk.credentialId} className="passkeys-item">
|
||||
<div className="passkeys-item-info">
|
||||
<div className="passkeys-item-label">{pk.label}</div>
|
||||
<div className="passkeys-item-meta">
|
||||
Erstellt {formatDate(pk.createdAt)} · Zuletzt {formatDate(pk.lastUsedAt)}
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => handleDelete(pk.credentialId)} className="passkeys-remove">
|
||||
Entfernen
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<div className="passkeys-add">
|
||||
<div className="passkeys-add-field">
|
||||
<label className="passkeys-add-label">Bezeichnung (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
placeholder="z. B. iPhone"
|
||||
maxLength={80}
|
||||
className="passkeys-input"
|
||||
/>
|
||||
</div>
|
||||
<button onClick={handleRegister} disabled={busy} className="passkeys-add-btn">
|
||||
{busy ? 'Läuft…' : 'Passkey hinzufügen'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <div className="login-error passkeys-error">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user