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 ausgaben_passkeys), Logout-Button und der Passwort-Hash-
Generator (scripts/generate-password.js) entfallen. Der Einstellungen-Tab
enthielt ausschließlich Passkey-Verwaltung und wurde daher ebenfalls
entfernt. Nicht mehr benötigte Abhängigkeiten (@simplewebauthn/*,
bcryptjs, jose) sowie die zugehörigen Env-Variablen (AUTH_USERS,
AUTH_SECRET, AUSGABEN_RP_*) wurden aus package.json, Docker-Compose-
Dateien und Dokumentation gestrichen. AUTH_README.md komplett entfernt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 13:58:46 +02:00
parent d3718da1f8
commit bacd11f722
25 changed files with 22 additions and 1816 deletions
-23
View File
@@ -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 || "px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors"}
>
{children || 'Abmelden'}
</button>
);
}
-151
View File
@@ -1,151 +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="max-w-md">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Passkeys</h3>
<p className="text-xs text-gray-500 mb-3">
Melde dich künftig per Fingerabdruck, Gesichtserkennung oder Geräte-PIN an. Das Passwort
bleibt als Alternative bestehen.
</p>
{loading ? (
<p className="text-sm text-gray-500">Lädt</p>
) : passkeys.length === 0 ? (
<p className="text-sm text-gray-500 mb-3">Noch keine Passkeys registriert.</p>
) : (
<ul className="space-y-2 mb-4">
{passkeys.map((pk) => (
<li
key={pk.credentialId}
className="flex items-center justify-between gap-3 border-2 border-gray-300 rounded-lg px-3 py-2 bg-white"
>
<div className="min-w-0">
<div className="text-sm font-medium text-gray-900 truncate">{pk.label}</div>
<div className="text-xs text-gray-500">
Erstellt {formatDate(pk.createdAt)} · Zuletzt {formatDate(pk.lastUsedAt)}
</div>
</div>
<button
onClick={() => handleDelete(pk.credentialId)}
className="text-xs px-2 py-1 text-red-600 hover:text-red-800 shrink-0"
>
Entfernen
</button>
</li>
))}
</ul>
)}
<div className="flex items-end gap-2">
<div className="flex-1">
<label className="block text-xs font-medium text-gray-700 mb-1">
Bezeichnung (optional)
</label>
<input
type="text"
value={label}
onChange={(e) => setLabel(e.target.value)}
placeholder="z. B. iPhone"
maxLength={80}
className="w-full px-3 py-2 border-2 border-gray-400 rounded-lg bg-white text-gray-900 text-sm focus:border-blue-500 focus:outline-none"
/>
</div>
<button
onClick={handleRegister}
disabled={busy}
className="px-4 py-2 bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium rounded-lg transition-colors disabled:opacity-50 text-sm shrink-0"
>
{busy ? 'Läuft…' : 'Passkey hinzufügen'}
</button>
</div>
{error && (
<div className="mt-3 bg-red-50 border border-red-300 text-red-700 px-3 py-2 rounded-lg text-sm">
{error}
</div>
)}
</div>
);
}
+18 -28
View File
@@ -1,7 +1,6 @@
'use client';
import { ReactNode } from 'react';
import LogoutButton from '@/components/LogoutButton';
import packageJson from '@/package.json';
interface Tab {
@@ -9,9 +8,6 @@ interface Tab {
index: number;
}
/** Tab-Index der Einstellungen (0/1 sind die Ausgaben-Typen Haushalt/Privat). */
export const SETTINGS_TAB = 2;
interface TabLayoutProps {
children: ReactNode;
activeTab: number;
@@ -21,7 +17,6 @@ interface TabLayoutProps {
const TABS: Tab[] = [
{ label: 'Haushalt', index: 0 },
{ label: 'Privat', index: 1 },
{ label: 'Einstellungen', index: SETTINGS_TAB },
];
export default function TabLayout({ children, activeTab, onTabChange }: TabLayoutProps) {
@@ -42,29 +37,24 @@ export default function TabLayout({ children, activeTab, onTabChange }: TabLayou
<div className="max-w-6xl mx-auto">
{/* Tab bar */}
<div className="flex justify-between items-end">
<div className="flex">
{TABS.map(tab => {
const isActive = activeTab === tab.index;
return (
<button
key={tab.index}
onClick={() => onTabChange(tab.index)}
className="px-6 py-2 text-sm font-semibold border-t-2 border-l-2 border-r-2 rounded-tl-lg rounded-tr-lg mr-1 transition-colors"
style={
isActive
? { backgroundColor: '#FFFFDD', color: '#000000', borderColor: '#000000', borderBottom: '2px solid #FFFFDD', marginBottom: '-2px', position: 'relative', zIndex: 10 }
: { backgroundColor: '#85B7D7', color: '#374151', borderColor: '#000000' }
}
>
{tab.label}
</button>
);
})}
</div>
<div className="pb-1">
<LogoutButton className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg shadow-md" />
</div>
<div className="flex items-end">
{TABS.map(tab => {
const isActive = activeTab === tab.index;
return (
<button
key={tab.index}
onClick={() => onTabChange(tab.index)}
className="px-6 py-2 text-sm font-semibold border-t-2 border-l-2 border-r-2 rounded-tl-lg rounded-tr-lg mr-1 transition-colors"
style={
isActive
? { backgroundColor: '#FFFFDD', color: '#000000', borderColor: '#000000', borderBottom: '2px solid #FFFFDD', marginBottom: '-2px', position: 'relative', zIndex: 10 }
: { backgroundColor: '#85B7D7', color: '#374151', borderColor: '#000000' }
}
>
{tab.label}
</button>
);
})}
</div>
{/* Content panel */}