Speichern

This commit is contained in:
Moritz Utcke
2025-03-07 14:47:09 -03:00
parent f174cf8428
commit d0e0f4aa27
17 changed files with 214 additions and 27 deletions

View File

@@ -206,6 +206,7 @@ Table benutzer {
rolle BenutzerRolle [not null, default: 'USER'] rolle BenutzerRolle [not null, default: 'USER']
firma String firma String
lex_office_id String lex_office_id String
verified Boolean [not null, default: false]
BedarfsausweisWohnen BedarfsausweisWohnen [not null] BedarfsausweisWohnen BedarfsausweisWohnen [not null]
documenttemplates documenttemplates [not null] documenttemplates documenttemplates [not null]
objekte Objekt [not null] objekte Objekt [not null]

View File

@@ -0,0 +1,48 @@
-- AlterTable
ALTER TABLE "Anteilshaber" ALTER COLUMN "uid" SET DEFAULT 'ant-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "Aufnahme" ALTER COLUMN "uid" SET DEFAULT 'auf-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "BedarfsausweisGewerbe" ALTER COLUMN "uid" SET DEFAULT 'bag-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "BedarfsausweisWohnen" ALTER COLUMN "uid" SET DEFAULT 'baw-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "Bild" ALTER COLUMN "uid" SET DEFAULT 'img-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "Event" ALTER COLUMN "uid" SET DEFAULT 'evt-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "GEGEinpreisung" ALTER COLUMN "uid" SET DEFAULT 'gge-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "GEGNachweisGewerbe" ALTER COLUMN "uid" SET DEFAULT 'gnw-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "GEGNachweisWohnen" ALTER COLUMN "uid" SET DEFAULT 'gnw-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "Objekt" ALTER COLUMN "uid" SET DEFAULT 'obj-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "Rechnung" ALTER COLUMN "uid" SET DEFAULT 'inv-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "Tickets" ALTER COLUMN "uid" SET DEFAULT 'tkt-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "Unterlage" ALTER COLUMN "uid" SET DEFAULT 'pln-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "VerbrauchsausweisGewerbe" ALTER COLUMN "uid" SET DEFAULT 'vag-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "VerbrauchsausweisWohnen" ALTER COLUMN "uid" SET DEFAULT 'vaw-' || gen_random_uuid();
-- AlterTable
ALTER TABLE "benutzer" ADD COLUMN "verified" BOOLEAN NOT NULL DEFAULT false,
ALTER COLUMN "uid" SET DEFAULT 'usr-' || gen_random_uuid();

View File

@@ -21,6 +21,8 @@ model Benutzer {
firma String? firma String?
lex_office_id String? lex_office_id String?
verified Boolean @default(false)
Anteilshaber Anteilshaber[] @ignore Anteilshaber Anteilshaber[] @ignore
BedarfsausweisWohnen BedarfsausweisWohnen[] BedarfsausweisWohnen BedarfsausweisWohnen[]
documenttemplates documenttemplates[] documenttemplates documenttemplates[]

View File

@@ -12,8 +12,8 @@ export const createCaller = createCallerFactory({
"admin/post-ausstellen": await import("../src/pages/api/admin/post-ausstellen.ts"), "admin/post-ausstellen": await import("../src/pages/api/admin/post-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"),
"ausweise": await import("../src/pages/api/ausweise/index.ts"), "ausweise": await import("../src/pages/api/ausweise/index.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/forgot-password": await import("../src/pages/api/auth/forgot-password.ts"), "auth/forgot-password": await import("../src/pages/api/auth/forgot-password.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"),

View File

@@ -15,7 +15,11 @@ export async function ausweisSpeichern(
) { ) {
if (objekt.uid) { if (objekt.uid) {
await api.objekt._uid.PATCH.fetch({ await api.objekt._uid.PATCH.fetch({
...exclude(objekt, ["uid"]) adresse: objekt.adresse,
latitude: 0,
longitude: 0,
ort: objekt.ort,
plz: objekt.plz
}, { }, {
params: { params: {
uid: objekt.uid uid: objekt.uid
@@ -26,7 +30,11 @@ export async function ausweisSpeichern(
}) })
} else { } else {
const { uid } = await api.objekt.PUT.fetch({ const { uid } = await api.objekt.PUT.fetch({
...exclude(objekt, ["uid"]) adresse: objekt.adresse,
latitude: 0,
longitude: 0,
ort: objekt.ort,
plz: objekt.plz
}, { }, {
headers: { headers: {
"Authorization": `Bearer ${Cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)}` "Authorization": `Bearer ${Cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)}`
@@ -50,8 +58,64 @@ export async function ausweisSpeichern(
} }
}) })
} else { } else {
console.log(aufnahme);
const { uid } = await api.aufnahme.PUT.fetch({ const { uid } = await api.aufnahme.PUT.fetch({
aufnahme, aufnahme: {
baujahr_gebaeude: aufnahme.baujahr_gebaeude,
baujahr_heizung: aufnahme.baujahr_heizung,
alternative_heizung: aufnahme.alternative_heizung,
alternative_kuehlung: aufnahme.alternative_kuehlung,
alternative_lueftung: aufnahme.alternative_lueftung,
alternative_warmwasser: aufnahme.alternative_warmwasser,
aussenwand_gedaemmt: aufnahme.aussenwand_gedaemmt,
aussenwand_min_12cm_gedaemmt: aufnahme.aussenwand_min_12cm_gedaemmt,
ausweisart: aufnahme.ausweisart,
baujahr_klima: aufnahme.baujahr_klima,
brennstoff_1: aufnahme.brennstoff_1,
brennstoff_2: aufnahme.brennstoff_2,
brennwert_kessel: aufnahme.brennwert_kessel,
dachgeschoss: aufnahme.dachgeschoss,
dachgeschoss_gedaemmt: aufnahme.dachgeschoss_gedaemmt,
dachgeschoss_min_12cm_gedaemmt: aufnahme.dachgeschoss_min_12cm_gedaemmt,
doppel_verglasung: aufnahme.doppel_verglasung,
dreifach_verglasung: aufnahme.dreifach_verglasung,
durchlauf_erhitzer: aufnahme.durchlauf_erhitzer,
einfach_verglasung: aufnahme.einfach_verglasung,
einheiten: aufnahme.einheiten,
einzelofen: aufnahme.einzelofen,
energieeffizienzklasse: aufnahme.energieeffizienzklasse,
erstellungsdatum: aufnahme.erstellungsdatum,
fenster_dicht: aufnahme.fenster_dicht,
fenster_teilweise_undicht: aufnahme.fenster_teilweise_undicht,
flaeche: aufnahme.flaeche,
gebaeudeteil: aufnahme.gebaeudeteil,
gebaeudetyp: aufnahme.gebaeudetyp,
heizungsrohre_gedaemmt: aufnahme.heizungsrohre_gedaemmt,
isolier_verglasung: aufnahme.isolier_verglasung,
keller: aufnahme.keller,
keller_decke_gedaemmt: aufnahme.keller_decke_gedaemmt,
keller_wand_gedaemmt: aufnahme.keller_wand_gedaemmt,
kuehlung: aufnahme.kuehlung,
leerstand: aufnahme.leerstand,
lueftung: aufnahme.lueftung,
niedertemperatur_kessel: aufnahme.niedertemperatur_kessel,
nutzflaeche: aufnahme.nutzflaeche,
oberste_geschossdecke_gedaemmt: aufnahme.oberste_geschossdecke_gedaemmt,
oberste_geschossdecke_min_12cm_gedaemmt: aufnahme.oberste_geschossdecke_min_12cm_gedaemmt,
photovoltaik: aufnahme.photovoltaik,
raum_temperatur_regler: aufnahme.raum_temperatur_regler,
rolllaeden_kaesten_gedaemmt: aufnahme.rolllaeden_kaesten_gedaemmt,
saniert: aufnahme.saniert,
solarsystem_warmwasser: aufnahme.solarsystem_warmwasser,
standard_kessel: aufnahme.standard_kessel,
tueren_dicht: aufnahme.tueren_dicht,
tueren_undicht: aufnahme.tueren_undicht,
waermepumpe: aufnahme.waermepumpe,
warmwasser_rohre_gedaemmt: aufnahme.warmwasser_rohre_gedaemmt,
zentralheizung: aufnahme.zentralheizung,
zirkulation: aufnahme.zirkulation
},
uid_objekt: objekt.uid uid_objekt: objekt.uid
}, { }, {
headers: { headers: {

View File

@@ -2,8 +2,6 @@
import HelpLabel from "#components/labels/HelpLabel.svelte"; import HelpLabel from "#components/labels/HelpLabel.svelte";
import Inputlabel from "#components/labels/InputLabel.svelte"; import Inputlabel from "#components/labels/InputLabel.svelte";
//import Label from "#components/Label.svelte";
import { auditHeizungGebaeudeBaujahr } from "../Verbrauchsausweis/audits/HeizungGebaeudeBaujahr.js"; import { auditHeizungGebaeudeBaujahr } from "../Verbrauchsausweis/audits/HeizungGebaeudeBaujahr.js";
import { addNotification, deleteNotification } from "#components/Notifications/shared.js"; import { addNotification, deleteNotification } from "#components/Notifications/shared.js";
import TagInput from "../TagInput.svelte"; import TagInput from "../TagInput.svelte";
@@ -48,7 +46,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
required required
data-cy="ausstellgrund" data-cy="ausstellgrund"
> >
<option disabled selected value>Bitte auswählen</option> <option disabled selected value={null}>Bitte auswählen</option>
{#each Object.entries(Enums.Ausstellgrund) as [name, ausstellgrund]} {#each Object.entries(Enums.Ausstellgrund) as [name, ausstellgrund]}
<option value={ausstellgrund}>{name}</option> <option value={ausstellgrund}>{name}</option>
{/each} {/each}
@@ -224,7 +222,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
required required
bind:value={aufnahme.saniert} bind:value={aufnahme.saniert}
> >
<option disabled selected value>Bitte auswählen</option> <option disabled selected value={null}>Bitte auswählen</option>
<option value={true}>saniert</option> <option value={true}>saniert</option>
<option value={false}>unsaniert</option> <option value={false}>unsaniert</option>
</select> </select>

View File

@@ -62,7 +62,7 @@
async function spaeterWeitermachen() { async function spaeterWeitermachen() {
loginAction = spaeterWeitermachen; loginAction = spaeterWeitermachen;
if (!await validateAccessTokenClient()) { if (!(await validateAccessTokenClient())) {
loginOverlayHidden = false; loginOverlayHidden = false;
return return
} }
@@ -120,18 +120,16 @@ sm:grid-cols-[1fr_min-content_min-content_min-content] sm:justify-self-end">
{#if showWeiter} {#if showWeiter}
<div>
<Overlay bind:hidden={loginOverlayHidden}>
<div class="bg-white w-full max-w-screen-sm py-8">
<EmbeddedAuthFlowModule onLogin={loginAction} email={""}></EmbeddedAuthFlowModule>
</div>
</Overlay>
<button on:click={ausweisAbschicken} type="button" class="button" data-cy="weiter">Weiter</button> <button on:click={ausweisAbschicken} type="button" class="button" data-cy="weiter">Weiter</button>
</div>
{/if} {/if}
</div> </div>
<Overlay bind:hidden={loginOverlayHidden}>
<div class="bg-white w-full max-w-screen-sm py-8">
<EmbeddedAuthFlowModule onLogin={loginAction} email={""}></EmbeddedAuthFlowModule>
</div>
</Overlay>
{#if showHelp} {#if showHelp}
<div class="w-full col-start-1 row-start-2 sm:row-start-2 col-span-4 mt-4"> <div class="w-full col-start-1 row-start-2 sm:row-start-2 col-span-4 mt-4">
<div <div

View File

@@ -121,7 +121,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
bind:value={aufnahme.dachgeschoss} bind:value={aufnahme.dachgeschoss}
required required
> >
<option disabled selected value>Bitte auswählen</option> <option disabled selected value={null}>Bitte auswählen</option>
<option value={Enums.Heizungsstatus.NICHT_VORHANDEN} <option value={Enums.Heizungsstatus.NICHT_VORHANDEN}
>nicht vorhanden</option >nicht vorhanden</option
> >
@@ -150,7 +150,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
required required
bind:value={aufnahme.keller} bind:value={aufnahme.keller}
> >
<option disabled selected value>Bitte auswählen</option> <option disabled selected value={null}>Bitte auswählen</option>
<option value={Enums.Heizungsstatus.NICHT_VORHANDEN} <option value={Enums.Heizungsstatus.NICHT_VORHANDEN}
>nicht vorhanden</option >nicht vorhanden</option
> >

View File

@@ -75,7 +75,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
required required
bind:value={aufnahme.lueftung} bind:value={aufnahme.lueftung}
> >
<option disabled selected value>Bitte auswählen</option> <option disabled selected value={null}>Bitte auswählen</option>
<option value="Fensterlueftung">Fensterlüftung</option> <option value="Fensterlueftung">Fensterlüftung</option>
<option value="Schachtlueftung">Schachtlüftung</option> <option value="Schachtlueftung">Schachtlüftung</option>
<option value="LueftungsanlageOhneWaermerueckgewinnung" <option value="LueftungsanlageOhneWaermerueckgewinnung"

View File

@@ -17,4 +17,5 @@ export const BenutzerSchema = z.object({
rolle: z.nativeEnum(BenutzerRolle), rolle: z.nativeEnum(BenutzerRolle),
firma: z.string().nullish(), firma: z.string().nullish(),
lex_office_id: z.string().nullish(), lex_office_id: z.string().nullish(),
verified: z.boolean(),
}) })

View File

@@ -1,7 +1,8 @@
export enum TokenType { export enum TokenType {
Refresh, Refresh,
Access, Access,
Reset Reset,
Verify
} }
export type TokenData = { uid: string, typ: TokenType, exp: number } export type TokenData = { uid: string, typ: TokenType, exp: number }

View File

@@ -0,0 +1,37 @@
import { BASE_URI } from "#lib/constants.js";
import { transport } from "#lib/mail.js";
import {
Benutzer,
} from "#lib/client/prisma.js";
import { encodeToken } from "#lib/auth/token.js";
import { TokenType } from "#lib/auth/types.js";
export async function sendRegisterMail(
user: Benutzer
) {
const verificationJwt = encodeToken({
typ: TokenType.Verify,
exp: Date.now() + (15 * 60 * 1000),
uid: user.uid
})
await transport.sendMail({
from: `"IBCornelsen" <info@online-energieausweis.org>`,
to: user.email,
subject: `Ihre Registrierung bei IBCornelsen`,
bcc: "info@online-energieausweis.org",
text: `Hallo ${user.vorname},
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:
https://${BASE_URI}/auth/verify?t=${verificationJwt}
Falls Sie diese Registrierung nicht durchgeführt haben, ignorieren Sie bitte diese E-Mail. Bei Fragen oder Problemen steht Ihnen unser Support-Team jederzeit zur Verfügung.
Wir freuen uns, Sie beim IBC willkommen zu heißen!
Mit freundlichen Grüßen
Ihr IBCornelsen-Team`
});
}

View File

@@ -1,6 +1,6 @@
import { UUidWithPrefix } from "#components/Ausweis/types.js" import { UUidWithPrefix } from "#components/Ausweis/types.js"
import { authorizationMiddleware } from "#lib/middleware/authorization.js" import { authorizationMiddleware } from "#lib/middleware/authorization.js"
import { AufnahmeSchema, ObjektSchema, prisma } from "#lib/server/prisma" import { AufnahmeSchema, ObjektSchema, prisma } from "#lib/server/prisma.js"
import { APIError, defineApiRoute } from "astro-typesafe-api/server" import { APIError, defineApiRoute } from "astro-typesafe-api/server"
import { z } from "zod" import { z } from "zod"

View File

@@ -9,7 +9,8 @@ export const PUT = defineApiRoute({
input: ObjektSchema.omit({ input: ObjektSchema.omit({
id: true, id: true,
uid: true, uid: true,
benutzer_id: true benutzer_id: true,
erstellungsdatum: true
}), }),
output: z.object({ output: z.object({
uid: UUidWithPrefix uid: UUidWithPrefix

View File

@@ -1,6 +1,7 @@
import { UUidWithPrefix } from "#components/Ausweis/types.js"; import { UUidWithPrefix } from "#components/Ausweis/types.js";
import { authorizationMiddleware } from "#lib/middleware/authorization.js"; import { authorizationMiddleware } from "#lib/middleware/authorization.js";
import { hashPassword } from "#lib/password.js"; import { hashPassword } from "#lib/password.js";
import { sendRegisterMail } from "#lib/server/mail/registrierung.js";
import { BenutzerSchema, prisma } from "#lib/server/prisma.js"; import { BenutzerSchema, prisma } from "#lib/server/prisma.js";
import { APIError, defineApiRoute } from "astro-typesafe-api/server"; import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { z } from "zod"; import { z } from "zod";
@@ -50,20 +51,20 @@ export const PUT = defineApiRoute({
uid: UUidWithPrefix uid: UUidWithPrefix
}), }),
async fetch(input) { async fetch(input) {
const user = await prisma.benutzer.findUnique({ const existingUser = await prisma.benutzer.findUnique({
where: { where: {
email: input.email email: input.email
} }
}) })
if (user) { if (existingUser) {
throw new APIError({ throw new APIError({
code: "CONFLICT", code: "CONFLICT",
message: "Email Adresse ist bereits vergeben." message: "Email Adresse ist bereits vergeben."
}) })
} }
const { uid } = await prisma.benutzer.create({ const user = await prisma.benutzer.create({
data: { data: {
email: input.email, email: input.email,
passwort: hashPassword(input.passwort), passwort: hashPassword(input.passwort),
@@ -72,6 +73,8 @@ export const PUT = defineApiRoute({
} }
}) })
return { uid } await sendRegisterMail(user)
return { uid: user.uid }
}, },
}) })

View File

@@ -0,0 +1,32 @@
---
import Layout from "#layouts/Layout.astro";
import { decodeToken } from "#lib/auth/token";
import { TokenType } from "#lib/auth/types";
import { prisma } from "#lib/server/prisma";
const token = Astro.url.searchParams.get("t");
if (!token) {
return Astro.redirect("/")
}
const payload = decodeToken(token)
if (payload.typ !== TokenType.Verify || !payload.uid || !payload.exp || payload.exp < Date.now()) {
return Astro.redirect("/")
}
await prisma.benutzer.update({
where: {
uid: payload.uid
},
data: {
verified: true
}
})
---
<Layout title="Bestätigung">
<h1>Vielen Dank</h1>
<p>Ihre Email Adresse wurde bestätigt, sie können diese Seite nun schließen.</p>
</Layout>

View File

@@ -338,6 +338,7 @@ export function fakeBenutzerComplete() {
rolle: BenutzerRolle.USER, rolle: BenutzerRolle.USER,
firma: undefined, firma: undefined,
lex_office_id: undefined, lex_office_id: undefined,
verified: false,
}; };
} }
export function fakeBild() { export function fakeBild() {