Verbesserungen Registrierung

This commit is contained in:
Moritz Utcke
2025-10-17 10:05:07 -04:00
parent 462fa79592
commit 0b01a76953
17 changed files with 146 additions and 530 deletions

View File

@@ -16,7 +16,6 @@ export const createCaller = createCallerFactory({
"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"),

View File

@@ -186,7 +186,15 @@ sm:grid-cols-[1fr_min-content_min-content_min-content] sm:justify-self-end">
<Overlay bind:hidden={loginOverlayHidden}>
<div class="bg-white w-full max-w-screen-sm py-8">
<EmbeddedAuthFlowModule onLogin={loginAction} email={""}></EmbeddedAuthFlowModule>
<EmbeddedAuthFlowModule onLogin={loginAction} email={""} route="signup" title={
{
login: "Ausweisdaten speichern",
signup: "Ausweisdaten speichern"
}
} buttonText={{
login: "Speichern",
signup: "Speichern"
}}></EmbeddedAuthFlowModule>
</div>
</Overlay>

View File

@@ -5,7 +5,7 @@
export let readonly: boolean = false;
export let city: string | null | undefined;
export let zip: string | null = "";
export let onchange: (event: Event) => void;
export let onchange: (event: Event) => void = () => {};
let hideZipDropdown: boolean = true;
let zipCodes: inferOutput<API["postleitzahlen"]["GET"]> = [];

View File

@@ -1,59 +0,0 @@
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;
}

View File

