Verbesserungen Registrierung
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"]> = [];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
>
|
||||
<button type="submit" class="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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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 }
|
||||
},
|
||||
})
|
||||
@@ -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 }
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user