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"
E-Mail-Adresse 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,
Bitte geben Sie die 4-stellige PIN ein, den Sie per E-Mail erhalten haben. Sehr geehrte*r ${user.vorname} ${user.name}, vielen Dank für Ihre Registrierung bei IBCornelsen. Ihr Benutzerkonto wurde erfolgreich erstellt. Alternativ können Sie den folgenden Bestätigungscode verwenden:
+ 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: AwaitedVerifizieren
+
+
+ Um Ihre Registrierung abzuschließen, klicken Sie bitte auf den folgenden Link, um Ihre E-Mail-Adresse zu bestätigen:
+
+ E-Mail-Adresse bestätigen
+ ${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.
+
+ Dipl.-Ing. Jens Cornelsen
+
+
+ IB Cornelsen
+
+ Katendeich 5A
+
+ 21035 Hamburg
+
+ www.online-energieausweis.org
+
+
+ fon 040 · 209339850
+
+ fax 040 · 209339859
+