Vereinfachter Registrierungsprozess
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.
This commit is contained in:
2
.env
2
.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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
59
src/lib/pin.ts
Normal file
59
src/lib/pin.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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})
|
||||
|
||||
@@ -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" <info@online-energieausweis.org>`,
|
||||
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:<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>
|
||||
|
||||
@@ -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<ReturnType<typeof loginClient>>) => 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: "",
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if route == "login"}
|
||||
<EmbeddedLoginModule onLogin={onLogin} bind:email bind:password {navigate} />
|
||||
{:else}
|
||||
{:else if route == "signup"}
|
||||
<EmbeddedRegisterModule bind:email bind:password onRegister={(response) => {
|
||||
email = response.email
|
||||
navigate("verify")
|
||||
}} {navigate} />
|
||||
{:else if route == "verify"}
|
||||
<EmbeddedVerifyModule bind:email onVerify={() => {
|
||||
navigate("login")
|
||||
}} {navigate} />
|
||||
{/if}
|
||||
@@ -122,7 +122,7 @@
|
||||
</div>
|
||||
<button class="button" type="submit">Registrieren</button>
|
||||
<div class="flex flex-row justify-between" style="margin-top: 10px">
|
||||
<button on:click={() => navigate("login")}>Einloggen</button>
|
||||
<button on:click={() => navigate("verify")}>Einloggen</button>
|
||||
<a href="/auth/passwort-vergessen?r={window.location.href}">Passwort Vergessen?</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
130
src/modules/EmbeddedVerifyModule.svelte
Normal file
130
src/modules/EmbeddedVerifyModule.svelte
Normal file
@@ -0,0 +1,130 @@
|
||||
<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>
|
||||
145
src/modules/PINVerifyModule.svelte
Normal file
145
src/modules/PINVerifyModule.svelte
Normal file
@@ -0,0 +1,145 @@
|
||||
<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>
|
||||
@@ -42,11 +42,11 @@
|
||||
})
|
||||
|
||||
if (redirect) {
|
||||
window.location.href = redirect
|
||||
window.location.href = `/auth/verify-pin?e=${encodeURIComponent(email)}&r=${redirect}`
|
||||
return
|
||||
}
|
||||
|
||||
window.location.href = "/auth/login";
|
||||
window.location.href = "/auth/verify-pin?e=" + encodeURIComponent(email);
|
||||
} catch (e) {
|
||||
errorHidden = false;
|
||||
}
|
||||
|
||||
65
src/pages/api/auth/verify.ts
Normal file
65
src/pages/api/auth/verify.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
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,
|
||||
};
|
||||
},
|
||||
});
|
||||
95
src/pages/auth/request-pin.astro
Normal file
95
src/pages/auth/request-pin.astro
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
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>
|
||||
39
src/pages/auth/verify-pin.astro
Normal file
39
src/pages/auth/verify-pin.astro
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
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>
|
||||
@@ -12,13 +12,13 @@ if (!token) {
|
||||
|
||||
const payload = decodeToken(token)
|
||||
|
||||
if (payload.typ !== TokenType.Verify || !payload.uid || !payload.exp || payload.exp < Date.now()) {
|
||||
if (payload.typ !== TokenType.Verify || !payload.id || !payload.exp || payload.exp < Date.now()) {
|
||||
return Astro.redirect("/")
|
||||
}
|
||||
|
||||
await prisma.benutzer.update({
|
||||
where: {
|
||||
uid: payload.uid
|
||||
id: payload.id
|
||||
},
|
||||
data: {
|
||||
verified: true
|
||||
|
||||
@@ -59,10 +59,13 @@ let loadFromDatabase = false;
|
||||
|
||||
if (typ === AusstellungsTyp.Neuausstellung) {
|
||||
if (!user) {
|
||||
// Der Nutzer muss eingeloggt sein um eine Neuausstellung anzufordern,
|
||||
// sonst können wir nicht sicher sein, dass der Nutzer berechtigt ist auf den Ausweis zuzugreifen.
|
||||
return Astro.redirect(`/auth/login?r=${Astro.url.toString()}`);
|
||||
}
|
||||
|
||||
if (!ausweis_id) {
|
||||
// Falls es keine Ausweis ID gibt können wir nicht fortfahren.
|
||||
return Astro.redirect("/400");
|
||||
}
|
||||
|
||||
@@ -97,9 +100,10 @@ if (typ === AusstellungsTyp.Neuausstellung) {
|
||||
return Astro.redirect("/405");
|
||||
}
|
||||
|
||||
ausweis.id = null;
|
||||
aufnahme.id = null;
|
||||
delete aufnahme.erstellungsdatum;
|
||||
// Wir setzen alle Daten vom Ausweis zurück, sonst könnte es passieren, dass der Ausweis als der alte Ausweis gespeichert wird.
|
||||
ausweis.id = "";
|
||||
aufnahme.id = "";
|
||||
aufnahme.erstellungsdatum = null;
|
||||
ausweis.created_at = new Date()
|
||||
ausweis.updated_at = new Date();
|
||||
ausweis.alte_ausweis_id = null;
|
||||
@@ -132,6 +136,11 @@ if (typ === AusstellungsTyp.Neuausstellung) {
|
||||
ausweis = await getBedarfsausweisWohnen(ausweis_id);
|
||||
}
|
||||
|
||||
if (!ausweis) {
|
||||
// Falls der Ausweis nicht gefunden wurde, können wir nicht fortfahren.
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
|
||||
ausweistyp = ausweis.ausweistyp;
|
||||
|
||||
aufnahme = (await getAufnahme(ausweis.aufnahme_id)) as Aufnahme;
|
||||
@@ -192,8 +201,8 @@ if (typ === AusstellungsTyp.Neuausstellung) {
|
||||
return Astro.redirect("/405");
|
||||
}
|
||||
|
||||
ausweis.id = null;
|
||||
delete aufnahme.erstellungsdatum;
|
||||
ausweis.id = "";
|
||||
aufnahme.erstellungsdatum = null;
|
||||
ausweis.created_at = new Date()
|
||||
ausweis.updated_at = new Date();
|
||||
ausweis.alte_ausweis_id = null;
|
||||
|
||||
Reference in New Issue
Block a user