From 16474d96c591e246a103b9d6aa15ca30d436d11b Mon Sep 17 00:00:00 2001 From: Karl Ludwig Weise Date: Sun, 31 May 2026 21:43:51 +0200 Subject: [PATCH] Fix notifications to show appointment data; reverting comming a65c00bb8c6fb9634e5dee251422fb01972bcde4 and adding permission check --- .../appointments/[appointmentId]/+server.ts | 179 +++++++++++++++++- .../appointments/add-to-tunnel/+server.ts | 4 +- 2 files changed, 181 insertions(+), 2 deletions(-) diff --git a/src/routes/api/tenants/[id]/appointments/[appointmentId]/+server.ts b/src/routes/api/tenants/[id]/appointments/[appointmentId]/+server.ts index db25a0a..0cde0d9 100644 --- a/src/routes/api/tenants/[id]/appointments/[appointmentId]/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/[appointmentId]/+server.ts @@ -1,11 +1,142 @@ import { json } from "@sveltejs/kit"; import { AppointmentService } from "$lib/server/services/appointment-service"; -import { BackendError, InternalError, logError, ValidationError } from "$lib/server/utils/errors"; +import { + BackendError, + InternalError, + logError, + NotFoundError, + ValidationError, +} from "$lib/server/utils/errors"; import type { RequestHandler } from "@sveltejs/kit"; import { registerOpenAPIRoute } from "$lib/server/openapi"; import logger from "$lib/logger"; import { checkPermission } from "$lib/server/utils/permissions"; +// Register OpenAPI documentation for GET +registerOpenAPIRoute("/tenants/{id}/appointments/{appointmentId}", "GET", { + summary: "Get appointment by ID", + description: + "Retrieves a specific appointment by its ID. Accessible to dashboard users. Required for notification previews.", + tags: ["Appointments"], + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + description: "Tenant ID", + }, + { + name: "appointmentId", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + description: "Appointment ID", + }, + ], + responses: { + "200": { + description: "Appointment retrieved successfully", + content: { + "application/json": { + schema: { + type: "object", + properties: { + appointment: { + type: "object", + properties: { + id: { type: "string", format: "uuid", description: "Appointment ID" }, + tunnelId: { type: "string", format: "uuid", description: "Client tunnel ID" }, + channelId: { type: "string", format: "uuid", description: "Channel ID" }, + appointmentDate: { + type: "string", + format: "date-time", + description: "Appointment date and time", + }, + expiryDate: { + type: "string", + format: "date", + description: "Data expiry date (nullable)", + }, + status: { + type: "string", + enum: ["NEW", "CONFIRMED", "HELD", "REJECTED", "NO_SHOW"], + description: "Appointment status", + }, + encryptedPayload: { + type: "string", + description: "Encrypted appointment data (nullable)", + }, + iv: { + type: "string", + description: "Initialization vector for encryption (nullable)", + }, + authTag: { + type: "string", + description: "Authentication tag for encryption (nullable)", + }, + createdAt: { + type: "string", + format: "date-time", + description: "Creation timestamp (nullable)", + }, + updatedAt: { + type: "string", + format: "date-time", + description: "Last update timestamp (nullable)", + }, + }, + required: ["id", "tunnelId", "channelId", "appointmentDate", "status"], + }, + }, + required: ["appointment"], + }, + }, + }, + }, + "400": { + description: "Invalid input data", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + "401": { + description: "Authentication required", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + "403": { + description: "Insufficient permissions", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + "404": { + description: "Appointment not found", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + "500": { + description: "Internal server error", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + }, +}); + // Register OpenAPI documentation for DELETE registerOpenAPIRoute("/tenants/{id}/appointments/{appointmentId}", "DELETE", { summary: "Delete appointment", @@ -85,6 +216,52 @@ registerOpenAPIRoute("/tenants/{id}/appointments/{appointmentId}", "DELETE", { }, }); +export const GET: RequestHandler = async ({ params, locals }) => { + const log = logger.setContext("API"); + + try { + const tenantId = params.id; + const appointmentId = params.appointmentId; + + if (!tenantId || !appointmentId) { + throw new ValidationError("Tenant ID and appointment ID are required"); + } + + checkPermission(locals, tenantId, true); + + log.debug("Getting appointment by ID", { + tenantId, + appointmentId, + requestedBy: locals.user?.id, + }); + + const appointmentService = await AppointmentService.forTenant(tenantId); + const appointment = await appointmentService.getAppointmentById(appointmentId); + + if (!appointment) { + throw new NotFoundError("Appointment not found"); + } + + log.debug("Appointment retrieved successfully", { + tenantId, + appointmentId, + requestedBy: locals.user?.id, + }); + + return json({ + appointment, + }); + } catch (error) { + logError(log)("Error getting appointment", error, locals.user?.id, params.id); + + if (error instanceof BackendError) { + return error.toJson(); + } + + return new InternalError().toJson(); + } +}; + export const DELETE: RequestHandler = async ({ params, locals }) => { const log = logger.setContext("API"); diff --git a/src/routes/api/tenants/[id]/appointments/add-to-tunnel/+server.ts b/src/routes/api/tenants/[id]/appointments/add-to-tunnel/+server.ts index 1249503..b4536ff 100644 --- a/src/routes/api/tenants/[id]/appointments/add-to-tunnel/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/add-to-tunnel/+server.ts @@ -381,7 +381,9 @@ export const POST: RequestHandler = async ({ request, params }) => { (await notificationService).createNotification({ type: "APPOINTMENT_REQUESTED", channelId: validatedData.channelId, - metaData: { appointmentId: result.id }, + metaData: { + appointmentId: result.id, + }, }); } if (validatedData.clientEmail) {