From 31b31c779b6d03863f078d6dbfc36236a19759c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20X=2E=20F=C3=BCrst?= Date: Fri, 21 Aug 2026 13:42:58 +0200 Subject: [PATCH] =?UTF-8?q?Login-/Session-/Passkey-Logik=20vollst=C3=A4ndi?= =?UTF-8?q?g=20entfernen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) und der Logout-Button 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, RP_ID, RP_ORIGIN, RP_NAME) wurden aus package.json, Docker-Compose-Dateien und Dokumentation gestrichen. Co-Authored-By: Claude Sonnet 5 --- .env.example | 7 - DEPLOYMENT.md | 12 -- README.md | 18 -- app/api/passkey/authenticate/route.ts | 28 --- app/api/passkey/register/route.ts | 36 ---- app/api/passkey/route.ts | 40 ---- app/einstellungen/page.tsx | 13 -- app/login/actions.ts | 28 --- app/login/page.tsx | 175 --------------- components/LogoutButton.tsx | 23 -- components/Passkeys.tsx | 151 ------------- components/TabLayout.tsx | 43 ++-- create_table.sql | 15 -- docker-compose.local.yml | 6 - docker-compose.prod.yml | 6 - lib/auth.ts | 39 ---- lib/passkeys.ts | 142 ------------- lib/session.ts | 102 --------- lib/webauthn.ts | 176 --------------- package-lock.json | 295 +------------------------- package.json | 5 - proxy.ts | 73 ------- 22 files changed, 20 insertions(+), 1413 deletions(-) delete mode 100644 app/api/passkey/authenticate/route.ts delete mode 100644 app/api/passkey/register/route.ts delete mode 100644 app/api/passkey/route.ts delete mode 100644 app/einstellungen/page.tsx delete mode 100644 app/login/actions.ts delete mode 100644 app/login/page.tsx delete mode 100644 components/LogoutButton.tsx delete mode 100644 components/Passkeys.tsx delete mode 100644 lib/auth.ts delete mode 100644 lib/passkeys.ts delete mode 100644 lib/session.ts delete mode 100644 lib/webauthn.ts delete mode 100644 proxy.ts diff --git a/.env.example b/.env.example index ddf2e12..c352b08 100644 --- a/.env.example +++ b/.env.example @@ -6,11 +6,4 @@ DB_USER=root DB_PASS=MyPassword DB_NAME=RXF -# Passkeys (WebAuthn) — Relying Party -# RP_ID = nur der Host (ohne Protokoll/Port), RP_ORIGIN = volle URL -# Lokal: RP_ID=localhost / RP_ORIGIN=http://localhost:3000 -RP_ID=werte.fuerst-stuttgart.de -RP_ORIGIN=https://werte.fuerst-stuttgart.de -RP_NAME=Werte-Log - # Build-Datum wird automatisch beim Build gesetzt - nicht hier definieren! diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index ceb66c2..791ae69 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -41,22 +41,10 @@ Passen Sie die Werte in der `.env`-Datei an: DB_USER=root DB_PASS=IhrMySQLPasswort DB_NAME=RXF - -# Passkeys (WebAuthn): Host bzw. volle URL, unter der die App erreichbar ist -RP_ID=werte.fuerst-stuttgart.de -RP_ORIGIN=https://werte.fuerst-stuttgart.de -RP_NAME=Werte-Log ``` **Wichtig**: Die `.env`-Datei enthält sensible Daten und wird NICHT ins Git-Repository committed! -**Hinweis zu Passkeys**: `RP_ID` muss exakt dem Hostnamen entsprechen (ohne `https://` -und ohne Port), `RP_ORIGIN` der vollen URL. Passen die Werte nicht zur aufgerufenen -Adresse, schlägt die Passkey-Anmeldung fehl. Bereits registrierte Passkeys werden bei -einer Änderung von `RP_ID` ungültig. WebAuthn setzt HTTPS voraus (via Traefik). -Die Tabelle `passkeys` legt die Anwendung beim ersten Zugriff selbst an — hat der -DB-Benutzer keine CREATE-Rechte, das SQL aus `create_table.sql` manuell ausführen. - ### 3. Docker Images bauen und Container starten ```bash diff --git a/README.md b/README.md index 5e2f97f..624c6d5 100644 --- a/README.md +++ b/README.md @@ -16,24 +16,6 @@ Eine moderne Next.js-Anwendung zur Erfassung und Verwaltung von Gesundheitsdaten - ✅ **Responsive Design** mit Tailwind CSS - ✅ **TypeScript** für Type-Safety - ✅ **MySQL-Datenbankanbindung** (Docker-Container) -- ✅ **Anmeldung per Passkey** (WebAuthn) alternativ zu Benutzername/Passwort - -## Anmeldung & Passkeys - -Benutzer und bcrypt-Passworthashes stehen in `AUTH_USERS` -(Format `name:hash,name2:hash2`), die Session ist ein JWT-Cookie (`AUTH_SECRET`). - -Zusätzlich kann sich jeder Benutzer per **Passkey** anmelden (Fingerabdruck, -Gesichtserkennung oder Geräte-PIN). Das Passwort bleibt als Alternative bestehen. - -- **Registrieren**: nach der Anmeldung im Tab *Einstellungen* → „Passkey hinzufügen". - Der Passkey wird dem angemeldeten Benutzer zugeordnet; mehrere Geräte sind möglich. -- **Anmelden**: auf der Login-Seite „Mit Passkey anmelden". -- **Speicherung**: Tabelle `passkeys` (siehe `create_table.sql`); sie wird beim ersten - Zugriff automatisch angelegt, falls der DB-Benutzer CREATE-Rechte hat. -- **Konfiguration**: `RP_ID` (nur der Host), `RP_ORIGIN` (volle URL), `RP_NAME`. - Lokal sind `localhost` / `http://localhost:3000` voreingestellt. -- **Wichtig**: WebAuthn funktioniert nur über **HTTPS** oder auf `localhost`. ## Technologie-Stack diff --git a/app/api/passkey/authenticate/route.ts b/app/api/passkey/authenticate/route.ts deleted file mode 100644 index 2d61442..0000000 --- a/app/api/passkey/authenticate/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { buildAuthenticationOptions, finishAuthentication } from '@/lib/webauthn'; -import { createSession } from '@/lib/session'; - -// Öffentlich (kein Login): Optionen für die Anmeldung per Passkey. -export async function GET() { - try { - return NextResponse.json(await buildAuthenticationOptions()); - } catch (error) { - console.error('GET /api/passkey/authenticate:', error); - return NextResponse.json({ error: 'Serverfehler' }, { status: 500 }); - } -} - -// Öffentlich: Verifiziert die Anmelde-Antwort und erstellt bei Erfolg die Session. -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const result = await finishAuthentication(body?.response); - if (!result.ok) return NextResponse.json({ error: result.error }, { status: 401 }); - - await createSession(result.username); - return NextResponse.json({ ok: true }); - } catch (error) { - console.error('POST /api/passkey/authenticate:', error); - return NextResponse.json({ error: 'Serverfehler' }, { status: 500 }); - } -} diff --git a/app/api/passkey/register/route.ts b/app/api/passkey/register/route.ts deleted file mode 100644 index af684c7..0000000 --- a/app/api/passkey/register/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getSession } from '@/lib/session'; -import { buildRegistrationOptions, finishRegistration } from '@/lib/webauthn'; - -// Optionen für die Registrierung eines neuen Passkeys (Session erforderlich). -export async function GET() { - const session = await getSession(); - if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 }); - - try { - return NextResponse.json(await buildRegistrationOptions(session.username)); - } catch (error) { - console.error('GET /api/passkey/register:', error); - return NextResponse.json({ error: 'Serverfehler' }, { status: 500 }); - } -} - -// Verifiziert die Registrierungs-Antwort und speichert den Passkey. -export async function POST(request: NextRequest) { - const session = await getSession(); - if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 }); - - try { - const body = await request.json(); - const result = await finishRegistration( - session.username, - body?.response, - String(body?.label ?? '') - ); - if (!result.ok) return NextResponse.json({ error: result.error }, { status: 400 }); - return NextResponse.json({ ok: true }); - } catch (error) { - console.error('POST /api/passkey/register:', error); - return NextResponse.json({ error: 'Serverfehler' }, { status: 500 }); - } -} diff --git a/app/api/passkey/route.ts b/app/api/passkey/route.ts deleted file mode 100644 index 86291ec..0000000 --- a/app/api/passkey/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getSession } from '@/lib/session'; -import { listPasskeys, deletePasskey } from '@/lib/passkeys'; - -// Liste der eigenen Passkeys (ohne öffentlichen Schlüssel). -export async function GET() { - const session = await getSession(); - if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 }); - - try { - const passkeys = (await listPasskeys(session.username)).map((pk) => ({ - credentialId: pk.credentialId, - label: pk.label, - createdAt: pk.createdAt, - lastUsedAt: pk.lastUsedAt, - })); - return NextResponse.json({ passkeys }); - } catch (error) { - console.error('GET /api/passkey:', error); - return NextResponse.json({ error: 'Serverfehler' }, { status: 500 }); - } -} - -// Löscht einen eigenen Passkey anhand seiner credentialId (?id=...). -export async function DELETE(request: NextRequest) { - const session = await getSession(); - if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 }); - - const id = request.nextUrl.searchParams.get('id'); - if (!id) return NextResponse.json({ error: 'Keine ID angegeben.' }, { status: 400 }); - - try { - const removed = await deletePasskey(id, session.username); - if (!removed) return NextResponse.json({ error: 'Passkey nicht gefunden.' }, { status: 404 }); - return NextResponse.json({ ok: true }); - } catch (error) { - console.error('DELETE /api/passkey:', error); - return NextResponse.json({ error: 'Serverfehler' }, { status: 500 }); - } -} diff --git a/app/einstellungen/page.tsx b/app/einstellungen/page.tsx deleted file mode 100644 index 4f0a227..0000000 --- a/app/einstellungen/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -'use client'; - -import TabLayout from '@/components/TabLayout'; -import Passkeys from '@/components/Passkeys'; - -export default function EinstellungenPage() { - return ( - -

