API Vollständig Umgezogen

This commit is contained in:
Moritz Utcke
2025-01-25 09:02:51 +07:00
parent 6f3ddedd96
commit 51fb7ad9b6
18 changed files with 329 additions and 113 deletions

View File

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

View File

@@ -1,65 +1,95 @@
import { GebaeudeClient, UploadedGebaeudeBild, VerbrauchsausweisWohnenClient } from "#components/Ausweis/types"; import {
ObjektClient,
UploadedGebaeudeBild,
VerbrauchsausweisWohnenClient,
} from "#components/Ausweis/types.js";
import { API_ACCESS_TOKEN_COOKIE_NAME } from "#lib/constants.js";
import { Enums } from "@ibcornelsen/database/client"; import { Enums } from "@ibcornelsen/database/client";
import { addNotification, updateNotification } from "@ibcornelsen/ui"; import { addNotification, updateNotification } from "@ibcornelsen/ui";
import { client } from "src/trpc"; import { api } from "astro-typesafe-api/client";
import Cookies from "js-cookie";
export async function bilderHochladen(images: (UploadedGebaeudeBild & { base64?: string })[], gebaeude_uid: string) { export async function bilderHochladen(
if (images.length == 0) { images: (UploadedGebaeudeBild & { base64?: string, update?: boolean })[],
return images; gebaeude_uid: string
} ) {
if (images.length == 0) {
return images;
}
// Wenn Bilder hochgeladen werden konvertieren wir sie zu base64, das heißt, dass die base64 Eigenschaft bei diesen Bildern // Wenn Bilder hochgeladen werden konvertieren wir sie zu base64, das heißt, dass die base64 Eigenschaft bei diesen Bildern
// existiert. Das müssen wir TypeScript nur wissen lassen, damit es uns in Ruhe lässt. // existiert. Das müssen wir TypeScript nur wissen lassen, damit es uns in Ruhe lässt.
const imagesToUpload = images.filter(image => !image.uid || image.update) as unknown as { base64: string, kategorie: string, uid?: string, update: boolean }[]; const imagesToUpload = images.filter(
(image) => !image.uid || image.update
) as unknown as {
base64: string;
kategorie: string;
uid?: string;
update: boolean;
}[];
if (imagesToUpload.length == 0) { if (imagesToUpload.length == 0) {
return images; return images;
} }
// Alle Bilder hochladen // Alle Bilder hochladen
const notification = addNotification({ const notification = addNotification({
dismissable: false, dismissable: false,
message: "Bilder hochladen.", message: "Bilder hochladen.",
subtext: `${imagesToUpload.length} Bilder werden hochgeladen, bitte haben sie Geduld.`, subtext: `${imagesToUpload.length} Bilder werden hochgeladen, bitte haben sie Geduld.`,
timeout: 0, timeout: 0,
type: "info" type: "info",
}) });
for (let i = 0; i < imagesToUpload.length; i++) { for (let i = 0; i < imagesToUpload.length; i++) {
const image = imagesToUpload[i]; const image = imagesToUpload[i];
try { try {
if (image.update) { if (image.update) {
await client.v1.bilder.update.mutate({ await api.bilder._uid.PATCH.fetch({
uid: image.uid as string, base64: image.base64,
base64: image.base64, kategorie: image.kategorie as Enums.BilderKategorie,
kategorie: image.kategorie as Enums.BilderKategorie }, {
}) params: {
} else { uid: image.uid as string,
const response = await client.v1.bilder.upload.mutate({ },
base64: image.base64, headers: {
kategorie: image.kategorie as Enums.BilderKategorie, "Authorization": `Bearer ${Cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)}`
gebaeude_uid }
}) });
} else {
const response = await api.objekt._uid.bilder.PUT.fetch({
base64: image.base64,
kategorie: image.kategorie as Enums.BilderKategorie
}, {
params: {
uid: gebaeude_uid
},
headers: {
"Authorization": `Bearer ${Cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)}`
}
});
image.uid = response.uid image.uid = response.uid;
} }
updateNotification(notification, { updateNotification(notification, {
dismissable: true, dismissable: true,
message: "Bild hochgeladen.", message: "Bild hochgeladen.",
subtext: `${i + 1}/${imagesToUpload.length} Bildern wurden erfolgreich hochgeladen.`, subtext: `${i + 1}/${
timeout: 3000 imagesToUpload.length
}) } Bildern wurden erfolgreich hochgeladen.`,
} catch (e) { timeout: 3000,
updateNotification(notification, { });
dismissable: true, } catch (e) {
message: "Bild konnte nicht hochgeladen werden.", updateNotification(notification, {
subtext: `Eines ihrer Bilder konnte nicht hochgeladen werden. Wir haben bereits ein Ticket erstellt und melden uns so schnell wie möglich bei ihnen.`, dismissable: true,
timeout: 15000, message: "Bild konnte nicht hochgeladen werden.",
type: "error" subtext: `Eines ihrer Bilder konnte nicht hochgeladen werden. Wir haben bereits ein Ticket erstellt und melden uns so schnell wie möglich bei ihnen.`,
}) timeout: 15000,
} type: "error",
} });
}
}
return images; return images;
} }

View File

@@ -1,6 +1,6 @@
import { dialogs } from "../../../svelte-dialogs.config"; import { dialogs } from "../../../svelte-dialogs.config.js";
import { addNotification } from "#components/Notifications/shared"; import { addNotification } from "#components/Notifications/shared.js";
import { client } from "src/trpc"; import { api } from "astro-typesafe-api/client";
export async function spawnSignupPrompt() { export async function spawnSignupPrompt() {
const result = await dialogs.prompt( const result = await dialogs.prompt(
@@ -46,7 +46,7 @@ export async function spawnSignupPrompt() {
const [vorname, name, email, passwort] = result; const [vorname, name, email, passwort] = result;
try { try {
const response = await client.v1.benutzer.erstellen.mutate({ const response = await api.user.PUT.fetch({
email, email,
passwort, passwort,
vorname, vorname,

View File

@@ -1,7 +1,6 @@
import { AppRouter } from "@ibcornelsen/api"; import { OmitKeys, TicketClient } from "#components/Ausweis/types.js";
import { inferProcedureInput } from "@trpc/server"; import { api } from "astro-typesafe-api/client";
import { client } from "src/trpc";
export async function createTicket(info: inferProcedureInput<AppRouter["v1"]["tickets"]["erstellen"]>) { export async function createTicket(info: OmitKeys<TicketClient, "created_at" | "deleted_at" | "prioritaet" | "updated_at" | "status" | "uid">) {
return await client.v1.tickets.erstellen.mutate(info) return await api.ticket.PUT.fetch(info)
} }

View File

@@ -5,12 +5,14 @@ import {
Benutzer, Benutzer,
GebaeudeBilder, GebaeudeBilder,
Objekt, Objekt,
Rechnung,
Tickets,
VerbrauchsausweisGewerbe, VerbrauchsausweisGewerbe,
VerbrauchsausweisWohnen, VerbrauchsausweisWohnen,
} from "@ibcornelsen/database/client"; } from "@ibcornelsen/database/client";
import { z, ZodSchema } from "zod"; import { z, ZodSchema } from "zod";
type OmitKeys<T, K extends keyof T> = Omit<T, K>; export type OmitKeys<T, K extends keyof T> = Omit<T, K>;
export type UploadedGebaeudeBild = OmitKeys<GebaeudeBilder, "id" | "objekt_id"> & { export type UploadedGebaeudeBild = OmitKeys<GebaeudeBilder, "id" | "objekt_id"> & {
base64: string base64: string
@@ -84,9 +86,11 @@ export type AufnahmeClient = OmitKeys<
uid_objekt: string uid_objekt: string
}; };
export type TicketClient = OmitKeys<Tickets, "bearbeiter_id" | "benutzer_id" | "id">
export type BenutzerClient = OmitKeys<Benutzer, "id" | "passwort">; export type BenutzerClient = OmitKeys<Benutzer, "id" | "passwort">;
type ZodOverlapType<T> = z.ZodType<T, z.ZodTypeDef, T>; export type RechnungClient = OmitKeys<Rechnung, "aufnahme_id" | "benutzer_id" | "id">
export function ZodOverlap<T, S = z.ZodType<T, z.ZodTypeDef, T>>(arg: S): S { export function ZodOverlap<T, S = z.ZodType<T, z.ZodTypeDef, T>>(arg: S): S {
return arg; return arg;

View File

@@ -1,5 +1,8 @@
<script lang="ts"> <script lang="ts">
import { import {
AufnahmeClient,
ObjektClient,
UploadedGebaeudeBild,
VerbrauchsausweisWohnenClient, VerbrauchsausweisWohnenClient,
} from "#components/Ausweis/types.js"; } from "#components/Ausweis/types.js";
import moment from "moment"; import moment from "moment";
@@ -12,9 +15,14 @@
QuestionMarkCircled, QuestionMarkCircled,
} from "radix-svelte-icons"; } from "radix-svelte-icons";
import { endEnergieVerbrauchVerbrauchsausweis_2016 } from "#lib/Berechnungen/VerbrauchsausweisWohnen/VerbrauchsausweisWohnen_2016.js"; import { endEnergieVerbrauchVerbrauchsausweis_2016 } from "#lib/Berechnungen/VerbrauchsausweisWohnen/VerbrauchsausweisWohnen_2016.js";
import { client } from "src/trpc.js"; import { api } from "astro-typesafe-api/client";
import Cookies from "js-cookie";
import { API_ACCESS_TOKEN_COOKIE_NAME } from "#lib/constants.js";
export let ausweis: VerbrauchsausweisWohnenClient; export let ausweis: VerbrauchsausweisWohnenClient;
export let aufnahme: AufnahmeClient;
export let bilder: UploadedGebaeudeBild[];
export let objekt: ObjektClient;
export let progress: number; export let progress: number;
async function ausweisStornieren() { async function ausweisStornieren() {
@@ -33,11 +41,16 @@
}); });
if (result === true) { if (result === true) {
await client.v1.verbrauchsausweisWohnen.stornieren.mutate({ await api["verbrauchsausweis-wohnen"]._uid.DELETE.fetch(undefined, {
uid: ausweis.uid params: {
uid: ausweis.uid
},
headers: {
Authorization: `Bearer ${Cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)}`
}
}) })
ausweis.aufnahme.storniert = true; aufnahme.storniert = true;
ausweis = ausweis; ausweis = ausweis;
dialogs.alert({ dialogs.alert({
@@ -58,14 +71,14 @@
</script> </script>
<div class="card lg:card-side bg-base-200 card-bordered border-base-300"> <div class="card lg:card-side bg-base-200 card-bordered border-base-300">
{#if ausweis.aufnahme.storniert} {#if aufnahme.storniert}
<div class="absolute top-0 left-0 w-full h-full bg-[rgba(0,0,0,0.7)] z-[5] rounded-lg select-none"> <div class="absolute top-0 left-0 w-full h-full bg-[rgba(0,0,0,0.7)] z-[5] rounded-lg select-none">
<h1 class="absolute -rotate-[25deg] text-5xl md:text-7xl tracking-wide uppercase text-red-500 border-4 border-red-500 rounded-lg top-[50%] translate-y-[-50%] left-[50%] translate-x-[-50%]">Storniert</h1> <h1 class="absolute -rotate-[25deg] text-5xl md:text-7xl tracking-wide uppercase text-red-500 border-4 border-red-500 rounded-lg top-[50%] translate-y-[-50%] left-[50%] translate-x-[-50%]">Storniert</h1>
</div> </div>
{/if} {/if}
<figure class="lg:w-1/2"> <figure class="lg:w-1/2">
<img <img
src={(ausweis.aufnahme.objekt.gebaeude_bilder && `/bilder/${ausweis.aufnahme.objekt.gebaeude_bilder[0]?.uid}.webp`) || "/images/placeholder.jpg"} src={(bilder.length > 0 && `/bilder/${bilder[0].uid}.webp`) || "/images/placeholder.jpg"}
class="object-cover w-full h-full" class="object-cover w-full h-full"
alt="Gebäudebild" alt="Gebäudebild"
/> />
@@ -95,25 +108,25 @@
</ul> </ul>
</div> </div>
<div class="flex flex-row flex-wrap gap-2"> <div class="flex flex-row flex-wrap gap-2">
{#if ausweis.aufnahme.ausweisart == "VerbrauchsausweisWohnen"} {#if aufnahme.ausweisart == "VerbrauchsausweisWohnen"}
<div class="badge badge-accent font-semibold"> <div class="badge badge-accent font-semibold">
Verbrauchsausweis Wohnen Verbrauchsausweis Wohnen
</div> </div>
{:else if ausweis.aufnahme.ausweisart == "BedarfsausweisWohnen"} {:else if aufnahme.ausweisart == "BedarfsausweisWohnen"}
<div class="badge badge-accent font-semibold"> <div class="badge badge-accent font-semibold">
Bedarfsausweis Wohnen Bedarfsausweis Wohnen
</div> </div>
{:else if ausweis.aufnahme.ausweisart == "VerbrauchsausweisGewerbe"} {:else if aufnahme.ausweisart == "VerbrauchsausweisGewerbe"}
<div class="badge badge-accent font-semibold"> <div class="badge badge-accent font-semibold">
Verbrauchsausweis Gewerbe Verbrauchsausweis Gewerbe
</div> </div>
{/if} {/if}
{#if ausweis.erledigt} {#if aufnahme.erledigt}
<div class="badge badge-success font-semibold">Ausgestellt</div> <div class="badge badge-success font-semibold">Ausgestellt</div>
{/if} {/if}
</div> </div>
<h2 class="card-title">{ausweis.aufnahme.objekt.adresse}</h2> <h2 class="card-title">{objekt.adresse}</h2>
<div class="mb-4 flex flex-row items-center gap-4"> <div class="mb-4 flex flex-row items-center gap-4">
<progress class="progress w-full" value={progress} max="100"></progress> <progress class="progress w-full" value={progress} max="100"></progress>
<!-- TODO: Metrics für den Fortschritt festlegen --> <!-- TODO: Metrics für den Fortschritt festlegen -->
@@ -134,7 +147,7 @@
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<span>Erstellungsdatum</span> <span>Erstellungsdatum</span>
<span class="font-bold text-base-content" <span class="font-bold text-base-content"
>{moment(ausweis.erstellungsdatum).format( >{moment(aufnahme.erstellungsdatum).format(
"DD.MM.YYYY" "DD.MM.YYYY"
)}</span )}</span
> >
@@ -144,16 +157,16 @@
<span <span
class="font-bold text-base-content" class="font-bold text-base-content"
title="Gebäude / Heizung" title="Gebäude / Heizung"
>{ausweis.aufnahme.baujahr_gebaeude[0] || "N/A"} / >{aufnahme.baujahr_gebaeude[0] || "N/A"} /
{ausweis.aufnahme.baujahr_heizung[0] || {aufnahme.baujahr_heizung[0] ||
"N/A"}</span "N/A"}</span
> >
</div> </div>
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<span>Wohnfläche</span> <span>Wohnfläche</span>
<span class="font-bold text-base-content" <span class="font-bold text-base-content"
>{ausweis.aufnahme.flaeche >{aufnahme.flaeche
? `${ausweis.aufnahme.flaeche}m²` ? `${aufnahme.flaeche}m²`
: "N/A"}</span : "N/A"}</span
> >
</div> </div>

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import type { inferProcedureOutput } from "@trpc/server";
import { API, api, inferOutput } from "astro-typesafe-api/client"; import { API, api, inferOutput } from "astro-typesafe-api/client";
export let name: string; export let name: string;

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { addNotification } from "#components/Notifications/shared"; import { api } from "astro-typesafe-api/client";
import { client } from "src/trpc";
import { getClose } from "svelte-dialogs"; import { getClose } from "svelte-dialogs";
const close = getClose(); const close = getClose();
@@ -8,7 +7,7 @@
async function createTicket(e: SubmitEvent) { async function createTicket(e: SubmitEvent) {
e.preventDefault(); e.preventDefault();
try { try {
await client.v1.tickets.erstellen.mutate({ await api.ticket.PUT.fetch({
beschreibung: description, beschreibung: description,
email: email, email: email,
metadata: { metadata: {

View File

@@ -1,6 +1,5 @@
import moment from "moment"; import moment from "moment";
import { memoize } from "./Memoization.js"; import { memoize } from "./Memoization.js";
import { client } from "src/trpc.js";
import { api } from "astro-typesafe-api/client" import { api } from "astro-typesafe-api/client"
export const getKlimafaktoren = memoize(async (date: Date, plz: string) => { export const getKlimafaktoren = memoize(async (date: Date, plz: string) => {

View File

@@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import { BenutzerClient, VerbrauchsausweisWohnenClient } from "#components/Ausweis/types"; import { BenutzerClient, VerbrauchsausweisWohnenClient } from "#components/Ausweis/types.js";
import DashboardAusweis from "#components/Dashboard/DashboardAusweis.svelte"; import DashboardAusweis from "#components/Dashboard/DashboardAusweis.svelte";
import DashboardAusweisSkeleton from "#components/Dashboard/DashboardAusweisSkeleton.svelte"; import DashboardAusweisSkeleton from "#components/Dashboard/DashboardAusweisSkeleton.svelte";
import { verbrauchsausweisWohnenCalculateFormProgress } from "#lib/VerbrauchsausweisWohnen/calculateFormProgress"; import { verbrauchsausweisWohnenCalculateFormProgress } from "#lib/VerbrauchsausweisWohnen/calculateFormProgress";
import { client } from "src/trpc";
import { onMount } from "svelte"; import { onMount } from "svelte";
export let user: BenutzerClient; export let user: BenutzerClient;

View File

@@ -8,7 +8,7 @@
import { Tabs, Tab, TabList, TabPanel } from "../../components/Tabs"; import { Tabs, Tab, TabList, TabPanel } from "../../components/Tabs";
import { dialogs } from "../../../svelte-dialogs.config"; import { dialogs } from "../../../svelte-dialogs.config";
import { BenutzerClient } from "#components/Ausweis/types"; import { BenutzerClient } from "#components/Ausweis/types";
import { client } from "src/trpc"; // import { client } from "src/trpc";
import { exclude } from "#lib/exclude"; import { exclude } from "#lib/exclude";
export let benutzer: BenutzerClient; export let benutzer: BenutzerClient;

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { addNotification } from "@ibcornelsen/ui"; import { addNotification } from "@ibcornelsen/ui";
import { client } from "src/trpc"; import { api } from "astro-typesafe-api/client";
export let navigate: (target: string) => void; export let navigate: (target: string) => void;
export let onRegister: (response: { email: string, name: string, vorname: string }) => void; export let onRegister: (response: { email: string, name: string, vorname: string }) => void;
@@ -12,7 +12,7 @@
async function signUp(e: SubmitEvent) { async function signUp(e: SubmitEvent) {
e.preventDefault() e.preventDefault()
try { try {
const response = await client.v1.benutzer.erstellen.mutate({ const response = await api.user.PUT.fetch({
email, email,
passwort, passwort,
vorname, vorname,

View File

@@ -6,20 +6,16 @@
} from "@ibcornelsen/database/client"; } from "@ibcornelsen/database/client";
import { Enums } from "@ibcornelsen/database/client"; import { Enums } from "@ibcornelsen/database/client";
import PaymentOption from "#components/PaymentOption.svelte"; import PaymentOption from "#components/PaymentOption.svelte";
import { client } from "src/trpc";
import type { inferProcedureInput } from "@trpc/server";
import type { AppRouter } from "@ibcornelsen/api";
import CheckoutItem from "#components/CheckoutItem.svelte"; import CheckoutItem from "#components/CheckoutItem.svelte";
import { BenutzerClient, VerbrauchsausweisWohnenClient } from "#components/Ausweis/types"; import { BenutzerClient, VerbrauchsausweisWohnenClient } from "#components/Ausweis/types.js";
import { PRICES } from "#lib/constants"; import { PRICES } from "#lib/constants.js";
import { RechnungClient } from "#components/Ausweis/types.js";
export let user: BenutzerClient; export let user: BenutzerClient;
export let ausweis: export let ausweis:
| VerbrauchsausweisWohnenClient; | VerbrauchsausweisWohnenClient;
// TODO: überarbeiten und zu inferProcedureOutput machen // TODO: überarbeiten und zu inferProcedureOutput machen
let rechnung: inferProcedureInput< let rechnung: RechnungClient = {
AppRouter["v1"]["rechnungen"]["erstellen"]
> = {
email: user.email, email: user.email,
empfaenger: user.vorname + " " + user.name, empfaenger: user.vorname + " " + user.name,
strasse: user.adresse, strasse: user.adresse,
@@ -65,6 +61,7 @@
async function createPayment(e: SubmitEvent) { async function createPayment(e: SubmitEvent) {
e.preventDefault(); e.preventDefault();
// TODO
const response = await client.v1.rechnungen.erstellen.mutate({ const response = await client.v1.rechnungen.erstellen.mutate({
...rechnung, ...rechnung,
ausweisart: Enums.Ausweisart.VerbrauchsausweisWohnen, ausweisart: Enums.Ausweisart.VerbrauchsausweisWohnen,

View File

@@ -2,8 +2,7 @@ import { z } from "zod";
import moment from "moment"; import moment from "moment";
import { prisma } from "@ibcornelsen/database/server"; import { prisma } from "@ibcornelsen/database/server";
import { encodeToken } from "../../../lib/auth/token.js"; import { encodeToken } from "../../../lib/auth/token.js";
import { TRPCError } from "@trpc/server"; import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { defineApiRoute } from "astro-typesafe-api/server";
import { TokenType } from "#lib/auth/types.js"; import { TokenType } from "#lib/auth/types.js";
export const GET = defineApiRoute({ export const GET = defineApiRoute({
@@ -35,7 +34,7 @@ export const GET = defineApiRoute({
}); });
if (!response) { if (!response) {
throw new TRPCError({ throw new APIError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Der gegebene refresh token ist nicht gültig.", message: "Der gegebene refresh token ist nicht gültig.",
}); });
@@ -48,7 +47,7 @@ export const GET = defineApiRoute({
id: response.id, id: response.id,
}, },
}); });
throw new TRPCError({ throw new APIError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Der gegebene refresh token ist nicht gültig.", message: "Der gegebene refresh token ist nicht gültig.",
}); });
@@ -62,7 +61,7 @@ export const GET = defineApiRoute({
// id: response.id // id: response.id
// } // }
// }) // })
// throw new TRPCError({ code: "BAD_REQUEST", message: "Der gegebene refresh token wurde von einer anderen IP-Adresse ausgestellt, aus Sicherheitsgründen haben wir uns entschieden diesen zu invalidieren." }); // throw new APIError({ code: "BAD_REQUEST", message: "Der gegebene refresh token wurde von einer anderen IP-Adresse ausgestellt, aus Sicherheitsgründen haben wir uns entschieden diesen zu invalidieren." });
// } // }
const user = await prisma.benutzer.findUnique({ const user = await prisma.benutzer.findUnique({
@@ -78,7 +77,7 @@ export const GET = defineApiRoute({
id: response.id, id: response.id,
}, },
}); });
throw new TRPCError({ throw new APIError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Der gegebene refresh token ist nicht mehr gültig.", message: "Der gegebene refresh token ist nicht mehr gültig.",
}); });

View File

@@ -0,0 +1,84 @@
import { authorizationMiddleware } from "#lib/middleware/authorization.js";
import { GebaeudeBilderSchema, prisma } from "@ibcornelsen/database/server";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { z } from "zod";
import isBase64 from "is-base64";
import { fileURLToPath } from "url";
import { writeFileSync } from "fs";
import { UUidWithPrefix } from "#components/Ausweis/types.js";
export const PATCH = defineApiRoute({
input: GebaeudeBilderSchema.pick({
kategorie: true,
}).merge(z.object({
base64: z.string()
})),
output: z.void(),
middleware: authorizationMiddleware,
async fetch(input, ctx, user) {
if (!UUidWithPrefix.safeParse(ctx.params.uid).success) {
throw new APIError({
code: "BAD_REQUEST",
message: "UID konnte nicht verifiziert werden."
})
}
const image = await prisma.gebaeudeBilder.findUnique({
where: {
uid: ctx.params.uid,
objekt: {
benutzer_id: user.id
}
}
})
if (!image) {
throw new APIError({
code: "BAD_REQUEST",
message: "Bild existiert nicht oder gehört einem anderen Benutzer."
})
}
const base64 = input.base64;
if (!isBase64(base64, { mimeRequired: true })) {
throw new APIError({
code: "BAD_REQUEST",
message: "Das Bild ist nicht base64.",
});
}
const dataWithoutPrefix = base64.replace(
/^data:image\/\w+;base64,/,
""
);
const buffer = Buffer.from(dataWithoutPrefix, "base64");
if (input.kategorie !== image.kategorie) {
await prisma.gebaeudeBilder.update({
where: {
id: image.id
},
data: {
kategorie: input.kategorie
}
});
}
const filePath = fileURLToPath(new URL(`../../../../../persistent/images/${image.uid}.webp`, import.meta.url));
try {
// Wir optimieren das Bild und konvertieren es in WebP
// TODO: Sharp scheint nicht zu funktionieren, wir müssen das nochmal testen
// const optimizedBuffer = await sharp(buffer).webp({ quality: 80 }).toArray();
writeFileSync(filePath, buffer)
} catch(e) {
// Und geben einen Fehler zurück
throw new APIError({
code: "INTERNAL_SERVER_ERROR",
message: "Bild konnte nicht gespeichert werden.",
});
}
},
})

View File

@@ -1,6 +1,6 @@
import { OptionalNullable, UUidWithPrefix, VerbrauchsausweisWohnenClient, ZodOverlap } from "#components/Ausweis/types.js"; import { OptionalNullable, UUidWithPrefix, VerbrauchsausweisWohnenClient, ZodOverlap } from "#components/Ausweis/types.js";
import { exclude } from "#lib/exclude.js"; import { exclude } from "#lib/exclude.js";
import { authorizationMiddleware } from "#lib/middleware/authorization.js"; import { authorizationHeaders, authorizationMiddleware } from "#lib/middleware/authorization.js";
import { prisma, VerbrauchsausweisWohnenSchema } from "@ibcornelsen/database/server"; import { prisma, VerbrauchsausweisWohnenSchema } from "@ibcornelsen/database/server";
import { APIError, defineApiRoute } from "astro-typesafe-api/server"; import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { z } from "zod"; import { z } from "zod";
@@ -43,6 +43,101 @@ export const PATCH = defineApiRoute({
}, },
}) })
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 Verbrauchsausweis
// Dieser MUSS mit dem Nutzer verknüpft sein.
const ausweis = await prisma.verbrauchsausweisWohnen.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({ export const GET = defineApiRoute({
meta: { meta: {
description: "Gibt ein spezifisches Gebäude des Benutzers zurück.", description: "Gibt ein spezifisches Gebäude des Benutzers zurück.",

View File

@@ -1,9 +1,7 @@
import { Enums } from "@ibcornelsen/database/client"; import { Enums } from "@ibcornelsen/database/client";
import { test, describe, expect } from "bun:test"; import { test, describe, expect } from "bun:test";
import {faker} from "@faker-js/faker"; import {faker} from "@faker-js/faker";
import { client } from "src/trpc"; // import { client } from "src/trpc";
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '@ibcornelsen/api';
import { Buffer } from 'buffer'; import { Buffer } from 'buffer';
describe("Bilder hochladen", async () => { describe("Bilder hochladen", async () => {