Bedarfsausweis Wohnen API

This commit is contained in:
Moritz Utcke
2025-02-12 14:27:56 +07:00
parent a4fe9b4f80
commit 6e2f29c673
10 changed files with 381 additions and 16 deletions

View File

@@ -17,7 +17,7 @@
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"astro": "^4.16.17",
"astro-typesafe-api": "^0.2.1",
"astro-typesafe-api": "^0.2.2",
"body-scroll-lock": "^4.0.0-beta.0",
"buffer": "^6.0.3",
"bun": "^1.1.45",
@@ -656,7 +656,7 @@
"astro": ["astro@4.16.18", "", { "dependencies": { "@astrojs/compiler": "^2.10.3", "@astrojs/internal-helpers": "0.4.1", "@astrojs/markdown-remark": "5.3.0", "@astrojs/telemetry": "3.1.0", "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx": "^7.25.9", "@babel/types": "^7.26.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.3", "@types/babel__core": "^7.20.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.1.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^0.7.2", "cssesc": "^3.0.0", "debug": "^4.3.7", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.5.4", "esbuild": "^0.21.5", "estree-walker": "^3.0.3", "fast-glob": "^3.3.2", "flattie": "^1.1.1", "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "html-escaper": "^3.0.3", "http-cache-semantics": "^4.1.1", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.14", "magicast": "^0.3.5", "micromatch": "^4.0.8", "mrmime": "^2.0.0", "neotraverse": "^0.6.18", "ora": "^8.1.1", "p-limit": "^6.1.0", "p-queue": "^8.0.1", "preferred-pm": "^4.0.0", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.6.3", "shiki": "^1.23.1", "tinyexec": "^0.3.1", "tsconfck": "^3.1.4", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3", "vite": "^5.4.11", "vitefu": "^1.0.4", "which-pm": "^3.0.0", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "zod": "^3.23.8", "zod-to-json-schema": "^3.23.5", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-G7zfwJt9BDHEZwlaLNvjbInIw2hPryyD654314KV/XT34pJU6SfN1S+mWa8RAkALcZNJnJXCJmT3JXLQStD3Lw=="],
"astro-typesafe-api": ["astro-typesafe-api@0.2.1", "", { "dependencies": { "es-codec": "^0.5.0", "globby": "^14.0.2" }, "peerDependencies": { "astro": "^4.16.17", "typescript": "^5.0.0", "zod": "^3.24.1" }, "bin": { "astro-typesafe-api": "src/cli.ts" } }, "sha512-8f0McZj9fWIzT19njJ2z/1zETnbper3ejuba93t72Xvsy6aMTEDXaIGDG3xc9KWUQ9zEcNg+VS52JNWGfYm6CQ=="],
"astro-typesafe-api": ["astro-typesafe-api@0.2.2", "", { "dependencies": { "es-codec": "^0.5.0", "globby": "^14.0.2" }, "peerDependencies": { "astro": "^4.16.17", "typescript": "^5.0.0", "zod": "^3.24.1" }, "bin": { "astro-typesafe-api": "src/cli.ts" } }, "sha512-SEHV2iPyIrdpYdYb0mIN1WmcvC61bvsCQqb/X+R4EOcFjuozJ9fJhSiFGxJMvNoxJ9S3P3GKLyDnxXvFlKq0mw=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],

View File

@@ -31,7 +31,7 @@
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"astro": "^4.16.17",
"astro-typesafe-api": "^0.2.1",
"astro-typesafe-api": "^0.2.2",
"body-scroll-lock": "^4.0.0-beta.0",
"buffer": "^6.0.3",
"bun": "^1.1.45",

View File

@@ -3,13 +3,14 @@ import { createCallerFactory } from "astro-typesafe-api/server";
export const createCaller = createCallerFactory({
"klimafaktoren": await import("../src/pages/api/klimafaktoren.ts"),
"postleitzahlen": await import("../src/pages/api/postleitzahlen.ts"),
"aufnahme/[uid]": await import("../src/pages/api/aufnahme/[uid].ts"),
"aufnahme": await import("../src/pages/api/aufnahme/index.ts"),
"bedarfsausweis-wohnen": await import("../src/pages/api/bedarfsausweis-wohnen/index.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/refresh-token": await import("../src/pages/api/auth/refresh-token.ts"),
"aufnahme/[uid]": await import("../src/pages/api/aufnahme/[uid].ts"),
"aufnahme": await import("../src/pages/api/aufnahme/index.ts"),
"bilder/[uid]": await import("../src/pages/api/bilder/[uid].ts"),
"bedarfsausweis-wohnen/[uid]": await import("../src/pages/api/bedarfsausweis-wohnen/[uid].ts"),
"bedarfsausweis-wohnen": await import("../src/pages/api/bedarfsausweis-wohnen/index.ts"),
"objekt": await import("../src/pages/api/objekt/index.ts"),
"rechnung": await import("../src/pages/api/rechnung/index.ts"),
"ticket": await import("../src/pages/api/ticket/index.ts"),

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import { ripple } from "svelte-ripple-action";
import type { RippleOptions } from "svelte-ripple-action/dist/constants";
import type { RippleOptions } from "svelte-ripple-action/dist/constants.js";
import { Home, Reader, EnvelopeClosed, Cube, Bell, Gear, LockClosed, HamburgerMenu } from "radix-svelte-icons"
import NotificationProvider from "#components/NotificationProvider/NotificationProvider.svelte";
import DashboardNotification from "./DashboardNotification.svelte";
import { notifications } from "#components/NotificationProvider/shared";
import { notifications } from "#components/NotificationProvider/shared.js";
import ThemeController from "#components/ThemeController.svelte";
import { BenutzerClient } from "#components/Ausweis/types";
import { BenutzerClient } from "#components/Ausweis/types.js";
import Cross1 from "radix-svelte-icons/src/lib/icons/Cross1.svelte";
export let lightTheme: boolean;

View File

@@ -16,7 +16,7 @@
}
try {
const result = await api.postleitzahlen.GET.fetch({ plz: zip });
const result = await api.postleitzahlen.GET.fetch({ plz: zip, limit: 10 });
if (result.length > 0) {
zipCodes = result;

View File

@@ -7,6 +7,7 @@ import DashboardSidebar from "../components/Dashboard/DashboardSidebar.svelte"
import { validateAccessTokenServer } from "#server/lib/validateAccessToken";
import { createCaller } from "src/astro-typesafe-api-caller";
import { API_ACCESS_TOKEN_COOKIE_NAME } from "#lib/constants";
import { BenutzerClient } from "#components/Ausweis/types";
const valid = validateAccessTokenServer(Astro)
@@ -17,11 +18,11 @@ if (!valid) {
const caller = createCaller(Astro)
const benutzer = await caller.user.self.GET.fetch(null, {
const benutzer = (await caller.user.self.GET.fetch(undefined, {
headers: {
Authorization: `Bearer ${Astro.cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)?.value}`
}
})
})) || {} as BenutzerClient;
export interface Props {
title: string;
@@ -106,7 +107,7 @@ let lightTheme = Astro.cookies.get("theme")?.value === "light";
</head>
<body class="min-h-screen grid md:grid-cols-[300px_1fr]">
<DashboardSidebar lightTheme={lightTheme} benutzer={benutzer} client:load></DashboardSidebar>
<DashboardSidebar lightTheme={lightTheme} {benutzer} client:load></DashboardSidebar>
<main class="p-4 md:p-8 overflow-auto h-screen bg-base-100 pt-20 md:!pt-24">
<slot />
</main>

View File

@@ -4,7 +4,7 @@ import { getEmpfehlungen } from "#lib/XML/getEmpfehlungen.js";
import { Enums } from "@ibcornelsen/database/server";
import * as fs from "fs"
import moment from "moment";
import { PDFDocument, PDFFont, PDFName, PDFNumber, PDFPage, RotationTypes, StandardFonts, TextAlignment } from "pdf-lib";
import { PDFDocument, PDFFont, PDFPage, RotationTypes, StandardFonts, TextAlignment } from "pdf-lib";
/* -------------------------------- Pdf Tools ------------------------------- */

View File

@@ -0,0 +1,217 @@
import { BedarfsausweisWohnenClient, OptionalNullable, UUidWithPrefix, ZodOverlap } from "#components/Ausweis/types.js";
import { exclude } from "#lib/exclude.js";
import { authorizationHeaders, authorizationMiddleware } from "#lib/middleware/authorization.js";
import { BedarfsausweisWohnenSchema, prisma } from "@ibcornelsen/database/server";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { z } from "zod";
export const PATCH = defineApiRoute({
input: BedarfsausweisWohnenSchema.omit({
uid: true,
id: true,
benutzer_id: true,
aufnahme_id: true,
}),
output: z.void(),
headers: {
"Authorization": z.string()
},
middleware: authorizationMiddleware,
async fetch(input, ctx, user) {
const objekt = await prisma.bedarfsausweisWohnen.findUnique({
where: {
uid: ctx.params.uid,
benutzer: {
id: user.id
}
}
})
if (!objekt) {
throw new APIError({
code: "NOT_FOUND",
message: "Ausweis konnte nicht gefunden werden oder gehört einem anderen Benutzer."
})
}
await prisma.bedarfsausweisWohnen.update({
where: {
uid: ctx.params.uid
},
data: input
})
},
})
export const DELETE = defineApiRoute({
meta: {
description: "Storniert einen Ausweis"
},
headers: authorizationHeaders,
middleware: authorizationMiddleware,
async fetch(input, ctx, user) {
const { uid } = ctx.params;
if (!UUidWithPrefix.safeParse(uid).success) {
throw new APIError({
code: "BAD_REQUEST",
message: "UID konnte nicht verifiziert werden."
})
}
// Wir holen uns den Bedarfsausweis
// Dieser MUSS mit dem Nutzer verknüpft sein.
const ausweis = await prisma.bedarfsausweisWohnen.findUnique({
where: {
uid,
},
include: {
aufnahme: {
select: {
storniert: true
}
}
}
});
if (!ausweis) {
// Falls wir den Ausweis nicht finden können, werfen wir einen Fehler
throw new APIError({
code: "NOT_FOUND",
message: "Ausweis konnte nicht gefunden werden.",
});
}
// Wir dürfen den Ausweis nur stornieren, wenn er noch nicht ausgestellt wurde
// Außerdem müssen wir schauen, ob wir Admin oder der Besitzer des Ausweises sind.
if ((ausweis.benutzer_id !== user.id) && user.rolle !== "ADMIN") {
// Falls der Ausweis nicht dem Nutzer gehört, werfen wir einen Fehler
throw new APIError({
code: "FORBIDDEN",
message: "Ausweis gehört nicht dem Nutzer.",
});
}
// if (ausweis.erledigt) {
// // Falls der Ausweis bereits ausgestellt wurde, werfen wir einen Fehler
// throw new TRPCError({
// code: "BAD_REQUEST",
// message: "Ausweis wurde bereits ausgestellt.",
// });
// }
if (ausweis.aufnahme.storniert) {
// Falls der Ausweis bereits storniert ist, werfen wir einen Fehler
throw new APIError({
code: "BAD_REQUEST",
message: "Ausweis wurde bereits storniert.",
});
}
await prisma.aufnahme.update({
where: {
id: ausweis.aufnahme_id
},
data: {
storniert: true
}
})
// Wir erstellen ein Event, dass der Ausweis storniert wurde
// Dann können wir das in der Historie anzeigen
await prisma.event.create({
data: {
title: "Ausweis storniert",
description: ((user.rolle === "ADMIN") && (ausweis.benutzer_id !== user.id)) ? "Ausweis wurde von einem Administrator storniert." : "Ausweis wurde vom Besitzer storniert.",
benutzer: {
connect: {
id: user.id
}
},
aufnahme: {
connect: {
id: ausweis.aufnahme_id
}
}
}
})
},
})
export const GET = defineApiRoute({
meta: {
description: "Gibt ein spezifisches Gebäude des Benutzers zurück.",
tags: ["Gebäude"],
headers: {
"Authorization": {
description: "Ein gültiger Authentifizierungstoken",
required: true,
allowEmptyValue: false,
examples: {
Bearer: {
value: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
}
}
},
output: ZodOverlap<OptionalNullable<BedarfsausweisWohnenClient>>(BedarfsausweisWohnenSchema.merge(z.object({
uid_aufnahme: UUidWithPrefix,
uid_objekt: UUidWithPrefix,
uid_benutzer: UUidWithPrefix.optional()
})).omit({
id: true,
aufnahme_id: true,
benutzer_id: true
})),
middleware: authorizationMiddleware,
async fetch(input, context, user) {
const { uid } = context.params;
if (!uid) {
throw new APIError({
code: "BAD_REQUEST",
message: "Missing uid in request params"
})
}
const ausweis = await prisma.bedarfsausweisWohnen.findUnique({
where: {
uid,
benutzer_id: user.id
},
include: {
benutzer: {
select: {
uid: true
}
},
aufnahme: {
select: {
uid: true,
objekt: {
select: {
uid: true
}
}
}
}
}
});
if (!ausweis) {
// Falls wir den Ausweis nicht finden können, werfen wir einen Fehler
throw new APIError({
code: "NOT_FOUND",
message: "Ausweis konnte nicht gefunden werden.",
});
}
return {
uid_aufnahme: ausweis.aufnahme.uid,
uid_objekt: ausweis.aufnahme.objekt.uid,
uid_benutzer: ausweis.benutzer?.uid,
...exclude(ausweis, ["id", "aufnahme_id", "benutzer_id", "aufnahme"])
}
},
});

View File

@@ -0,0 +1,146 @@
import { UUidWithPrefix } from "#components/Ausweis/types.js";
import { authorizationHeaders, authorizationMiddleware } from "#lib/middleware/authorization.js";
import { BedarfsausweisWohnenSchema, prisma } from "@ibcornelsen/database/server";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { z } from "zod";
export const PUT = defineApiRoute({
meta: {
contentTypes: ["application/json"],
description:
"Erstellt einen neuen Bedarfsausweis für Wohngebäude nach dem Schema der EnEV von 2016. Als Input wird ein bestehendes Gebäude benötigt. Falls keine UID einer bestehenden Gebäudeaufnahme mitgegeben wird, wird automatisch eine erstellt.",
tags: ["Bedarfsausweis Wohnen"],
},
input: z.object({
ausweis: BedarfsausweisWohnenSchema.omit({
id: true,
benutzer_id: true,
uid: true,
aufnahme_id: true
}),
uid_aufnahme: UUidWithPrefix
}),
output: z.object({
uid: UUidWithPrefix,
objekt_uid: UUidWithPrefix,
aufnahme_uid: UUidWithPrefix,
}),
headers: authorizationHeaders,
middleware: authorizationMiddleware,
async fetch(input, ctx, user) {
const aufnahme = await prisma.aufnahme.findUnique({
where: {
uid: input.uid_aufnahme
}
})
if (!aufnahme || aufnahme.benutzer_id !== user.id) {
throw new APIError({
code: "FORBIDDEN",
message: "Aufnahme konnte nicht gefunden werden oder gehört nicht zu diesem Benutzer."
})
}
const createdAusweis = await prisma.bedarfsausweisWohnen.create({
data: {
...input.ausweis,
benutzer: {
connect: {
id: user.id,
},
},
aufnahme: {
connect: {
uid: aufnahme.uid,
},
},
},
select: {
uid: true,
aufnahme: {
select: {
uid: true,
objekt: {
select: {
uid: true,
},
},
},
},
},
});
return {
uid: createdAusweis.uid,
objekt_uid: createdAusweis.aufnahme.objekt.uid,
aufnahme_uid: createdAusweis.aufnahme.uid,
};
},
});
export const GET = defineApiRoute({
meta: {
description: "Gibt ein spezifisches Gebäude des Benutzers zurück.",
tags: ["Gebäude"],
headers: {
Authorization: {
description: "Ein gültiger Authentifizierungstoken",
required: true,
allowEmptyValue: false,
examples: {
Bearer: {
value: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
},
},
},
},
},
middleware: authorizationMiddleware,
async fetch(input, context, user) {
const { uid } = context.params;
const ausweis = await prisma.bedarfsausweisWohnen.findUnique({
where: {
uid,
},
include: {
benutzer: true,
aufnahme: {
include: {
objekt: {
include: {
gebaeude_bilder: true,
},
},
rechnungen: true,
events: {
include: {
benutzer: {
select: {
uid: true,
},
},
},
orderBy: {
date: "asc",
},
},
},
},
},
});
if (
!ausweis ||
(ausweis.benutzer_id !== null && ausweis.benutzer_id !== user.id)
) {
// Falls wir den Ausweis nicht finden können, werfen wir einen Fehler
throw new APIError({
code: "NOT_FOUND",
message: "Ausweis konnte nicht gefunden werden.",
});
}
return ausweis;
},
});

View File

@@ -59,10 +59,10 @@ export const GET = defineApiRoute({
}
}
},
output: ZodOverlap<OptionalNullable<ObjektClient>>(ObjektSchema.omit({
output: ObjektSchema.omit({
benutzer_id: true,
id: true
})),
}),
middleware: authorizationMiddleware,
async fetch(input, ctx, user) {
const { uid } = ctx.params;