diff --git a/README.md b/README.md index 3a3698d..e215bc4 100644 --- a/README.md +++ b/README.md @@ -34,22 +34,3 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. - -## 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 oben rechts auf *Einstellungen* → „Passkey hinzufügen". -- **Anmelden**: auf der Login-Seite „Mit Passkey anmelden". -- **Speicherung**: Tabelle `tabletten_passkeys` in der Datenbank `medizin`; sie wird beim - ersten Zugriff automatisch angelegt, falls der DB-Benutzer CREATE-Rechte hat. Der - app-spezifische Name verhindert Kollisionen mit anderen Apps desselben Stacks. -- **Konfiguration**: Im Container `RP_ID` (nur der Host), `RP_ORIGIN` (volle URL) und - `RP_NAME`. In den Compose-Dateien werden diese aus `TABLETTEN_RP_*` befüllt, weil sich - die `.env` auf dem Server mit anderen Apps teilt, die `RP_ID`/`RP_ORIGIN` bereits belegen. -- **Wichtig**: WebAuthn funktioniert nur über **HTTPS** oder auf `localhost`. Ändert sich - `RP_ID`, werden bereits registrierte Passkeys ungültig. 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 d1c4ef0..0000000 --- a/app/einstellungen/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import Link from 'next/link'; -import AppLayout from '@/components/AppLayout'; -import Passkeys from '@/components/Passkeys'; - -export default function EinstellungenPage() { - return ( - -

Einstellungen

- -

- ← Zurück zur Übersicht -

