From 5cca85735847eb81cc0fbc06b0b9b5c62a747281 Mon Sep 17 00:00:00 2001
From: Moritz Utcke
Date: Thu, 16 Oct 2025 10:24:35 -0400
Subject: [PATCH] Vereinfachter Registrierungsprozess
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Nicht-eingeloggte Nutzer sollen sich einfacher registrieren/authentifizieren können.
Speichern oder Bestellen nicht-eingeloggte Nutzer auf der Kundendaten-Seite, so soll ein Popup aufgehen, wo sie einen Bestätigungscode eingeben können. Dieser Code wird umgehend an ihre eingegene Email gesendet.
Wird der Code efolgreich eingegeben gibt es zwei Fälle:
Unter der Email existiert bereits ein Account → Der Ausweis wird dann diesem User zugewiesen
Unter der Email existiet noch kein Account → Ein neuer Account wird angelegt und der Ausweise dem neuen Account zugewiesen
Wird ein neuner Account automatisch angelegt, so benötigen wir auch noch einen Prozess, wie der Nutzer dann sein Passwort vergeben kann. Idealerweise erhält er in seiner Willkommensmail einen Link zur Passwort setzung. Alternativ nutzt er einfach die bestehende Passwortrücksetzen-Funktion auf der Webseite.
Um bei der Erstregistrierung soll ein Zahlencode an die eingegebene E-Mail verschickt werden. Dieser muss dann vom User eingegeben werden um die Registrierung bzw. meistens ja dann die Erstbestellung abzuschließen. Der Zahlencode kann ja dann das Passwort sein. Wir weisen den Kunden darauf hin sich ein eigenes Passwort zu vergeben.
---
.env | 2 +
src/astro-typesafe-api-caller.ts | 19 +--
src/lib/pin.ts | 59 +++++++
src/lib/server/constants.ts | 1 +
src/lib/server/mail/registrierung.ts | 16 ++
src/modules/EmbeddedAuthFlowModule.svelte | 14 +-
src/modules/EmbeddedRegisterModule.svelte | 2 +-
src/modules/EmbeddedVerifyModule.svelte | 130 ++++++++++++++++
src/modules/PINVerifyModule.svelte | 145 ++++++++++++++++++
src/modules/RegisterModule.svelte | 4 +-
src/pages/api/auth/verify.ts | 65 ++++++++
src/pages/auth/request-pin.astro | 95 ++++++++++++
src/pages/auth/verify-pin.astro | 39 +++++
src/pages/auth/verify.astro | 4 +-
.../[ausweisart]/index.astro | 19 ++-
15 files changed, 588 insertions(+), 26 deletions(-)
create mode 100644 src/lib/pin.ts
create mode 100644 src/modules/EmbeddedVerifyModule.svelte
create mode 100644 src/modules/PINVerifyModule.svelte
create mode 100644 src/pages/api/auth/verify.ts
create mode 100644 src/pages/auth/request-pin.astro
create mode 100644 src/pages/auth/verify-pin.astro
diff --git a/.env b/.env
index 29afc948..12f7be79 100644
--- a/.env
+++ b/.env
@@ -4,6 +4,8 @@
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
+SECRET="jg57cya4mrNkGlnb2R85X1gI8LdY7iNZ9MF0Jbn0K5zQBshOxv"
+
POSTGRES_DB=main
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
diff --git a/src/astro-typesafe-api-caller.ts b/src/astro-typesafe-api-caller.ts
index 0432563b..1d1730fb 100644
--- a/src/astro-typesafe-api-caller.ts
+++ b/src/astro-typesafe-api-caller.ts
@@ -12,31 +12,32 @@ export const createCaller = createCallerFactory({
"admin/nicht-ausstellen": await import("../src/pages/api/admin/nicht-ausstellen.ts"),
"admin/registriernummer": await import("../src/pages/api/admin/registriernummer.ts"),
"admin/stornieren": await import("../src/pages/api/admin/stornieren.ts"),
- "ausweise": await import("../src/pages/api/ausweise/index.ts"),
- "bedarfsausweis-gewerbe/[id]": await import("../src/pages/api/bedarfsausweis-gewerbe/[id].ts"),
- "bedarfsausweis-gewerbe": await import("../src/pages/api/bedarfsausweis-gewerbe/index.ts"),
"auth/access-token": await import("../src/pages/api/auth/access-token.ts"),
"auth/passwort-vergessen": await import("../src/pages/api/auth/passwort-vergessen.ts"),
"auth/refresh-token": await import("../src/pages/api/auth/refresh-token.ts"),
+ "auth/verify": await import("../src/pages/api/auth/verify.ts"),
+ "ausweise": await import("../src/pages/api/ausweise/index.ts"),
+ "bedarfsausweis-gewerbe/[id]": await import("../src/pages/api/bedarfsausweis-gewerbe/[id].ts"),
+ "bedarfsausweis-gewerbe": await import("../src/pages/api/bedarfsausweis-gewerbe/index.ts"),
+ "aufnahme": await import("../src/pages/api/aufnahme/index.ts"),
"bedarfsausweis-wohnen/[id]": await import("../src/pages/api/bedarfsausweis-wohnen/[id].ts"),
"bedarfsausweis-wohnen": await import("../src/pages/api/bedarfsausweis-wohnen/index.ts"),
- "aufnahme": await import("../src/pages/api/aufnahme/index.ts"),
"bilder/[id]": await import("../src/pages/api/bilder/[id].ts"),
"geg-nachweis-gewerbe/[id]": await import("../src/pages/api/geg-nachweis-gewerbe/[id].ts"),
"geg-nachweis-gewerbe": await import("../src/pages/api/geg-nachweis-gewerbe/index.ts"),
"geg-nachweis-wohnen/[id]": await import("../src/pages/api/geg-nachweis-wohnen/[id].ts"),
"geg-nachweis-wohnen": await import("../src/pages/api/geg-nachweis-wohnen/index.ts"),
"objekt": await import("../src/pages/api/objekt/index.ts"),
- "ticket": await import("../src/pages/api/ticket/index.ts"),
- "user": await import("../src/pages/api/user/index.ts"),
- "user/self": await import("../src/pages/api/user/self.ts"),
"rechnung/[id]": await import("../src/pages/api/rechnung/[id].ts"),
"rechnung/anfordern": await import("../src/pages/api/rechnung/anfordern.ts"),
"rechnung": await import("../src/pages/api/rechnung/index.ts"),
- "verbrauchsausweis-wohnen/[id]": await import("../src/pages/api/verbrauchsausweis-wohnen/[id].ts"),
- "verbrauchsausweis-wohnen": await import("../src/pages/api/verbrauchsausweis-wohnen/index.ts"),
+ "ticket": await import("../src/pages/api/ticket/index.ts"),
"verbrauchsausweis-gewerbe/[id]": await import("../src/pages/api/verbrauchsausweis-gewerbe/[id].ts"),
"verbrauchsausweis-gewerbe": await import("../src/pages/api/verbrauchsausweis-gewerbe/index.ts"),
+ "user": await import("../src/pages/api/user/index.ts"),
+ "user/self": await import("../src/pages/api/user/self.ts"),
+ "verbrauchsausweis-wohnen/[id]": await import("../src/pages/api/verbrauchsausweis-wohnen/[id].ts"),
+ "verbrauchsausweis-wohnen": await import("../src/pages/api/verbrauchsausweis-wohnen/index.ts"),
"webhooks/mollie": await import("../src/pages/api/webhooks/mollie.ts"),
"aufnahme/[id]/bilder": await import("../src/pages/api/aufnahme/[id]/bilder.ts"),
"aufnahme/[id]": await import("../src/pages/api/aufnahme/[id]/index.ts"),
diff --git a/src/lib/pin.ts b/src/lib/pin.ts
new file mode 100644
index 00000000..66ef666a
--- /dev/null
+++ b/src/lib/pin.ts
@@ -0,0 +1,59 @@
+import crypto from "crypto";
+import { SECRET } from "./server/constants.js";
+
+// Convert integer to 8-byte Buffer (big-endian)
+function intToBuffer(i: number): Buffer {
+ const buf = Buffer.alloc(8);
+ buf.writeBigUInt64BE(BigInt(i));
+ return buf;
+}
+
+// Derive a stable key per email
+function deriveKey(email: string): Buffer {
+ return crypto.createHmac("sha256", Buffer.from(SECRET, "utf8"))
+ .update(email)
+ .digest();
+}
+
+/**
+ * Erstellt eine 4-stellige PIN.
+ * @param email - Die Email-Adresse, für die die PIN generiert wird
+ * @param forTime - Der Zeitpunkt, für den die PIN generiert wird (Standard: aktueller Zeitpunkt)
+ * @param validSeconds - Die Anzahl der Sekunden, für die die PIN gültig ist (Standard: 1800 Sekunden = 30 Minuten)
+ * @returns Die generierte PIN
+ */
+export function generatePinCode(email: string, forTime: number = Math.floor(Date.now() / 1000), validSeconds: number = 1800): string {
+ const timestep = Math.floor(forTime / validSeconds);
+ console.log(timestep, forTime, validSeconds, Date.now());
+
+ const key = deriveKey(email);
+ const msg = intToBuffer(timestep);
+
+ const mac = crypto.createHmac("sha256", key).update(msg).digest();
+
+ // Dynamic truncation (RFC4226-style)
+ const offset = mac[mac.length - 1] & 0x0f;
+ const codeInt = (mac.readUInt32BE(offset) & 0x7fffffff) % 10000;
+ return codeInt.toString().padStart(4, "0");
+}
+
+/**
+ * Verifiziert eine PIN für eine Email-Adresse.
+ * @param email - Die Email-Adresse, für die die PIN verifiziert wird
+ * @param code - Die zu verifizierende PIN
+ * @param validSeconds - Die Anzahl der Sekunden, für die die PIN gültig ist (Standard: 1800 Sekunden = 30 Minuten)
+ * @param allowedWindows - Die Anzahl der erlaubten Zeitfenster für Zeitabweichungen (Standard: 1, erlaubt ±1 Fenster)
+ * @returns true, wenn die PIN gültig ist, sonst false
+ */
+export function verifyCode(email: string, code: string, validSeconds: number = 1800, allowedWindows: number = 1): boolean {
+ const now = Math.floor(Date.now() / 1000);
+
+ for (let w = -allowedWindows; w <= allowedWindows; w++) {
+ const time = now + w * validSeconds;
+ if (generatePinCode(email, time, validSeconds) === code) {
+ return true;
+ }
+ }
+
+ return false;
+}
\ No newline at end of file
diff --git a/src/lib/server/constants.ts b/src/lib/server/constants.ts
index 343b3c96..f0562041 100644
--- a/src/lib/server/constants.ts
+++ b/src/lib/server/constants.ts
@@ -2,6 +2,7 @@ import os from "os"
import fs from "fs"
export const PERSISTENT_DIR = `${os.homedir()}/persistent/online-energieausweis`
+export const SECRET = process.env.SECRET as string
if (!fs.existsSync(PERSISTENT_DIR)) {
fs.mkdirSync(PERSISTENT_DIR, {recursive: true})
diff --git a/src/lib/server/mail/registrierung.ts b/src/lib/server/mail/registrierung.ts
index 5eb476ae..f92da0b5 100644
--- a/src/lib/server/mail/registrierung.ts
+++ b/src/lib/server/mail/registrierung.ts
@@ -5,6 +5,8 @@ import {
} from "#lib/client/prisma.js";
import { encodeToken } from "#lib/auth/token.js";
import { TokenType } from "#lib/auth/types.js";
+import { generatePinCode } from "#lib/pin.js";
+import { logger } from "#lib/logger.js";
export async function sendRegisterMail(
user: Benutzer
@@ -15,6 +17,13 @@ export async function sendRegisterMail(
id: user.id
})
+ // Bei der Registrierung soll ein Code an die Email des Benutzers geschickt werden.
+ // Der Benutzer muss diesen Code dann eingeben, um seine Email zu verifizieren.
+ // Bis dahin kann er sich nicht einloggen.
+ // Diese Pin soll nach 30 Minuten ablaufen, wir können sie ganz einfach erstellen indem wir einen zeitbasierten Hash der Email generieren.
+ const pin = generatePinCode(user.email);
+ logger.info(`Generated PIN for ${user.email}: ${pin}`);
+
await transport.sendMail({
from: `"IBCornelsen" `,
to: user.email,
@@ -26,6 +35,13 @@ export async function sendRegisterMail(
Um Ihre Registrierung abzuschließen, klicken Sie bitte auf den folgenden Link, um Ihre E-Mail-Adresse zu bestätigen:
Alternativ können Sie den folgenden Bestätigungscode verwenden:
+ ${pin}
+ Dieser Code ist 30 Minuten gültig.
+
+ Sollten Sie diese Registrierung nicht vorgenommen haben, können Sie diese E-Mail einfach ignorieren. Ihr Benutzerkonto wird in diesem Fall nicht aktiviert.
+
+ Falls Sie Fragen haben oder Unterstützung benötigen, stehen wir Ihnen gerne zur Verfügung. Kontaktieren Sie uns einfach unter support@online-energieausweis.org.
Mit freundlichen Grüßen,
diff --git a/src/modules/EmbeddedAuthFlowModule.svelte b/src/modules/EmbeddedAuthFlowModule.svelte
index 7637e0d4..75878b07 100644
--- a/src/modules/EmbeddedAuthFlowModule.svelte
+++ b/src/modules/EmbeddedAuthFlowModule.svelte
@@ -2,27 +2,27 @@
import { loginClient } from "#lib/login.js";
import EmbeddedLoginModule from "./EmbeddedLoginModule.svelte"
import EmbeddedRegisterModule from "./EmbeddedRegisterModule.svelte"
+ import EmbeddedVerifyModule from "./EmbeddedVerifyModule.svelte";
export let onLogin: (response: Awaited>) => any;
export let email: string = "";
export let password: string = "";
- export let route: "login" | "signup" = "login"
+ export let route: "login" | "signup" | "verify" = "login"
const navigate = (target: string) => {
route = target as typeof route;
}
-
- const loginData = {
- email,
- passwort: "",
- }
{#if route == "login"}
-{:else}
+{:else if route == "signup"}
{
email = response.email
+ navigate("verify")
+ }} {navigate} />
+{:else if route == "verify"}
+ {
navigate("login")
}} {navigate} />
{/if}
\ No newline at end of file
diff --git a/src/modules/EmbeddedRegisterModule.svelte b/src/modules/EmbeddedRegisterModule.svelte
index 1d16a668..508d3c9d 100644
--- a/src/modules/EmbeddedRegisterModule.svelte
+++ b/src/modules/EmbeddedRegisterModule.svelte
@@ -122,7 +122,7 @@
Alternativ können Sie den folgenden Bestätigungscode verwenden:
+ ${pin}
+ Dieser Code ist 30 Minuten gültig.
+
+ Sollten Sie diese Registrierung nicht vorgenommen haben, können Sie diese E-Mail einfach ignorieren. Ihr Benutzerkonto wird in diesem Fall nicht aktiviert.
+
+ Falls Sie Fragen haben oder Unterstützung benötigen, stehen wir Ihnen gerne zur Verfügung. Kontaktieren Sie uns einfach unter support@online-energieausweis.org.
+