API größtenteils umgezogen und Funktionen angepasst

This commit is contained in:
Moritz Utcke
2025-01-21 23:34:01 +07:00
parent de8c859826
commit 5d73f5f7c7
74 changed files with 1715 additions and 818 deletions

View File

@@ -1,9 +1,41 @@
import { authorizationMiddleware } from "#lib/middleware/authorization.js";
import { prisma } from "@ibcornelsen/database/server";
import { AufnahmeSchema, prisma } from "@ibcornelsen/database/server";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { z } from "zod";
export const PATCH = defineApiRoute({
fetch(input, context) {},
input: AufnahmeSchema.omit({
id: true,
uid: true,
benutzer_id: true,
objekt_id: true,
}),
output: z.void(),
middleware: authorizationMiddleware,
async fetch(input, ctx, user) {
const { uid } = ctx.params
const aufnahme = await prisma.aufnahme.findUnique({
where: {
uid,
benutzer_id: user.id
},
});
if (!aufnahme) {
throw new APIError({
code: "NOT_FOUND",
message: "Aufnahme mit dieser UID existiert nicht oder gehört nicht dem aktuellen Benutzer."
})
}
await prisma.aufnahme.update({
where: {
uid
},
data: input
})
},
});
export const GET = defineApiRoute({

View File

@@ -0,0 +1,59 @@
import { authorizationMiddleware } from "#lib/middleware/authorization.js"
import { AufnahmeSchema, ObjektSchema, prisma } from "@ibcornelsen/database/server"
import { APIError, defineApiRoute } from "astro-typesafe-api/server"
import { z } from "zod"
export const PUT = defineApiRoute({
input: z.object({
aufnahme: AufnahmeSchema.omit({
id: true,
uid: true,
benutzer_id: true,
objekt_id: true,
}).merge(z.object({
baujahr_klima: z.array(z.number().int().positive()).optional()
})),
uid_objekt: z.string().uuid()
}),
output: z.object({
uid: z.string().uuid()
}),
middleware: authorizationMiddleware,
async fetch(input, context, user) {
const objekt = await prisma.objekt.findUnique({
where: {
uid: input.uid_objekt,
benutzer: {
id: user.id
}
}
})
if (!objekt) {
throw new APIError({
code: "NOT_FOUND",
message: "Objekt konnte nicht gefunden werden oder gehört nicht diesem Benutzer."
})
}
const aufnahme = await prisma.aufnahme.create({
data: {
...input.aufnahme,
benutzer: {
connect: {
id: user.id
}
},
objekt: {
connect: {
uid: input.uid_objekt
}
}
},
})
return {
uid: aufnahme.uid
}
},
})

View File

@@ -5,7 +5,7 @@ import { TokenType, encodeToken } from "../../../lib/auth/token.js";
import { TRPCError } from "@trpc/server";
import { defineApiRoute } from "astro-typesafe-api/server";
export const tRPC_V1_BenutzerGetAccessTokenProcedure = defineApiRoute({
export const GET = defineApiRoute({
meta: {
description:
"Erstellt, basierend auf einem existierenden und gültigen Refresh Tokens, einen neuen Access Token, welcher zur Authentifizierung genutzt werden kann. Der resultierende Access Token ist nur 2 Tage gültig und muss danach neu generiert werden. Diese Funktion gibt ebenfalls einen neuen Refresh Token zurück, der alte wird dadurch invalidiert.",

View File

@@ -0,0 +1,77 @@
import { z } from "zod";
import moment from "moment";
import { prisma } from "@ibcornelsen/database/server";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
export const GET = defineApiRoute({
input: z.object({
plz: z.string().min(4).max(5),
startdatum: z.coerce.date(),
enddatum: z.coerce.date(),
genauigkeit: z.enum(["months", "years"]),
}),
output: z.array(
z.object({
month: z.number(),
year: z.number(),
klimafaktor: z.number(),
})
),
async fetch(input, ctx) {
const start = moment(input.startdatum);
const end = moment(input.enddatum);
if (start.isSameOrAfter(end)) {
throw new APIError({
code: "PRECONDITION_FAILED",
message: "Das Startdatum kann nicht vor dem Enddatum liegen.",
});
}
const intervals = [];
let currentDate = start.clone();
while (currentDate.isSameOrBefore(end)) {
let copy = currentDate.clone();
intervals.push(copy);
currentDate.add(1, input.genauigkeit);
}
let klimafaktoren = await prisma.klimafaktoren.findMany({
where: {
plz: input.plz,
month: intervals[0].month(),
OR: intervals.map((date) => {
return {
year: date.year(),
};
}),
},
});
if (!klimafaktoren) {
throw new APIError({
code: "NOT_FOUND",
message:
"Die Klimafaktoren konnten nicht geladen werden. Das kann daran liegen, dass sie für diesen Zeitraum oder Ort nicht verfügbar sind.",
});
}
// NOTE: Sollten wir hier lieber den Output padden und trotzdem die gefundenen zurückgeben?
if (klimafaktoren.length !== intervals.length) {
throw new APIError({
code: "NOT_FOUND",
message:
"Für diesen Zeitraum konnten nicht alle Klimafaktoren gefunden werden.",
});
}
return klimafaktoren.map((klimafaktor) => ({
month: klimafaktor.month,
year: klimafaktor.year,
klimafaktor: klimafaktor.klimafaktor,
}));
},
});

View File

@@ -0,0 +1,122 @@
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 "astro:content";
import isBase64 from "is-base64";
export const PUT = defineApiRoute({
input: GebaeudeBilderSchema.pick({
kategorie: true,
}).merge(z.object({
base64: z.string()
})),
output: z.object({
uid: z.string({ description: "Die UID des Bildes." })
}),
middleware: authorizationMiddleware,
async fetch(input, ctx, user) {
const base64 = input.base64;
if (!isBase64(base64, { mimeRequired: true })) {
throw new APIError({
code: "BAD_REQUEST",
message: "Das Bild ist nicht base64 kodiert.",
});
}
let objekt = await prisma.objekt.findUnique({
where: {
uid: ctx.params.uid,
benutzer_id: user.id
},
});
if (!objekt) {
throw new APIError({
code: "NOT_FOUND",
message: "Objekt nicht gefunden oder gehört einem anderen Benutzer.",
});
}
const dataWithoutPrefix = base64.replace(
/^data:image\/\w+;base64,/,
""
);
const buffer = Buffer.from(dataWithoutPrefix, "base64");
const bild = await prisma.gebaeudeBilder.create({
data: {
kategorie: input.kategorie,
objekt: {
connect: {
id: objekt.id,
},
},
},
select: {
uid: true,
},
});
const filePath = `/persistent/images/${bild.uid}.webp`;
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();
await Bun.write(filePath, buffer)
} catch(e) {
// Bild wurde nicht gespeichert, wir löschen den Eintrag wieder
await prisma.gebaeudeBilder.delete({
where: {
uid: bild.uid
}
})
// Und geben einen Fehler zurück
throw new APIError({
code: "INTERNAL_SERVER_ERROR",
message: "Bild konnte nicht gespeichert werden.",
});
}
return {
uid: bild.uid
};
},
})
export const GET = defineApiRoute({
middleware: authorizationMiddleware,
output: z.array(GebaeudeBilderSchema.pick({
kategorie: true,
uid: true
})),
async fetch(input, ctx, user) {
const { uid } = ctx.params;
const objekt = await prisma.objekt.findUnique({
where: {
uid
},
select: {
benutzer_id: true,
gebaeude_bilder: {
select: {
kategorie: true,
uid: true
}
}
}
})
if (!objekt || objekt.benutzer_id !== user.id) {
throw new APIError({
code: "FORBIDDEN",
message: "Objekt existiert nicht oder gehört einem anderen Benutzer."
})
}
return objekt.gebaeude_bilder
},
})

View File

@@ -1,10 +1,44 @@
import { authorizationMiddleware } from "#lib/middleware/authorization.js";
import { prisma } from "@ibcornelsen/database/server";
import { ObjektSchema, prisma } from "@ibcornelsen/database/server";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { z } from "zod";
export const PATCH = defineApiRoute({
fetch(input, context) {},
});
input: ObjektSchema.omit({
uid: true,
id: true,
benutzer_id: true
}),
output: z.void(),
headers: {
"Authorization": z.string()
},
middleware: authorizationMiddleware,
async fetch(input, ctx, user) {
const objekt = await prisma.objekt.findUnique({
where: {
uid: ctx.params.uid,
benutzer: {
id: user.id
}
}
})
if (!objekt) {
throw new APIError({
code: "NOT_FOUND",
message: "Objekt konnte nicht gefunden werden."
})
}
await prisma.objekt.update({
where: {
uid: ctx.params.uid
},
data: input
})
},
})
export const GET = defineApiRoute({
meta: {
@@ -24,8 +58,8 @@ export const GET = defineApiRoute({
}
},
middleware: authorizationMiddleware,
async fetch(input, context, user) {
const { uid } = context.params;
async fetch(input, ctx, user) {
const { uid } = ctx.params;
const objekt = await prisma.objekt.findUnique({
where: {

View File

@@ -3,9 +3,31 @@ import { ObjektSchema, prisma } from "@ibcornelsen/database/server";
import { defineApiRoute } from "astro-typesafe-api/server";
import { z } from "zod";
export const POST = defineApiRoute({
fetch(input, context) {
export const PUT = defineApiRoute({
input: ObjektSchema.omit({
id: true,
uid: true,
benutzer_id: true
}),
output: z.object({
uid: z.string().uuid()
}),
middleware: authorizationMiddleware,
async fetch(input, context, user) {
const objekt = await prisma.objekt.create({
data: {
...input,
benutzer: {
connect: {
id: user.id
}
}
},
})
return {
uid: objekt.uid
}
},
})

View File

@@ -0,0 +1,49 @@
import { z } from "zod";
import { prisma } from "@ibcornelsen/database/server";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
export const GET = defineApiRoute({
input: z.object({
plz: z.string().min(1).max(5),
limit: z.number().int().max(50).min(1).default(10).optional(),
}),
output: z.array(
z.object({
plz: z.string().min(4).max(5),
stadt: z.string(),
bundesland: z.string(),
landkreis: z.string(),
lat: z.number(),
lon: z.number(),
})
),
async fetch(input, context) {
const plz = input.plz;
const postleitzahlen = await prisma.postleitzahlen.findMany({
where: {
plz: {
startsWith: plz,
},
},
take: input.limit,
select: {
plz: true,
stadt: true,
bundesland: true,
landkreis: true,
lat: true,
lon: true,
},
});
if (postleitzahlen.length === 0) {
throw new APIError({
code: "NOT_FOUND",
message: "Postleitzahl nicht gefunden",
});
}
return postleitzahlen;
},
});

73
src/pages/api/ticket.ts Normal file
View File

@@ -0,0 +1,73 @@
import { z } from "zod";
import { TicketsSchema, prisma } from "@ibcornelsen/database/server";
import { defineApiRoute } from "astro-typesafe-api/server";
import { maybeAuthorizationMiddleware } from "#lib/middleware/authorization.js";
export const PUT = defineApiRoute({
meta: {
contentTypes: ["application/json"],
description:
"Erstellt ein neues Support Ticket und weist den Ersteller diesem zu, falls ein Authorization Header mitgegeben wurde.",
summary: "Erstellt ein neues Support Ticket.",
tags: ["Tickets"],
},
input: TicketsSchema.omit({
bearbeiter_id: true,
benutzer_id: true,
created_at: true,
deleted_at: true,
id: true,
prioritaet: true,
status: true,
uid: true,
updated_at: true,
}),
output: z.object({
uid: z.string().uuid(),
}),
middleware: maybeAuthorizationMiddleware,
async fetch(input, ctx, user) {
if (user === null) {
// Der Benutzer ist nicht authentifiziert.
// Wir erstellen das Ticket anonym.
const ticket = await prisma.tickets.create({
data: {
beschreibung: input.beschreibung,
email: input.email,
titel: input.titel,
metadata: input.metadata,
},
select: {
uid: true,
},
});
return {
uid: ticket.uid,
};
}
// Der Benutzer ist authentifiziert.
// Wir verlinken den Benutzer und das Ticket.
const ticket = await prisma.tickets.create({
data: {
benutzer: {
connect: {
id: user.id,
},
},
beschreibung: input.beschreibung,
email: input.email,
titel: input.titel,
metadata: input.metadata,
},
select: {
uid: true,
},
});
return {
uid: ticket.uid,
};
},
});

View File

@@ -1,71 +0,0 @@
import { authorizationMiddleware } from "#lib/middleware/authorization.js";
import { prisma } from "@ibcornelsen/database/server";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
export const PATCH = defineApiRoute({
fetch(input, context) {},
});
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.verbrauchsausweisWohnen.findUnique({
where: {
uid: input.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

@@ -0,0 +1,117 @@
import { exclude } from "#lib/exclude.js";
import { authorizationMiddleware } from "#lib/middleware/authorization.js";
import { prisma, VerbrauchsausweisWohnenSchema } from "@ibcornelsen/database/server";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { z } from "zod";
export const PATCH = defineApiRoute({
input: VerbrauchsausweisWohnenSchema.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.verbrauchsausweisWohnen.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.verbrauchsausweisWohnen.update({
where: {
uid: ctx.params.uid
},
data: input
})
},
})
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: VerbrauchsausweisWohnenSchema.merge(z.object({
uid_aufnahme: z.string().uuid(),
uid_objekt: z.string().uuid(),
uid_benutzer: z.string().uuid().optional()
})).omit({
id: true,
aufnahme_id: true,
benutzer_id: true,
}),
middleware: authorizationMiddleware,
async fetch(input, context, user) {
const { uid } = context.params;
console.log(uid);
const ausweis = await prisma.verbrauchsausweisWohnen.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

@@ -1,27 +1,101 @@
import { authorizationMiddleware } from "#lib/middleware/authorization.js";
import { prisma } from "@ibcornelsen/database/server";
import { prisma, VerbrauchsausweisWohnenSchema } from "@ibcornelsen/database/server";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { z } from "zod";
export const PATCH = defineApiRoute({
fetch(input, context) {},
});
export const PUT = defineApiRoute({
meta: {
contentTypes: ["application/json"],
description:
"Erstellt einen neuen Verbrauchsausweis 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: ["Verbrauchsausweis Wohnen"],
},
input: z.object({
ausweis: VerbrauchsausweisWohnenSchema.omit({
id: true,
benutzer_id: true,
uid: true,
aufnahme_id: true
}),
uid_aufnahme: z.string().uuid()
}),
output: z.object({
uid: z.string().uuid(),
objekt_uid: z.string().uuid(),
aufnahme_uid: z.string().uuid(),
}),
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.verbrauchsausweisWohnen.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": {
Authorization: {
description: "Ein gültiger Authentifizierungstoken",
required: true,
allowEmptyValue: false,
examples: {
Bearer: {
value: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
}
}
value: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
},
},
},
},
},
middleware: authorizationMiddleware,
async fetch(input, context, user) {
@@ -37,7 +111,7 @@ export const GET = defineApiRoute({
include: {
objekt: {
include: {
gebaeude_bilder: true
gebaeude_bilder: true,
},
},
rechnungen: true,
@@ -45,20 +119,23 @@ export const GET = defineApiRoute({
include: {
benutzer: {
select: {
uid: true
}
}
uid: true,
},
},
},
orderBy: {
date: "asc"
}
}
date: "asc",
},
},
},
}
},
},
});
if (!ausweis || (ausweis.benutzer_id !== null && ausweis.benutzer_id !== user.id)) {
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",
@@ -66,6 +143,6 @@ export const GET = defineApiRoute({
});
}
return ausweis
return ausweis;
},
});