-
- {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)$).*)',
- ],
-};