+
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 0f1c722..32b905d 100644
--- a/components/TabLayout.tsx
+++ b/components/TabLayout.tsx
@@ -12,6 +12,7 @@ interface TabLayoutProps {
const TABS = [
{ href: '/', label: 'Eingabe' },
{ href: '/charts', label: 'Verlauf' },
+ { href: '/einstellungen', label: 'Einstellungen' },
];
export default function TabLayout({ children }: TabLayoutProps) {
diff --git a/create_table.sql b/create_table.sql
index f46d2e0..ec9610a 100644
--- a/create_table.sql
+++ b/create_table.sql
@@ -16,3 +16,18 @@ 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 8b1bb2d..ce05349 100644
--- a/docker-compose.local.yml
+++ b/docker-compose.local.yml
@@ -20,6 +20,10 @@ services:
- 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 b2d22f4..77270b6 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -14,6 +14,10 @@ services:
- 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
index b494be3..94abb10 100644
--- a/lib/auth.ts
+++ b/lib/auth.ts
@@ -29,6 +29,11 @@ export async function verifyCredentials(username: string, password: string): Pro
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
new file mode 100644
index 0000000..80e03c9
--- /dev/null
+++ b/lib/passkeys.ts
@@ -0,0 +1,142 @@
+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