-
- ); -} diff --git a/app/login/actions.ts b/app/login/actions.ts deleted file mode 100644 index 4ff702b..0000000 --- a/app/login/actions.ts +++ /dev/null @@ -1,27 +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: { error: string } | null | undefined, 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 3959bf7..0000000 --- a/app/login/page.tsx +++ /dev/null @@ -1,163 +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 ( -
-
-
-

Tabletten-Übersicht

-
- -
-
-

Anmeldung

- -
-
- - -
- -
- -
- - -
-
- - {state?.error && ( -
- {state.error} -
- )} - - -
- -
- oder -
- - - - {passkeyError && ( -
- {passkeyError} -
- )} -
-
- - -
-
- ); -} diff --git a/components/AppLayout.tsx b/components/AppLayout.tsx index 51e5f6d..fdd7c5e 100644 --- a/components/AppLayout.tsx +++ b/components/AppLayout.tsx @@ -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) {
- {/* Einstellungen und Logout oben rechts */} -
- {pathname !== '/einstellungen' && ( - - Einstellungen - - )} - -
- {/* Inhaltsbereich */}
{children} diff --git a/components/LogoutButton.tsx b/components/LogoutButton.tsx deleted file mode 100644 index 39ba1d9..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 b60ef27..0000000 --- a/components/Passkeys.tsx +++ /dev/null @@ -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([]); - 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="passkeys-input" - /> -
- -
- - {error &&
{error}
} -
- ); -} diff --git a/compose.yml b/compose.yml index 4a74142..d65b7f4 100644 --- a/compose.yml +++ b/compose.yml @@ -10,14 +10,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. - # Eigene Variablennamen, weil sich mehrere Apps im Stack eine .env teilen - # und RP_ID/RP_ORIGIN dort bereits von werte-next belegt sind. - - RP_ID=${TABLETTEN_RP_ID:-tabletten.fuerst-stuttgart.de} - - RP_ORIGIN=${TABLETTEN_RP_ORIGIN:-https://tabletten.fuerst-stuttgart.de} - - RP_NAME=${TABLETTEN_RP_NAME:-Tabletten-Check} labels: - traefik.enable=true - traefik.http.routers.tabletten.entrypoints=http diff --git a/docker-compose.yml b/docker-compose.yml index 0ef6dd8..dbc8a92 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,11 +40,5 @@ services: # App VORLAUF: ${VORLAUF:-14} CHECK_SECRET: ${CHECK_SECRET} - AUTH_USERS: ${AUTH_USERS} - AUTH_SECRET: ${AUTH_SECRET} - # Passkeys (WebAuthn) — braucht HTTPS oder localhost - RP_ID: ${TABLETTEN_RP_ID:-localhost} - RP_ORIGIN: ${TABLETTEN_RP_ORIGIN:-http://localhost:3000} - RP_NAME: ${TABLETTEN_RP_NAME:-Tabletten-Check} env_file: - .env.local diff --git a/lib/auth.ts b/lib/auth.ts deleted file mode 100644 index 59257be..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 nicht in .env konfiguriert'); - 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 b04201d..0000000 --- a/lib/passkeys.ts +++ /dev/null @@ -1,144 +0,0 @@ -import pool from './mysql'; -import type { RowDataPacket, ResultSetHeader } from 'mysql2/promise'; - -// Eigener Tabellenname je Anwendung: Die DB-Zugangsdaten sind auf dem Server für -// alle Apps im Stack dieselben. Ein generisches `passkeys` würde sich mit anderen -// Apps überschneiden — sie sähen und löschten dann gegenseitig ihre Passkeys. -const TABLE = 'tabletten_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). - */ -function ensureSchema(): Promise { - if (!schemaReady) { - schemaReady = pool - .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_tabletten_passkeys_username (username) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_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 pool.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 pool.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 pool.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 pool.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 pool.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 51f4393..0000000 --- a/lib/session.ts +++ /dev/null @@ -1,70 +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 Tage - -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; -} - -async function encrypt(payload: SessionData): Promise { - return await new SignJWT(payload as unknown as Record) - .setProtectedHeader({ alg: 'HS256' }) - .setIssuedAt() - .setExpirationTime(new Date(payload.expiresAt)) - .sign(key); -} - -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 { - return null; - } -} - -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: '/', - }); -} - -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; -} - -export async function deleteSession(): Promise { - const cookieStore = await cookies(); - cookieStore.delete(SESSION_COOKIE_NAME); -} - -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 05f4837..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 = 'tabletten_pk_reg'; -const AUTH_CHALLENGE_COOKIE = 'tabletten_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 || 'Tabletten-Check'; - 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 36a9218..5fb736e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,13 @@ { "name": "tabletten_next", - "version": "1.2.2", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tabletten_next", - "version": "1.2.2", + "version": "1.3.0", "dependencies": { - "@simplewebauthn/browser": "^13.3.0", - "@simplewebauthn/server": "^13.3.2", - "bcryptjs": "^3.0.3", - "jose": "^6.2.1", "moment": "^2.30.1", "mysql2": "^3.19.1", "next": "16.1.6", @@ -21,7 +17,6 @@ "react-dom": "19.2.3" }, "devDependencies": { - "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/node-schedule": "^2.1.8", "@types/nodemailer": "^7.0.11", @@ -451,12 +446,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", @@ -1025,12 +1014,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/@mongodb-js/saslprep": { "version": "1.4.6", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", @@ -1246,174 +1229,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", @@ -1421,31 +1236,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", @@ -1466,13 +1256,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", @@ -2361,20 +2144,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", @@ -2456,15 +2225,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", @@ -4489,15 +4249,6 @@ "node": ">= 0.4" } }, - "node_modules/jose": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz", - "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==", - "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", @@ -5345,24 +5096,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.2.0", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", - "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", - "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", @@ -5414,12 +5147,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", @@ -6189,24 +5916,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 ffba390..794478c 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,6 @@ "lint": "eslint" }, "dependencies": { - "@simplewebauthn/browser": "^13.3.0", - "@simplewebauthn/server": "^13.3.2", - "bcryptjs": "^3.0.3", - "jose": "^6.2.1", "moment": "^2.30.1", "mysql2": "^3.19.1", "next": "16.1.6", @@ -22,7 +18,6 @@ "react-dom": "19.2.3" }, "devDependencies": { - "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/node-schedule": "^2.1.8", "@types/nodemailer": "^7.0.11", diff --git a/proxy.ts b/proxy.ts deleted file mode 100644 index 624f557..0000000 --- a/proxy.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { jwtVerify } from 'jose'; - -const SESSION_COOKIE_NAME = 'auth_session'; - -export async function proxy(request: NextRequest) { - const { pathname } = request.nextUrl; - - // Wenn AUTH_USERS nicht gesetzt, alles durchlassen - if (!process.env.AUTH_USERS) { - return NextResponse.next(); - } - - // /login und /api/check sind öffentlich - // (Passkey-Anmeldung muss ohne Session erreichbar sein) - const publicPaths = ['/login', '/api/check', '/api/passkey/authenticate']; - if (publicPaths.some(p => pathname.startsWith(p))) { - return NextResponse.next(); - } - - const sessionCookie = request.cookies.get(SESSION_COOKIE_NAME); - if (!sessionCookie) { - return NextResponse.redirect(new URL('/login', request.url)); - } - - 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'], - }); - - 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 { - const response = NextResponse.redirect(new URL('/login', request.url)); - response.cookies.delete(SESSION_COOKIE_NAME); - return response; - } -} - -export const config = { - matcher: [ - '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', - ], -};