diff --git a/Makefile b/Makefile index b59f8b73..7eed7013 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,7 @@ all: bun run dev 2>&1 | tee ~/logs/`date '+%d-%m-%Y_%H:%M:%S'`.log 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 bun run build diff --git a/docker-compose.yml b/docker-compose.yml index a96bb452..81ed8964 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,5 @@ version: '3' +name: database services: database: build: ./ diff --git a/recover-db-dev.bash b/recover-db-dev.bash index a26cf714..ca6d43b2 100644 --- a/recover-db-dev.bash +++ b/recover-db-dev.bash @@ -3,27 +3,33 @@ # === Configuration === BUCKET_NAME="ibc-db-backup" 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 === -LATEST_FILE=$(aws s3api list-objects-v2 \ - --bucket "$BUCKET_NAME" \ - --prefix "data-dump" \ - --endpoint-url "$ENDPOINT_URL" \ - --query 'Contents | sort_by(@, &LastModified) | [-1].Key' \ - --output text) +# === Use filename from argument if provided === +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" \ + --prefix "full-dump" \ + --endpoint-url "$ENDPOINT_URL" \ + --query 'Contents | sort_by(@, &LastModified) | [-1].Key' \ + --output text) -# === Check if file was found === -if [ "$LATEST_FILE" == "None" ] || [ -z "$LATEST_FILE" ]; then - echo "❌ No matching .sql.br file found." - exit 1 + # === Check if file was found === + if [ "$LATEST_FILE" == "None" ] || [ -z "$LATEST_FILE" ]; then + echo "❌ No matching .sql.br file found." + exit 1 + fi fi FILENAME=$(basename "$LATEST_FILE") SQL_FILE="${FILENAME%.br}" # Remove .br suffix 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" # === Decompress with Brotli === @@ -31,8 +37,8 @@ echo "🗜️ Decompressing $FILENAME -> $SQL_FILE" brotli -d "$FILENAME" # === Import into Postgres inside Docker === -echo "🐘 Importing into PostgreSQL (online-energieausweis-database-1:main)" -docker exec -i "online-energieausweis-database-1" env PGPASSWORD="hHMP8cd^N3SnzGRR" \ +echo "🐘 Importing into PostgreSQL (database:main)" +docker exec -i "database" env PGPASSWORD="hHMP8cd^N3SnzGRR" \ psql -U "main" -d "main" < "$SQL_FILE" echo "✅ Import complete." diff --git a/src/astro-typesafe-api-caller.ts b/src/astro-typesafe-api-caller.ts index c1e5a9e7..a9498bdd 100644 --- a/src/astro-typesafe-api-caller.ts +++ b/src/astro-typesafe-api-caller.ts @@ -17,8 +17,11 @@ 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/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": 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": await import("../src/pages/api/bedarfsausweis-wohnen/index.ts"), "bilder/[id]": await import("../src/pages/api/bilder/[id].ts"), diff --git a/src/components/Abrechnung/AbrechungTable.svelte b/src/components/Abrechnung/AbrechungTable.svelte index 03aa4efb..7cf3ba60 100644 --- a/src/components/Abrechnung/AbrechungTable.svelte +++ b/src/components/Abrechnung/AbrechungTable.svelte @@ -1,7 +1,6 @@ -
-
- - -
- - - +
Anlass
- + > - + @@ -77,10 +73,8 @@ $: standardXL =
Gebäudetyp
- + > @@ -91,97 +85,94 @@ $: standardXL =
{#if isTwoBoxReason} -
-
Sanierungsstand
- -
+
+
Sanierungsstand
+ +
{/if} -
{#if isTwoBoxReason} -
-
-
Baujahr
- -
+
+
+
Baujahr
+ +
Heizungsalter
-
-
-
Wohneinheiten
- -
+
+
Wohneinheiten
+ +
-
-
Leerstand
- +
+
Leerstand
+ +
-
{/if} -
- - - {#if isTwoBoxReason && (gebaeudetyp != "Gewerbegebäude") && (ausnahme === false)} - - +
+ {#if isTwoBoxReason && gebaeudetyp != "Gewerbegebäude" && ausnahme === false} - + 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_buy3={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/${standardXL ? "?ausweistyp=OfflineXL" : "?ausweistyp=Offline"}`} + > {/if} - {#if isTwoBoxReason && (gebaeudetyp != "Gewerbegebäude")} - + {#if isTwoBoxReason && gebaeudetyp != "Gewerbegebäude"} - {/if} - {#if isTwoBoxReason && isGewerbe && (leerStand != "mehr als 30")} - + {#if isTwoBoxReason && isGewerbe && leerStand != "mehr als 30"} - {/if} {#if isTwoBoxReason && isGewerbe} - - {/if} - {#if (anlass != "bitte auswählen") && !isTwoBoxReason && (gebaeudetyp != "Gewerbegebäude")} - - - + {#if anlass != "bitte auswählen" && !isTwoBoxReason && gebaeudetyp != "Gewerbegebäude"} + {/if} - {#if (anlass != "bitte auswählen") && !isTwoBoxReason && isGewerbe} - - - + {#if anlass != "bitte auswählen" && !isTwoBoxReason && isGewerbe} + {/if} -
- - diff --git a/src/components/widgets/immowelt/WidgetCardTemplate.svelte b/src/components/widgets/immowelt/WidgetCardTemplate.svelte index 7ea5f26b..ed96d2b8 100644 --- a/src/components/widgets/immowelt/WidgetCardTemplate.svelte +++ b/src/components/widgets/immowelt/WidgetCardTemplate.svelte @@ -34,7 +34,6 @@ ab {price} €

-

@@ -50,11 +49,13 @@ {/each}
-
+
-
+
`, + to: user.email, + subject: `Ihre Registrierung bei IBCornelsen`, + bcc: "info@online-energieausweis.org", + html: `

Sehr geehrte*r ${user.vorname} ${user.name},

+

Um Ihre Registrierung abzuschließen, geben Sie folgenden Bestätigungscode auf der Website ein, um Ihre E-Mail-Adresse zu bestätigen:

+ ${code} +

+

+ Mit freundlichen Grüßen, +
+ Dipl.-Ing. Jens Cornelsen +
+
+ IB Cornelsen +
+ Katendeich 5A +
+ 21035 Hamburg +
+ www.online-energieausweis.org +
+
+ fon 040 · 209339850 +
+ fax 040 · 209339859 +

` + }); +} diff --git a/src/lib/server/mail/registrierung.ts b/src/lib/server/mail/registrierung.ts index 445f3f14..1e0d07d6 100644 --- a/src/lib/server/mail/registrierung.ts +++ b/src/lib/server/mail/registrierung.ts @@ -5,6 +5,7 @@ import { } from "#lib/client/prisma.js"; import { encodeToken } from "#lib/auth/token.js"; import { TokenType } from "#lib/auth/types.js"; +import { createTimeBasedHash } from "#lib/auth/time-based-hash.js"; export async function sendRegisterMail( user: Benutzer @@ -12,9 +13,11 @@ export async function sendRegisterMail( const verificationJwt = encodeToken({ typ: TokenType.Verify, exp: Date.now() + (15 * 60 * 1000), - uid: user.uid + id: user.id }) + const code = createTimeBasedHash(user.email) + await transport.sendMail({ from: `"IBCornelsen" `, to: user.email, @@ -22,10 +25,11 @@ export async function sendRegisterMail( bcc: "info@online-energieausweis.org", html: `

Sehr geehrte*r ${user.vorname} ${user.name},

vielen Dank für Ihre Registrierung bei IBCornelsen. Ihr Benutzerkonto wurde erfolgreich erstellt.

- Um Ihre Registrierung abzuschließen, klicken Sie bitte auf den folgenden Link, um Ihre E-Mail-Adresse zu bestätigen:

- -
E-Mail-Adresse bestätigen

+ E-Mail-Adresse bestätigen

+ Oder geben Sie folgenden Bestätigungscode auf der Website ein:
+ ${code} +

Mit freundlichen Grüßen,
diff --git a/src/modules/RegisterModule.svelte b/src/modules/RegisterModule.svelte index fa49eac4..d467e0e5 100644 --- a/src/modules/RegisterModule.svelte +++ b/src/modules/RegisterModule.svelte @@ -30,7 +30,7 @@ } try { - const { uid } = await api.user.PUT.fetch({ + await api.user.PUT.fetch({ email, passwort, vorname, @@ -42,7 +42,7 @@ return } - window.location.href = "/auth/login"; + window.location.href = `/auth/code?email=${email}`; } catch (e) { errorHidden = false; } diff --git a/src/modules/auth/CodeModule.svelte b/src/modules/auth/CodeModule.svelte new file mode 100644 index 00000000..4592bd5b --- /dev/null +++ b/src/modules/auth/CodeModule.svelte @@ -0,0 +1,89 @@ + + +

+

Verifizierung

+

Wir haben ihnen einen Verifizierungscode per Email geschickt, bitte geben sie diesen ein um ihre Registrierung fertigzustellen.

+
+
+ {#each { length: 6 } as _, i} + + {/each} +
+ +
+ + + + +
+ {#if !errorHidden} +
+ + Da ist wohl etwas schiefgelaufen. Der eingegebene Verifizierungscode ist ungültig. +
+ {/if} +
+ +
\ No newline at end of file diff --git a/src/modules/Auth/PasswortVergessenModule.svelte b/src/modules/auth/PasswortVergessenModule.svelte similarity index 100% rename from src/modules/Auth/PasswortVergessenModule.svelte rename to src/modules/auth/PasswortVergessenModule.svelte diff --git a/src/modules/Auth/PasswortZuruecksetzenModule.svelte b/src/modules/auth/PasswortZuruecksetzenModule.svelte similarity index 100% rename from src/modules/Auth/PasswortZuruecksetzenModule.svelte rename to src/modules/auth/PasswortZuruecksetzenModule.svelte diff --git a/src/pages/api/auth/verification-code.ts b/src/pages/api/auth/verification-code.ts new file mode 100644 index 00000000..1d06bdeb --- /dev/null +++ b/src/pages/api/auth/verification-code.ts @@ -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, + }, + }); + }, +}); \ No newline at end of file diff --git a/src/pages/auth/code.astro b/src/pages/auth/code.astro new file mode 100644 index 00000000..6bdb9bc7 --- /dev/null +++ b/src/pages/auth/code.astro @@ -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("/"); +} + + +--- + + + + diff --git a/wipe-database.bash b/wipe-database.bash index 13d4c8a3..8a4ff3fc 100644 --- a/wipe-database.bash +++ b/wipe-database.bash @@ -3,7 +3,7 @@ set -e # Config -CONTAINER_NAME="online-energieausweis-database-1" +CONTAINER_NAME="database" DB_USER="main" DB_NAME="main" TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") @@ -58,7 +58,7 @@ BEGIN FROM pg_tables WHERE schemaname = 'public' LOOP - sql := sql || FORMAT('TRUNCATE TABLE public.%I CASCADE;', r.tablename); + sql := sql || FORMAT('DROP TABLE public.%I CASCADE;', r.tablename); END LOOP; -- Drop all sequences