API Verbessert - Verbrauchsausweis Funktioniert

This commit is contained in:
Moritz Utcke
2024-01-08 14:38:28 +07:00
parent ff16c3b547
commit ee5133b3f8
40 changed files with 535 additions and 700 deletions

View File

@@ -51,7 +51,7 @@
"trpc-openapi": "^1.2.0",
"uuid": "^9.0.0",
"vite-tsconfig-paths": "^4.2.0",
"zod": "^3.21.4"
"zod": "^3.22.4"
},
"devDependencies": {
"@types/js-cookie": "^3.0.6",

View File

@@ -107,7 +107,7 @@
</HelpLabel>
<div>
<select
name="energietraeger_einheit_heizquelle_1"
name="einheit_1"
required
bind:value={ausweis.einheit_1}
>
@@ -140,7 +140,7 @@
<Label>Einheit *</Label>
<div>
<select
name="energietraeger_einheit_heizquelle_2"
name="einheit_2"
disabled={!ausweis.zusaetzliche_heizquelle}
bind:value={ausweis.einheit_2}
required

View File

@@ -1,297 +0,0 @@
<script lang="ts">
import HorizontalDots from "#components/Icons/HorizontalDots.svelte";
import type { Verbrauchsausweis } from "#lib/Ausweis/Verbrauchsausweis";
import moment from "moment";
import Cross from "#components/Icons/Cross.svelte";
import { Dachgeschoss } from "#lib/Ausweis/types";
interface Service {
name: string;
url: string;
}
export let ausweis: Verbrauchsausweis;
export let services: Service[];
export let hidden: boolean;
export let i: number;
let left: string = "";
$: {
if (i == 0 || i % 3 == 0) {
left = "0";
} else if (i % 2 == 0) {
left = "-50%";
} else {
left = "-100%";
}
}
$: energieverbrauchDaten = [
moment(ausweis.energieverbrauch_zeitraum),
moment(ausweis.energieverbrauch_zeitraum).add("1", "year"),
moment(ausweis.energieverbrauch_zeitraum).add("2", "years"),
moment(ausweis.energieverbrauch_zeitraum).add("3", "years"),
];
let showDropdown: boolean = false;
</script>
<div class="rounded-lg bg-white shadow-sm border px-4 py-1 relative">
<div class="flex flex-row justify-between items-center cursor-pointer">
<h3>
{ausweis.objekt_strasse}, {ausweis.objekt_plz}
{ausweis.objekt_ort}
</h3>
<button
class="cursor-pointer relative hover:bg-gray-100 rounded-lg p-1"
on:click={() => {
showDropdown = !showDropdown;
}}
>
<HorizontalDots width={20} height={20} />
<div
class="absolute right-0 top-full w-[150px] border shadow-md rounded-lg bg-white z-50"
hidden={!showDropdown}
>
<button
class="cursor-pointer hover:bg-gray-100 w-full p-1"
on:click={() => {
hidden = !hidden;
}}>Berechnung</button
>
<button
class="cursor-pointer hover:bg-gray-100 w-full p-1"
on:click={() => {}}>Gebäudeansicht</button
>
<button
class="cursor-pointer hover:bg-gray-100 w-full p-1"
on:click={() => {}}>Ausweis Ansehen</button
>
</div>
</button>
</div>
<a href="/ausweis/{ausweis.uid}/cad">Gebäudeansicht</a>
<div
class="absolute top-[calc(100%+16px)] left-0 w-[200%] rounded-lg z-50 bg-white border p-2"
style="left: {left};"
{hidden}
>
<div class="flex flex-row justify-between">
<h4 class="px-2 py-1 bg-violet-50 rounded-lg">
Datenprüfung Verbrauchsausweis - ID {ausweis.id} - {ausweis.objekt_strasse},
{ausweis.objekt_plz}
{ausweis.objekt_ort}
</h4>
<Cross
on:click={() => (hidden = true)}
className="p-1 hover:bg-gray-100 cursor-pointer w-[25px] h-[25px] rounded-lg"
/>
</div>
<h5>Abschnitt A,B,C,D und E</h5>
<div class="grid grid-cols-4 gap-4">
<div class="border rounded-lg p-1">
<table>
<tr>
<td>AL</td><td>:</td>
<td>{ausweis.ausstellgrund}</td>
</tr>
<tr>
<td>BH</td><td>:</td>
<td>{ausweis.baujahr_anlage}</td>
</tr>
<tr>
<td>BG</td><td>:</td>
<td>{ausweis.baujahr_gebaeude}</td>
</tr>
<tr>
<td>AW</td><td>:</td>
<td>{ausweis.anzahl_einheiten}</td>
</tr>
<tr>
<td>ST</td><td>:</td>
<td>{ausweis.objekt_saniert}</td>
</tr>
</table>
</div>
<div class="border rounded-lg p-1">
<table>
<tr>
<td>WF</td><td>:</td>
<td>{ausweis.wohnflaeche}</td>
</tr>
<tr>
<td>F</td><td>:</td>
<td>{ausweis.baujahr_anlage}</td>
</tr>
<tr>
<td>AN</td><td>:</td>
<td>{ausweis.baujahr_gebaeude}</td>
</tr>
<tr>
<td>KB</td><td>:</td>
<td>{ausweis.keller_beheizt}</td>
</tr>
<tr>
<td>DB</td><td>:</td>
<td>{ausweis.dachgeschoss == Dachgeschoss.BEHEIZT ? "Ja" : "Nein"}</td>
</tr>
</table>
</div>
<div class="border rounded-lg p-1">
<table>
<tr>
<td>WWE</td><td>:</td>
<td>{ausweis.warmwasser_enthalten}</td>
</tr>
<tr>
<td>WWA</td><td>:</td>
<td>{ausweis.anteil_warmwasser_1}</td>
</tr>
<tr>
<td>WWAZH</td><td>:</td>
<td>{ausweis.anteil_warmwasser_2}</td>
</tr>
<tr>
<td>AEVS</td><td>:</td>
<td />
</tr>
<tr>
<td>SSWW</td><td>:</td>
<td>{ausweis.dachgeschoss}</td>
</tr>
</table>
</div>
<div class="border rounded-lg p-1">
<table>
<tr>
<td>GT</td><td>:</td>
<td>{ausweis.objekt_gebaeudeteil}</td>
</tr>
<tr>
<td>GTL</td><td>:</td>
<td />
</tr>
<tr>
<td>L</td><td>:</td>
<td>{ausweis.lueftungskonzept}</td>
</tr>
<tr>
<td>AK</td><td>:</td>
<td />
</tr>
<tr>
<td>LS</td><td>:</td>
<td>{ausweis.leerstand}</td>
</tr>
</table>
</div>
</div>
<div class="border rounded-lg p-1 gap-2 grid grid-cols-4 justify-between">
<table>
<tr>
<td>{energieverbrauchDaten[0].format("MMMM YYYY")}</td>
</tr>
<tr>
<td
>{energieverbrauchDaten[0].format("MM.YYYY")} - {energieverbrauchDaten[1].format(
"MM.YYYY"
)}</td
>
</tr>
<tr>
<td
>{energieverbrauchDaten[1].format("MM.YYYY")} - {energieverbrauchDaten[2].format(
"MM.YYYY"
)}</td
>
</tr>
<tr>
<td
>{energieverbrauchDaten[2].format("MM.YYYY")} - {energieverbrauchDaten[3].format(
"MM.YYYY"
)}</td
>
</tr>
<tr>
<td>Faktoren Hi / PF / COE</td>
</tr>
</table>
<table>
<tr>
<td>Primäre Heizquelle (1)</td>
</tr>
<tr>
<td
>{ausweis.energieverbrauch_1_heizquelle_1}
{ausweis.energietraeger_einheit_heizquelle_1}
{ausweis.energietraeger_1}</td
>
</tr>
<tr>
<td
>{ausweis.energieverbrauch_2_heizquelle_1}
{ausweis.energietraeger_einheit_heizquelle_1}
{ausweis.energietraeger_1}</td
>
</tr>
<tr>
<td
>{ausweis.energieverbrauch_3_heizquelle_1}
{ausweis.energietraeger_einheit_heizquelle_1}
{ausweis.energietraeger_1}</td
>
</tr>
<tr>
<td>10 / 1.1 / 2.5</td>
</tr>
</table>
<table>
<tr>
<td>Sekundäre Heizquelle (2)</td>
</tr>
<tr>
<td
>{ausweis.energieverbrauch_1_heizquelle_2}
{ausweis.energietraeger_einheit_heizquelle_2}
{ausweis.energietraeger_2}</td
>
</tr>
<tr>
<td
>{ausweis.energieverbrauch_2_heizquelle_2}
{ausweis.energietraeger_einheit_heizquelle_2}
{ausweis.energietraeger_2}</td
>
</tr>
<tr>
<td
>{ausweis.energieverbrauch_3_heizquelle_2}
{ausweis.energietraeger_einheit_heizquelle_2}
{ausweis.energietraeger_2}</td
>
</tr>
<tr>
<td>10 / 1.1 / 2.5</td>
</tr>
</table>
<table>
<tr>
<td>Klimafaktoren</td>
</tr>
<tr>
<td>1.12</td>
</tr>
<tr>
<td>1.15</td>
</tr>
<tr>
<td>1.12</td>
</tr>
<tr>
<td>-</td>
</tr>
</table>
</div>
</div>
</div>

View File

@@ -56,17 +56,17 @@ const loggedIn = isLoggedIn(Astro);
>Energieausweis erstellen</a
>
<a class="headerButton" href="/energieausweis-kontakt.php"
>{t("header.kontakt")}</a
>Kontakt</a
>
<a class="headerButton" href="/agb">AGB</a>
{
loggedIn ? (
<a class="headerButton" href="/user">
{t("header.profil")}
Profil
</a>
) : (
<a class="headerButton" href="/login">
{t("header.login")}
Login
</a>
)
}

View File

@@ -4,7 +4,7 @@ import {
VerbrauchsausweisWohnen,
} from "@ibcornelsen/database";
import moment from "moment";
import trpc from "src/trpc";
import { client } from "src/trpc";
export function energetischeNutzflaecheVerbrauchsausweisWohnen_2016(
ausweis: VerbrauchsausweisWohnen & {
@@ -28,7 +28,7 @@ export async function endEnergieVerbrauchVerbrauchsausweis_2016(
return null
}
const klimafaktoren = await trpc.klimafaktoren.query({
const klimafaktoren = await client.v1.klimafaktoren.query({
plz: ausweis.gebaeude_stammdaten.plz,
genauigkeit: "years",
startdatum: ausweis.startdatum,

View File

@@ -1,4 +1,4 @@
import { getKlimafaktoren } from "#lib/Klimafaktoren";
import { getKlimafaktoren } from "#lib/getKlimafaktoren";
import { getHeizwertfaktor } from "#lib/server/Heizwertfaktor";
import { GebaeudeStammdaten, VerbrauchsausweisWohnen } from "@ibcornelsen/database";

View File

@@ -1,10 +1,12 @@
import jwt from "jwt-simple";
export function encodeToken(data: Record<string, any>) {
type TokenData = { uid: string, exp: number }
export function encodeToken(data: TokenData) {
const token = jwt.encode(data, "yIvbgS$k7Bfc+mpV%TWDZAhje9#uJad4", "HS256");
return token;
}
export function decodeToken<T>(token: string): Partial<T> {
export function decodeToken(token: string): Partial<TokenData> {
return jwt.decode(token, "yIvbgS$k7Bfc+mpV%TWDZAhje9#uJad4");
}

View File

@@ -1,13 +1,13 @@
import moment from "moment";
import { memoize } from "./Memoization";
import trpc from "src/trpc";
import { client } from "src/trpc";
export const getKlimafaktoren = memoize(async (date: Date, plz: string) => {
if (!plz || !date) {
return null;
}
const response = await trpc.klimafaktoren.query({
const response = await client.v1.klimafaktoren.query({
plz,
genauigkeit: "years",
startdatum: date,

View File

@@ -69,33 +69,4 @@ export class User {
return user;
}
public static async create(user: UserType): Promise<{uid: string, id: number} | null> {
if (!user || UserRegisterValidator.safeParse(user).success == false) {
return null;
}
const uid = uuid();
const hashedPassword = hashPassword(user.passwort);
const result = await prisma.benutzer.create({
data: {
email: user.email,
passwort: hashedPassword,
uid: uid
},
select: {
id: true
}
})
if (!result) {
return null;
}
return {
uid,
id: result.id
}
}
}

View File

@@ -2,7 +2,7 @@ import {
endEnergieVerbrauchVerbrauchsausweis_2016,
energetischeNutzflaecheVerbrauchsausweisWohnen_2016,
} from "#lib/Berechnungen/VerbrauchsausweisWohnen/VerbrauchsausweisWohnen_2016";
import { getKlimafaktoren } from "#lib/Klimafaktoren";
import { getKlimafaktoren } from "#lib/getKlimafaktoren";
import { getHeizwertfaktor } from "#lib/server/Heizwertfaktor";
import {
GebaeudeStammdaten,
@@ -28,6 +28,11 @@ export async function xmlVerbrauchsausweisWohnen_2016(
let postleitzahl = gebaeude.plz?.substring(0, 3) + "XX";
const result = await endEnergieVerbrauchVerbrauchsausweis_2016(ausweis);
if (!result) {
throw new Error("Verbrauchsausweis konnte nicht zur Berechnung verarbeitet werden.");
}
const berechnungen = new AusweisBerechnungen2016(
ausweis,
ausweis.gebaeude_stammdaten,
@@ -79,12 +84,12 @@ export async function xmlVerbrauchsausweisWohnen_2016(
: "true";
let heizwertfaktor_1 = getHeizwertfaktor(
ausweis.brennstoff_1,
ausweis.einheit_1
ausweis.brennstoff_1 as string,
ausweis.einheit_1 as string
);
let heizwertfaktor_2 = getHeizwertfaktor(
ausweis.brennstoff_2,
ausweis.einheit_2
ausweis.brennstoff_2 as string,
ausweis.einheit_2 as string
);
let warmWasserErmittlung =
@@ -158,12 +163,12 @@ export async function xmlVerbrauchsausweisWohnen_2016(
);
let energieTraeger = berechnungen.getEnergietraegerBezeichnung(
ausweis.brennstoff_1,
ausweis.einheit_1
ausweis.brennstoff_1 as string,
ausweis.einheit_1 as string
);
let energieTraeger_2 = berechnungen.getEnergietraegerBezeichnung(
ausweis.brennstoff_2,
ausweis.einheit_2
ausweis.brennstoff_2 as string,
ausweis.einheit_2 as string
);
let warmwasserZuschlag = Math.round(result.energieVerbrauchWarmwasser_1);

View File

@@ -1,5 +0,0 @@
export async function getKlimafaktorenServer(date: Date, zip: string, accuracy: "months" | "years" = "months"): Promise<number[] | null> {
return null;
}

View File

@@ -1,21 +0,0 @@
/**
* Dies ist die Server-Side Implementierung von fetch, die Daten werden direkt vom Server abgerufen.
* Dadurch können unnötige Requests vermieden werden.
* @date 9/20/2023 - 11:33:30 AM
*
* @export
* @async
* @param {string} resourceUri
* @param {?RequestInit} [options]
* @returns {Promise<any>}
*/
export async function fetch(resourceUri: string, options?: RequestInit): Promise<any> {
const response = await fetch(`http://localhost:3000/api/${resourceUri}`, options);
if (!response.ok) {
throw new Error("Fehler beim Abrufen der Daten.");
}
return response.json();
}

View File

@@ -1,10 +1,15 @@
import { TRPCError, initTRPC } from "@trpc/server";
import { initTRPC } from "@trpc/server";
import { ZodError } from "zod";
import { OpenApiMeta } from "trpc-openapi";
type Context = { uid?: string };
type Context = {
authorization: string | null;
ip: string;
req: Request;
}
export const t = initTRPC.context<Context>().meta<OpenApiMeta>().create({
errorFormatter(opts) {
const { shape, error } = opts;
return {
@@ -19,16 +24,3 @@ export const t = initTRPC.context<Context>().meta<OpenApiMeta>().create({
};
}
});
export const publicProcedure = t.procedure
export const privateProcedure = t.procedure.use((opts) => {
if (!opts.ctx.uid) {
throw new TRPCError({
code: 'FORBIDDEN',
message: "Diese Ressource benötigt eine UID, welche im 'Authorization' Header gegeben sein muss.",
});
}
return opts.next();
});

View File

@@ -0,0 +1,26 @@
import { prisma } from "@ibcornelsen/database";
import { privateProcedure } from "./privateProcedure";
// NOTE: Muss noch mit der Datenbank eingebunden werden.
export const loggedProcedure = privateProcedure.use(async (opts) => {
const start = Date.now();
const result = await opts.next();
const durationMs = Date.now() - start;
await prisma.apiRequests.create({
data: {
ip: opts.ctx.ip,
method: opts.type,
path: opts.path,
responseSize: JSON.stringify(result).length,
responseTime: durationMs,
userAgent: opts.ctx.req.headers["user-agent"] || "",
status: result.ok ? 200 : 500,
user_id: opts.ctx.user.id
}
})
return result;
});

View File

@@ -0,0 +1,44 @@
import { TRPCError } from "@trpc/server";
import { publicProcedure } from "./publicProcedure";
import { decodeToken } from "#lib/JsonWebToken";
import { User } from "#lib/User";
export const privateProcedure = publicProcedure.use(async (opts) => {
const { ctx } = opts;
if (!ctx.authorization) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Für diese Action ist ein Bearer Token verpflichtend." });
}
const [authorizationType, value] = ctx.authorization.split(" ");
if (authorizationType != "Bearer") {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Für diese Action ist ein Bearer Token verpflichtend." });
}
const stringToken = Buffer.from(value, "base64").toString();
try {
const token = decodeToken(
stringToken
);
const uid = token.uid;
if (!uid) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Der gegebene Token ist fehlerhaft." });
}
const user = await User.fromUID(uid);
if (!user) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Der gegebene Token ist fehlerhaft." });
}
return opts.next({
ctx: {
user,
},
});
} catch (e) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Der gegebene Token ist fehlerhaft." });
}
});

View File

@@ -0,0 +1,3 @@
import { t } from "#lib/trpc/context";
export const publicProcedure = t.procedure;

View File

@@ -0,0 +1,64 @@
import { z } from "zod";
import moment from "moment";
import { prisma } from "@ibcornelsen/database";
import { publicProcedure } from "#lib/trpc/middlewares/publicProcedure";
import { encodeToken } from "#lib/JsonWebToken";
import { hashPassword } from "#lib/Password";
import { TRPCError } from "@trpc/server";
export const tRPC_V1_BenutzerErstellenProcedure = publicProcedure
.input(
z.object({
email: z.string().email(),
passwort: z.string().min(8).max(100),
vorname: z.string().min(1).max(100),
name: z.string().min(1).max(100),
})
)
.output(
z.object({
uid: z.string().uuid(),
token: z.string(),
exp: z.number(),
})
)
.query(async (opts) => {
const hashedPassword = hashPassword(opts.input.passwort);
// Vielleicht existiert der Benutzer ja schon?
const existingUser = await prisma.benutzer.findUnique({
where: {
email: opts.input.email
}
})
if (existingUser) {
throw new TRPCError({ code: "CONFLICT", message: "Der Benutzer existiert bereits." });
}
const user = await prisma.benutzer.create({
data: {
email: opts.input.email,
passwort: hashedPassword,
vorname: opts.input.vorname,
name: opts.input.name
},
select: {
id: true,
uid: true
}
})
if (!user) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Der Benutzer konnte nicht erstellt werden." });
}
const expiry = moment().add(2, "days").unix();
const token = encodeToken({ uid: user.uid, exp: expiry })
return {
uid: user.uid,
token: token,
exp: expiry
}
});

View File

@@ -0,0 +1,31 @@
import { z } from "zod";
import { BenutzerSchema, prisma } from "@ibcornelsen/database";
import { TRPCError } from "@trpc/server";
import { loggedProcedure } from "#lib/trpc/middlewares/loggedProcedure";
export const tRPC_V1_BenutzerFromPrivateIdProcedure = loggedProcedure
.input(
z.object({
id: z.number(),
})
)
.output(
BenutzerSchema
)
.query(async (opts) => {
if (opts.ctx.user.id !== opts.input.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Sie sind nicht dazu authorisiert die Daten dieses Benutzers einzusehen." });
}
const user = await prisma.benutzer.findUnique({
where: {
id: opts.input.id,
},
});
if (!user) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Der gesuchte Benutzer existiert nicht." });
}
return user;
});

View File

@@ -0,0 +1,31 @@
import { z } from "zod";
import { BenutzerSchema, prisma } from "@ibcornelsen/database";
import { TRPCError } from "@trpc/server";
import { loggedProcedure } from "#lib/trpc/middlewares/loggedProcedure";
export const tRPC_V1_BenutzerFromPublicIdProcedure = loggedProcedure
.input(
z.object({
uid: z.string().uuid(),
})
)
.output(
BenutzerSchema
)
.query(async (opts) => {
if (opts.ctx.user.uid !== opts.input.uid) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Sie sind nicht dazu authorisiert die Daten dieses Benutzers einzusehen." });
}
const user = await prisma.benutzer.findUnique({
where: {
uid: opts.input.uid,
},
});
if (!user) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Der gesuchte Benutzer existiert nicht." });
}
return user;
});

View File

@@ -0,0 +1,50 @@
import { z } from "zod";
import moment from "moment";
import { prisma } from "@ibcornelsen/database";
import { publicProcedure } from "#lib/trpc/middlewares/publicProcedure";
import { encodeToken } from "#lib/JsonWebToken";
import { hashPassword } from "#lib/Password";
import { TRPCError } from "@trpc/server";
export const tRPC_V1_BenutzerTokenErneuernProcedure = publicProcedure
.input(
z.object({
email: z.string().email(),
passwort: z.string().min(8).max(100),
})
)
.output(
z.object({
uid: z.string().uuid(),
token: z.string(),
exp: z.number(),
})
)
.query(async (opts) => {
const hashedPassword = hashPassword(opts.input.passwort);
// Falls der Nutzer nicht existiert, wird eine Fehlermeldung zurückgegeben.
const user = await prisma.benutzer.findUnique({
where: {
email: opts.input.email
}
})
if (!user) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Der gesuchte Benutzer existiert nicht oder das Password ist falsch." });
}
// Falls das Passwort nicht stimmt, wird eine Fehlermeldung zurückgegeben.
if (user.passwort !== hashedPassword) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Der gesuchte Benutzer existiert nicht oder das Password ist falsch." });
}
const expiry = moment().add(2, "days").unix();
const token = encodeToken({ uid: user.uid, exp: expiry })
return {
uid: user.uid,
token: token,
exp: expiry
}
});

View File

@@ -0,0 +1,49 @@
import { z } from "zod";
import { prisma } from "@ibcornelsen/database";
import { publicProcedure } from "#lib/trpc/middlewares/publicProcedure";
import { decodeToken } from "#lib/JsonWebToken";
export const tRPC_V1_BenutzerTokenValidierenProcedure = publicProcedure
.input(
z.object({
token: z.string(),
})
)
.output(
z.object({
uid: z.string().uuid(),
valid: z.boolean(),
exp: z.number()
})
)
.query(async (opts) => {
const decodedToken = decodeToken(opts.input.token);
if (!decodedToken || !decodedToken.uid || !decodedToken.exp) {
return {
uid: "",
valid: false,
exp: 0,
};
}
const user = await prisma.benutzer.findUnique({
where: {
uid: decodedToken.uid,
},
});
if (!user) {
return {
uid: "",
valid: false,
exp: 0,
};
}
return {
uid: decodedToken.uid,
valid: true,
exp: decodedToken.exp
};
});

View File

@@ -0,0 +1,45 @@
import { t } from "#lib/trpc/context";
import { tRPC_V1_KlimafaktorenProcedure } from "./klimafaktoren";
import { VerbrauchsausweisWohnen2016Erstellen } from "./verbrauchsausweis-wohnen/2016/erstellen";
import { tRPC_V1_BenutzerErstellenProcedure } from "./benutzer/erstellen";
import { tRPC_V1_BenutzerTokenErneuernProcedure } from "./benutzer/tokenErneuern";
import { tRPC_V1_BenutzerTokenValidierenProcedure } from "./benutzer/tokenValidieren";
import { tRPC_V1_BenutzerFromPublicIdProcedure } from "./benutzer/fromPublicId";
import { tRPC_V1_BenutzerFromPrivateIdProcedure } from "./benutzer/fromPrivateId";
const router = t.router;
export const v1Router = router({
verbrauchsausweisWohnen: router({
2016: router({
erstellen: VerbrauchsausweisWohnen2016Erstellen
}),
2023: router({
})
}),
verbrauchsausweisGewerbe: router({
2016: router({
}),
2023: router({
})
}),
bedarfsausweisWohen: router({
2016: router({
}),
2023: router({
})
}),
klimafaktoren: tRPC_V1_KlimafaktorenProcedure,
benutzer: router({
erstellen: tRPC_V1_BenutzerErstellenProcedure,
tokenErneuern: tRPC_V1_BenutzerTokenErneuernProcedure,
tokenValidieren: tRPC_V1_BenutzerTokenValidierenProcedure,
fromPublicId: tRPC_V1_BenutzerFromPublicIdProcedure,
fromPrivateId: tRPC_V1_BenutzerFromPrivateIdProcedure
})
})

View File

@@ -1,10 +1,10 @@
import { z } from "zod";
import { t } from "../../context";
import moment from "moment";
import { TRPCError } from "@trpc/server";
import { prisma } from "@ibcornelsen/database";
import { publicProcedure } from "#lib/trpc/middlewares/publicProcedure";
export const tRPCKlimafaktorenProcedure = t.procedure
export const tRPC_V1_KlimafaktorenProcedure = publicProcedure
.input(
z.object({
plz: z.string().min(4).max(5),

View File

@@ -1,6 +1,6 @@
import { ZodSchema, z } from "zod";
import { publicProcedure } from "../../../../context";
import { GebaeudeStammdaten, VerbrauchsausweisWohnen, prisma } from "@ibcornelsen/database";
import { z } from "zod";
import { prisma } from "@ibcornelsen/database";
import { publicProcedure } from "#lib/trpc/middlewares/publicProcedure";
export const VerbrauchsausweisWohnen2016Erstellen = publicProcedure
.meta({
@@ -16,11 +16,11 @@ export const VerbrauchsausweisWohnen2016Erstellen = publicProcedure
baujahr_heizung: z.array(z.number()).optional(),
zusaetzliche_heizquelle: z.boolean().optional(),
brennstoff_1: z.string().max(50).optional(),
einheit_1: z.string().max(10).optional(),
einheit_1: z.string().max(50).optional(),
brennstoff_2: z.string().max(50).optional(),
einheit_2: z.string().max(10).optional(),
startdatum: z.date().optional(),
enddatum: z.date().optional(),
einheit_2: z.string().max(50).optional(),
startdatum: z.coerce.date().optional(),
enddatum: z.coerce.date().optional(),
verbrauch_1: z.number().optional(),
verbrauch_2: z.number().optional(),
verbrauch_3: z.number().optional(),
@@ -86,8 +86,8 @@ export const VerbrauchsausweisWohnen2016Erstellen = publicProcedure
aussenwand_min_12cm_gedaemmt: z.boolean().optional(),
dachgeschoss_min_12cm_gedaemmt: z.boolean().optional(),
oberste_geschossdecke_min_12cm_gedaemmt: z.boolean().optional(),
} satisfies ZodSchema<GebaeudeStammdaten>))
} satisfies ZodSchema<VerbrauchsausweisWohnen>))
}))
}))
.output(
z.object({
uid: z.string().uuid(),
@@ -121,7 +121,23 @@ export const VerbrauchsausweisWohnen2016Erstellen = publicProcedure
});
return { uid: verbrauchsausweis.uid };
}
return { uid: "" };
} else {
// Gebäude existiert noch nicht
const gebaeude = await prisma.gebaeudeStammdaten.create({
data: opts.input.gebaeude_stammdaten
});
const verbrauchsausweis = await prisma.verbrauchsausweisWohnen.create({
data: {
...opts.input,
gebaeude_stammdaten: {
connect: {
uid: gebaeude.uid
}
}
}
});
return { uid: verbrauchsausweis.uid };
}
});