Einstellungen

- -
- ); -} diff --git a/app/login/actions.ts b/app/login/actions.ts deleted file mode 100644 index 5438a3c..0000000 --- a/app/login/actions.ts +++ /dev/null @@ -1,28 +0,0 @@ -'use server'; - -import { verifyCredentials } from '@/lib/auth'; -import { createSession, deleteSession } from '@/lib/session'; -import { redirect } from 'next/navigation'; - -export async function login(prevState: any, formData: FormData) { - const username = formData.get('username') as string; - const password = formData.get('password') as string; - - if (!username || !password) { - return { error: 'Bitte Benutzername und Passwort eingeben' }; - } - - const isValid = await verifyCredentials(username, password); - - if (!isValid) { - return { error: 'Ungültige Anmeldedaten' }; - } - - await createSession(username); - redirect('/'); -} - -export async function logout() { - await deleteSession(); - redirect('/login'); -} diff --git a/app/login/page.tsx b/app/login/page.tsx deleted file mode 100644 index b91fa05..0000000 --- a/app/login/page.tsx +++ /dev/null @@ -1,175 +0,0 @@ -'use client'; - -import { useActionState, useState } from 'react'; -import { startAuthentication } from '@simplewebauthn/browser'; -import { login } from './actions'; -import packageJson from '@/package.json'; - -export default function LoginPage() { - const [state, loginAction, isPending] = useActionState(login, undefined); - const [showPassword, setShowPassword] = useState(false); - const [passkeyBusy, setPasskeyBusy] = useState(false); - const [passkeyError, setPasskeyError] = useState(null); - - async function handlePasskeyLogin() { - setPasskeyError(null); - setPasskeyBusy(true); - try { - const optRes = await fetch('/api/passkey/authenticate'); - if (!optRes.ok) throw new Error('Optionen konnten nicht geladen werden.'); - const optionsJSON = await optRes.json(); - - const response = await startAuthentication({ optionsJSON }); - - const verifyRes = await fetch('/api/passkey/authenticate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ response }), - }); - if (!verifyRes.ok) { - const data = await verifyRes.json().catch(() => null); - throw new Error(data?.error ?? 'Anmeldung mit Passkey fehlgeschlagen.'); - } - window.location.href = '/'; - } catch (err) { - // Abbruch durch Nutzer (NotAllowedError) nicht als Fehler anzeigen. - if (err instanceof Error && err.name === 'NotAllowedError') { - setPasskeyError(null); - } else { - setPasskeyError(err instanceof Error ? err.message : 'Anmeldung mit Passkey fehlgeschlagen.'); - } - } finally { - setPasskeyBusy(false); - } - } - - const version = packageJson.version; - const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE || new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); - - return ( -
-
-
-

Werte - Log

-
- -
-
-

Anmeldung

- -
-
- - -
- -
- -
- - -
-
- - {state?.error && ( -
- {state.error} -
- )} - - -
- -
-
- oder -
-
- - - - {passkeyError && ( -
- {passkeyError} -
- )} -
-
- - {/* Footer */} - -
-
- ); -} - diff --git a/components/LogoutButton.tsx b/components/LogoutButton.tsx deleted file mode 100644 index 1423a9e..0000000 --- a/components/LogoutButton.tsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/components/Passkeys.tsx b/components/Passkeys.tsx deleted file mode 100644 index d6eb7d6..0000000 --- a/components/Passkeys.tsx +++ /dev/null @@ -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([]); - const [loading, setLoading] = useState(true); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(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 ( -
-

Passkeys

-

- Melde dich künftig per Fingerabdruck, Gesichtserkennung oder Geräte-PIN an. Das Passwort - bleibt als Alternative bestehen. -

- - {loading ? ( -

Lädt…

- ) : passkeys.length === 0 ? ( -

Noch keine Passkeys registriert.

- ) : ( -
    - {passkeys.map((pk) => ( -
  • -
    -
    {pk.label}
    -
    - Erstellt {formatDate(pk.createdAt)} · Zuletzt {formatDate(pk.lastUsedAt)} -
    -
    - -
  • - ))} -
- )} - -
-
- - 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" - /> -
- -
- - {error && ( -
- {error} -
- )} -
- ); -} diff --git a/components/TabLayout.tsx b/components/TabLayout.tsx index 32b905d..9493774 100644 --- a/components/TabLayout.tsx +++ b/components/TabLayout.tsx @@ -2,7 +2,6 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import LogoutButton from '@/components/LogoutButton'; import packageJson from '@/package.json'; interface TabLayoutProps { @@ -12,7 +11,6 @@ interface TabLayoutProps { const TABS = [ { href: '/', label: 'Eingabe' }, { href: '/charts', label: 'Verlauf' }, - { href: '/einstellungen', label: 'Einstellungen' }, ]; export default function TabLayout({ children }: TabLayoutProps) { @@ -32,29 +30,24 @@ export default function TabLayout({ children }: TabLayoutProps) {
{/* Tab bar */} -
-
- {TABS.map(tab => { - const isActive = pathname === tab.href; - return ( - - {tab.label} - - ); - })} -
-
- -
+
+ {TABS.map(tab => { + const isActive = pathname === tab.href; + return ( + + {tab.label} + + ); + })}
{/* Content panel */} diff --git a/create_table.sql b/create_table.sql index ec9610a..f46d2e0 100644 --- a/create_table.sql +++ b/create_table.sql @@ -16,18 +16,3 @@ CREATE TABLE IF NOT EXISTS Werte_BZG ( Puls INT NULL COMMENT 'Pulse rate', INDEX idx_datum_zeit (Datum, Zeit) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; - --- WebAuthn-Passkeys je Benutzer (Benutzer stammen aus AUTH_USERS). --- Die Anwendung legt diese Tabelle beim ersten Zugriff selbst an (lib/passkeys.ts), --- sofern der DB-Benutzer CREATE-Rechte hat. -CREATE TABLE IF NOT EXISTS passkeys ( - credential_id VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL PRIMARY KEY, - username VARCHAR(64) NOT NULL, - public_key VARBINARY(1024) NOT NULL, - counter INT UNSIGNED NOT NULL DEFAULT 0, - transports VARCHAR(255) NOT NULL DEFAULT '[]', - label VARCHAR(80) NOT NULL DEFAULT '', - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_used_at DATETIME NULL, - INDEX idx_passkeys_username (username) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; diff --git a/docker-compose.local.yml b/docker-compose.local.yml index ce05349..3475267 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -18,12 +18,6 @@ services: - DB_USER=${DB_USER} - DB_PASS=${DB_PASS} - DB_NAME=${DB_NAME} - - AUTH_USERS=${AUTH_USERS} - - AUTH_SECRET=${AUTH_SECRET} - # Passkeys (WebAuthn) — WebAuthn braucht HTTPS oder localhost - - RP_ID=${RP_ID:-localhost} - - RP_ORIGIN=${RP_ORIGIN:-http://localhost:3000} - - RP_NAME=${RP_NAME:-Werte-Log} networks: - sternwarte_default diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 77270b6..62d4247 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -12,12 +12,6 @@ services: - DB_USER=${DB_USER} - DB_PASS=${DB_PASS} - DB_NAME=${DB_NAME} - - AUTH_USERS=${AUTH_USERS} - - AUTH_SECRET=${AUTH_SECRET} - # Passkeys (WebAuthn): Host bzw. volle URL der Anwendung - - RP_ID=${RP_ID:-werte.fuerst-stuttgart.de} - - RP_ORIGIN=${RP_ORIGIN:-https://werte.fuerst-stuttgart.de} - - RP_NAME=${RP_NAME:-Werte-Log} labels: - traefik.enable=true - traefik.http.routers.werte.entrypoints=http diff --git a/lib/auth.ts b/lib/auth.ts deleted file mode 100644 index 94abb10..0000000 --- a/lib/auth.ts +++ /dev/null @@ -1,39 +0,0 @@ -import bcrypt from 'bcryptjs'; - -export interface User { - username: string; - password: string; -} - -export function getUsers(): User[] { - const usersString = process.env.AUTH_USERS || ''; - if (!usersString) { - console.warn('AUTH_USERS not configured in .env'); - return []; - } - return usersString - .split(',') - .map((userPair) => { - const [username, password] = userPair.trim().split(':'); - return { username: username?.trim(), password: password?.trim() }; - }) - .filter((user) => user.username && user.password); -} - -export async function verifyCredentials(username: string, password: string): Promise { - const users = getUsers(); - const user = users.find(u => u.username === username); - if (!user) { - return false; - } - return bcrypt.compare(password, user.password); -} - -/** Prüft, ob der Benutzername (noch) in AUTH_USERS hinterlegt ist. */ -export function isKnownUser(username: string): boolean { - return getUsers().some(u => u.username === username); -} - -export function isAuthEnabled(): boolean { - return !!process.env.AUTH_USERS; -} diff --git a/lib/passkeys.ts b/lib/passkeys.ts deleted file mode 100644 index 80e03c9..0000000 --- a/lib/passkeys.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { getPool } from './db'; -import type { RowDataPacket, ResultSetHeader } from 'mysql2/promise'; - -const TABLE = 'passkeys'; - -/** Ein gespeicherter Passkey (WebAuthn-Credential), einem Benutzer zugeordnet. */ -export interface PasskeyRecord { - credentialId: string; // base64url - username: string; - publicKey: Uint8Array; - counter: number; - transports: string[]; - label: string; - createdAt: Date; - lastUsedAt: Date | null; -} - -interface PasskeyRow extends RowDataPacket { - credential_id: string; - username: string; - public_key: Buffer; - counter: number; - transports: string; - label: string; - created_at: Date; - last_used_at: Date | null; -} - -let schemaReady: Promise | null = null; - -/** - * Legt die Passkey-Tabelle beim ersten Zugriff an (einmal pro Prozess). - * Das Schema steht zusätzlich in create_table.sql. - */ -function ensureSchema(): Promise { - if (!schemaReady) { - schemaReady = getPool() - .query( - `CREATE TABLE IF NOT EXISTS ${TABLE} ( - credential_id VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL PRIMARY KEY, - username VARCHAR(64) NOT NULL, - public_key VARBINARY(1024) NOT NULL, - counter INT UNSIGNED NOT NULL DEFAULT 0, - transports VARCHAR(255) NOT NULL DEFAULT '[]', - label VARCHAR(80) NOT NULL DEFAULT '', - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_used_at DATETIME NULL, - INDEX idx_passkeys_username (username) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci` - ) - .then(() => undefined) - .catch((error) => { - schemaReady = null; // nächster Aufruf darf es erneut versuchen - throw error; - }); - } - return schemaReady; -} - -function fromRow(row: PasskeyRow): PasskeyRecord { - let transports: string[] = []; - try { - const parsed = JSON.parse(row.transports); - if (Array.isArray(parsed)) transports = parsed; - } catch { - /* fehlerhafte transports ignorieren */ - } - return { - credentialId: row.credential_id, - username: row.username, - // Uint8Array.from kopiert in einen frischen ArrayBuffer (passt zum von - // @simplewebauthn erwarteten Uint8Array). - publicKey: Uint8Array.from(row.public_key), - counter: row.counter, - transports, - label: row.label, - createdAt: row.created_at, - lastUsedAt: row.last_used_at, - }; -} - -/** Alle Passkeys eines Benutzers, neueste zuerst. */ -export async function listPasskeys(username: string): Promise { - await ensureSchema(); - const [rows] = await getPool().execute( - `SELECT * FROM ${TABLE} WHERE username = ? ORDER BY created_at DESC`, - [username] - ); - return rows.map(fromRow); -} - -/** Passkey anhand der Credential-ID — benutzerübergreifend, für die Anmeldung. */ -export async function getPasskey(credentialId: string): Promise { - await ensureSchema(); - const [rows] = await getPool().execute( - `SELECT * FROM ${TABLE} WHERE credential_id = ?`, - [credentialId] - ); - return rows.length > 0 ? fromRow(rows[0]) : undefined; -} - -export async function insertPasskey(record: { - credentialId: string; - username: string; - publicKey: Uint8Array; - counter: number; - transports: string[]; - label: string; -}): Promise { - await ensureSchema(); - await getPool().execute( - `INSERT INTO ${TABLE} (credential_id, username, public_key, counter, transports, label) - VALUES (?, ?, ?, ?, ?, ?)`, - [ - record.credentialId, - record.username, - Buffer.from(record.publicKey), - record.counter, - JSON.stringify(record.transports ?? []), - record.label, - ] - ); -} - -/** Aktualisiert Counter und last_used_at nach erfolgreicher Anmeldung (Replay-Schutz). */ -export async function updatePasskeyUsage(credentialId: string, counter: number): Promise { - await ensureSchema(); - await getPool().execute( - `UPDATE ${TABLE} SET counter = ?, last_used_at = NOW() WHERE credential_id = ?`, - [counter, credentialId] - ); -} - -/** Löscht einen Passkey — nur wenn er dem angegebenen Benutzer gehört. */ -export async function deletePasskey(credentialId: string, username: string): Promise { - await ensureSchema(); - const [result] = await getPool().execute( - `DELETE FROM ${TABLE} WHERE credential_id = ? AND username = ?`, - [credentialId, username] - ); - return result.affectedRows > 0; -} diff --git a/lib/session.ts b/lib/session.ts deleted file mode 100644 index d9bd8e0..0000000 --- a/lib/session.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { cookies } from 'next/headers'; -import { SignJWT, jwtVerify } from 'jose'; - -const SESSION_COOKIE_NAME = 'auth_session'; -const SESSION_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days - -const secretKey = process.env.AUTH_SECRET || 'default-secret-change-in-production'; -const key = new TextEncoder().encode(secretKey); - -export interface SessionData { - username: string; - isAuthenticated: boolean; - expiresAt: number; -} - -/** - * Encrypt session data to JWT - */ -async function encrypt(payload: SessionData): Promise { - return await new SignJWT(payload as any) - .setProtectedHeader({ alg: 'HS256' }) - .setIssuedAt() - .setExpirationTime(new Date(payload.expiresAt)) - .sign(key); -} - -/** - * Decrypt JWT to session data - */ -async function decrypt(token: string): Promise { - try { - const { payload } = await jwtVerify(token, key, { - algorithms: ['HS256'], - }); - return { - username: payload.username as string, - isAuthenticated: payload.isAuthenticated as boolean, - expiresAt: payload.expiresAt as number, - }; - } catch (error) { - return null; - } -} - -/** - * Create a new session - */ -export async function createSession(username: string): Promise { - const expiresAt = Date.now() + SESSION_DURATION; - const session: SessionData = { - username, - isAuthenticated: true, - expiresAt, - }; - - const encryptedSession = await encrypt(session); - const cookieStore = await cookies(); - - cookieStore.set(SESSION_COOKIE_NAME, encryptedSession, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - expires: expiresAt, - sameSite: 'lax', - path: '/', - }); -} - -/** - * Get current session - */ -export async function getSession(): Promise { - const cookieStore = await cookies(); - const cookie = cookieStore.get(SESSION_COOKIE_NAME); - - if (!cookie?.value) { - return null; - } - - const session = await decrypt(cookie.value); - - if (!session || session.expiresAt < Date.now()) { - return null; - } - - return session; -} - -/** - * Delete session (logout) - */ -export async function deleteSession(): Promise { - const cookieStore = await cookies(); - cookieStore.delete(SESSION_COOKIE_NAME); -} - -/** - * Verify if user is authenticated - */ -export async function isAuthenticated(): Promise { - const session = await getSession(); - return session?.isAuthenticated ?? false; -} diff --git a/lib/webauthn.ts b/lib/webauthn.ts deleted file mode 100644 index 76b2a3e..0000000 --- a/lib/webauthn.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { cookies } from 'next/headers'; -import { - generateRegistrationOptions, - verifyRegistrationResponse, - generateAuthenticationOptions, - verifyAuthenticationResponse, -} from '@simplewebauthn/server'; -import type { - RegistrationResponseJSON, - AuthenticationResponseJSON, - AuthenticatorTransportFuture, -} from '@simplewebauthn/server'; -import { isKnownUser } from './auth'; -import { listPasskeys, getPasskey, insertPasskey, updatePasskeyUsage } from './passkeys'; - -const REG_CHALLENGE_COOKIE = 'werte_pk_reg'; -const AUTH_CHALLENGE_COOKIE = 'werte_pk_auth'; -const CHALLENGE_TTL_S = 300; // 5 Minuten - -/** - * Relying-Party-Konfiguration. In Produktion über Env-Vars gesetzt - * (RP_ID = Host, RP_ORIGIN = volle URL). Default-Werte für lokale Entwicklung. - */ -export function getRpConfig() { - const rpID = process.env.RP_ID || 'localhost'; - const rpName = process.env.RP_NAME || 'Werte-Log'; - const origin = process.env.RP_ORIGIN || 'http://localhost:3000'; - return { rpID, rpName, origin }; -} - -// --- Challenge-Cookie (kurzlebig, httpOnly) ------------------------------ - -async function setChallengeCookie(name: string, challenge: string): Promise { - const cookieStore = await cookies(); - cookieStore.set(name, challenge, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - path: '/', - maxAge: CHALLENGE_TTL_S, - }); -} - -async function takeChallengeCookie(name: string): Promise { - const cookieStore = await cookies(); - const value = cookieStore.get(name)?.value ?? null; - cookieStore.delete(name); - return value; -} - -// --- Registrierung (Enrollment, Session erforderlich) -------------------- - -export async function buildRegistrationOptions(username: string) { - const { rpID, rpName } = getRpConfig(); - const existing = await listPasskeys(username); - - const options = await generateRegistrationOptions({ - rpName, - rpID, - userName: username, - userDisplayName: username, - userID: new TextEncoder().encode(username), - attestationType: 'none', - excludeCredentials: existing.map((pk) => ({ - id: pk.credentialId, - transports: pk.transports as AuthenticatorTransportFuture[], - })), - authenticatorSelection: { - residentKey: 'preferred', - userVerification: 'preferred', - }, - }); - - await setChallengeCookie(REG_CHALLENGE_COOKIE, options.challenge); - return options; -} - -/** Verifiziert die Registrierungs-Antwort und speichert den Passkey für den Benutzer. */ -export async function finishRegistration( - username: string, - response: RegistrationResponseJSON, - label: string -): Promise<{ ok: true } | { ok: false; error: string }> { - const expectedChallenge = await takeChallengeCookie(REG_CHALLENGE_COOKIE); - if (!expectedChallenge) return { ok: false, error: 'Challenge abgelaufen. Bitte erneut versuchen.' }; - - const { rpID, origin } = getRpConfig(); - let verification; - try { - verification = await verifyRegistrationResponse({ - response, - expectedChallenge, - expectedOrigin: origin, - expectedRPID: rpID, - requireUserVerification: false, - }); - } catch { - return { ok: false, error: 'Passkey konnte nicht verifiziert werden.' }; - } - - if (!verification.verified || !verification.registrationInfo) { - return { ok: false, error: 'Passkey-Registrierung fehlgeschlagen.' }; - } - - const { credential } = verification.registrationInfo; - if (await getPasskey(credential.id)) { - return { ok: false, error: 'Dieser Passkey ist bereits registriert.' }; - } - - await insertPasskey({ - credentialId: credential.id, - username, - publicKey: credential.publicKey, - counter: credential.counter, - transports: credential.transports ?? [], - label: label.trim().slice(0, 80) || 'Passkey', - }); - return { ok: true }; -} - -// --- Anmeldung (öffentlich, keine Session) ------------------------------- - -export async function buildAuthenticationOptions() { - const { rpID } = getRpConfig(); - const options = await generateAuthenticationOptions({ - rpID, - userVerification: 'preferred', - // allowCredentials leer lassen → erlaubt Discoverable Credentials / Browser-Auswahl. - }); - await setChallengeCookie(AUTH_CHALLENGE_COOKIE, options.challenge); - return options; -} - -/** - * Verifiziert die Anmelde-Antwort gegen einen gespeicherten Passkey und liefert - * den zugehörigen Benutzer zurück. - */ -export async function finishAuthentication( - response: AuthenticationResponseJSON -): Promise<{ ok: true; username: string } | { ok: false; error: string }> { - const expectedChallenge = await takeChallengeCookie(AUTH_CHALLENGE_COOKIE); - if (!expectedChallenge) return { ok: false, error: 'Challenge abgelaufen. Bitte erneut versuchen.' }; - - const passkey = await getPasskey(response.id); - if (!passkey) return { ok: false, error: 'Unbekannter Passkey.' }; - - // Benutzer könnte inzwischen aus AUTH_USERS entfernt worden sein. - if (!isKnownUser(passkey.username)) { - return { ok: false, error: 'Benutzer existiert nicht mehr.' }; - } - - const { rpID, origin } = getRpConfig(); - let verification; - try { - verification = await verifyAuthenticationResponse({ - response, - expectedChallenge, - expectedOrigin: origin, - expectedRPID: rpID, - requireUserVerification: false, - credential: { - id: passkey.credentialId, - publicKey: passkey.publicKey, - counter: passkey.counter, - transports: passkey.transports as AuthenticatorTransportFuture[], - }, - }); - } catch { - return { ok: false, error: 'Anmeldung mit Passkey fehlgeschlagen.' }; - } - - if (!verification.verified) return { ok: false, error: 'Anmeldung mit Passkey fehlgeschlagen.' }; - - await updatePasskeyUsage(passkey.credentialId, verification.authenticationInfo.newCounter); - return { ok: true, username: passkey.username }; -} diff --git a/package-lock.json b/package-lock.json index 91d3741..5f2ebfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,15 @@ { "name": "werte_next", - "version": "2.1.1", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "werte_next", - "version": "2.1.1", + "version": "2.2.0", "dependencies": { - "@simplewebauthn/browser": "^13.3.0", - "@simplewebauthn/server": "^13.3.2", - "bcryptjs": "^3.0.3", "highcharts": "^12.5.0", "highcharts-react-official": "^3.2.3", - "jose": "^6.1.3", "mysql2": "^3.17.4", "next": "16.1.6", "react": "19.2.3", @@ -21,7 +17,6 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", - "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -462,12 +457,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@hexagon/base64": { - "version": "1.1.28", - "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", - "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", - "license": "MIT" - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1036,12 +1025,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@levischuck/tiny-cbor": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", - "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", - "license": "MIT" - }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1247,174 +1230,6 @@ "node": ">=12.4.0" } }, - "node_modules/@peculiar/asn1-android": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.8.0.tgz", - "integrity": "sha512-skLbS+IOGv1lUgDqtChr8xvtvEr3HMse/JGBaL2r1J1o/n7a8wqOrovMtlRq/UXLhxvmLaONP67hwtshgzwfzA==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.8.0", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-cms": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz", - "integrity": "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.8.0", - "@peculiar/asn1-x509": "^2.8.0", - "@peculiar/asn1-x509-attr": "^2.8.0", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-csr": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.8.0.tgz", - "integrity": "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.8.0", - "@peculiar/asn1-x509": "^2.8.0", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-ecc": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.8.0.tgz", - "integrity": "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.8.0", - "@peculiar/asn1-x509": "^2.8.0", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-pfx": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.8.0.tgz", - "integrity": "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-cms": "^2.8.0", - "@peculiar/asn1-pkcs8": "^2.8.0", - "@peculiar/asn1-rsa": "^2.8.0", - "@peculiar/asn1-schema": "^2.8.0", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-pkcs8": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.8.0.tgz", - "integrity": "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.8.0", - "@peculiar/asn1-x509": "^2.8.0", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-pkcs9": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.8.0.tgz", - "integrity": "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-cms": "^2.8.0", - "@peculiar/asn1-pfx": "^2.8.0", - "@peculiar/asn1-pkcs8": "^2.8.0", - "@peculiar/asn1-schema": "^2.8.0", - "@peculiar/asn1-x509": "^2.8.0", - "@peculiar/asn1-x509-attr": "^2.8.0", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-rsa": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.8.0.tgz", - "integrity": "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.8.0", - "@peculiar/asn1-x509": "^2.8.0", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", - "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", - "license": "MIT", - "dependencies": { - "@peculiar/utils": "^2.0.2", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-x509": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.8.0.tgz", - "integrity": "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.8.0", - "@peculiar/utils": "^2.0.2", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.8.0.tgz", - "integrity": "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.8.0", - "@peculiar/asn1-x509": "^2.8.0", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", - "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/x509": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", - "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-csr": "^2.6.0", - "@peculiar/asn1-ecc": "^2.6.0", - "@peculiar/asn1-pkcs9": "^2.6.0", - "@peculiar/asn1-rsa": "^2.6.0", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "pvtsutils": "^1.3.6", - "reflect-metadata": "^0.2.2", - "tslib": "^2.8.1", - "tsyringe": "^4.10.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1422,31 +1237,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@simplewebauthn/browser": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", - "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", - "license": "MIT" - }, - "node_modules/@simplewebauthn/server": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.3.2.tgz", - "integrity": "sha512-KEDhfcGP1PAKRVSDjA3npTQFqS2b/srm+ipoNBNHdkzrHAlaRQUTE+a5f4ywsx6thxAw1NU2rYcLEY1949RGbQ==", - "license": "MIT", - "dependencies": { - "@hexagon/base64": "^1.1.27", - "@levischuck/tiny-cbor": "^0.2.2", - "@peculiar/asn1-android": "^2.6.0", - "@peculiar/asn1-ecc": "^2.6.1", - "@peculiar/asn1-rsa": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/x509": "^1.14.3" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1738,13 +1528,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/bcryptjs": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", - "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2596,20 +2379,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1js": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", - "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", - "license": "BSD-3-Clause", - "dependencies": { - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.5", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2691,15 +2460,6 @@ "node": ">=6.0.0" } }, - "node_modules/bcryptjs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", - "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", - "license": "BSD-3-Clause", - "bin": { - "bcrypt": "bin/bcrypt" - } - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4762,15 +4522,6 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5800,24 +5551,6 @@ "node": ">=6" } }, - "node_modules/pvtsutils": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", - "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/pvutils": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", - "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5869,12 +5602,6 @@ "dev": true, "license": "MIT" }, - "node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0" - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6636,24 +6363,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tsyringe": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", - "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", - "license": "MIT", - "dependencies": { - "tslib": "^1.9.3" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/tsyringe/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 96e5b46..14ab7fa 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,8 @@ "lint": "eslint" }, "dependencies": { - "@simplewebauthn/browser": "^13.3.0", - "@simplewebauthn/server": "^13.3.2", - "bcryptjs": "^3.0.3", "highcharts": "^12.5.0", "highcharts-react-official": "^3.2.3", - "jose": "^6.1.3", "mysql2": "^3.17.4", "next": "16.1.6", "react": "19.2.3", @@ -22,7 +18,6 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", - "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/proxy.ts b/proxy.ts deleted file mode 100644 index ede4288..0000000 --- a/proxy.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { jwtVerify } from 'jose'; - -const SESSION_COOKIE_NAME = 'auth_session'; - -/** - * Proxy to protect routes with authentication - * Reusable for other projects - just copy this file - */ -export async function proxy(request: NextRequest) { - const { pathname } = request.nextUrl; - - // Check if authentication is enabled - const authEnabled = !!process.env.AUTH_USERS; - - // If auth is not enabled, allow all requests - if (!authEnabled) { - return NextResponse.next(); - } - - // Public paths that don't require authentication - // (Passkey-Anmeldung muss ohne Session erreichbar sein) - const publicPaths = ['/login', '/api/passkey/authenticate']; - const isPublicPath = publicPaths.some(path => pathname.startsWith(path)); - - if (isPublicPath) { - return NextResponse.next(); - } - - // Check for session cookie - const sessionCookie = request.cookies.get(SESSION_COOKIE_NAME); - - if (!sessionCookie) { - return NextResponse.redirect(new URL('/login', request.url)); - } - - // Verify session token - try { - const secretKey = process.env.AUTH_SECRET || 'default-secret-change-in-production'; - const key = new TextEncoder().encode(secretKey); - - const { payload } = await jwtVerify(sessionCookie.value, key, { - algorithms: ['HS256'], - }); - - // Check if session is expired - if (payload.expiresAt && (payload.expiresAt as number) < Date.now()) { - const response = NextResponse.redirect(new URL('/login', request.url)); - response.cookies.delete(SESSION_COOKIE_NAME); - return response; - } - - return NextResponse.next(); - } catch (error) { - // Invalid token - redirect to login - const response = NextResponse.redirect(new URL('/login', request.url)); - response.cookies.delete(SESSION_COOKIE_NAME); - return response; - } -} - -export const config = { - matcher: [ - /* - * Match all request paths except for the ones starting with: - * - _next/static (static files) - * - _next/image (image optimization files) - * - favicon.ico (favicon file) - * - public folder - */ - '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', - ], -};