diff --git a/openapi.json b/openapi.json index 872d426b..d69505c3 100644 --- a/openapi.json +++ b/openapi.json @@ -1 +1 @@ -{"openapi":"3.0.3","info":{"title":"Title","version":"1.0.0","description":""},"paths":{"postleitzahlen":{"get":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"plz":{"type":"string","minLength":1,"maxLength":5},"limit":{"type":"integer","maximum":50,"minimum":1,"default":10}},"required":["plz"],"additionalProperties":false}}}},"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"plz":{"type":"string","minLength":4,"maxLength":5},"stadt":{"type":"string"},"bundesland":{"type":"string"},"landkreis":{"type":"string"},"lat":{"type":"number"},"lon":{"type":"number"}},"required":["plz","stadt","bundesland","landkreis","lat","lon"],"additionalProperties":false}}}}}}}},"index":{"post":{"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}}}}},"[id]":{"patch":{"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}}}}},"refresh-token":{"get":{"description":"Erstellt sowohl einen neuen Refresh Token als auch einen Access Token für den gegebenen Benutzer. Der Refresh Token kann später für die Erstellung neuer Access Token genutzt werden.","tags":["Benutzer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"passwort":{"type":"string","minLength":8,"maxLength":100}},"required":["email","passwort"],"additionalProperties":false}}}},"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"uid":{"type":"string","format":"uuid"},"accessToken":{"type":"string"},"refreshToken":{"type":"string"},"refreshTokenBase64":{"type":"string"},"accessTokenBase64":{"type":"string"},"exp":{"type":"number"}},"required":["uid","accessToken","refreshToken","refreshTokenBase64","accessTokenBase64","exp"],"additionalProperties":false}}}}}}},"self":{"get":{"description":"Gibt die Daten des momentan eingeloggten Benutzers zurück. Falls der Authorization Key invalid ist wird stattdessen null zurückgegeben.","tags":["Benutzer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"uid":{"type":"string"},"name":{"type":"string","nullable":true},"vorname":{"type":"string","nullable":true},"email":{"type":"string"},"profilbild":{"type":"string","nullable":true},"plz":{"type":"string","nullable":true},"ort":{"type":"string","nullable":true},"adresse":{"type":"string","nullable":true},"telefon":{"type":"string","nullable":true},"anrede":{"type":"string","nullable":true},"rolle":{"type":"string","enum":["USER","ADMIN"]}},"required":["uid","email","rolle"],"additionalProperties":false},{"enum":["null"],"nullable":true}]}}}}}}}}} \ No newline at end of file +{"openapi":"3.0.3","info":{"title":"Title","version":"1.0.0","description":""},"paths":{"klimafaktoren":{"get":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"plz":{"type":"string","minLength":4,"maxLength":5},"startdatum":{"type":"string","format":"date-time"},"enddatum":{"type":"string","format":"date-time"},"genauigkeit":{"type":"string","enum":["months","years"]}},"required":["plz","startdatum","enddatum","genauigkeit"],"additionalProperties":false}}}},"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"month":{"type":"number"},"year":{"type":"number"},"klimafaktor":{"type":"number"}},"required":["month","year","klimafaktor"],"additionalProperties":false}}}}}}}},"postleitzahlen":{"get":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"plz":{"type":"string","minLength":1,"maxLength":5},"limit":{"type":"integer","maximum":50,"minimum":1,"default":10}},"required":["plz"],"additionalProperties":false}}}},"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"plz":{"type":"string","minLength":4,"maxLength":5},"stadt":{"type":"string"},"bundesland":{"type":"string"},"landkreis":{"type":"string"},"lat":{"type":"number"},"lon":{"type":"number"}},"required":["plz","stadt","bundesland","landkreis","lat","lon"],"additionalProperties":false}}}}}}}},"index":{"put":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"latitude":{"type":"number","nullable":true},"longitude":{"type":"number","nullable":true},"plz":{"type":"string","nullable":true,"description":"Postleitzahl des Gebäudes"},"ort":{"type":"string","nullable":true,"description":"Ort des Gebäudes"},"adresse":{"type":"string","nullable":true,"description":"Adresse (Straße und Hausnummer) des Gebäudes"}},"required":["latitude","longitude","plz","ort","adresse"],"additionalProperties":false}}}},"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"uid":{"type":"string","format":"uuid"}},"required":["uid"],"additionalProperties":false}}}}}}},"[uid]":{"patch":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ausweisart":{"type":"string","enum":["VerbrauchsausweisWohnen","VerbrauchsausweisGewerbe","BedarfsausweisWohnen","BedarfsausweisGewerbe"],"description":"Art des korrespondierenden Ausweises, wie z.B. VerbrauchsausweisWohnen","nullable":true},"gebaeudetyp":{"type":"string","nullable":true,"description":"Art des Gebäudes und seiner primären Nutzungsart"},"gebaeudeteil":{"type":"string","nullable":true,"description":"Betrachteter Teil des Gebäudes, z.B. Gesamtgebäude, Wohnteil, Gewerbeteil"},"baujahr_gebaeude":{"type":"array","items":{"type":"integer"},"description":"Alle Jahre in denen das Gebäude konstruiert oder grundlegend verändert wurde"},"baujahr_heizung":{"type":"array","items":{"type":"integer"},"description":"Alle Jahre in denen die Heizung eingebaut oder grundlegend verändert wurde"},"baujahr_klima":{"type":"array","items":{"type":"integer"},"description":"Alle Jahre in denen die Klimaanlage eingebaut oder grundlegend verändert wurde"},"einheiten":{"type":"integer","description":"Anzahl der (Wohn)Einheiten im Gebäude","nullable":true},"flaeche":{"type":"integer","description":"Wohnfläche bei Wohngebäuden, Nutzfläche bei Gewerbegebäuden","nullable":true},"nutzflaeche":{"type":"integer","description":"(energetische) Nutzfläche des Gebäudes. Bei Gewerbegebäuden entspricht Sie der Nutzfläche","nullable":true},"saniert":{"type":"boolean","nullable":true,"description":"Falls das Gebäude energetisch saniert ist, sollte dieser Wert auf true stehen"},"keller":{"type":"string","enum":["BEHEIZT","UNBEHEIZT","NICHT_VORHANDEN"],"description":"Ob ein Keller vorhanden, beheizt oder unbeheizt ist","nullable":true},"dachgeschoss":{"type":"string","enum":["BEHEIZT","UNBEHEIZT","NICHT_VORHANDEN"],"description":"Ob ein Dachgeschoss vorhanden, beheizt oder unbeheizt ist","nullable":true},"lueftung":{"type":"string","enum":["Fensterlueftung","Schachtlueftung","LueftungsanlageMitWaermerueckgewinnung","LueftungsanlageOhneWaermerueckgewinnung"],"description":"Art der Gebäudelüftung","nullable":true},"kuehlung":{"type":"string","nullable":true,"description":"Art der Gebäudekühlung"},"leerstand":{"type":"integer","description":"Prozentualer Leerstand des Gebäudes in einem durchschnittlichen Jahr","nullable":true},"alternative_heizung":{"type":"boolean","nullable":true,"description":"Falls der Heizungsverbrauch alternative Energieversorgungssysteme beinhaltet sollte dieser Wert auf true stehen"},"alternative_warmwasser":{"type":"boolean","nullable":true,"description":"Falls der Warmwasserverbrauch alternative Energieversorgungssysteme beinhaltet sollte dieser Wert auf true stehen"},"alternative_lueftung":{"type":"boolean","nullable":true,"description":"Falls die Lüftung alternative Energieversorgungssysteme beinhaltet sollte dieser Wert auf true stehen"},"alternative_kuehlung":{"type":"boolean","nullable":true,"description":"Falls die Kühlung alternative Energieversorgungssysteme beinhaltet sollte dieser Wert auf true stehen"},"brennstoff_1":{"type":"string","nullable":true,"description":"Genutzer Brennstoff der primären Energiequelle"},"brennstoff_2":{"type":"string","nullable":true,"description":"Genutzer Brennstoff der sekundären Energiequelle"},"storniert":{"type":"boolean","nullable":true,"description":"Falls der Ausweis storniert wurde, sollte dieser Wert auf true stehen"},"erledigt":{"type":"boolean","nullable":true,"description":"Falls der Ausweis erledigt ist, sollte dieser Wert auf true stehen"},"bestellt":{"type":"boolean","nullable":true,"description":"Falls der Ausweis bestellt wurde, sollte dieser Wert auf true stehen"},"zurueckgestellt":{"type":"boolean","nullable":true,"description":"Falls der Ausweis vom Aussteller zurückgestellt wurde, sollte dieser Wert auf true stehen"},"prueftext":{"type":"string","nullable":true,"description":"Durch den Kunden hinzugefügte Anmerkung zur Vorabprüfung"},"boxpruefung":{"type":"boolean","nullable":true},"energieeffizienzklasse":{"type":"string","nullable":true,"description":"Die aus der Berechnung hervorgehende Energieeffizienzklasse des Gebäudes"},"erstellungsdatum":{"type":"string","format":"date-time","description":"Datum an dem der Kunde den Ausweis erstellt hat","nullable":true},"ausstellungsdatum":{"type":"string","format":"date-time","description":"Datum an dem der Aussteller den Ausweis ausgestellt hat","nullable":true},"zentralheizung":{"type":"boolean","nullable":true,"description":"Falls das Gebäude über eine Zentralbeheizung verfügt, sollte dieser Wert auf true stehen"},"solarsystem_warmwasser":{"type":"boolean","nullable":true,"description":"Falls das Gebäude über ein Solarsystem für Warmwasser verfügt, sollte dieser Wert auf true stehen"},"warmwasser_rohre_gedaemmt":{"type":"boolean","nullable":true,"description":"Falls die Warmwasserrohre des Gebäudes gedämmt sind, sollte dieser Wert auf true stehen"},"niedertemperatur_kessel":{"type":"boolean","nullable":true,"description":"Falls das Gebäude über einen Niedertemperaturkessel verfügt, sollte dieser Wert auf true stehen"},"brennwert_kessel":{"type":"boolean","nullable":true,"description":"Falls das Gebäude über einen Brennwertkessel verfügt, sollte dieser Wert auf true stehen"},"heizungsrohre_gedaemmt":{"type":"boolean","nullable":true,"description":"Falls die Heizungsrohre des Gebäudes gedämmt sind, sollte dieser Wert auf true stehen"},"standard_kessel":{"type":"boolean","nullable":true},"waermepumpe":{"type":"boolean","nullable":true,"description":"Falls das Gebäude über eine Wärmepumpe verfügt, sollte dieser Wert auf true stehen"},"raum_temperatur_regler":{"type":"boolean","nullable":true,"description":"Falls das Gebäude über einen Raumtemperaturregler verfügt, sollte dieser Wert auf true stehen"},"photovoltaik":{"type":"boolean","nullable":true,"description":"Falls das Gebäude über eine Photovoltaikanlage verfügt, sollte dieser Wert auf true stehen"},"durchlauf_erhitzer":{"type":"boolean","nullable":true,"description":"Falls das Gebäude über einen Durchlauferhitzer verfügt, sollte dieser Wert auf true stehen"},"einzelofen":{"type":"boolean","nullable":true},"zirkulation":{"type":"boolean","nullable":true,"description":"Falls das Gebäude über eine Zirkulationspumpe verfügt, sollte dieser Wert auf true stehen"},"einfach_verglasung":{"type":"boolean","nullable":true,"description":"Falls die Fenster des Gebäudes einfach gedämmt sind, sollte dieser Wert auf true stehen"},"dreifach_verglasung":{"type":"boolean","nullable":true,"description":"Falls die Fenster des Gebäudes dreifach gedämmt sind, sollte dieser Wert auf true stehen"},"fenster_teilweise_undicht":{"type":"boolean","nullable":true,"description":"Falls die Fenster des Gebäudes teilweise undicht sind, sollte dieser Wert auf true stehen"},"doppel_verglasung":{"type":"boolean","nullable":true,"description":"Falls die Fenster des Gebäudes doppelt gedämmt sind, sollte dieser Wert auf true stehen"},"fenster_dicht":{"type":"boolean","nullable":true,"description":"Falls die Fenster des Gebäudes dicht sind, sollte dieser Wert auf true stehen"},"rolllaeden_kaesten_gedaemmt":{"type":"boolean","nullable":true,"description":"Falls das Gebäude über gedämmte Rolllädenkästen verfügt, sollte dieser Wert auf true stehen"},"isolier_verglasung":{"type":"boolean","nullable":true,"description":"Falls die Fenster des Gebäudes isolier Verglasung haben, sollte dieser Wert auf true stehen"},"tueren_undicht":{"type":"boolean","nullable":true,"description":"Falls die Türen des Gebäudes undicht sind, sollte dieser Wert auf true stehen"},"tueren_dicht":{"type":"boolean","nullable":true,"description":"Falls die Türen des Gebäudes dicht sind, sollte dieser Wert auf true stehen"},"dachgeschoss_gedaemmt":{"type":"boolean","nullable":true,"description":"Falls das Dachgeschoss des Gebäudes gedämmt ist, sollte dieser Wert auf true stehen"},"keller_decke_gedaemmt":{"type":"boolean","nullable":true,"description":"Falls die Kellerdecke des Gebäudes gedämmt ist, sollte dieser Wert auf true stehen"},"keller_wand_gedaemmt":{"type":"boolean","nullable":true,"description":"Falls die Kellerwände des Gebäudes gedämmt sind, sollte dieser Wert auf true stehen"},"aussenwand_gedaemmt":{"type":"boolean","nullable":true,"description":"Falls die Außenwände des Gebäudes gedämmt sind, sollte dieser Wert auf true stehen"},"oberste_geschossdecke_gedaemmt":{"type":"boolean","nullable":true,"description":"Falls die oberste Geschossdecke des Gebäudes gedämmt ist, sollte dieser Wert auf true stehen"},"aussenwand_min_12cm_gedaemmt":{"type":"boolean","nullable":true,"description":"Falls die Außenwände des Gebäudes mindestens 12cm gedämmt sind, sollte dieser Wert auf true stehen"},"dachgeschoss_min_12cm_gedaemmt":{"type":"boolean","nullable":true,"description":"Falls das Dachgeschoss des Gebäudes mindestens 12cm gedämmt ist, sollte dieser Wert auf true stehen"},"oberste_geschossdecke_min_12cm_gedaemmt":{"type":"boolean","nullable":true,"description":"Falls die oberste Geschossdecke des Gebäudes mindestens 12cm gedämmt ist, sollte dieser Wert auf true stehen"}},"required":["ausweisart","gebaeudetyp","gebaeudeteil","baujahr_gebaeude","baujahr_heizung","baujahr_klima","einheiten","flaeche","nutzflaeche","saniert","keller","dachgeschoss","lueftung","kuehlung","leerstand","alternative_heizung","alternative_warmwasser","alternative_lueftung","alternative_kuehlung","brennstoff_1","brennstoff_2","storniert","erledigt","bestellt","zurueckgestellt","prueftext","boxpruefung","energieeffizienzklasse","erstellungsdatum","ausstellungsdatum","zentralheizung","solarsystem_warmwasser","warmwasser_rohre_gedaemmt","niedertemperatur_kessel","brennwert_kessel","heizungsrohre_gedaemmt","standard_kessel","waermepumpe","raum_temperatur_regler","photovoltaik","durchlauf_erhitzer","einzelofen","zirkulation","einfach_verglasung","dreifach_verglasung","fenster_teilweise_undicht","doppel_verglasung","fenster_dicht","rolllaeden_kaesten_gedaemmt","isolier_verglasung","tueren_undicht","tueren_dicht","dachgeschoss_gedaemmt","keller_decke_gedaemmt","keller_wand_gedaemmt","aussenwand_gedaemmt","oberste_geschossdecke_gedaemmt","aussenwand_min_12cm_gedaemmt","dachgeschoss_min_12cm_gedaemmt","oberste_geschossdecke_min_12cm_gedaemmt"],"additionalProperties":false}}}},"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}}}}},"ticket":{"put":{"description":"Erstellt ein neues Support Ticket und weist den Ersteller diesem zu, falls ein Authorization Header mitgegeben wurde.","tags":["Tickets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"titel":{"type":"string"},"beschreibung":{"type":"string"},"metadata":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]},{"type":"array","items":{"$ref":"#/properties/metadata"}},{"type":"object","additionalProperties":{"$ref":"#/properties/metadata"}}]},"email":{"type":"string"}},"required":["titel","beschreibung","metadata","email"],"additionalProperties":false}}}},"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"uid":{"type":"string","format":"uuid"}},"required":["uid"],"additionalProperties":false}}}}}}},"access-token":{"get":{"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.","tags":["Benutzer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"refreshToken":{"type":"string"}},"required":["refreshToken"],"additionalProperties":false}}}},"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"accessToken":{"type":"string"},"accessTokenExpiry":{"type":"number"},"refreshToken":{"type":"string"},"refreshTokenExpiry":{"type":"number"}},"required":["accessToken","accessTokenExpiry","refreshToken","refreshTokenExpiry"],"additionalProperties":false}}}}}}},"refresh-token":{"get":{"description":"Erstellt sowohl einen neuen Refresh Token als auch einen Access Token für den gegebenen Benutzer. Der Refresh Token kann später für die Erstellung neuer Access Token genutzt werden.","tags":["Benutzer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"passwort":{"type":"string","minLength":8,"maxLength":100}},"required":["email","passwort"],"additionalProperties":false}}}},"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"uid":{"type":"string","format":"uuid"},"accessToken":{"type":"string"},"refreshToken":{"type":"string"},"refreshTokenBase64":{"type":"string"},"accessTokenBase64":{"type":"string"},"exp":{"type":"number"}},"required":["uid","accessToken","refreshToken","refreshTokenBase64","accessTokenBase64","exp"],"additionalProperties":false}}}}}}},"self":{"get":{"description":"Gibt die Daten des momentan eingeloggten Benutzers zurück. Falls der Authorization Key invalid ist wird stattdessen null zurückgegeben.","tags":["Benutzer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"uid":{"type":"string"},"name":{"type":"string","nullable":true},"vorname":{"type":"string","nullable":true},"email":{"type":"string"},"profilbild":{"type":"string","nullable":true},"plz":{"type":"string","nullable":true},"ort":{"type":"string","nullable":true},"adresse":{"type":"string","nullable":true},"telefon":{"type":"string","nullable":true},"anrede":{"type":"string","nullable":true},"rolle":{"type":"string","enum":["USER","ADMIN"]}},"required":["uid","name","vorname","email","profilbild","plz","ort","adresse","telefon","anrede","rolle"],"additionalProperties":false},{"enum":["null"],"nullable":true}]}}}}}}},"bilder":{"put":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"kategorie":{"type":"string","enum":["Heizung","Fenster","Gebaeude","Daemmung"]},"base64":{"type":"string"}},"required":["kategorie","base64"],"additionalProperties":false}}}},"parameters":[],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"uid":{"type":"string","description":"Die UID des Bildes."}},"required":["uid"],"additionalProperties":false}}}}}}}}} \ No newline at end of file diff --git a/package.json b/package.json index 40918a4b..dcac997c 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "format": "prettier --write .", "build:production": "astro build && bun --bun server.ts", "i18n:generate": "bunx astro-i18next generate", - "prisma:studio": "bunx prisma studio --schema=./node_modules/@ibcornelsen/database/prisma/schema.prisma" + "prisma:studio": "bunx prisma studio --schema=./node_modules/@ibcornelsen/database/prisma/schema.prisma", + "openapi:generate": "bun astro-typesafe-api generate" }, "private": true, "dependencies": { @@ -43,6 +44,7 @@ "js-cookie": "^3.0.5", "js-interpolate": "^1.3.2", "jsonwebtoken": "^9.0.2", + "jwt-decode": "^4.0.0", "moment": "^2.30.1", "moment-timezone": "^0.5.46", "pdf-lib": "^1.17.1", diff --git a/persistent/images/3c319927-d226-4c84-8650-11bcc3d90670.webp b/persistent/images/3c319927-d226-4c84-8650-11bcc3d90670.webp new file mode 100644 index 00000000..68fcce0a Binary files /dev/null and b/persistent/images/3c319927-d226-4c84-8650-11bcc3d90670.webp differ diff --git a/persistent/uploads/images/3bfcee31-723c-45fd-a716-1118e877bdbb.jpg b/persistent/uploads/images/3bfcee31-723c-45fd-a716-1118e877bdbb.jpg deleted file mode 100644 index 2ee7916b..00000000 Binary files a/persistent/uploads/images/3bfcee31-723c-45fd-a716-1118e877bdbb.jpg and /dev/null differ diff --git a/persistent/uploads/images/c646b143-5cdf-47c9-859a-87731595b75f.jpg b/persistent/uploads/images/c646b143-5cdf-47c9-859a-87731595b75f.jpg deleted file mode 100644 index 30a8069f..00000000 Binary files a/persistent/uploads/images/c646b143-5cdf-47c9-859a-87731595b75f.jpg and /dev/null differ diff --git a/persistent/uploads/images/c6ef6b29-d1ba-4076-996c-1cbf7d470eb9.jpg b/persistent/uploads/images/c6ef6b29-d1ba-4076-996c-1cbf7d470eb9.jpg deleted file mode 100644 index cf8f91a5..00000000 Binary files a/persistent/uploads/images/c6ef6b29-d1ba-4076-996c-1cbf7d470eb9.jpg and /dev/null differ diff --git a/persistent/uploads/images/f89fb8e5-6902-4a50-8c07-ca2094c91dd6.jpg b/persistent/uploads/images/f89fb8e5-6902-4a50-8c07-ca2094c91dd6.jpg deleted file mode 100644 index 963f40b8..00000000 Binary files a/persistent/uploads/images/f89fb8e5-6902-4a50-8c07-ca2094c91dd6.jpg and /dev/null differ diff --git a/src/astro-typesafe-api-caller.ts b/src/astro-typesafe-api-caller.ts index ad7dc724..704b51a5 100644 --- a/src/astro-typesafe-api-caller.ts +++ b/src/astro-typesafe-api-caller.ts @@ -6,9 +6,9 @@ export const createCaller = createCallerFactory({ "ticket": await import("../src/pages/api/ticket.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/refresh-token": await import("../src/pages/api/auth/refresh-token.ts"), + "bedarfsausweis-wohnen": await import("../src/pages/api/bedarfsausweis-wohnen/index.ts"), "objekt": await import("../src/pages/api/objekt/index.ts"), "user": await import("../src/pages/api/user/index.ts"), "user/self": await import("../src/pages/api/user/self.ts"), diff --git a/src/client/lib/validateAccessToken.ts b/src/client/lib/validateAccessToken.ts index 02f0999d..1fa7c42e 100644 --- a/src/client/lib/validateAccessToken.ts +++ b/src/client/lib/validateAccessToken.ts @@ -1,9 +1,9 @@ import Cookies from "js-cookie"; import { API_ACCESS_TOKEN_COOKIE_NAME, API_REFRESH_TOKEN_COOKIE_NAME, API_UID_COOKIE_NAME } from "#lib/constants.js"; import moment from "moment"; -import jwt from "jsonwebtoken" -import { TokenData, TokenType } from "#lib/auth/token.js"; +import { TokenData, TokenType } from "#lib/auth/types.js"; import { api } from "astro-typesafe-api/client"; +import { jwtDecode } from "jwt-decode" export async function validateAccessTokenClient() { @@ -11,9 +11,7 @@ export async function validateAccessTokenClient() { const refreshToken = Cookies.get(API_REFRESH_TOKEN_COOKIE_NAME); if (accessToken) { - const { exp, typ, uid } = jwt.decode(accessToken, { - json: true - }) as TokenData + const { exp, typ, uid } = jwtDecode(accessToken) as TokenData if (exp > Date.now() && typ === TokenType.Access) { return true; diff --git a/src/client/lib/verbrauchsausweisWohnenSpeichern.ts b/src/client/lib/verbrauchsausweisWohnenSpeichern.ts index 1c830ee3..73967fcb 100644 --- a/src/client/lib/verbrauchsausweisWohnenSpeichern.ts +++ b/src/client/lib/verbrauchsausweisWohnenSpeichern.ts @@ -2,11 +2,9 @@ import { api } from "astro-typesafe-api/client" import { exclude } from "#lib/exclude.js"; -import { bilderHochladen } from "./bilderHochladen.js"; -import { prisma } from "@ibcornelsen/database/server"; import Cookies from "js-cookie"; import { API_ACCESS_TOKEN_COOKIE_NAME } from "#lib/constants.js"; -import { AufnahmeClient, BenutzerClient, ObjektClient, UploadedGebaeudeBild, VerbrauchsausweisWohnenClient, } from "#components/Ausweis/types.js"; +import { AufnahmeClient, ObjektClient, UploadedGebaeudeBild, VerbrauchsausweisWohnenClient, } from "#components/Ausweis/types.js"; // import { addNotification } from "@ibcornelsen/ui"; export async function verbrauchsausweisWohnenSpeichern( diff --git a/src/components/Ausweis/ButtonWeiterHilfe.svelte b/src/components/Ausweis/ButtonWeiterHilfe.svelte index 8928036d..b0698fb3 100644 --- a/src/components/Ausweis/ButtonWeiterHilfe.svelte +++ b/src/components/Ausweis/ButtonWeiterHilfe.svelte @@ -1,12 +1,13 @@ diff --git a/src/components/Ausweis/GebaeudeDaten.svelte b/src/components/Ausweis/GebaeudeDaten.svelte index f5563ed1..2e4f41a5 100644 --- a/src/components/Ausweis/GebaeudeDaten.svelte +++ b/src/components/Ausweis/GebaeudeDaten.svelte @@ -5,8 +5,10 @@ import ZipSearch from "#components/PlzSuche.svelte"; import { Enums } from "@ibcornelsen/database/client" + import { AufnahmeClient, ObjektClient } from "./types.js"; - export let aufnahme: GebaeudeAufnahmeClient; + export let aufnahme: AufnahmeClient; + export let objekt: ObjektClient; @@ -29,7 +31,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8 required data-msg-minlength="min. 5 Zeichen" data-msg-maxlength="max. 40 Zeichen" - bind:value={aufnahme.adresse} + bind:value={objekt.adresse} />
@@ -47,8 +49,8 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8 @@ -61,7 +63,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8 name="ort" data-test="ort" readonly={true} - bind:value={aufnahme.ort} + bind:value={objekt.ort} type="text" /> diff --git a/src/components/Ausweis/Pruefung.svelte b/src/components/Ausweis/Pruefung.svelte index 09e388d8..80b1f2a8 100644 --- a/src/components/Ausweis/Pruefung.svelte +++ b/src/components/Ausweis/Pruefung.svelte @@ -1,27 +1,34 @@
- {bereich} - {title} -
- - {#each bullets as [bullet, check]} -
+ {bereich} - {title} +
+ {#each bullets as [bullet, check]} +
{@html bullet} -
{check ? "✔" : "✘"}
+
+ {check ? "✔" : "✘"} +
- {/each} - -
+ {/each} +
- \ No newline at end of file + .check { + @apply self-center font-bold text-green-700; + } + .check-no { + @apply self-center font-bold text-red-700; + } + diff --git a/src/components/Ausweis/types.ts b/src/components/Ausweis/types.ts index a215d04d..345a2836 100644 --- a/src/components/Ausweis/types.ts +++ b/src/components/Ausweis/types.ts @@ -89,4 +89,18 @@ type ZodOverlapType = z.ZodType; export function ZodOverlap>(arg: S): S { return arg; -} \ No newline at end of file +} + +type PickNullable = { + [P in keyof T as null extends T[P] ? P : never]: T[P] +} + +type PickNotNullable = { + [P in keyof T as null extends T[P] ? never : P]: T[P] +} + +export type OptionalNullable = T extends object ? { + [K in keyof PickNullable]?: OptionalNullable +} & { + [K in keyof PickNotNullable]: OptionalNullable +} : T; \ No newline at end of file diff --git a/src/lib/auth/token.ts b/src/lib/auth/token.ts index b0541460..269752dd 100644 --- a/src/lib/auth/token.ts +++ b/src/lib/auth/token.ts @@ -1,12 +1,5 @@ import jwt from "jsonwebtoken"; - -export enum TokenType { - Refresh, - Access, - Reset -} - -export type TokenData = { uid: string, typ: TokenType, exp: number } +import { TokenData } from "./types.js"; export function encodeToken(data: TokenData) { const token = jwt.sign(data, "yIvbgS$k7Bfc+mpV%TWDZAhje9#uJad4", { diff --git a/src/lib/auth/types.ts b/src/lib/auth/types.ts new file mode 100644 index 00000000..bad520a0 --- /dev/null +++ b/src/lib/auth/types.ts @@ -0,0 +1,7 @@ +export enum TokenType { + Refresh, + Access, + Reset +} + +export type TokenData = { uid: string, typ: TokenType, exp: number } \ No newline at end of file diff --git a/src/modules/KundendatenModule.svelte b/src/modules/KundendatenModule.svelte index 9b8597cf..f5fc546e 100644 --- a/src/modules/KundendatenModule.svelte +++ b/src/modules/KundendatenModule.svelte @@ -2,7 +2,6 @@ import PerformanceScore from "#components/Ausweis/PerformanceScore.svelte"; import ProgressBar from "#components/Ausweis/Progressbar.svelte"; import Pruefung from "#components/Ausweis/Pruefung.svelte"; - import ButtonZurueckSpeichernKaufabschluss from "#components/Ausweis/ButtonZurueckSpeichernKaufabschluss.svelte"; import type { Bezahlmethoden } from "@ibcornelsen/database/client"; import { Enums } from "@ibcornelsen/database/client"; @@ -10,21 +9,17 @@ import LoginDialog from "#components/LoginDialog.svelte"; import { PRICES } from "#lib/constants.js"; import { + AufnahmeClient, BenutzerClient, + ObjektClient, VerbrauchsausweisWohnenClient, } from "#components/Ausweis/types.js"; import { validateAccessTokenClient } from "src/client/lib/validateAccessToken.js"; - import { client } from "src/trpc.js"; export let user: BenutzerClient; export let ausweis: VerbrauchsausweisWohnenClient; - - export let bereich; - export let title; - export let bullets; - - let aufnahme = ausweis.aufnahme || {}; - let gebaeude = ausweis.aufnahme?.objekt || {}; + export let aufnahme: AufnahmeClient; + export let objekt: ObjektClient; let services = [ { @@ -58,8 +53,8 @@ let prices: number[] = []; - if (ausweis.aufnahme.ausweisart) { - prices = PRICES[ausweis.aufnahme.ausweisart]; + if (aufnahme.ausweisart) { + prices = PRICES[aufnahme.ausweisart]; } let basePrice: number = prices[0]; @@ -71,7 +66,7 @@ 0 ); - async function speichern(e: MouseEvent) { + async function speichern(e: SubmitEvent) { e.preventDefault(); // Um einen Ausweis zu speichern müssen wir eingeloggt sein, andernfalls wird die API den call ablehnen. @@ -102,8 +97,8 @@
@@ -301,7 +296,7 @@
- + diff --git a/src/modules/VerbrauchsausweisWohnen/AusweisWeiter.svelte b/src/modules/VerbrauchsausweisWohnen/AusweisWeiter.svelte index 24bfcd21..3945e5f9 100644 --- a/src/modules/VerbrauchsausweisWohnen/AusweisWeiter.svelte +++ b/src/modules/VerbrauchsausweisWohnen/AusweisWeiter.svelte @@ -9,7 +9,7 @@ import { api } from "astro-typesafe-api/client" export let objekt: ObjektClient; - export let images: UploadedGebaeudeBild[]; + export let bilder: UploadedGebaeudeBild[]; export let ausweis: VerbrauchsausweisWohnenClient; export let user: BenutzerClient; export let aufnahme: AufnahmeClient; @@ -31,7 +31,7 @@ const response = await verbrauchsausweisWohnenSpeichern(ausweis, objekt, aufnahme, - images) + bilder) if (response !== null) { // Falls der Nutzer zurück navigiert, sollte er wieder auf seinen Vorgang kommen. @@ -43,6 +43,8 @@ return true } } catch (e: any) { + console.log(e); + await api.ticket.PUT.fetch({ titel: "Ausweis konnte nicht gespeichert werden", beschreibung: e.stack, diff --git a/src/modules/VerbrauchsausweisWohnen/VerbrauchsausweisWohnenModule.svelte b/src/modules/VerbrauchsausweisWohnen/VerbrauchsausweisWohnenModule.svelte index bcb77a4a..2990e24a 100644 --- a/src/modules/VerbrauchsausweisWohnen/VerbrauchsausweisWohnenModule.svelte +++ b/src/modules/VerbrauchsausweisWohnen/VerbrauchsausweisWohnenModule.svelte @@ -220,7 +220,7 @@ const ausweisart: Enums.Ausweisart = "VerbrauchsausweisWohnen" @@ -296,10 +296,10 @@ const ausweisart: Enums.Ausweisart = "VerbrauchsausweisWohnen" diff --git a/src/pages/api/aufnahme/[uid].ts b/src/pages/api/aufnahme/[uid].ts index 8569a9ad..e22bb302 100644 --- a/src/pages/api/aufnahme/[uid].ts +++ b/src/pages/api/aufnahme/[uid].ts @@ -1,4 +1,4 @@ -import { AufnahmeClient, ZodOverlap } from "#components/Ausweis/types.js"; +import { AufnahmeClient, OptionalNullable, ZodOverlap } from "#components/Ausweis/types.js"; import { exclude } from "#lib/exclude.js"; import { authorizationMiddleware } from "#lib/middleware/authorization.js"; import { AufnahmeSchema, prisma } from "@ibcornelsen/database/server"; @@ -57,7 +57,7 @@ export const GET = defineApiRoute({ } } }, - output: ZodOverlap(AufnahmeSchema.omit({ + output: ZodOverlap>(AufnahmeSchema.omit({ id: true, objekt_id: true, benutzer_id: true diff --git a/src/pages/api/aufnahme/index.ts b/src/pages/api/aufnahme/index.ts index 50b24f8f..000c90f8 100644 --- a/src/pages/api/aufnahme/index.ts +++ b/src/pages/api/aufnahme/index.ts @@ -11,7 +11,7 @@ export const PUT = defineApiRoute({ benutzer_id: true, objekt_id: true, }).merge(z.object({ - baujahr_klima: z.array(z.number().int().positive()).optional() + baujahr_klima: z.array(z.number().int().positive()).nullish() })), uid_objekt: z.string().uuid() }), diff --git a/src/pages/api/auth/access-token.ts b/src/pages/api/auth/access-token.ts index 8c687c70..a7313a77 100644 --- a/src/pages/api/auth/access-token.ts +++ b/src/pages/api/auth/access-token.ts @@ -1,9 +1,10 @@ import { z } from "zod"; import moment from "moment"; import { prisma } from "@ibcornelsen/database/server"; -import { TokenType, encodeToken } from "../../../lib/auth/token.js"; +import { encodeToken } from "../../../lib/auth/token.js"; import { TRPCError } from "@trpc/server"; import { defineApiRoute } from "astro-typesafe-api/server"; +import { TokenType } from "#lib/auth/types.js"; export const GET = defineApiRoute({ meta: { diff --git a/src/pages/api/auth/refresh-token.ts b/src/pages/api/auth/refresh-token.ts index 0e240e45..7616f46f 100644 --- a/src/pages/api/auth/refresh-token.ts +++ b/src/pages/api/auth/refresh-token.ts @@ -1,9 +1,10 @@ import { z } from "zod"; import moment from "moment"; import { prisma } from "@ibcornelsen/database/server"; -import { TokenType, encodeToken } from "../../../lib/auth/token.js"; -import { hashPassword, validatePassword } from "../../../lib/password.js"; +import { encodeToken } from "../../../lib/auth/token.js"; +import { validatePassword } from "../../../lib/password.js"; import { APIError, defineApiRoute } from "astro-typesafe-api/server"; +import { TokenType } from "#lib/auth/types.js"; export const GET = defineApiRoute({ meta: { diff --git a/src/pages/api/objekt/[uid]/bilder.ts b/src/pages/api/objekt/[uid]/bilder.ts index ddbd1923..2f478df3 100644 --- a/src/pages/api/objekt/[uid]/bilder.ts +++ b/src/pages/api/objekt/[uid]/bilder.ts @@ -1,8 +1,10 @@ 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 { z } from "zod"; import isBase64 from "is-base64"; +import { fileURLToPath } from "url"; +import { writeFileSync } from "fs"; export const PUT = defineApiRoute({ input: GebaeudeBilderSchema.pick({ @@ -20,7 +22,7 @@ export const PUT = defineApiRoute({ if (!isBase64(base64, { mimeRequired: true })) { throw new APIError({ code: "BAD_REQUEST", - message: "Das Bild ist nicht base64 kodiert.", + message: "Das Bild ist nicht base64.", }); } @@ -58,15 +60,17 @@ export const PUT = defineApiRoute({ }, }); - const filePath = `/persistent/images/${bild.uid}.webp`; + const filePath = fileURLToPath(new URL(`../../../../../persistent/images/${bild.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(); - await Bun.write(filePath, buffer) + writeFileSync(filePath, buffer) } catch(e) { + console.log(e); + // Bild wurde nicht gespeichert, wir löschen den Eintrag wieder await prisma.gebaeudeBilder.delete({ where: { @@ -97,7 +101,8 @@ export const GET = defineApiRoute({ const objekt = await prisma.objekt.findUnique({ where: { - uid + uid, + benutzer_id: user.id }, select: { benutzer_id: true, @@ -110,7 +115,7 @@ export const GET = defineApiRoute({ } }) - if (!objekt || objekt.benutzer_id !== user.id) { + if (!objekt) { throw new APIError({ code: "FORBIDDEN", message: "Objekt existiert nicht oder gehört einem anderen Benutzer." diff --git a/src/pages/api/objekt/[uid]/index.ts b/src/pages/api/objekt/[uid]/index.ts index 17b95c57..30097039 100644 --- a/src/pages/api/objekt/[uid]/index.ts +++ b/src/pages/api/objekt/[uid]/index.ts @@ -1,4 +1,4 @@ -import { ObjektClient, ZodOverlap } from "#components/Ausweis/types.js"; +import { ObjektClient, OptionalNullable, ZodOverlap } from "#components/Ausweis/types.js"; import { exclude } from "#lib/exclude.js"; import { authorizationMiddleware } from "#lib/middleware/authorization.js"; import { ObjektSchema, prisma } from "@ibcornelsen/database/server"; @@ -59,7 +59,7 @@ export const GET = defineApiRoute({ } } }, - output: ZodOverlap(ObjektSchema.omit({ + output: ZodOverlap>(ObjektSchema.omit({ benutzer_id: true, id: true })), diff --git a/src/pages/api/verbrauchsausweis-wohnen/[uid].ts b/src/pages/api/verbrauchsausweis-wohnen/[uid].ts index 2aacf4ae..388349a6 100644 --- a/src/pages/api/verbrauchsausweis-wohnen/[uid].ts +++ b/src/pages/api/verbrauchsausweis-wohnen/[uid].ts @@ -1,4 +1,4 @@ -import { VerbrauchsausweisWohnenClient, ZodOverlap } from "#components/Ausweis/types.js"; +import { OptionalNullable, VerbrauchsausweisWohnenClient, ZodOverlap } from "#components/Ausweis/types.js"; import { exclude } from "#lib/exclude.js"; import { authorizationMiddleware } from "#lib/middleware/authorization.js"; import { prisma, VerbrauchsausweisWohnenSchema } from "@ibcornelsen/database/server"; @@ -60,7 +60,7 @@ export const GET = defineApiRoute({ } } }, - output: ZodOverlap(VerbrauchsausweisWohnenSchema.merge(z.object({ + output: ZodOverlap>(VerbrauchsausweisWohnenSchema.merge(z.object({ uid_aufnahme: z.string().uuid(), uid_objekt: z.string().uuid(), uid_benutzer: z.string().uuid().optional() @@ -73,8 +73,12 @@ export const GET = defineApiRoute({ async fetch(input, context, user) { const { uid } = context.params; - console.log(uid); - + if (!uid) { + throw new APIError({ + code: "BAD_REQUEST", + message: "Missing uid in request params" + }) + } const ausweis = await prisma.verbrauchsausweisWohnen.findUnique({ where: { diff --git a/src/pages/kundendaten.astro b/src/pages/kundendaten.astro index 5e4f140d..50cbaf4c 100644 --- a/src/pages/kundendaten.astro +++ b/src/pages/kundendaten.astro @@ -3,30 +3,65 @@ import KundendatenModule from "#modules/KundendatenModule.svelte"; import AusweisLayout from "#layouts/AusweisLayoutPruefung.astro"; import { Enums } from "@ibcornelsen/database/client"; -import { createCaller } from "#lib/caller"; +import { createCaller } from "../astro-typesafe-api-caller"; +import { API_ACCESS_TOKEN_COOKIE_NAME, API_REFRESH_TOKEN_COOKIE_NAME } from "#lib/constants"; +import { validateAccessTokenServer } from "#server/lib/validateAccessToken"; // Man sollte nur auf diese Seite kommen, wenn ein Ausweis bereits vorliegt und in der Datenbank abgespeichert wurde. const uid = Astro.url.searchParams.get("uid"); +const valid = await validateAccessTokenServer(Astro) -if (!uid) { +if (!uid || !valid) { return Astro.redirect("/404"); } -const caller = createCaller(Astro); -const ausweis = await caller.v1.verbrauchsausweisWohnen.get({ - uid +const caller = createCaller(Astro) + +const ausweis = await caller["verbrauchsausweis-wohnen"]._uid.GET.fetch(undefined, { + headers: { + Authorization: `Bearer ${Astro.cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)?.value}` + }, + params: { + uid + } }) -const user = await caller.v1.benutzer.self(); +const aufnahme = await caller.aufnahme._uid.GET.fetch(undefined, { + headers: { + Authorization: `Bearer ${Astro.cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)?.value}` + }, + params: { + uid: ausweis.uid_aufnahme + } +}) + +const objekt = await caller.objekt._uid.GET.fetch(undefined, { + headers: { + Authorization: `Bearer ${Astro.cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)?.value}` + }, + params: { + uid: aufnahme.uid_objekt + } +}) + +const user = await caller.user.self.GET.fetch(undefined, { + headers: { + Authorization: `Bearer ${Astro.cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)?.value}` + } +}); + +aufnahme.ausweisart = "VerbrauchsausweisWohnen" -if (!ausweis) { + + +if (!ausweis || !user) { return Astro.redirect("/404"); } --- - + diff --git a/src/server/lib/validateAccessToken.ts b/src/server/lib/validateAccessToken.ts index 0c06608f..408e45a8 100644 --- a/src/server/lib/validateAccessToken.ts +++ b/src/server/lib/validateAccessToken.ts @@ -3,7 +3,7 @@ import type { AstroGlobal } from "astro"; import moment from "moment"; import { createCaller } from "../../astro-typesafe-api-caller.js" import jwt from "jsonwebtoken" -import { TokenData, TokenType } from "#lib/auth/token.js"; +import { TokenData, TokenType } from "#lib/auth/types.js"; export async function validateAccessTokenServer(astro: AstroGlobal) { const accessToken = astro.cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)?.value;