View File

@@ -15,13 +15,14 @@
import { auditBedarfsausweisBenoetigt } from "#components/Verbrauchsausweis/audits/BedarfsausweisBenoetigt";
import { auditVerbrauchAbweichung } from "#components/Verbrauchsausweis/audits/VerbrauchAbweichung";
import { GebaeudeStammdaten, VerbrauchsausweisWohnen } from "@ibcornelsen/database";
import trpc from "src/trpc";
import { client } from "src/trpc";
export let uid: string = "";
let gebaeude: GebaeudeStammdaten = {} as GebaeudeStammdaten;
let ausweis: VerbrauchsausweisWohnen = {} as VerbrauchsausweisWohnen;
if (uid) {
// NOTE: Funktioniert nicht mehr
async () => {
const result = await fetch(`/api/verbrauchsausweis?uid=${uid}`, {
method: "GET",
@@ -62,11 +63,16 @@
async function ausweisAbschicken() {
console.log(ausweis);
overlay.ariaHidden = "false";
const response = await trpc.v1.verbrauchsausweisWohnen[2016].erstellen.mutate({
const response = await client.v1.verbrauchsausweisWohnen[2016].erstellen.mutate({
...ausweis,
gebaeude_stammdaten: gebaeude
})
console.log(response.uid);
}
let overlay: HTMLDivElement;

View File

@@ -1,34 +1,18 @@
<script lang="ts">
import Cookies from "js-cookie"
import { addNotification } from "@ibcornelsen/ui";
import { client } from "src/trpc";
let email: string;
let password: string;
let passwort: string;
async function login() {
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({
const response = await client.v1.benutzer.tokenErneuern.query({
email,
password,
}),
});
const json = await response.json();
if (json.success == true) {
const options = {
domain: `.${window.location.hostname}`,
path: "/",
expires: json.data.expires
}
Cookies.set("token", json.data.token, options);
Cookies.set("expires", json.data.expires, options);
localStorage.setItem("token", json.data.token);
localStorage.setItem("expires", json.data.expires);
window.location.href = "/user";
} else {
passwort
})
if (!response.token || !response.exp) {
addNotification({
message: "Ups...",
subtext: "Das hat leider nicht geklappt, haben sie ihr Passwort und ihre Email Adresse richtig eingegeben?",
@@ -36,7 +20,16 @@
timeout: 6000,
dismissable: true
})
return;
}
const options = {
domain: `.${window.location.hostname}`,
path: "/",
expires: response.exp
}
Cookies.set("token", response.token, options);
window.location.href = "/user";
}
</script>
@@ -61,7 +54,7 @@
type="password"
placeholder="********"
name="password"
bind:value={password}
bind:value={passwort}
required
/>
</div>

View File

@@ -1,20 +1,28 @@
<script lang="ts">
import { addNotification } from "@ibcornelsen/ui";
import Cookies from "js-cookie";
import {client} from "src/trpc";
let passwort: string;
let email: string;
let vorname: string;
let name: string;
async function login() {
const response = await fetch("/api/user", {
method: "PUT",
body: JSON.stringify({
passwort, email
})
const response = await client.v1.benutzer.erstellen.query({
email,
passwort,
vorname,
name
})
const json = await response.json()
if (json.success == true) {
if (response.token) {
const options = {
domain: `.${window.location.hostname}`,
path: "/",
expires: response.exp
}
Cookies.set("token", response.token, options);
window.location.href = "/login";
} else {
addNotification({
@@ -31,6 +39,28 @@
<div style="width:50%;margin: 0 auto">
<h1>Registrieren:</h1>
<div class="flex flex-col gap-4">
<div class="flex flex-row gap-4 w-full">
<div class="w-1/2">
<h4>Vorname</h4>
<input
type="text"
placeholder="Vorname"
class="px-2.5 py-1.5 rounded-lg border bg-gray-50"
bind:value={vorname}
required
/>
</div>
<div class="w-1/2">
<h4>Nachname</h4>
<input
type="text"
placeholder="Nachname"
class="px-2.5 py-1.5 rounded-lg border bg-gray-50"
bind:value={name}
required
/>
</div>
</div>
<div>
<h4>Email</h4>
<input

42
src/pages/api/[trpc].ts Normal file
View File

@@ -0,0 +1,42 @@
// NOTE: Öffentliche API benötigt OpenApiMeta. Das Package bräuchte momentan noch einen extra Server, deshalb nehmen wir es momentan noch nicht mit rein.
//import { OpenApiMeta } from "trpc-openapi";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { APIRoute } from "astro";
import { t } from "../../lib/trpc/context";
import { v1Router } from "#lib/trpc/procedures/v1";
export const appRouter = t.router({
v1: v1Router,
});
export const all: APIRoute = ({ request }) => {
return fetchRequestHandler({
req: request,
endpoint: "/api",
router: appRouter,
createContext: async ({ req }) => {
const ip = req.headers.get("x-forwarded-for");
const authorization = req.headers.get("authorization") || null;
return {
authorization,
ip: ip?.toString() || "",
req: req,
};
},
});
};
export function tRPCCaller(request: Request) {
const { authorization } = {
authorization: request.headers.get("authorization") || "",
};
const createCaller = t.createCallerFactory(appRouter);
return createCaller({
authorization,
req: request,
ip: request.headers.get("x-forwarded-for") || "",
});
}
export type AppRouter = typeof appRouter;

View File

@@ -1,34 +0,0 @@
import type { APIRoute } from "astro";
import { success, MissingPropertyError, error } from "../../lib/APIResponse";
import { validatePassword } from "../../lib/Password";
import { User } from "../../lib/User";
import moment from "moment";
import { encodeToken } from "../../lib/JsonWebToken";
/**
* Ruft einen Nutzer anhand seiner uid aus der Datenbank ab.
* @param param0 Die Request mit dem request body. Dieser enthält entweder eine uid mit der der Benutzer identifiziert werden kann.
*/
export const post: APIRoute = async ({ request }) => {
const body = await request.json();
if (!body.hasOwnProperty("email") || !body.hasOwnProperty("password")) {
return MissingPropertyError(["email", "password"]);
}
const user = await User.fromEmail(body.email);
if (!user) {
return error(["Invalid email or password."]);
}
// Validate Password
if (!validatePassword(user.passwort, body.password)) {
return error(["Invalid email or password."]);
}
const expiry = moment().add(2, "days").unix();
const token = encodeToken({ id: user.id, uid: user.uid, exp: expiry })
return success({ token, expires: expiry });
}

View File

@@ -1,34 +0,0 @@
import type { APIRoute } from "astro";
import { success, MissingPropertyError, error } from "../../lib/APIResponse";
import { validatePassword, hashPassword } from "../../lib/Password";
import { User } from "../../lib/User";
import moment from "moment";
import { encodeToken } from "../../lib/JsonWebToken";
/**
* Ruft einen Nutzer anhand seiner uid aus der Datenbank ab.
* @param param0 Die Request mit dem request body. Dieser enthält entweder eine uid mit der der Benutzer identifiziert werden kann.
*/
export const post: APIRoute = async ({ request }) => {
const body = await request.json();
if (!body.hasOwnProperty("email") || !body.hasOwnProperty("password")) {
return MissingPropertyError(["email", "password"]);
}
const user = await User.fromEmail(body.email);
if (!user) {
return error(["Invalid email or password."]);
}
// Validate Password
if (!validatePassword(user.passwort, body.password)) {
return error(["Invalid email or password."]);
}
const expiry = moment().add(2, "days").unix();
const token = encodeToken({ id: user.id, uid: user.uid, exp: expiry })
return success({ token, expires: expiry });
}

View File

@@ -1,31 +0,0 @@
// NOTE: Öffentliche API benötigt OpenApiMeta. Das Package bräuchte momentan noch einen extra Server, deshalb nehmen wir es momentan noch nicht mit rein.
//import { OpenApiMeta } from "trpc-openapi";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { APIRoute } from "astro";
import { t } from "./context";
import { v1Router } from "./procedures/v1";
export const AppRouter = t.router({
v1: v1Router,
})
export const all: APIRoute = ({ request }) => {
console.log(request);
return fetchRequestHandler({
req: request,
endpoint: "/api/trpc",
router: AppRouter,
createContext: async ({ req }) => {
return { uid: req.headers.get("X-Session") ?? undefined };
},
});
};
export function tRPCCaller(request: Request) {
const { uid } = { uid: request.headers.get("Authorization") || "" };
const createCaller = t.createCallerFactory(AppRouter);
return createCaller({ uid });
}
export type AppRouter = typeof AppRouter;

View File

@@ -1,42 +0,0 @@
import { z } from "zod";
import { t } from "../../context";
import { tRPCKlimafaktorenProcedure } from "./klimafaktoren";
import { VerbrauchsausweisWohnen2016Erstellen } from "./verbrauchsausweis-wohnen/2016/erstellen";
const router = t.router;
export const v1Router = router({
verbrauchsausweisWohnen: router({
2016: router({
erstellen: VerbrauchsausweisWohnen2016Erstellen
}),
2023: router({
})
}),
verbrauchsausweisGewerbe: router({
2016: router({
}),
2023: router({
})
}),
bedarfsausweisWohen: router({
2016: router({
}),
2023: router({
})
}),
klimafaktoren: tRPCKlimafaktorenProcedure,
test: t.procedure.meta({
openapi: {
method: "GET",
path: "/v1/test",
}
}).input(z.void({})).output(z.string()).query(async (opts) => {
return "Hello World!";
})
})

View File

@@ -1,50 +0,0 @@
import type { APIRoute } from "astro";
import { success, MissingPropertyError, MissingEntityError, ActionFailedError, InvalidDataError, error } from "../../lib/APIResponse";
import { User } from "../../lib/User";
import { UserRegisterValidator, UserType } from "../../lib/User/type";
import { z } from "zod";
/**
* Ruft einen Nutzer anhand seiner uid aus der Datenbank ab.
* @param param0 Die Request mit dem request body. Dieser enthält entweder eine uid mit der der Benutzer identifiziert werden kann.
*/
export const get: APIRoute = async ({ request }) => {
const body = await request.json();
if (!body.hasOwnProperty("uid")) {
return MissingPropertyError(["uid"]);
}
const user = User.fromUID(body.uid);
if (!user) {
return MissingEntityError("user");
}
return success(user);
}
export const put: APIRoute = async ({ request }) => {
const body: z.infer<typeof UserRegisterValidator> = await request.json();
const validate = UserRegisterValidator.safeParse(body);
if (validate.success == false) {
return InvalidDataError(validate.error);
}
const user = await User.fromEmail(body.email);
if (user) {
return error(["Diese Email Adresse wird bereits verwendet."]);
}
const result = await User.create(body as UserType);
if (!result) {
return ActionFailedError();
}
return success({ uid: result.uid, id: result.id });
}

View File

@@ -1,47 +0,0 @@
import type { APIRoute } from "astro";
import { success, MissingPropertyError, MissingEntityError, error } from "../../lib/APIResponse";
import { validateAuthorizationHeader } from "src/lib/server/Authorization";
import { prisma } from "@ibcornelsen/database";
/**
* Ruft einen Nutzer anhand seiner uid aus der Datenbank ab.
* @param param0 Die Request mit dem request body. Dieser enthält entweder eine uid mit der der Benutzer identifiziert werden kann.
*/
export const get: APIRoute = async ({ request }) => {
const user = await validateAuthorizationHeader(request, ["Bearer", "Basic"]);
if (!user) {
return error(["Invalid authentication credentials!"]);
}
const body = Object.fromEntries(new URLSearchParams(request.url.split("?")[1]))
let result;
if (body.zip) {
result = await prisma.postleitzahlen.findUnique({
where: {
plz: body.zip,
},
})
} else if (body.city) {
result = await prisma.postleitzahlen.findMany({
where: {
stadt: body.city,
},
})
} else if (body.state) {
result = await prisma.postleitzahlen.findMany({
where: {
bundesland: body.state,
},
})
} else {
return MissingPropertyError(["Either 'state', 'city' or 'zip' have to exist in request body."])
}
if (!result) {
return MissingEntityError("zip info")
}
return success(result);
}

View File

@@ -1,27 +1,26 @@
---
import { changeLanguage } from "i18next";
import moment from "moment";
import { decodeToken } from "#lib/JsonWebToken";
import { User } from "#lib/User";
import UserLayout from "#layouts/UserLayout.astro";
import AusweisCard from "#components/AusweisCard.svelte";
import { Verbrauchsausweis } from "#lib/Ausweis/Verbrauchsausweis";
changeLanguage("de");
import { User } from "#lib/User";
import { tRPCCaller } from "../api/[trpc]";
const token = Astro.cookies.get("token").value;
const expires = Astro.cookies.get("expires").number();
const now = moment().unix();
if (!token || now > expires) {
if (!token) {
Astro.cookies.delete("token");
Astro.cookies.delete("expires");
return Astro.redirect(`/login`);
}
const parsed = decodeToken(token);
const user = await User.fromUID(parsed.uid);
const response = await tRPCCaller(Astro.request).v1.benutzer.tokenValidieren({ token });
if (!response.valid) {
Astro.cookies.delete("token");
return Astro.redirect(`/login`);
}
const user = await User.fromUID(response.uid);
if (!user) {
Astro.cookies.delete("token");
Astro.cookies.delete("expires");
return Astro.redirect(`/login`);
}
---
@@ -31,8 +30,5 @@ if (!user) {
<h2>Ihre Ausweise</h2>
<div class="grid grid-flow-row grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<AusweisCard client:load hidden={true} i={0} ausweis={new Verbrauchsausweis()}></AusweisCard>
<AusweisCard client:load hidden={true} i={1} ausweis={new Verbrauchsausweis()}></AusweisCard>
<AusweisCard client:load hidden={true} i={2} ausweis={new Verbrauchsausweis()}></AusweisCard>
</div>
</UserLayout>

View File

@@ -1,11 +1,11 @@
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import cookies from 'js-cookie';
import type { AppRouter } from 'src/pages/api/trpc/[trpc]';
import type { AppRouter } from 'src/pages/api/[trpc]';
export default createTRPCProxyClient<AppRouter>({
export const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
url: 'http://localhost:3000/api',
headers() {
return {
'Authorization': `Bearer ${cookies.get('uid')}`,

View File

@@ -1,4 +1,4 @@
import { AppRouter } from "src/pages/api/trpc/[trpc]"
import { AppRouter } from "src/pages/api/[trpc]"
import { createOpenApiExpressMiddleware } from "trpc-openapi"
import express from 'express';