Implementiert Nutzer Verifizierung

Fügt einen Mechanismus zur Nutzerverifizierung per E-Mail ein.

Nach der Registrierung wird eine E-Mail mit einem zeitbasierten Verifizierungscode versandt. Der Nutzer muss diesen Code eingeben, um sein Konto zu aktivieren.

Die Methode zur Erstellung des Codes ist zeitbasiert und ändert sich alle 15 Minuten.
This commit is contained in:
Moritz Utcke
2025-07-30 09:39:30 -05:00
parent 056cbfa144
commit dc0509cac2
18 changed files with 664 additions and 310 deletions

View File

@@ -59,7 +59,7 @@ all:
bun run dev 2>&1 | tee ~/logs/`date '+%d-%m-%Y_%H:%M:%S'`.log bun run dev 2>&1 | tee ~/logs/`date '+%d-%m-%Y_%H:%M:%S'`.log
update-dwd-klimafaktoren-cron: update-dwd-klimafaktoren-cron:
pm2 start bun --name "update-dwd-klimafaktoren-cron" --cron "0 12 28 * *" -- src/cronjobs/update-dwd-klimafaktoren.ts pm2 start bun --name "update-dwd-klimafaktoren-cron" --no-autorestart --cron "0 12 28 * *" -- src/cronjobs/update-dwd-klimafaktoren.ts
prod: install-dependencies prisma-studio backup-database-cronjob update-dwd-klimafaktoren-cron prod: install-dependencies prisma-studio backup-database-cronjob update-dwd-klimafaktoren-cron
bun run build bun run build

View File

@@ -1,4 +1,5 @@
version: '3' version: '3'
name: database
services: services:
database: database:
build: ./ build: ./

View File

@@ -3,27 +3,33 @@
# === Configuration === # === Configuration ===
BUCKET_NAME="ibc-db-backup" BUCKET_NAME="ibc-db-backup"
ENDPOINT_URL="https://s3.eu-central-3.ionoscloud.com" ENDPOINT_URL="https://s3.eu-central-3.ionoscloud.com"
LOCAL_DOWNLOAD_DIR="./" # Where to save the file LOCAL_DOWNLOAD_DIR="./"
# === Get latest file from IONOS S3 bucket === # === Use filename from argument if provided ===
LATEST_FILE=$(aws s3api list-objects-v2 \ if [ -n "$1" ]; then
LATEST_FILE="$1"
else
echo "📡 No filename provided, fetching latest..."
# === Get latest file from IONOS S3 bucket ===
LATEST_FILE=$(aws --profile ionos s3api list-objects-v2 \
--bucket "$BUCKET_NAME" \ --bucket "$BUCKET_NAME" \
--prefix "data-dump" \ --prefix "full-dump" \
--endpoint-url "$ENDPOINT_URL" \ --endpoint-url "$ENDPOINT_URL" \
--query 'Contents | sort_by(@, &LastModified) | [-1].Key' \ --query 'Contents | sort_by(@, &LastModified) | [-1].Key' \
--output text) --output text)
# === Check if file was found === # === Check if file was found ===
if [ "$LATEST_FILE" == "None" ] || [ -z "$LATEST_FILE" ]; then if [ "$LATEST_FILE" == "None" ] || [ -z "$LATEST_FILE" ]; then
echo "❌ No matching .sql.br file found." echo "❌ No matching .sql.br file found."
exit 1 exit 1
fi
fi fi
FILENAME=$(basename "$LATEST_FILE") FILENAME=$(basename "$LATEST_FILE")
SQL_FILE="${FILENAME%.br}" # Remove .br suffix SQL_FILE="${FILENAME%.br}" # Remove .br suffix
echo "📥 Downloading $LATEST_FILE" echo "📥 Downloading $LATEST_FILE"
aws s3 cp "s3://$BUCKET_NAME/$LATEST_FILE" "$LOCAL_DOWNLOAD_DIR" \ aws --profile ionos s3 cp "s3://$BUCKET_NAME/$LATEST_FILE" "$LOCAL_DOWNLOAD_DIR" \
--endpoint-url "$ENDPOINT_URL" --endpoint-url "$ENDPOINT_URL"
# === Decompress with Brotli === # === Decompress with Brotli ===
@@ -31,8 +37,8 @@ echo "🗜️ Decompressing $FILENAME -> $SQL_FILE"
brotli -d "$FILENAME" brotli -d "$FILENAME"
# === Import into Postgres inside Docker === # === Import into Postgres inside Docker ===
echo "🐘 Importing into PostgreSQL (online-energieausweis-database-1:main)" echo "🐘 Importing into PostgreSQL (database:main)"
docker exec -i "online-energieausweis-database-1" env PGPASSWORD="hHMP8cd^N3SnzGRR" \ docker exec -i "database" env PGPASSWORD="hHMP8cd^N3SnzGRR" \
psql -U "main" -d "main" < "$SQL_FILE" psql -U "main" -d "main" < "$SQL_FILE"
echo "✅ Import complete." echo "✅ Import complete."

View File

