From af9c43a3f65fa2f71f7664eb6bf7f60840e4c11a Mon Sep 17 00:00:00 2001 From: Hendrik Belitz Date: Sat, 3 Jan 2026 14:06:47 +0100 Subject: [PATCH] Notification endpoints --- .../api/tenants/[id]/notifications/+server.ts | 260 ++++++++++++++++++ .../notifications/[notificationId]/+server.ts | 254 +++++++++++++++++ .../[id]/notifications/unread/+server.ts | 121 ++++++++ 3 files changed, 635 insertions(+) create mode 100644 src/routes/api/tenants/[id]/notifications/+server.ts create mode 100644 src/routes/api/tenants/[id]/notifications/[notificationId]/+server.ts create mode 100644 src/routes/api/tenants/[id]/notifications/unread/+server.ts diff --git a/src/routes/api/tenants/[id]/notifications/+server.ts b/src/routes/api/tenants/[id]/notifications/+server.ts new file mode 100644 index 0000000..ddaa4e9 --- /dev/null +++ b/src/routes/api/tenants/[id]/notifications/+server.ts @@ -0,0 +1,260 @@ +import { json } from "@sveltejs/kit"; +import { NotificationService } from "$lib/server/services/notification-service"; +import { ValidationError, logError, BackendError, InternalError } 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"; +import { ERRORS } from "$lib/errors"; + +// Register OpenAPI documentation for GET +registerOpenAPIRoute("/tenants/{id}/notifications", "GET", { + summary: "List all notifications for current staff member", + description: + "Retrieves all notifications for the authenticated staff member. Only accessible by staff and tenant admins.", + tags: ["Notifications"], + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + description: "Tenant ID", + }, + ], + responses: { + "200": { + description: "Notifications retrieved successfully", + content: { + "application/json": { + schema: { + type: "object", + properties: { + notifications: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "string", format: "uuid", description: "Notification ID" }, + staffId: { type: "string", format: "uuid", description: "Staff member ID" }, + title: { + type: "object", + description: "Notification titles", + }, + description: { + type: "object", + description: "Notification descriptions", + }, + isRead: { type: "boolean", description: "Whether notification has been read" }, + }, + required: ["id", "staffId", "title", "description", "isRead"], + }, + }, + }, + required: ["notifications"], + }, + }, + }, + }, + "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: "Tenant 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}/notifications", "DELETE", { + summary: "Delete all notifications for current staff member", + description: + "Deletes all notifications for the authenticated staff member. Optionally delete only read notifications. Only accessible by staff and tenant admins.", + tags: ["Notifications"], + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + description: "Tenant ID", + }, + { + name: "readOnly", + in: "query", + required: false, + schema: { type: "boolean" }, + description: "If true, only delete read notifications", + }, + ], + responses: { + "200": { + description: "Notifications deleted successfully", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { type: "string", description: "Success message" }, + deletedCount: { type: "number", description: "Number of deleted notifications" }, + }, + required: ["message", "deletedCount"], + }, + }, + }, + }, + "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: "Tenant not found", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + "500": { + description: "Internal server error", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + }, +}); + +export const GET: RequestHandler = async ({ params, locals }) => { + const log = logger.setContext("API"); + + try { + const tenantId = params.id; + + // Check if user is authenticated + if (!tenantId) { + throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID); + } + + checkPermission(locals, tenantId); + + if (!locals.user?.id) { + throw new ValidationError("User ID not found"); + } + + log.debug("Getting all notifications", { + tenantId, + staffId: locals.user.id, + }); + + const notificationService = await NotificationService.forTenant(tenantId); + const notifications = await notificationService.getNotificationsForStaff(locals.user.id); + + log.debug("Notifications retrieved successfully", { + tenantId, + staffId: locals.user.id, + count: notifications.length, + }); + + return json({ + notifications, + }); + } catch (error) { + logError(log)("Error getting notifications:", error, locals.user?.id, params.id); + + if (error instanceof BackendError) { + return error.toJson(); + } + + return new InternalError().toJson(); + } +}; + +export const DELETE: RequestHandler = async ({ params, url, locals }) => { + const log = logger.setContext("API"); + + try { + const tenantId = params.id; + + // Check if user is authenticated + if (!tenantId) { + throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID); + } + + checkPermission(locals, tenantId); + + if (!locals.user?.id) { + throw new ValidationError("User ID not found"); + } + + const readOnly = url.searchParams.get("readOnly") === "true"; + + log.debug("Deleting all notifications", { + tenantId, + staffId: locals.user.id, + readOnly, + }); + + const notificationService = await NotificationService.forTenant(tenantId); + const deletedCount = await notificationService.deleteAllNotifications(locals.user.id, readOnly); + + log.info("Notifications deleted successfully", { + tenantId, + staffId: locals.user.id, + deletedCount, + readOnly, + }); + + return json({ + message: "Notifications deleted successfully", + deletedCount, + }); + } catch (error) { + logError(log)("Error deleting notifications:", error, locals.user?.id, params.id); + + if (error instanceof BackendError) { + return error.toJson(); + } + + return new InternalError().toJson(); + } +}; diff --git a/src/routes/api/tenants/[id]/notifications/[notificationId]/+server.ts b/src/routes/api/tenants/[id]/notifications/[notificationId]/+server.ts new file mode 100644 index 0000000..93678e1 --- /dev/null +++ b/src/routes/api/tenants/[id]/notifications/[notificationId]/+server.ts @@ -0,0 +1,254 @@ +import { json } from "@sveltejs/kit"; +import { NotificationService } from "$lib/server/services/notification-service"; +import { ValidationError, logError, BackendError, InternalError } 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"; +import { ERRORS } from "$lib/errors"; + +// Register OpenAPI documentation for DELETE +registerOpenAPIRoute("/tenants/{id}/notifications/{notificationId}", "DELETE", { + summary: "Delete a specific notification", + description: + "Deletes a specific notification belonging to the authenticated staff member. Only accessible by staff and tenant admins.", + tags: ["Notifications"], + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + description: "Tenant ID", + }, + { + name: "notificationId", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + description: "Notification ID", + }, + ], + responses: { + "200": { + description: "Notification deleted successfully", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { type: "string", description: "Success message" }, + }, + required: ["message"], + }, + }, + }, + }, + "401": { + description: "Authentication required", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + "403": { + description: "Insufficient permissions or notification belongs to another user", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + "404": { + description: "Notification or tenant 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 PUT +registerOpenAPIRoute("/tenants/{id}/notifications/{notificationId}", "PUT", { + summary: "Mark a notification as read", + description: + "Marks a specific notification as read for the authenticated staff member. Only accessible by staff and tenant admins.", + tags: ["Notifications"], + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + description: "Tenant ID", + }, + { + name: "notificationId", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + description: "Notification ID", + }, + ], + responses: { + "200": { + description: "Notification marked as read successfully", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { type: "string", description: "Success message" }, + }, + required: ["message"], + }, + }, + }, + }, + "401": { + description: "Authentication required", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + "403": { + description: "Insufficient permissions or notification belongs to another user", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + "404": { + description: "Notification or tenant not found", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + "500": { + description: "Internal server error", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + }, +}); + +export const DELETE: RequestHandler = async ({ params, locals }) => { + const log = logger.setContext("API"); + + try { + const tenantId = params.id; + const notificationId = params.notificationId; + + // Check if user is authenticated + if (!tenantId) { + throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID); + } + + if (!notificationId) { + throw new ValidationError("Notification ID is required"); + } + + checkPermission(locals, tenantId); + + if (!locals.user?.id) { + throw new ValidationError("User ID not found"); + } + + log.debug("Deleting notification", { + tenantId, + staffId: locals.user.id, + notificationId, + }); + + const notificationService = await NotificationService.forTenant(tenantId); + await notificationService.deleteNotification(notificationId, locals.user.id); + + log.info("Notification deleted successfully", { + tenantId, + staffId: locals.user.id, + notificationId, + }); + + return json({ + message: "Notification deleted successfully", + }); + } catch (error) { + logError(log)("Error deleting notification:", error, locals.user?.id, params.id); + + if (error instanceof BackendError) { + return error.toJson(); + } + + return new InternalError().toJson(); + } +}; + +export const PUT: RequestHandler = async ({ params, locals }) => { + const log = logger.setContext("API"); + + try { + const tenantId = params.id; + const notificationId = params.notificationId; + + // Check if user is authenticated + if (!tenantId) { + throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID); + } + + if (!notificationId) { + throw new ValidationError("Notification ID is required"); + } + + checkPermission(locals, tenantId); + + if (!locals.user?.id) { + throw new ValidationError("User ID not found"); + } + + log.debug("Marking notification as read", { + tenantId, + staffId: locals.user.id, + notificationId, + }); + + const notificationService = await NotificationService.forTenant(tenantId); + await notificationService.markAsRead(notificationId, locals.user.id); + + log.info("Notification marked as read successfully", { + tenantId, + staffId: locals.user.id, + notificationId, + }); + + return json({ + message: "Notification marked as read successfully", + }); + } catch (error) { + logError(log)("Error marking notification as read:", error, locals.user?.id, params.id); + + if (error instanceof BackendError) { + return error.toJson(); + } + + return new InternalError().toJson(); + } +}; diff --git a/src/routes/api/tenants/[id]/notifications/unread/+server.ts b/src/routes/api/tenants/[id]/notifications/unread/+server.ts new file mode 100644 index 0000000..a697b30 --- /dev/null +++ b/src/routes/api/tenants/[id]/notifications/unread/+server.ts @@ -0,0 +1,121 @@ +import { json } from "@sveltejs/kit"; +import { NotificationService } from "$lib/server/services/notification-service"; +import { ValidationError, logError, BackendError, InternalError } 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"; +import { ERRORS } from "$lib/errors"; + +// Register OpenAPI documentation for GET +registerOpenAPIRoute("/tenants/{id}/notifications/unread", "GET", { + summary: "Check if staff member has unread notifications", + description: + "Checks whether the authenticated staff member has any unread notifications. Only accessible by staff and tenant admins.", + tags: ["Notifications"], + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + description: "Tenant ID", + }, + ], + responses: { + "200": { + description: "Unread status retrieved successfully", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hasUnread: { + type: "boolean", + description: "Whether the staff member has unread notifications", + }, + }, + required: ["hasUnread"], + }, + }, + }, + }, + "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: "Tenant not found", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + "500": { + description: "Internal server error", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + }, +}); + +export const GET: RequestHandler = async ({ params, locals }) => { + const log = logger.setContext("API"); + + try { + const tenantId = params.id; + + // Check if user is authenticated + if (!tenantId) { + throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID); + } + + checkPermission(locals, tenantId); + + if (!locals.user?.id) { + throw new ValidationError("User ID not found"); + } + + log.debug("Checking for unread notifications", { + tenantId, + staffId: locals.user.id, + }); + + const notificationService = await NotificationService.forTenant(tenantId); + const hasUnread = await notificationService.hasUnreadNotifications(locals.user.id); + + log.debug("Unread notification status retrieved", { + tenantId, + staffId: locals.user.id, + hasUnread, + }); + + return json({ + hasUnread, + }); + } catch (error) { + logError(log)("Error checking unread notifications:", error, locals.user?.id, params.id); + + if (error instanceof BackendError) { + return error.toJson(); + } + + return new InternalError().toJson(); + } +};