Registrierungsprozess verschlankt

This commit is contained in:
Moritz Utcke
2025-10-16 17:34:26 -04:00
parent 5cca857358
commit 5df588e8e9
7 changed files with 122 additions and 198 deletions

View File

@@ -12,6 +12,7 @@ export const createCaller = createCallerFactory({
"admin/nicht-ausstellen": await import("../src/pages/api/admin/nicht-ausstellen.ts"), "admin/nicht-ausstellen": await import("../src/pages/api/admin/nicht-ausstellen.ts"),
"admin/registriernummer": await import("../src/pages/api/admin/registriernummer.ts"), "admin/registriernummer": await import("../src/pages/api/admin/registriernummer.ts"),
"admin/stornieren": await import("../src/pages/api/admin/stornieren.ts"), "admin/stornieren": await import("../src/pages/api/admin/stornieren.ts"),
"aufnahme": await import("../src/pages/api/aufnahme/index.ts"),
"auth/access-token": await import("../src/pages/api/auth/access-token.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/passwort-vergessen": await import("../src/pages/api/auth/passwort-vergessen.ts"),
"auth/refresh-token": await import("../src/pages/api/auth/refresh-token.ts"), "auth/refresh-token": await import("../src/pages/api/auth/refresh-token.ts"),
@@ -19,7 +20,6 @@ export const createCaller = createCallerFactory({
"ausweise": await import("../src/pages/api/ausweise/index.ts"), "ausweise": await import("../src/pages/api/ausweise/index.ts"),
"bedarfsausweis-gewerbe/[id]": await import("../src/pages/api/bedarfsausweis-gewerbe/[id].ts"), "bedarfsausweis-gewerbe/[id]": await import("../src/pages/api/bedarfsausweis-gewerbe/[id].ts"),
"bedarfsausweis-gewerbe": await import("../src/pages/api/bedarfsausweis-gewerbe/index.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/[id]": await import("../src/pages/api/bedarfsausweis-wohnen/[id].ts"),
"bedarfsausweis-wohnen": await import("../src/pages/api/bedarfsausweis-wohnen/index.ts"), "bedarfsausweis-wohnen": await import("../src/pages/api/bedarfsausweis-wohnen/index.ts"),
"bilder/[id]": await import("../src/pages/api/bilder/[id].ts"), "bilder/[id]": await import("../src/pages/api/bilder/[id].ts"),
@@ -32,12 +32,13 @@ export const createCaller = createCallerFactory({
"rechnung/anfordern": await import("../src/pages/api/rechnung/anfordern.ts"), "rechnung/anfordern": await import("../src/pages/api/rechnung/anfordern.ts"),
"rechnung": await import("../src/pages/api/rechnung/index.ts"), "rechnung": await import("../src/pages/api/rechnung/index.ts"),
"ticket": await import("../src/pages/api/ticket/index.ts"), "ticket": await import("../src/pages/api/ticket/index.ts"),
"verbrauchsausweis-gewerbe/[id]": await import("../src/pages/api/verbrauchsausweis-gewerbe/[id].ts"), "user/autocreate": await import("../src/pages/api/user/autocreate.ts"),
"verbrauchsausweis-gewerbe": await import("../src/pages/api/verbrauchsausweis-gewerbe/index.ts"),
"user": await import("../src/pages/api/user/index.ts"), "user": await import("../src/pages/api/user/index.ts"),
"user/self": await import("../src/pages/api/user/self.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/[id]": await import("../src/pages/api/verbrauchsausweis-wohnen/[id].ts"),
"verbrauchsausweis-wohnen": await import("../src/pages/api/verbrauchsausweis-wohnen/index.ts"), "verbrauchsausweis-wohnen": await import("../src/pages/api/verbrauchsausweis-wohnen/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"),
"webhooks/mollie": await import("../src/pages/api/webhooks/mollie.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]/bilder": await import("../src/pages/api/aufnahme/[id]/bilder.ts"),
"aufnahme/[id]": await import("../src/pages/api/aufnahme/[id]/index.ts"), "aufnahme/[id]": await import("../src/pages/api/aufnahme/[id]/index.ts"),

View File

@@ -0,0 +1,45 @@
import { transport } from "#lib/mail.js";
import {
Benutzer,
} from "#lib/client/prisma.js";
export async function sendAutoRegisterMail(
user: Benutzer,
password: string
) {
await transport.sendMail({
from: `"IBCornelsen" <info@online-energieausweis.org>`,
to: user.email,
subject: `Ihre Registrierung bei IBCornelsen`,
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>
Nachfolgend finden Sie Ihre Zugangsdaten:<br><br>
E-Mail: ${user.email}<br>
Passwort: ${password}<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>`
});
}

View File

@@ -2,12 +2,11 @@
import { loginClient } from "#lib/login.js"; import { loginClient } from "#lib/login.js";
import EmbeddedLoginModule from "./EmbeddedLoginModule.svelte" import EmbeddedLoginModule from "./EmbeddedLoginModule.svelte"
import EmbeddedRegisterModule from "./EmbeddedRegisterModule.svelte" import EmbeddedRegisterModule from "./EmbeddedRegisterModule.svelte"
import EmbeddedVerifyModule from "./EmbeddedVerifyModule.svelte";
export let onLogin: (response: Awaited<ReturnType<typeof loginClient>>) => any; export let onLogin: (response: Awaited<ReturnType<typeof loginClient>>) => any;
export let email: string = ""; export let email: string = "";
export let password: string = ""; export let password: string = "";
export let route: "login" | "signup" | "verify" = "login" export let route: "login" | "signup" = "login"
const navigate = (target: string) => { const navigate = (target: string) => {
route = target as typeof route; route = target as typeof route;
@@ -17,12 +16,8 @@
{#if route == "login"} {#if route == "login"}
<EmbeddedLoginModule onLogin={onLogin} bind:email bind:password {navigate} /> <EmbeddedLoginModule onLogin={onLogin} bind:email bind:password {navigate} />
{:else if route == "signup"} {:else if route == "signup"}
<EmbeddedRegisterModule bind:email bind:password onRegister={(response) => { <EmbeddedRegisterModule bind:email onRegister={(response) => {
email = response.email email = response.email
navigate("verify") navigate("verify")
}} {navigate} /> }} {navigate} />
{:else if route == "verify"}
<EmbeddedVerifyModule bind:email onVerify={() => {
navigate("login")
}} {navigate} />
{/if} {/if}

View File

@@ -26,7 +26,7 @@
} }
</script> </script>
<form style="width:50%;margin: 0 auto" on:submit={login} name="login"> <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-2xl font-semibold mb-6">Einloggen</h1>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div> <div>

View File

@@ -3,26 +3,15 @@
import { api } from "astro-typesafe-api/client"; import { api } from "astro-typesafe-api/client";
export let navigate: (target: string) => void; export let navigate: (target: string) => void;
export let onRegister: (response: { email: string, name: string, vorname: string }) => void; export let onRegister: (response: { email: string }) => void;
export let password: string;
export let email: string; export let email: string;
let vorname: string;
let name: string;
let repeatEmail: string; let repeatEmail: string;
async function signUp(e: SubmitEvent) { async function signup(e: SubmitEvent) {
e.preventDefault() e.preventDefault()
if (password.length < 8) { if (email !== repeatEmail) {
addNotification({
message: "Passwort muss mindestens 8 Zeichen enthalten.",
dismissable: true,
timeout: 3000,
type: "error"
})
return;
} else if (email !== repeatEmail) {
addNotification({ addNotification({
message: "Die eingegebenen Email Adressen stimmen nicht überein.", message: "Die eingegebenen Email Adressen stimmen nicht überein.",
dismissable: true, dismissable: true,
@@ -33,18 +22,15 @@
} }
try { try {
const response = await api.user.PUT.fetch({ const response = await api.user.autocreate.PUT.fetch({
email, email,
passwort: password,
vorname,
name,
}); });
onRegister({ onRegister({
email, email,
name,
vorname
}) })
navigate("login")
} catch (e) { } catch (e) {
addNotification({ addNotification({
message: "Ups...", message: "Ups...",
@@ -58,33 +44,10 @@
} }
</script> </script>
<form style="width:50%;margin: 0 auto" name="signup" on:submit={signUp}> <form class="max-w-md mx-auto" name="signup" on:submit={signup}>
<h1>Registrieren</h1> <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>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex flex-row gap-4 w-full">
<div class="w-1/2">
<h4>Vorname</h4>
<input
type="text"
placeholder="Vorname"
name="vorname"
class="px-2.5 py-1.5 rounded-lg border bg-gray-50"
bind:value={vorname}
required
/>
</div>
<div class="w-1/2">
<h4>Nachname</h4>
<input
type="text"
placeholder="Nachname"
name="nachname"
class="px-2.5 py-1.5 rounded-lg border bg-gray-50"
bind:value={name}
required
/>
</div>
</div>
<div> <div>
<h4>Email</h4> <h4>Email</h4>
<input <input
@@ -109,20 +72,9 @@
required required
/> />
</div> </div>
<div>
<h4>Passwort</h4>
<input
type="password"
placeholder="********"
name="passwort"
class="px-2.5 py-1.5 rounded-lg border bg-gray-50"
bind:value={password}
required
/>
</div>
<button class="button" type="submit">Registrieren</button> <button class="button" type="submit">Registrieren</button>
<div class="flex flex-row justify-between" style="margin-top: 10px"> <div class="flex flex-row justify-between" style="margin-top: 10px">
<button on:click={() => navigate("verify")}>Einloggen</button> <button on:click={() => navigate("login")}>Einloggen</button>
<a href="/auth/passwort-vergessen?r={window.location.href}">Passwort Vergessen?</a> <a href="/auth/passwort-vergessen?r={window.location.href}">Passwort Vergessen?</a>
</div> </div>
</div> </div>

View File

@@ -1,130 +0,0 @@
<script lang="ts">
import { addNotification } from "@ibcornelsen/ui";
import { api } from "astro-typesafe-api/client";
export let navigate: (target: string) => void;
export let onVerify: (response: { email: string, name: string, vorname: string }) => void;
export let email: string;
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;
}
navigate("login");
} catch (e) {
addNotification({
message: "Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.",
dismissable: true,
timeout: 3000,
type: "error"
})
}
}
</script>
<form style="width:50%;margin: 0 auto" name="signup" on:submit={verify}>
<h1>Verifizieren</h1>
<p>Bitte geben Sie die 4-stellige PIN ein, den Sie per E-Mail erhalten haben.</p>
<div class="flex flex-col gap-4">
<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 on:click={() => navigate("login")} class="button"
>Verifizieren</button
>
</div>
<a class="link link-hover" href="/auth/request-pin?e={email}">PIN erneut anfordern</a>
</div>
</form>

View File

@@ -0,0 +1,61 @@
import { IDWithPrefix } from "#components/Ausweis/types.js";
import { VALID_UUID_PREFIXES } from "#lib/constants.js";
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 { prisma } from "#lib/server/prisma.js";
import { defineApiRoute, APIError } from "astro-typesafe-api/server";
import { z } from "astro:content";
export const PUT = defineApiRoute({
input: z.object({
email: z.string().email(),
}),
output: z.object({
id: IDWithPrefix
}),
async fetch(input) {
let { email } = input;
email = email.toLowerCase();
const existingUser = await prisma.benutzer.findUnique({
where: {
email
}
})
if (existingUser) {
throw new APIError({
code: "CONFLICT",
message: "Email Adresse ist bereits vergeben."
})
}
const id = generateIDWithPrefix(9, VALID_UUID_PREFIXES.User);
const password = crypto.randomUUID().slice(0, 8);
const user = await prisma.benutzer.create({
data: {
email,
passwort: hashPassword(password),
id
}
})
const lex_office_id = await createLexOfficeCustomer(user);
await prisma.benutzer.update({
where: {
id: user.id
},
data: {
lex_office_id
}
})
await sendAutoRegisterMail(user, password)
return { id }
},
})