@@ -17,8 +17,11 @@ export const createCaller = createCallerFactory({
"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"),
"auth/verification-code": await import("../src/pages/api/auth/verification-code.ts"),
"aufnahme": await import("../src/pages/api/aufnahme/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"),
"ausweise": await import("../src/pages/api/ausweise/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"),

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { BedarfsausweisWohnen, Enums, Rechnung, VerbrauchsausweisGewerbe, VerbrauchsausweisWohnen } from "#lib/server/prisma.js"; import { BedarfsausweisWohnen, Enums, Rechnung, VerbrauchsausweisGewerbe, VerbrauchsausweisWohnen } from "#lib/server/prisma.js";
import moment from "moment"; import moment from "moment";
import { format } from "sharp";
export let bestellungen: (Rechnung & { export let bestellungen: (Rechnung & {
verbrauchsausweis_wohnen: VerbrauchsausweisWohnen | null, verbrauchsausweis_wohnen: VerbrauchsausweisWohnen | null,

View File

@@ -1,9 +1,7 @@
<script lang="ts"> <script lang="ts">
import { fade } from "svelte/transition";
import WidgetCardTemplate from "#components/widgets/immowelt/WidgetCardTemplate.svelte"; import WidgetCardTemplate from "#components/widgets/immowelt/WidgetCardTemplate.svelte";
import { PRICES } from "#lib/constants.js"; import { PRICES } from "#lib/constants.js";
import { Enums } from "#lib/client/prisma"; import { Enums } from "#lib/client/prisma.js";
let ausnahme: boolean = false; let ausnahme: boolean = false;
let oneBOX: boolean = false; let oneBOX: boolean = false;
@@ -17,56 +15,54 @@
let heizungsAlter: string = "bitte auswählen"; let heizungsAlter: string = "bitte auswählen";
let leerStand: string = "bitte auswählen"; let leerStand: string = "bitte auswählen";
const partner:string = "immowelt"; const partner: string = "immowelt";
const twoBoxReason = ["Vermietung/Verkauf", "Aushangpflicht", "Sonstiges"]; const twoBoxReason = ["Vermietung/Verkauf", "Aushangpflicht", "Sonstiges"];
const gewerbeHouse = ["Gewerbegebäude", "Mischgebäude"]; const gewerbeHouse = ["Gewerbegebäude", "Mischgebäude"];
$: ausnahme = $: ausnahme =
leerStand === "mehr als 30" || leerStand === "mehr als 30" ||
heizungsAlter === "< 3" || heizungsAlter === "< 3" ||
(baujahr === "vor 1978" && einheiten === "bis 4 Wohneinheiten" && sanierungsstatus === "unsaniert"); (baujahr === "vor 1978" &&
einheiten === "bis 4 Wohneinheiten" &&
sanierungsstatus === "unsaniert");
$: isTwoBoxReason = twoBoxReason.includes(anlass); $: isTwoBoxReason = twoBoxReason.includes(anlass);
$: isGewerbe = gewerbeHouse.includes(gebaeudetyp); $: isGewerbe = gewerbeHouse.includes(gebaeudetyp);
$: oneBOX = $: oneBOX =
(ausnahme && !isGewerbe) || (ausnahme && !isGewerbe) ||
(!isTwoBoxReason && gebaeudetyp !== "Mischgebäude") || (!isTwoBoxReason && gebaeudetyp !== "Mischgebäude") ||
(gebaeudetyp === "Gewerbegebäude" && leerStand === "mehr als 30"); (gebaeudetyp === "Gewerbegebäude" && leerStand === "mehr als 30");
$: threeBOX = $: threeBOX =
(ausnahme && gebaeudetyp === "Mischgebäude" && isTwoBoxReason && leerStand !== "mehr als 30"); ausnahme &&
gebaeudetyp === "Mischgebäude" &&
$: standardXL = isTwoBoxReason &&
(einheiten === "mehr als 4 Wohneinheiten") leerStand !== "mehr als 30";
$: standardXL = einheiten === "mehr als 4 Wohneinheiten";
</script> </script>
<div id="IBC_app"> <div id="IBC_app">
<input id="recode" type="hidden" value="widgetvorlage" /> <input id="recode" type="hidden" value="widgetvorlage" />
<div id="OEA_input"> <div id="OEA_input">
<div
id="firstrow"
<div id="firstrow" class="firstrow" class="firstrow"
class:sm:grid-cols-3={isTwoBoxReason} class:sm:grid-cols-3={isTwoBoxReason}
class:sm:grid-cols-2={!isTwoBoxReason}> class:sm:grid-cols-2={!isTwoBoxReason}
>
<div class="auswahl"> <div class="auswahl">
<div class="titel">Anlass</div> <div class="titel">Anlass</div>
<select <select id="anlass" class="selectfeld" bind:value={anlass}>
id="anlass"
class="selectfeld"
bind:value={anlass}>
> >
<option selected disabled>bitte auswählen</option> <option selected disabled>bitte auswählen</option>
<option value="Vermietung/Verkauf">Vermietung/Verkauf</option> <option value="Vermietung/Verkauf"
>Vermietung/Verkauf</option
>
<option value="Modernisierung">Modernisierung</option> <option value="Modernisierung">Modernisierung</option>
<option value="Neubau">Neubau</option> <option value="Neubau">Neubau</option>
<option value="Erweiterung">Erweiterung</option> <option value="Erweiterung">Erweiterung</option>
@@ -77,9 +73,7 @@ $: standardXL =
<div class="auswahl"> <div class="auswahl">
<div class="titel">Gebäudetyp</div> <div class="titel">Gebäudetyp</div>
<select <select class="selectfeld" bind:value={gebaeudetyp}>
class="selectfeld"
bind:value={gebaeudetyp}>
> >
<option selected disabled>bitte auswählen</option> <option selected disabled>bitte auswählen</option>
<option value="Einfamilienhaus">Einfamilienhaus</option> <option value="Einfamilienhaus">Einfamilienhaus</option>
@@ -93,10 +87,7 @@ $: standardXL =
{#if isTwoBoxReason} {#if isTwoBoxReason}
<div class="auswahl"> <div class="auswahl">
<div class="titel">Sanierungsstand</div> <div class="titel">Sanierungsstand</div>
<select <select class="selectfeld" bind:value={sanierungsstatus}>
class="selectfeld"
bind:value={sanierungsstatus}>
> >
<option selected disabled>bitte auswählen</option> <option selected disabled>bitte auswählen</option>
<option value="saniert">saniert</option> <option value="saniert">saniert</option>
@@ -104,7 +95,6 @@ $: standardXL =
</select> </select>
</div> </div>
{/if} {/if}
</div> </div>
{#if isTwoBoxReason} {#if isTwoBoxReason}
@@ -115,7 +105,6 @@ $: standardXL =
id="baujahr" id="baujahr"
class="selectfeld" class="selectfeld"
bind:value={baujahr} bind:value={baujahr}
> >
<option selected disabled>bitte auswählen</option> <option selected disabled>bitte auswählen</option>
<option value="vor 1978">vor 1978</option> <option value="vor 1978">vor 1978</option>
@@ -125,10 +114,7 @@ $: standardXL =
<div class="auswahl"> <div class="auswahl">
<div class="titel">Heizungsalter</div> <div class="titel">Heizungsalter</div>
<select <select class="selectfeld" bind:value={heizungsAlter}>
class="selectfeld"
bind:value={heizungsAlter}
>
<option selected disabled>bitte auswählen</option> <option selected disabled>bitte auswählen</option>
<option value="< 3">jünger als 3 Jahre</option> <option value="< 3">jünger als 3 Jahre</option>
<option value=">= 3">3 Jahre oder älter</option> <option value=">= 3">3 Jahre oder älter</option>
@@ -137,10 +123,7 @@ $: standardXL =
<div class="auswahl"> <div class="auswahl">
<div class="titel">Wohneinheiten</div> <div class="titel">Wohneinheiten</div>
<select <select class="selectfeld" bind:value={einheiten}>
class="selectfeld"
bind:value={einheiten}
>
<option selected disabled>bitte auswählen</option> <option selected disabled>bitte auswählen</option>
<option value="bis 4 Wohneinheiten" <option value="bis 4 Wohneinheiten"
>bis 4 Wohneinheiten</option >bis 4 Wohneinheiten</option
@@ -153,10 +136,7 @@ $: standardXL =
<div class="OEA_item4"> <div class="OEA_item4">
<div class="titel">Leerstand</div> <div class="titel">Leerstand</div>
<select <select class="selectfeld ausnahmen" bind:value={leerStand}>
class="selectfeld ausnahmen"
bind:value={leerStand}
>
<option selected disabled>bitte auswählen</option> <option selected disabled>bitte auswählen</option>
<option value="bis 30">bis 30%</option> <option value="bis 30">bis 30%</option>
<option value="mehr als 30">mehr als 30%</option> <option value="mehr als 30">mehr als 30%</option>
@@ -165,23 +145,34 @@ $: standardXL =
</div> </div>
{/if} {/if}
<div id="thirdrow" class="thirdrow" <div
id="thirdrow"
class="thirdrow"
class:grid-cols-1={oneBOX} class:grid-cols-1={oneBOX}
class:md:grid-cols-6={threeBOX} class:md:grid-cols-6={threeBOX}
class:md:grid-cols-4={!oneBOX && !threeBOX} class:md:grid-cols-4={!oneBOX && !threeBOX}
> >
{#if isTwoBoxReason && gebaeudetyp != "Gewerbegebäude" && ausnahme === false}
{#if isTwoBoxReason && (gebaeudetyp != "Gewerbegebäude") && (ausnahme === false)}
<WidgetCardTemplate <WidgetCardTemplate
name="Verbrauchsausweis Wohngebäude" name="Verbrauchsausweis Wohngebäude"
price = {PRICES.VerbrauchsausweisWohnen[standardXL ? Enums.AusweisTyp.standardXL : Enums.AusweisTyp.Standard]} price={PRICES.VerbrauchsausweisWohnen[
price1 = {PRICES.VerbrauchsausweisWohnen[standardXL ? Enums.AusweisTyp.BeratungXL : Enums.AusweisTyp.Beratung]} standardXL
price2 = {PRICES.VerbrauchsausweisWohnen[standardXL ? Enums.AusweisTyp.OfflineXL : Enums.AusweisTyp.Offline]} ? Enums.AusweisTyp.standardXL
: Enums.AusweisTyp.Standard
src={'https://online-energieausweis.org/images/partner/'+partner+'/wohngebaeude.svg'} ]}
price1={PRICES.VerbrauchsausweisWohnen[
standardXL
? Enums.AusweisTyp.BeratungXL
: Enums.AusweisTyp.Beratung
]}
price2={PRICES.VerbrauchsausweisWohnen[
standardXL
? Enums.AusweisTyp.OfflineXL
: Enums.AusweisTyp.Offline
]}
src={"https://online-energieausweis.org/images/partner/" +
partner +
"/wohngebaeude.svg"}
alt="Wohnhaus Verbrauchsausweis" alt="Wohnhaus Verbrauchsausweis"
variant="einfach" variant="einfach"
empfehlung="nein" empfehlung="nein"
@@ -189,27 +180,40 @@ $: standardXL =
services={[ services={[
["3&nbsp;Jahresverbräuche der Heizung benötigt.", true], ["3&nbsp;Jahresverbräuche der Heizung benötigt.", true],
["Zulässig bei Vermietung oder Verkauf.", true], ["Zulässig bei Vermietung oder Verkauf.", true],
["Unzulässig bei unsanierten Gebäuden vor 1978.", false], [
"Unzulässig bei unsanierten Gebäuden vor 1978.",
false,
],
["Ungenau durch individuelles Heizverhalten.", false], ["Ungenau durch individuelles Heizverhalten.", false],
["Wird nicht immer bei den Banken akzeptiert.", false] ["Wird nicht immer bei den Banken akzeptiert.", false],
]} ]}
href_buy1={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/${standardXL ? "?ausweistyp=standardXL" : ""}`}
href_buy1={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/${standardXL ? '?ausweistyp=standardXL' : ''}`} href_buy2={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/${standardXL ? "?ausweistyp=BeratungXL" : "?ausweistyp=Beratung"}`}
href_buy2={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/${standardXL ? '?ausweistyp=BeratungXL' : '?ausweistyp=Beratung'}`} href_buy3={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/${standardXL ? "?ausweistyp=OfflineXL" : "?ausweistyp=Offline"}`}
href_buy3={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/${standardXL ? '?ausweistyp=OfflineXL' : '?ausweistyp=Offline'}`}
></WidgetCardTemplate> ></WidgetCardTemplate>
{/if} {/if}
{#if isTwoBoxReason && (gebaeudetyp != "Gewerbegebäude")} {#if isTwoBoxReason && gebaeudetyp != "Gewerbegebäude"}
<WidgetCardTemplate <WidgetCardTemplate
name="Bedarfsausweis Wohngebäude" name="Bedarfsausweis Wohngebäude"
price = {PRICES.BedarfsausweisWohnen[standardXL ? Enums.AusweisTyp.standardXL : Enums.AusweisTyp.Standard]} price={PRICES.BedarfsausweisWohnen[
price1 = {PRICES.BedarfsausweisWohnen[standardXL ? Enums.AusweisTyp.BeratungXL : Enums.AusweisTyp.Beratung]} standardXL
price2 = {PRICES.BedarfsausweisWohnen[standardXL ? Enums.AusweisTyp.OfflineXL : Enums.AusweisTyp.Offline]} ? Enums.AusweisTyp.standardXL
src={'https://online-energieausweis.org/images/partner/'+partner+'/wohngebaeude.svg'} : Enums.AusweisTyp.Standard
]}
price1={PRICES.BedarfsausweisWohnen[
standardXL
? Enums.AusweisTyp.BeratungXL
: Enums.AusweisTyp.Beratung
]}
price2={PRICES.BedarfsausweisWohnen[
standardXL
? Enums.AusweisTyp.OfflineXL
: Enums.AusweisTyp.Offline
]}
src={"https://online-energieausweis.org/images/partner/" +
partner +
"/wohngebaeude.svg"}
alt="Wohnhaus Bedarfsausweis" alt="Wohnhaus Bedarfsausweis"
variant="fundiert" variant="fundiert"
empfehlung="ja" empfehlung="ja"
@@ -219,200 +223,283 @@ $: standardXL =
["Für Vermietung, Verkauf und Finanzierung.", true], ["Für Vermietung, Verkauf und Finanzierung.", true],
["Zulässig auch für unsanierte Objekte.", true], ["Zulässig auch für unsanierte Objekte.", true],
["Kann als Grundlage für den ISFP dienen.", true], ["Kann als Grundlage für den ISFP dienen.", true],
["Objektivere Berechnungsmethode nach DIN 18599.", true], [
"Objektivere Berechnungsmethode nach DIN 18599.",
true,
],
]} ]}
href_buy1={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/${standardXL ? "?ausweistyp=standardXL" : ""}`}
href_buy1={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/${standardXL ? '?ausweistyp=standardXL' : ''}`} href_buy2={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/${standardXL ? "?ausweistyp=BeratungXL" : "?ausweistyp=Beratung"}`}
href_buy2={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/${standardXL ? '?ausweistyp=BeratungXL' : '?ausweistyp=Beratung'}`} href_buy3={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/${standardXL ? "?ausweistyp=OfflineXL" : "?ausweistyp=Offline"}`}
href_buy3={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/${standardXL ? '?ausweistyp=OfflineXL' : '?ausweistyp=Offline'}`}
></WidgetCardTemplate> ></WidgetCardTemplate>
{/if} {/if}
{#if isTwoBoxReason && isGewerbe && (leerStand != "mehr als 30")} {#if isTwoBoxReason && isGewerbe && leerStand != "mehr als 30"}
<WidgetCardTemplate <WidgetCardTemplate
name="Verbrauchsausweis Gewerbegebäude" name="Verbrauchsausweis Gewerbegebäude"
price = {PRICES.VerbrauchsausweisGewerbe[standardXL ? Enums.AusweisTyp.standardXL : Enums.AusweisTyp.Standard]} price={PRICES.VerbrauchsausweisGewerbe[
price1 = {PRICES.VerbrauchsausweisGewerbe[standardXL ? Enums.AusweisTyp.BeratungXL : Enums.AusweisTyp.Beratung]} standardXL
price2 = {PRICES.VerbrauchsausweisGewerbe[standardXL ? Enums.AusweisTyp.OfflineXL : Enums.AusweisTyp.Offline]} ? Enums.AusweisTyp.standardXL
src={'https://online-energieausweis.org/images/partner/'+partner+'/gewerbegebaeude.svg'} : Enums.AusweisTyp.Standard
]}
price1={PRICES.VerbrauchsausweisGewerbe[
standardXL
? Enums.AusweisTyp.BeratungXL
: Enums.AusweisTyp.Beratung
]}
price2={PRICES.VerbrauchsausweisGewerbe[
standardXL
? Enums.AusweisTyp.OfflineXL
: Enums.AusweisTyp.Offline
]}
src={"https://online-energieausweis.org/images/partner/" +
partner +
"/gewerbegebaeude.svg"}
alt="Gewerbe Verbrauchsausweis" alt="Gewerbe Verbrauchsausweis"
variant="einfach" variant="einfach"
empfehlung="nein" empfehlung="nein"
cta="jetzt&nbsp;online erstellen" cta="jetzt&nbsp;online erstellen"
services={[ services={[
[
["3&nbsp;Jahresverbräuche von Heizung Gebäudestrom&nbsp;nötig.", true], "3&nbsp;Jahresverbräuche von Heizung Gebäudestrom&nbsp;nötig.",
true,
],
["Zulässig bei Vermietung oder Verkauf.", true], ["Zulässig bei Vermietung oder Verkauf.", true],
["Für bauliche und energetische Maßnahmen ungeeignet.", false], [
"Für bauliche und energetische Maßnahmen ungeeignet.",
false,
],
["Wird nicht immer bei den Banken akzeptiert.", false], ["Wird nicht immer bei den Banken akzeptiert.", false],
["Ungenau durch individuelles Heizverhalten", false], ["Ungenau durch individuelles Heizverhalten", false],
]} ]}
href_buy1={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/${standardXL ? "?ausweistyp=standardXL" : ""}`}
href_buy1={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/${standardXL ? '?ausweistyp=standardXL' : ''}`} href_buy2={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/${standardXL ? "?ausweistyp=BeratungXL" : "?ausweistyp=Beratung"}`}
href_buy2={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/${standardXL ? '?ausweistyp=BeratungXL' : '?ausweistyp=Beratung'}`} href_buy3={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/${standardXL ? "?ausweistyp=OfflineXL" : "?ausweistyp=Offline"}`}
href_buy3={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/${standardXL ? '?ausweistyp=OfflineXL' : '?ausweistyp=Offline'}`}
></WidgetCardTemplate> ></WidgetCardTemplate>
{/if} {/if}
{#if isTwoBoxReason && isGewerbe} {#if isTwoBoxReason && isGewerbe}
<WidgetCardTemplate <WidgetCardTemplate
name="Bedarfsausweis Gewerbegebäude" name="Bedarfsausweis Gewerbegebäude"
price = {PRICES.BedarfsausweisGewerbe[standardXL ? Enums.AusweisTyp.standardXL : Enums.AusweisTyp.Standard]} price={PRICES.BedarfsausweisGewerbe[
price1 = {PRICES.BedarfsausweisGewerbe[standardXL ? Enums.AusweisTyp.BeratungXL : Enums.AusweisTyp.Beratung]} standardXL
price2 = {PRICES.BedarfsausweisGewerbe[standardXL ? Enums.AusweisTyp.OfflineXL : Enums.AusweisTyp.Offline]} ? Enums.AusweisTyp.standardXL
src={'https://online-energieausweis.org/images/partner/'+partner+'/gewerbegebaeude.svg'} : Enums.AusweisTyp.Standard
]}
price1={PRICES.BedarfsausweisGewerbe[
standardXL
? Enums.AusweisTyp.BeratungXL
: Enums.AusweisTyp.Beratung
]}
price2={PRICES.BedarfsausweisGewerbe[
standardXL
? Enums.AusweisTyp.OfflineXL
: Enums.AusweisTyp.Offline
]}
src={"https://online-energieausweis.org/images/partner/" +
partner +
"/gewerbegebaeude.svg"}
alt="Gewerbe Bedarfsausweis" alt="Gewerbe Bedarfsausweis"
variant="fundiert" variant="fundiert"
empfehlung="ja" empfehlung="ja"
cta="Angebot anfragen" cta="Angebot anfragen"
services={[ services={[
["Mehrzonenmodell nach DIN 18599.", true], ["Mehrzonenmodell nach DIN 18599.", true],
["Zulässig bei Vermietung oder Verkauf.", true], ["Zulässig bei Vermietung oder Verkauf.", true],
["Grundlage für Sanierung-Varianten.", true], ["Grundlage für Sanierung-Varianten.", true],
["Objektiveres, besser vergleichbares Ergebnis.", true], ["Objektiveres, besser vergleichbares Ergebnis.", true],
["Zulässig bei Leerstand oder fehlenden Verbräuchen", true], [
"Zulässig bei Leerstand oder fehlenden Verbräuchen",
true,
],
]} ]}
href_buy1={`https://online-energieausweis.org/${partner}/angebot-anfragen/bedarfsausweis-gewerbe-anfragen/${standardXL ? "?ausweistyp=standardXL" : ""}`}
href_buy1={`https://online-energieausweis.org/${partner}/angebot-anfragen/bedarfsausweis-gewerbe-anfragen/${standardXL ? '?ausweistyp=standardXL' : ''}`} href_buy2={`https://online-energieausweis.org/${partner}/angebot-anfragen/bedarfsausweis-gewerbe-anfragen/${standardXL ? "?ausweistyp=BeratungXL" : "?ausweistyp=Beratung"}`}
href_buy2={`https://online-energieausweis.org/${partner}/angebot-anfragen/bedarfsausweis-gewerbe-anfragen/${standardXL ? '?ausweistyp=BeratungXL' : '?ausweistyp=Beratung'}`}
></WidgetCardTemplate> ></WidgetCardTemplate>
{/if} {/if}
{#if (anlass != "bitte auswählen") && !isTwoBoxReason && (gebaeudetyp != "Gewerbegebäude")} {#if anlass != "bitte auswählen" && !isTwoBoxReason && gebaeudetyp != "Gewerbegebäude"}
<WidgetCardTemplate <WidgetCardTemplate
name="GEG-Nachweis Wohngebäude" name="GEG-Nachweis Wohngebäude"
price = {PRICES.GEGNachweisWohnen[standardXL ? Enums.AusweisTyp.standardXL : Enums.AusweisTyp.Standard]} price={PRICES.GEGNachweisWohnen[
price1 = {PRICES.GEGNachweisWohnen[standardXL ? Enums.AusweisTyp.BeratungXL : Enums.AusweisTyp.Beratung]} standardXL
price2 = {PRICES.GEGNachweisWohnen[standardXL ? Enums.AusweisTyp.OfflineXL : Enums.AusweisTyp.Offline]} ? Enums.AusweisTyp.standardXL
src={'https://online-energieausweis.org/images/partner/'+partner+'/wohngebaeude.svg'} : Enums.AusweisTyp.Standard
]}
price1={PRICES.GEGNachweisWohnen[
standardXL
? Enums.AusweisTyp.BeratungXL
: Enums.AusweisTyp.Beratung
]}
price2={PRICES.GEGNachweisWohnen[
standardXL
? Enums.AusweisTyp.OfflineXL
: Enums.AusweisTyp.Offline
]}
src={"https://online-energieausweis.org/images/partner/" +
partner +
"/wohngebaeude.svg"}
alt="GEG-Nachweis-Wohnen" alt="GEG-Nachweis-Wohnen"
variant="Bauvorlage" variant="Bauvorlage"
empfehlung="ja" empfehlung="ja"
cta="Angebot anfragen" cta="Angebot anfragen"
services={[ services={[
[
["Nachweis fürs Bauamt bei Neubau oder Modernisierung.", true], "Nachweis fürs Bauamt bei Neubau oder Modernisierung.",
["Beinhaltet die Effizienz der Bauteile und Anlagentechnik.", true], true,
["Erstellung des Energieausweises nach Abschluss inklusive.", true], ],
["Berechnung und Bilanzierung nach aktueller DIN 18599.", true], [
["Zonierung und Erstellung eines 3D Gebäudemodells.", true], "Beinhaltet die Effizienz der Bauteile und Anlagentechnik.",
true,
],
[
"Erstellung des Energieausweises nach Abschluss inklusive.",
true,
],
[
"Berechnung und Bilanzierung nach aktueller DIN 18599.",
true,
],
[
"Zonierung und Erstellung eines 3D Gebäudemodells.",
true,
],
]} ]}
href_buy1={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-wohnen-anfragen/${standardXL ? "?ausweistyp=standardXL" : ""}`}
href_buy1={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-wohnen-anfragen/${standardXL ? '?ausweistyp=standardXL' : ''}`} href_buy2={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-wohnen-anfragen/${standardXL ? "?ausweistyp=BeratungXL" : "?ausweistyp=Beratung"}`}
href_buy2={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-wohnen-anfragen/${standardXL ? '?ausweistyp=BeratungXL' : '?ausweistyp=Beratung'}`}
></WidgetCardTemplate> ></WidgetCardTemplate>
{/if} {/if}
{#if (anlass != "bitte auswählen") && !isTwoBoxReason && isGewerbe} {#if anlass != "bitte auswählen" && !isTwoBoxReason && isGewerbe}
<WidgetCardTemplate <WidgetCardTemplate
name="GEG-Nachweis Gewerbegebäude" name="GEG-Nachweis Gewerbegebäude"
price = {PRICES.GEGNachweisGewerbe[standardXL ? Enums.AusweisTyp.standardXL : Enums.AusweisTyp.Standard]} price={PRICES.GEGNachweisGewerbe[
price1 = {PRICES.GEGNachweisGewerbe[standardXL ? Enums.AusweisTyp.BeratungXL : Enums.AusweisTyp.Beratung]} standardXL
price2 = {PRICES.GEGNachweisGewerbe[standardXL ? Enums.AusweisTyp.OfflineXL : Enums.AusweisTyp.Offline]} ? Enums.AusweisTyp.standardXL
src={'https://online-energieausweis.org/images/partner/'+partner+'/gewerbegebaeude.svg'} : Enums.AusweisTyp.Standard
]}
price1={PRICES.GEGNachweisGewerbe[
standardXL
? Enums.AusweisTyp.BeratungXL
: Enums.AusweisTyp.Beratung
]}
price2={PRICES.GEGNachweisGewerbe[
standardXL
? Enums.AusweisTyp.OfflineXL
: Enums.AusweisTyp.Offline
]}
src={"https://online-energieausweis.org/images/partner/" +
partner +
"/gewerbegebaeude.svg"}
alt="GEG-Nachweis-Gewerbe" alt="GEG-Nachweis-Gewerbe"
variant="Bauvorlage" variant="Bauvorlage"
empfehlung="ja" empfehlung="ja"
cta="Angebot anfragen" cta="Angebot anfragen"
services={[ services={[
[
["Nachweis fürs Bauamt bei Neubau oder Modernisierung.", true], "Nachweis fürs Bauamt bei Neubau oder Modernisierung.",
["Beinhaltet die Effizienz der Bauteile und Anlagentechnik.", true], true,
["Erstellung des Energieausweises nach Abschluss inklusive.", true], ],
["Berechnung und Bilanzierung nach aktueller DIN 18599.", true], [
["Mehrzonenmodell inkl. Erstellung eines 3D Gebäudemodells.", true], "Beinhaltet die Effizienz der Bauteile und Anlagentechnik.",
true,
],
[
"Erstellung des Energieausweises nach Abschluss inklusive.",
true,
],
[
"Berechnung und Bilanzierung nach aktueller DIN 18599.",
true,
],
[
"Mehrzonenmodell inkl. Erstellung eines 3D Gebäudemodells.",
true,
],
]} ]}
href_buy1={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-gewerbe-anfragen/${standardXL ? "?ausweistyp=standardXL" : ""}`}
href_buy1={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-gewerbe-anfragen/${standardXL ? '?ausweistyp=standardXL' : ''}`} href_buy2={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-gewerbe-anfragen/${standardXL ? "?ausweistyp=BeratungXL" : "?ausweistyp=Beratung"}`}
href_buy2={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-gewerbe-anfragen/${standardXL ? '?ausweistyp=BeratungXL' : '?ausweistyp=Beratung'}`}
></WidgetCardTemplate> ></WidgetCardTemplate>
{/if} {/if}
</div> </div>
</div> </div>
</div> </div>
<style lang="postcss"> <style lang="postcss">
#IBC_app {
#IBC_app {
@font-face { @font-face {
font-family: "immo Sans"; font-family: "immo Sans";
src: url('/fonts/Immo-Sans/immoSans-Regular.eot'); src: url("/fonts/Immo-Sans/immoSans-Regular.eot");
src: url('/fonts/Immo-Sans/immoSans-Regular.eot?#iefix') format('embedded-opentype'), src:
url('/fonts/Immo-Sans/immoSans-Regular.woff2') format('woff2'), url("/fonts/Immo-Sans/immoSans-Regular.eot?#iefix")
url('/fonts/Immo-Sans/immoSans-Regular.woff') format('woff'); format("embedded-opentype"),
url("/fonts/Immo-Sans/immoSans-Regular.woff2") format("woff2"),
url("/fonts/Immo-Sans/immoSans-Regular.woff") format("woff");
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
} }
@font-face {
font-family: "immo Sans Bold";
src: url('/fonts/Immo-Sans/immoSans-Bold.eot');
src: url('/fonts/Immo-Sans/immoSans-Bold.eot?#iefix') format('embedded-opentype'), url('../../Fonts/Immo-Sans/immoSans-Bold.woff2') format('woff2'), url('../../Fonts/Immo-Sans/immoSans-Bold.woff') format('woff');
font-style: normal;
font-weight: 700;
}
@font-face { @font-face {
font-family: 'Antique Olive Compact bold'; font-family: "immo Sans Bold";
src: url("/fonts/Immo-Sans/immoSans-Bold.eot");
src:
url("/fonts/Immo-Sans/immoSans-Bold.eot?#iefix")
format("embedded-opentype"),
url("../../Fonts/Immo-Sans/immoSans-Bold.woff2") format("woff2"),
url("../../Fonts/Immo-Sans/immoSans-Bold.woff") format("woff");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Antique Olive Compact bold";
font-weight: 700; font-weight: 700;
font-style: normal; font-style: normal;
font-display:swap; font-display: swap;
src: url("/fonts/Antique Olive Std Compact.woff2") format('woff2'); src: url("/fonts/Antique Olive Std Compact.woff2") format("woff2");
} }
@apply min-w-[320px] max-w-[1920px] p-[4px] @apply min-w-[320px] max-w-[1920px] p-[4px]
sm:p-[10px]; sm:p-[10px];
font-family: "immo Sans"; font-family: "immo Sans";
select option{ select option {
font-family: "immo Sans",sans-serif;} font-family: "immo Sans", sans-serif;
}
.firstrow{@apply grid grid-cols-1 gap-x-4 gap-y-2 .firstrow {
@apply grid grid-cols-1 gap-x-4 gap-y-2
sm:grid-cols-2 sm:gap-x-4 sm:gap-y-2; sm:grid-cols-2 sm:gap-x-4 sm:gap-y-2;
.titel{@apply text-black font-bold bg-[#ffcc00] px-2 py-1 rounded-[0.25rem];} .titel {
@apply text-black font-bold bg-[#ffcc00] px-2 py-1 rounded-[0.25rem];
}
.selectfeld{@apply w-full px-2 py-1 min-h-[38px] ring-1 ring-black/15} .selectfeld {
@apply w-full px-2 py-1 min-h-[38px] ring-1 ring-black/15;
}
}
} .secondrow {
@apply grid grid-cols-2 gap-x-4 gap-y-2 mt-4
.secondrow{@apply grid grid-cols-2 gap-x-4 gap-y-2 mt-4
sm:grid-cols-4 sm:gap-x-4 sm:gap-y-2; sm:grid-cols-4 sm:gap-x-4 sm:gap-y-2;
.titel{@apply text-black font-bold bg-[#cccccc] px-2 py-1 rounded-[0.25rem];} .titel {
@apply text-black font-bold bg-[#cccccc] px-2 py-1 rounded-[0.25rem];
}
.selectfeld{@apply w-full px-2 py-1 min-h-[38px] ring-1 ring-black/15} .selectfeld {
@apply w-full px-2 py-1 min-h-[38px] ring-1 ring-black/15;
}
}
} #OEA_input {
@apply grid;
}
#OEA_input{@apply grid} .thirdrow {
@apply grid grid-cols-1 gap-x-4 gap-y-2 col-start-1
.thirdrow{@apply grid grid-cols-1 gap-x-4 gap-y-2 col-start-1
md:grid-cols-4 md:gap-x-4 md:gap-y-2; md:grid-cols-4 md:gap-x-4 md:gap-y-2;
}
}
}
}
</style> </style>

View File

@@ -34,7 +34,6 @@
ab {price} ab {price}
</p> </p>
</div> </div>
</div> </div>
<hr class="col-span-2 w-full md:w-[50%] md:m-auto bg-[#ffcc00] h-[2px]" /> <hr class="col-span-2 w-full md:w-[50%] md:m-auto bg-[#ffcc00] h-[2px]" />
@@ -52,9 +51,11 @@
<hr class="col-span-2 w-full md:w-[50%] md:m-auto bg-[#ffcc00] h-[2px]" /> <hr class="col-span-2 w-full md:w-[50%] md:m-auto bg-[#ffcc00] h-[2px]" />
<div class="sumCent buttoncols" <div
class="sumCent buttoncols"
class:md:grid-cols-3={href_buy3} class:md:grid-cols-3={href_buy3}
class:md:grid-cols-2={!href_buy3}> class:md:grid-cols-2={!href_buy3}
>
<a <a
href={href_buy1} href={href_buy1}
class="buttoncol" class="buttoncol"

View File

@@ -38,13 +38,13 @@ export const BedarfsausweisWohnenSchema = z.object({
volumen: z.number().nullish(), volumen: z.number().nullish(),
dicht: z.boolean().nullish(), dicht: z.boolean().nullish(),
fenster_flaeche_1: z.number().nullish(), fenster_flaeche_1: z.number().nullish(),
fenster_art_1: z.string().nullish(), fenster_art_1: z.number().nullish(),
fenster_flaeche_2: z.number().nullish(), fenster_flaeche_2: z.number().nullish(),
fenster_art_2: z.string().nullish(), fenster_art_2: z.number().nullish(),
dachfenster_flaeche: z.number().nullish(), dachfenster_flaeche: z.number().nullish(),
dachfenster_art: z.string().nullish(), dachfenster_art: z.number().nullish(),
haustuer_flaeche: z.number().nullish(), haustuer_flaeche: z.number().nullish(),
haustuer_art: z.string().nullish(), haustuer_art: z.number().nullish(),
dach_bauart: z.string().nullish(), dach_bauart: z.string().nullish(),
decke_bauart: z.string().nullish(), decke_bauart: z.string().nullish(),
dach_daemmung: z.string().nullish(), dach_daemmung: z.string().nullish(),

View File

@@ -0,0 +1,29 @@
import { uptime } from "os"
import crypto from "crypto";
/**
* Generiert einen zeitbasierten Hash der sich alle 15 Minuten ändert und an die Uptime des servers gekoppelt ist.
* @param email - Die E-Mail-Adresse des Benutzers, die als Teil des Hashes verwendet wird.
* @param time - Die Zeit in Millisekunden, die seit dem Start des Servers vergangen ist (Standard ist die Uptime des Servers).
* @param length - Die Länge des zurückgegebenen Hashes (Standard ist 6 Zeichen).
* @returns Ein zeitbasierter Hash, der sich alle 15 Minuten ändert und auf der E-Mail-Adresse basiert.
*/
export function createTimeBasedHash(email: string, time: number = uptime(), length: number = 6): string {
const now = Date.now();
const elapsed = now - time;
const window = Math.floor(elapsed / (15 * 60 * 1000)); // 15 minute windows
const data = `${email}:${window}`;
// Use a cryptographic hash (you can also use HMAC with a secret if you want)
const hash = crypto.createHash('sha256').update(data).digest();
// Convert part of the hash to an integer
const int = hash.readUInt32BE(0); // take first 4 bytes
// Modulo to get 6 digits
const pin = (int % 1000000).toString().padStart(6, '0');
return pin;
}

View File

@@ -0,0 +1,41 @@
import { transport } from "#lib/mail.js";
import {
Benutzer,
} from "#lib/client/prisma.js";
import { createTimeBasedHash } from "#lib/auth/time-based-hash.js";
export async function sendVerificationCodeMail(
user: Benutzer
) {
const code = createTimeBasedHash(user.email)
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>Um Ihre Registrierung abzuschließen, geben Sie folgenden Bestätigungscode auf der Website ein, um Ihre E-Mail-Adresse zu bestätigen:<br><br>
<b>${code}</b>
</p>
<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

@@ -5,6 +5,7 @@ import {
} from "#lib/client/prisma.js"; } from "#lib/client/prisma.js";
import { encodeToken } from "#lib/auth/token.js"; import { encodeToken } from "#lib/auth/token.js";
import { TokenType } from "#lib/auth/types.js"; import { TokenType } from "#lib/auth/types.js";
import { createTimeBasedHash } from "#lib/auth/time-based-hash.js";
export async function sendRegisterMail( export async function sendRegisterMail(
user: Benutzer user: Benutzer
@@ -12,9 +13,11 @@ export async function sendRegisterMail(
const verificationJwt = encodeToken({ const verificationJwt = encodeToken({
typ: TokenType.Verify, typ: TokenType.Verify,
exp: Date.now() + (15 * 60 * 1000), exp: Date.now() + (15 * 60 * 1000),
uid: user.uid id: user.id
}) })
const code = createTimeBasedHash(user.email)
await transport.sendMail({ await transport.sendMail({
from: `"IBCornelsen" <info@online-energieausweis.org>`, from: `"IBCornelsen" <info@online-energieausweis.org>`,
to: user.email, to: user.email,
@@ -22,10 +25,11 @@ export async function sendRegisterMail(
bcc: "info@online-energieausweis.org", bcc: "info@online-energieausweis.org",
html: `<p>Sehr geehrte*r ${user.vorname} ${user.name},</p> 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> <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> 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><br>
<a href="${BASE_URI}/auth/verify?t=${verificationJwt}">E-Mail-Adresse bestätigen</a><br></p> Oder geben Sie folgenden Bestätigungscode auf der Website ein:<br>
<b>${code}</b>
</p>
<p> <p>
Mit freundlichen Grüßen, Mit freundlichen Grüßen,
<br> <br>

View File

@@ -30,7 +30,7 @@
} }
try { try {
const { uid } = await api.user.PUT.fetch({ await api.user.PUT.fetch({
email, email,
passwort, passwort,
vorname, vorname,
@@ -42,7 +42,7 @@
return return
} }
window.location.href = "/auth/login"; window.location.href = `/auth/code?email=${email}`;
} catch (e) { } catch (e) {
errorHidden = false; errorHidden = false;
} }

View File

@@ -0,0 +1,89 @@
<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";
export let redirect: string | null = null;
export let email: string;
function verify(e: SubmitEvent) {
e.preventDefault();
const code = numbers.join("");
if (code.length !== 6) {
addNotification({
message: "Bitte geben Sie einen gültigen Verifizierungscode ein.",
dismissable: true,
timeout: 3000,
type: "error"
});
return;
}
api.auth["verification-code"].POST.fetch({ code, email }).then(() => {
if (redirect) {
window.location.href = redirect;
} else {
window.location.href = "/";
}
}).catch(() => {
errorHidden = false;
});
}
// TODO
function codeErneutAnfordern() {
api.auth["verification-code"].GET.fetch(null, {
headers: {
"Authorization": "Bearer"
}
})
}
let numbers = new Array(6).fill("");
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">Verifizierung</h1>
<p>Wir haben ihnen einen Verifizierungscode per Email geschickt, bitte geben sie diesen ein um ihre Registrierung fertigzustellen.</p>
<form on:submit={verify}>
<div class="flex flex-row gap-4 w-full justify-center my-12">
{#each { length: 6 } as _, i}
<input
type="text"
class="input input-bordered text-4xl text-base-content font-medium w-12 text-center"
bind:value={numbers[i]}
maxlength="1"
on:input={function(e) {
if (i !== 5) {
e.target.nextSibling.focus()
}
}}
required
/>
{/each}
</div>
<div class="flex justify-between">
<button type="submit" class="button"
>Abschicken</button
>
<!-- <button type="button" on:click={codeErneutAnfordern} class="button"
>Code erneut anfordern</button
> -->
</div>
{#if !errorHidden}
<div class="flex flex-row gap-4 mt-8" in:fade out:fade={{delay: 400}}>
<CrossCircled size={24} />
<span class="font-semibold"> Da ist wohl etwas schiefgelaufen. Der eingegebene Verifizierungscode ist ungültig.</span>
</div>
{/if}
</form>
<NotificationWrapper></NotificationWrapper>
</div>

View File

@@ -0,0 +1,70 @@
import { z } from "zod";
import { prisma } from "#lib/server/prisma.js";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { sendVerificationCodeMail } from "#lib/server/mail/code.js";
import { authorizationMiddleware } from "#lib/middleware/authorization.js";
import { createTimeBasedHash } from "#lib/auth/time-based-hash.js";
export const GET = defineApiRoute({
meta: {
description:
"Fragt einen neuen Verifizierungscode per Mail an.",
tags: ["Benutzer"],
summary: "Verifizierungscode anfragen.",
},
middleware: authorizationMiddleware,
output: z.void(),
async fetch(input, ctx) {
// Falls der Nutzer nicht existiert, wird eine Fehlermeldung zurückgegeben.
const user = await prisma.benutzer.findUnique({
where: {
email: input.email.toLowerCase(),
},
});
if (!user) {
throw new APIError({
code: "BAD_REQUEST",
message: "Benutzer konnte nicht gefunden werden.",
});
}
await sendVerificationCodeMail(user);
},
});
export const POST = defineApiRoute({
meta: {
description:
"Versucht den Nutzer mithilfe des abgeschickten Codes zu verifizieren.",
tags: ["Benutzer"],
summary: "Verifizieren.",
},
input: z.object({
code: z.string(),
email: z.string().email().toLowerCase(),
}),
output: z.void(),
async fetch({ code, email }, ctx) {
const generatedCode = createTimeBasedHash(email);
console.log(generatedCode, code);
if (code !== generatedCode) {
throw new APIError({
code: "BAD_REQUEST",
message: "Der eingegebene Verifizierungscode ist ungültig.",
});
}
await prisma.benutzer.update({
where: {
email: email.toLowerCase(),
},
data: {
verified: true,
},
});
},
});

24
src/pages/auth/code.astro Normal file
View File

@@ -0,0 +1,24 @@
---
import CodeModule from "../../modules/auth/CodeModule.svelte";
import MinimalLayout from "#layouts/MinimalLayout.astro";
import { validateAccessTokenServer } from "#server/lib/validateAccessToken";
const valid = await validateAccessTokenServer(Astro)
if (valid) {
return Astro.redirect("/dashboard")
}
const redirect = Astro.url.searchParams.get("redirect");
const email = Astro.url.searchParams.get("email");
if (!email) {
return Astro.redirect("/");
}
---
<MinimalLayout title="Verifizierung - IBCornelsen">
<CodeModule client:load {redirect} {email}></CodeModule>
</MinimalLayout>

View File

@@ -3,7 +3,7 @@
set -e set -e
# Config # Config
CONTAINER_NAME="online-energieausweis-database-1" CONTAINER_NAME="database"
DB_USER="main" DB_USER="main"
DB_NAME="main" DB_NAME="main"
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
@@ -58,7 +58,7 @@ BEGIN
FROM pg_tables FROM pg_tables
WHERE schemaname = 'public' WHERE schemaname = 'public'
LOOP LOOP
sql := sql || FORMAT('TRUNCATE TABLE public.%I CASCADE;', r.tablename); sql := sql || FORMAT('DROP TABLE public.%I CASCADE;', r.tablename);
END LOOP; END LOOP;
-- Drop all sequences -- Drop all sequences