Notification endpoints

This commit is contained in:
Hendrik Belitz
2026-01-03 14:06:47 +01:00
parent ccb1868d86
commit af9c43a3f6
3 changed files with 635 additions and 0 deletions
@@ -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();
}
};
@@ -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();
}
};
@@ -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();
}
};