@@ -1,29 +1,12 @@
import { BASE_URI } from "#lib/constants.js";
import { transport } from "#lib/mail.js";
import {
Benutzer,
} 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
user: Benutzer,
passwort: string
) {
const verificationJwt = encodeToken({
typ: TokenType.Verify,
exp: Date.now() + (15 * 60 * 1000),
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" <info@online-energieausweis.org>`,
to: user.email,
@@ -32,12 +15,9 @@ export async function sendRegisterMail(
html: `<p>Sehr geehrte*r ${user.vorname} ${user.name},</p>
<p>vielen Dank für Ihre Registrierung bei IBCornelsen. Ihr Benutzerkonto wurde erfolgreich erstellt.<br><br>
Um Ihre Registrierung abzuschließen, klicken Sie bitte auf den folgenden Link, um Ihre E-Mail-Adresse zu bestätigen:<br><br>
<a href="${BASE_URI}/auth/verify?t=${verificationJwt}">E-Mail-Adresse bestätigen</a><br></p>
<p>Alternativ können Sie den folgenden Bestätigungscode verwenden:<br><br>
<strong style="font-size: 24px;">${pin}</strong><br><br>
Dieser Code ist 30 Minuten gültig.<br><br>
Nachfolgend finden Sie Ihre Zugangsdaten:<br><br>
E-Mail: ${user.email}<br>
Passwort: ${passwort}<br><br>
Sollten Sie diese Registrierung nicht vorgenommen haben, können Sie diese E-Mail einfach ignorieren. Ihr Benutzerkonto wird in diesem Fall nicht aktiviert.<br><br>

View File

@@ -8,6 +8,14 @@
export let email: string = "";
export let password: string = "";
export let route: "login" | "signup" = "login"
export let title: Record<typeof route, string> = {
login: "Einloggen",
signup: "Registrieren"
}
export let buttonText: Record<typeof route, string> = {
login: "Einloggen",
signup: "Registrieren"
}
const navigate = (target: string) => {
route = target as typeof route;
@@ -15,7 +23,7 @@
</script>
{#if route == "login"}
<EmbeddedLoginModule onLogin={onLogin} bind:email bind:password {navigate} />
<EmbeddedLoginModule onLogin={onLogin} bind:email bind:password {navigate} title={title.login} buttonText={buttonText.login} />
{:else if route == "signup"}
<EmbeddedRegisterModule bind:email onRegister={(response) => {
addNotification({
@@ -26,5 +34,5 @@
dismissable: true
})
onLogin(response)
}} {navigate} />
}} {navigate} title={title.signup} buttonText={buttonText.signup} />
{/if}

View File

@@ -5,6 +5,8 @@
export let navigate: (target: string) => void;
export let email: string;
export let password: string;
export let title: string = "Einloggen";
export let buttonText: string = "Einloggen";
export let onLogin: (response: Awaited<ReturnType<typeof loginClient>>) => any;
@@ -27,7 +29,7 @@
</script>
<form class="max-w-md mx-auto" on:submit={login} name="login">
<h1 class="text-2xl font-semibold mb-6">Einloggen</h1>
<h1 class="text-3xl mb-4 p-0">{title}</h1>
<div class="flex flex-col gap-4">
<div>
<h4>Email</h4>
@@ -51,7 +53,7 @@
required
/>
</div>
<button class="button" type="submit">Einloggen</button>
<button class="button" type="submit">{buttonText}</button>
<div class="flex flex-row justify-between" style="margin-top: 10px">
<a on:click={() => navigate("signup")} class="cursor-pointer" data-cy="registrieren">Registrieren</a>
<a href="/auth/passwort-vergessen?r={window.location.href}">Passwort Vergessen?</a>

View File

@@ -6,33 +6,48 @@
export let navigate: (target: string) => void;
export let onRegister: (response: Awaited<ReturnType<typeof loginClient>>) => void;
export let email: string;
export let title: string = "Registrieren";
export let buttonText: string = "Registrieren";
let repeatEmail: string;
async function signup(e: SubmitEvent) {
e.preventDefault()
if (email !== repeatEmail) {
addNotification({
message: "Die eingegebenen Email Adressen stimmen nicht überein.",
dismissable: true,
timeout: 3000,
type: "error"
})
return;
}
try {
const { id, password } = await api.user.autocreate.PUT.fetch({
const { id, passwort } = await api.user.autocreate.PUT.fetch({
email,
});
const response = await loginClient(email, password)
const response = await loginClient(email, passwort)
onRegister(response);
} catch (e) {
addNotification({
message: "Ups...",
subtext:
"Da ist wohl etwas schiefgelaufen. Diese Email Adresse ist bereits in Benutzung, haben sie vielleicht bereits ein Konto bei uns?",
`Sie besitzen bereits ein Konto bei IBC. Bitte loggen Sie sich mit Ihrem Passwort ein oder vergeben sich über “Passwort vergessen” ein neues.`,
type: "error",
timeout: 0,
dismissable: true,
});
navigate("login");
}
}
</script>
<form class="max-w-md mx-auto" name="signup" on:submit={signup}>
<h1>Registrieren</h1>
<p>Bitte geben sie ihre Email ein, um einen Account beim IBC zu erstellen, ein Passwort wird ihnen per Email zugesendet, dieses können sie im Nachhinein jederzeit ändern.</p>
<h1 class="text-3xl mb-4 p-0">{title}</h1>
<p class="p-0 text-base">Ihre Ausweisdaten werden bei uns gespeichert und Ihnen wird ein vorläufiges Passwort erstellt. Sollte Ihre E-Mail bereits bei uns registriert sein, dann loggen Sie sich bitte mit Ihrem vorhandenen Passwort ein oder vergeben sich ein neues über “Passwort vergessen”.</p>
<hr>
<div class="flex flex-col gap-4">
<div>
<h4>Email</h4>
@@ -46,7 +61,19 @@
required
/>
</div>
<button class="button" type="submit">Registrieren</button>
<div class="flex flex-col gap-2">
<h4>Email erneut eingeben</h4>
<input
type="text"
placeholder="max.mustermann@email.de"
name="email"
class="input input-bordered text-base text-base-content font-medium"
bind:value={repeatEmail}
on:keyup={() => (repeatEmail = repeatEmail.toLowerCase())}
required
/>
</div>
<button class="button" type="submit">{buttonText}</button>
<div class="flex flex-row justify-between" style="margin-top: 10px">
<button on:click={() => navigate("login")}>Einloggen</button>
<a href="/auth/passwort-vergessen?r={window.location.href}">Passwort Vergessen?</a>

View File

@@ -1214,7 +1214,15 @@ sm:grid-cols-[min-content_min-content_min-content] sm:justify-self-end sm:mt-8"
<Overlay bind:hidden={loginOverlayHidden}>
<div class="bg-white w-full max-w-screen-sm py-8">
<EmbeddedAuthFlowModule onLogin={loginAction} email={email} route="signup"></EmbeddedAuthFlowModule>
<EmbeddedAuthFlowModule onLogin={loginAction} email={email} route="signup" title={
{
login: "Ausweis bestellen",
signup: "Ausweis bestellen"
}
} buttonText={{
login: "Bestellen",
signup: "Bestellen"
}}></EmbeddedAuthFlowModule>
</div>
</Overlay>

View File

@@ -1,145 +0,0 @@
<script lang="ts">
import { addNotification } from "#components/Notifications/shared.js";
import { api } from "astro-typesafe-api/client";
import NotificationWrapper from "#components/Notifications/NotificationWrapper.svelte";
export let email: string;
export let redirect: string | null = null;
// Falls die PIN gerade erst angefordert wurde zeigen wir eine Info an, damit sich der Kunde nicht wundert warum er weitergeleitet wurde.
export let requested: boolean = false;
if (requested) {
addNotification({
message: "Die PIN wurde erneut an Ihre Email Adresse gesendet.",
dismissable: true,
timeout: 5000,
type: "info"
})
}
let digits = new Array(4).fill(null);
let inputs: HTMLInputElement[] = new Array(4);
function focusInput(index: number) {
if (inputs[index]) {
inputs[index].focus();
}
}
function paste(e: ClipboardEvent) {
e.preventDefault()
if (!e.clipboardData) return;
const paste = e.clipboardData.getData('text').slice(0, 4).split('');
for (let i = 0; i < 4; i++) {
if (inputs[i] && /^\d$/.test(paste[i])) {
inputs[i].value = paste[i] || '';
digits[i] = paste[i] || null;
}
}
if (paste.length >= 4) {
inputs.forEach(input => input.blur());
} else {
focusInput(paste.length);
}
e.preventDefault();
}
function handleInput(e: InputEvent, index: number) {
const input = e.target as HTMLInputElement;
const value = input.value;
if (e.inputType === 'deleteContentBackward' || e.inputType === 'deleteContentForward') {
if (index > 0) {
focusInput(index - 1);
}
return;
}
if (/^\d$/.test(value)) {
digits[index] = value;
if (index < 3) {
focusInput(index + 1);
} else {
input.blur();
}
} else {
input.value = '';
digits[index] = null;
}
}
async function verify(e: SubmitEvent) {
e.preventDefault()
const pin = digits.join('');
if (pin.length < 4) {
addNotification({
message: "PIN muss 4 Zeichen enthalten.",
dismissable: true,
timeout: 3000,
type: "error"
})
return;
}
try {
const { verified } = await api.auth.verify.PUT.fetch({
email,
pin
})
if (!verified) {
addNotification({
message: "Die eingegebene PIN ist ungültig oder abgelaufen.",
dismissable: true,
timeout: 3000,
type: "error"
})
return;
}
if (redirect) {
window.location.href = redirect
return
}
window.location.href = "/auth/login";
} catch (e) {
addNotification({
message: "Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.",
dismissable: true,
timeout: 3000,
type: "error"
})
}
}
</script>
<div class="mx-auto w-1/3 bg-base-200 p-8 border border-base-300 rounded-lg">
<h1 class="text-3xl mb-4">Verifizieren</h1>
<p>Bitte geben Sie die 4-stellige PIN ein, den Sie per E-Mail erhalten haben.</p>
<form class="flex flex-col gap-4" on:submit={verify}>
<div class="flex flex-row gap-4 justify-center my-8">
<input type="text" bind:value={digits[0]} bind:this={inputs[0]} maxlength="1" class="input input-bordered w-12 text-center text-2xl" on:paste={paste} on:input={(e) => {
handleInput(e, 0);
}} />
<input type="text" bind:value={digits[1]} bind:this={inputs[1]} maxlength="1" class="input input-bordered w-12 text-center text-2xl" on:input={(e) => {
handleInput(e, 1);
}} />
<input type="text" bind:value={digits[2]} bind:this={inputs[2]} maxlength="1" class="input input-bordered w-12 text-center text-2xl" on:input={(e) => {
handleInput(e, 2);
}} />
<input type="text" bind:value={digits[3]} bind:this={inputs[3]} maxlength="1" class="input input-bordered w-12 text-center text-2xl" on:input={(e) => {
handleInput(e, 3);
}} />
</div>
<div class="flex flex-row justify-center mb-8">
<button type="submit" class="button"
>Verifizieren</button
>
</div>
<a class="link link-hover" href="/auth/request-pin?e={email}{redirect ? `&r=${redirect}` : ""}">PIN erneut anfordern</a>
</form>
<NotificationWrapper></NotificationWrapper>
</div>

View File

@@ -1,29 +1,24 @@
<script lang="ts">
import { addNotification } from "#components/Notifications/shared.js";
import { CrossCircled } from "radix-svelte-icons";
import { fade } from "svelte/transition";
import { api } from "astro-typesafe-api/client";
import NotificationWrapper from "#components/Notifications/NotificationWrapper.svelte";
import { loginClient } from "#lib/login.js";
import PlzSuche from "#components/PlzSuche.svelte";
let passwort: string;
let email: string;
let vorname: string;
let name: string;
let repeatEmail: string;
let adresse: string;
let plz: string;
let ort: string;
export let redirect: string | null = null;
async function login(e: SubmitEvent) {
e.preventDefault()
if (passwort.length < 8) {
addNotification({
message: "Passwort muss mindestens 6 Zeichen enthalten.",
dismissable: true,
timeout: 3000,
type: "error"
})
return;
} else if (email !== repeatEmail) {
if (email !== repeatEmail) {
addNotification({
message: "Die eingegebenen Email Adressen stimmen nicht überein.",
dismissable: true,
@@ -34,31 +29,37 @@
}
try {
const { id } = await api.user.PUT.fetch({
email,
passwort,
const { id, passwort } = await api.user.PUT.fetch({
vorname,
name
name, email,
adresse, plz, ort
})
await loginClient(email, passwort)
if (redirect) {
window.location.href = `/auth/verify-pin?e=${encodeURIComponent(email)}&r=${redirect}`
window.location.href = redirect
return
}
window.location.href = "/auth/verify-pin?e=" + encodeURIComponent(email);
window.location.href = "/dashboard";
} catch (e) {
errorHidden = false;
addNotification({
message: "Ups...",
subtext:
"Da ist wohl etwas schiefgelaufen. Diese Email Adresse ist bereits in Benutzung, haben sie vielleicht bereits ein Konto bei uns?",
type: "error",
timeout: 0,
dismissable: true,
});
}
}
let errorHidden = true;
</script>
<div class="mx-auto w-1/3 bg-base-200 p-8 border border-base-300 rounded-lg">
<h1 class="text-3xl mb-4">Registrieren</h1>
<form class="flex flex-col gap-4" on:submit={login}>
<p>Bitte geben sie Email, Ansprechpartner und Adressdaten ein, um einen Account beim IBC zu erstellen, ein Passwort wird ihnen per Email zugesendet, dieses können sie im Nachhinein jederzeit ändern.</p>
<form class="flex flex-col gap-4 mt-8" on:submit={login}>
<div class="flex flex-row gap-4 w-full">
<div class="w-1/2 flex flex-col gap-2">
<h4>Vorname</h4>
@@ -83,6 +84,35 @@
/>
</div>
</div>
<div class="flex flex-row gap-4 w-full">
<div class="w-1/2 flex flex-col gap-2">
<h4>Straße</h4>
<input
type="text"
placeholder="Straße"
name="strasse"
class="input input-bordered text-base text-base-content font-medium"
bind:value={adresse}
required
/>
</div>
<div class="w-1/2 flex flex-col gap-2">
<h4>PLZ</h4>
<PlzSuche bind:zip={plz} bind:city={ort} name="plz" readonly={false} />
</div>
<div class="w-1/2 flex flex-col gap-2">
<h4>Ort</h4>
<input
type="text"
placeholder="Ort"
name="ort"
class="input input-bordered text-base text-base-content font-medium"
bind:value={ort}
required
/>
</div>
</div>
<div class="flex flex-col gap-2">
<h4>Email</h4>
<input
@@ -107,27 +137,8 @@
required
/>
</div>
<div class="flex flex-col gap-2">
<h4>Passwort</h4>
<input
type="password"
placeholder="********"
minlength="8"
name="passwort"
class="input input-bordered text-base text-base-content font-medium"
bind:value={passwort}
required
/>
</div>
{#if !errorHidden}
<div role="alert" class="alert alert-error" in:fade out:fade={{delay: 400}}>
<CrossCircled size={24} />
<span class="font-semibold">Da ist wohl etwas schiefgelaufen. Diese Email Adresse ist bereits in Benutzung, haben sie vielleicht bereits ein Konto bei uns?</span>
</div>
{/if}
<button type="submit" class="button"
>Registrieren</button
>
>Registrieren</button>
<div class="flex flex-row justify-between" style="margin-top: 10px">
<a class="link link-hover"
href="/auth/login{redirect ? `?r=${redirect}` : ""}">Einloggen</a

View File

@@ -1,65 +0,0 @@
import { z } from "zod";
import { prisma } from "#lib/server/prisma.js";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { verifyCode } from "#lib/pin.js";
import { logger } from "#lib/logger.js";
export const PUT = defineApiRoute({
meta: {
description:
"Erstellt, basierend auf einem existierenden und gültigen Refresh Tokens, einen neuen Access Token, welcher zur Authentifizierung genutzt werden kann. Der resultierende Access Token ist nur 2 Tage gültig und muss danach neu generiert werden. Diese Funktion gibt ebenfalls einen neuen Refresh Token zurück, der alte wird dadurch invalidiert.",
tags: ["Benutzer"],
summary: "Access Token anfragen.",
},
input: z.object({
pin: z.string(),
email: z.string(),
}),
output: z.object({
verified: z.boolean(),
}),
async fetch(input, ctx) {
const valid = verifyCode(input.email, input.pin);
if (!valid) {
throw new APIError({
code: "BAD_REQUEST",
message: "Die gegebene PIN ist nicht gültig.",
});
}
const user = await prisma.benutzer.findUnique({
where: {
email: input.email,
},
});
if (!user) {
throw new APIError({
code: "BAD_REQUEST",
message: "Die gegebene PIN ist nicht gültig.",
});
}
logger.info(`Verified ${input.email}`);
if (user.verified) {
return {
verified: true,
};
}
await prisma.benutzer.update({
where: {
id: user.id,
},
data: {
verified: true,
},
});
return {
verified: true,
};
},
});

View File

@@ -4,6 +4,7 @@ import { generateIDWithPrefix } from "#lib/db.js";
import { hashPassword } from "#lib/password.js";
import { createLexOfficeCustomer } from "#lib/server/lexoffice.js";
import { sendAutoRegisterMail } from "#lib/server/mail/auto-registrierung.js";
import { sendRegisterMail } from "#lib/server/mail/registrierung.js";
import { prisma } from "#lib/server/prisma.js";
import { defineApiRoute, APIError } from "astro-typesafe-api/server";
import { z } from "astro:content";
@@ -15,7 +16,7 @@ export const PUT = defineApiRoute({
output: z.object({
email: z.string().email(),
id: IDWithPrefix,
password: z.string().min(8).max(100)
passwort: z.string().min(8).max(100)
}),
async fetch(input) {
let { email } = input;
@@ -35,12 +36,12 @@ export const PUT = defineApiRoute({
}
const id = generateIDWithPrefix(9, VALID_UUID_PREFIXES.User);
const password = crypto.randomUUID().slice(0, 8);
const passwort = crypto.randomUUID().slice(0, 8);
const user = await prisma.benutzer.create({
data: {
email,
passwort: hashPassword(password),
passwort: hashPassword(passwort),
id
}
})
@@ -56,8 +57,8 @@ export const PUT = defineApiRoute({
}
})
await sendAutoRegisterMail(user, password)
await sendRegisterMail(user, passwort)
return { id, email: user.email, password }
return { id, email: user.email, passwort }
},
})

View File

@@ -106,15 +106,18 @@ export const GET = defineApiRoute({
export const PUT = defineApiRoute({
input: z.object({
email: z.string().email(),
passwort: z.string().min(8),
vorname: z.string(),
name: z.string()
name: z.string(),
adresse: z.string(),
plz: z.string(),
ort: z.string(),
}),
output: z.object({
id: IDWithPrefix
id: IDWithPrefix,
passwort: z.string().min(8).max(100)
}),
async fetch(input) {
let { email, passwort, vorname, name } = input;
let { email, vorname, name, adresse, plz, ort } = input;
email = email.toLowerCase();
const existingUser = await prisma.benutzer.findUnique({
@@ -131,13 +134,17 @@ export const PUT = defineApiRoute({
}
const id = generateIDWithPrefix(9, VALID_UUID_PREFIXES.User);
const passwort = crypto.randomUUID().slice(0, 8);
const user = await prisma.benutzer.create({
data: {
email,
passwort: hashPassword(passwort),
vorname,
passwort: hashPassword(passwort),
name,
adresse,
plz,
ort,
id
}
})
@@ -153,8 +160,8 @@ export const PUT = defineApiRoute({
}
})
await sendRegisterMail(user)
await sendRegisterMail(user, passwort)
return { id }
return { id, passwort }
},
})

View File

@@ -1,95 +0,0 @@
---
import MinimalLayout from "#layouts/MinimalLayout.astro";
import { validateAccessTokenServer } from "#server/lib/validateAccessToken";
import PINVerifyModule from "#modules/PINVerifyModule.svelte";
import { generatePinCode } from "#lib/pin";
import { logger } from "#lib/logger";
import { transport } from "#lib/mail";
import { prisma } from "#lib/server/prisma";
import { TokenType } from "#lib/auth/types";
import { encodeToken } from "#lib/auth/token";
import { BASE_URI } from "#lib/constants";
const valid = await validateAccessTokenServer(Astro)
if (valid) {
return Astro.redirect("/dashboard")
}
const redirect = Astro.url.searchParams.get("r");
const email = Astro.url.searchParams.get("e");
if (!email) {
return Astro.redirect("/auth/signup")
}
const user = await prisma.benutzer.findUnique({
where: {
email: email
}
})
if (!user) {
// Der Nutzer existiert nicht, weiter zur Registrierung
return Astro.redirect("/auth/signup")
}
if (user?.verified) {
// Der Nutzer ist bereits verifiziert, wir können ihn auch direkt zum Login weiterleiten
return Astro.redirect(redirect ?? "/auth/login")
}
const verificationJwt = encodeToken({
typ: TokenType.Verify,
exp: Date.now() + (15 * 60 * 1000),
id: user.id
})
const pin = generatePinCode(email); // 30 Minuten
logger.info(`Generated PIN ${email}: ${pin}`);
await transport.sendMail({
from: `"IBCornelsen" <info@online-energieausweis.org>`,
to: email,
subject: `Ihre Registrierung bei IBCornelsen - Bitte bestätigen Sie Ihre E-Mail-Adresse`,
bcc: "info@online-energieausweis.org",
html: `<p>Sehr geehrte*r ${user.vorname} ${user.name},</p>
<p>vielen Dank für Ihre Registrierung bei IBCornelsen. Ihr Benutzerkonto wurde erfolgreich erstellt.<br><br>
Um Ihre Registrierung abzuschließen, klicken Sie bitte auf den folgenden Link, um Ihre E-Mail-Adresse zu bestätigen:<br><br>
<a href="${BASE_URI}/auth/verify?t=${verificationJwt}">E-Mail-Adresse bestätigen</a><br></p>
<p>Alternativ können Sie den folgenden Bestätigungscode verwenden:<br><br>
<strong style="font-size: 24px;">${pin}</strong><br><br>
Dieser Code ist 30 Minuten gültig.<br><br>
Sollten Sie diese Registrierung nicht vorgenommen haben, können Sie diese E-Mail einfach ignorieren. Ihr Benutzerkonto wird in diesem Fall nicht aktiviert.<br><br>
Falls Sie Fragen haben oder Unterstützung benötigen, stehen wir Ihnen gerne zur Verfügung. Kontaktieren Sie uns einfach unter <a href="mailto:support@online-energieausweis.org">support@online-energieausweis.org</a>.
<p>
Mit freundlichen Grüßen,
<br>
Dipl.-Ing. Jens Cornelsen
<br>
<br>
<strong>IB Cornelsen</strong>
<br>
Katendeich 5A
<br>
21035 Hamburg
<br>
www.online-energieausweis.org
<br>
<br>
fon 040 · 209339850
<br>
fax 040 · 209339859
</p>`
});
---
<MinimalLayout title="Verifizierung - IBCornelsen">
<PINVerifyModule client:load redirect={redirect} email={email} requested={true}></PINVerifyModule>
</MinimalLayout>

View File

@@ -1,39 +0,0 @@
---
import MinimalLayout from "#layouts/MinimalLayout.astro";
import { validateAccessTokenServer } from "#server/lib/validateAccessToken";
import PINVerifyModule from "#modules/PINVerifyModule.svelte";
import { prisma } from "#lib/server/prisma";
const valid = await validateAccessTokenServer(Astro)
if (valid) {
return Astro.redirect("/dashboard")
}
const redirect = Astro.url.searchParams.get("r");
const email = Astro.url.searchParams.get("e");
if (!email) {
return Astro.redirect("/auth/signup")
}
const user = await prisma.benutzer.findUnique({
where: {
email: email
}
})
if (!user) {
// Der Nutzer existiert nicht, weiter zur Registrierung
return Astro.redirect("/auth/signup")
}
if (user?.verified) {
// Der Nutzer ist bereits verifiziert, wir können ihn auch direkt zum Login weiterleiten
return Astro.redirect(redirect ?? "/auth/login")
}
---
<MinimalLayout title="Verifizierung - IBCornelsen">
<PINVerifyModule client:load redirect={redirect} email={email}></PINVerifyModule>
</MinimalLayout>

View File

@@ -1,32 +0,0 @@
---
import Layout from "#layouts/Layout.astro";
import { decodeToken } from "#lib/auth/token";
import { TokenType } from "#lib/auth/types";
import { prisma } from "#lib/server/prisma";
const token = Astro.url.searchParams.get("t");
if (!token) {
return Astro.redirect("/")
}
const payload = decodeToken(token)
if (payload.typ !== TokenType.Verify || !payload.id || !payload.exp || payload.exp < Date.now()) {
return Astro.redirect("/")
}
await prisma.benutzer.update({
where: {
id: payload.id
},
data: {
verified: true
}
})
---
<Layout title="Bestätigung">
<h1>Vielen Dank</h1>
<p>Ihre Email Adresse wurde bestätigt, sie können diese Seite nun schließen.</p>
</Layout>