Refactor/error and api handling (#82)

* DB migrations

* Format errors in migrations

* Implemented new error and permission handling.

* Updated and fixed tests

* Update src/routes/api/tenants/[id]/appointments/[appointmentId]/cancel/+server.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/routes/api/tenants/[id]/agents/[agentId]/absences/[absenceId]/+server.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/routes/api/tenants/[id]/channels/+server.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fixed test

* Added error constants

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Hendrik
2025-09-18 15:04:06 +02:00
committed by GitHub
co-authored by Copilot
parent ae0a54a4c4
commit e486072ffa
42 changed files with 1747 additions and 574 deletions
+28
View File
@@ -0,0 +1,28 @@
CREATE TYPE "public"."setup_state" AS ENUM('NEW', 'SETTINGS_CREATED', 'AGENTS_SET_UP', 'FIRST_CHANNEL_CREATED');--> statement-breakpoint
CREATE TABLE "user_invite" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"invite_code" uuid DEFAULT gen_random_uuid() NOT NULL,
"email" text NOT NULL,
"name" text NOT NULL,
"role" "user_role" NOT NULL,
"tenant_id" uuid NOT NULL,
"invited_by" uuid NOT NULL,
"language" text DEFAULT 'de' NOT NULL,
"used" boolean DEFAULT false NOT NULL,
"used_at" timestamp,
"created_user_id" uuid,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now(),
"expires_at" timestamp NOT NULL,
CONSTRAINT "user_invite_invite_code_unique" UNIQUE("invite_code")
);
--> statement-breakpoint
ALTER TABLE "user" ALTER COLUMN "role" SET DEFAULT 'STAFF';--> statement-breakpoint
ALTER TABLE "tenant" ADD COLUMN "setup_state" "setup_state" DEFAULT 'NEW' NOT NULL;--> statement-breakpoint
ALTER TABLE "user" ADD COLUMN "language" text DEFAULT 'de' NOT NULL;--> statement-breakpoint
ALTER TABLE "user_invite" ADD CONSTRAINT "user_invite_tenant_id_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenant"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_invite" ADD CONSTRAINT "user_invite_invited_by_user_id_fk" FOREIGN KEY ("invited_by") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_invite" ADD CONSTRAINT "user_invite_created_user_id_user_id_fk" FOREIGN KEY ("created_user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "user_invite_code_idx" ON "user_invite" USING btree ("invite_code");--> statement-breakpoint
CREATE INDEX "user_invite_email_idx" ON "user_invite" USING btree ("email");--> statement-breakpoint
CREATE INDEX "user_invite_tenant_idx" ON "user_invite" USING btree ("tenant_id");
+767
View File
@@ -0,0 +1,767 @@
{
"id": "54aa77f5-cee4-419c-8c48-0d0b4b5194bc",
"prevId": "bdfafee4-65d2-4381-b4e7-df8c9337d0c1",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.tenant": {
"name": "tenant",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"short_name": {
"name": "short_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"long_name": {
"name": "long_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"logo": {
"name": "logo",
"type": "bytea",
"primaryKey": false,
"notNull": false
},
"database_url": {
"name": "database_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"setup_state": {
"name": "setup_state",
"type": "setup_state",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'NEW'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {
"tenant_database_url_idx": {
"name": "tenant_database_url_idx",
"columns": [
{
"expression": "database_url",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"tenant_short_name_unique": {
"name": "tenant_short_name_unique",
"nullsNotDistinct": false,
"columns": ["short_name"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.tenant_config": {
"name": "tenant_config",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"tenant_id": {
"name": "tenant_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "config_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {
"tenant_config_tenant_name_idx": {
"name": "tenant_config_tenant_name_idx",
"columns": [
{
"expression": "tenant_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "name",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"tenant_config_tenant_id_tenant_id_fk": {
"name": "tenant_config_tenant_id_tenant_id_fk",
"tableFrom": "tenant_config",
"tableTo": "tenant",
"columnsFrom": ["tenant_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "user_role",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'STAFF'"
},
"tenant_id": {
"name": "tenant_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"last_login_at": {
"name": "last_login_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"is_active": {
"name": "is_active",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"confirmed": {
"name": "confirmed",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"token_valid_until": {
"name": "token_valid_until",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"passphrase_hash": {
"name": "passphrase_hash",
"type": "text",
"primaryKey": false,
"notNull": false
},
"recovery_passphrase": {
"name": "recovery_passphrase",
"type": "text",
"primaryKey": false,
"notNull": false
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'de'"
}
},
"indexes": {
"user_email_idx": {
"name": "user_email_idx",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_tenant_id_tenant_id_fk": {
"name": "user_tenant_id_tenant_id_fk",
"tableFrom": "user",
"tableTo": "tenant",
"columnsFrom": ["tenant_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user_invite": {
"name": "user_invite",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"invite_code": {
"name": "invite_code",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "user_role",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"tenant_id": {
"name": "tenant_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"invited_by": {
"name": "invited_by",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'de'"
},
"used": {
"name": "used",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"used_at": {
"name": "used_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_user_id": {
"name": "created_user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"user_invite_code_idx": {
"name": "user_invite_code_idx",
"columns": [
{
"expression": "invite_code",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"user_invite_email_idx": {
"name": "user_invite_email_idx",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"user_invite_tenant_idx": {
"name": "user_invite_tenant_idx",
"columns": [
{
"expression": "tenant_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_invite_tenant_id_tenant_id_fk": {
"name": "user_invite_tenant_id_tenant_id_fk",
"tableFrom": "user_invite",
"tableTo": "tenant",
"columnsFrom": ["tenant_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"user_invite_invited_by_user_id_fk": {
"name": "user_invite_invited_by_user_id_fk",
"tableFrom": "user_invite",
"tableTo": "user",
"columnsFrom": ["invited_by"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"user_invite_created_user_id_user_id_fk": {
"name": "user_invite_created_user_id_user_id_fk",
"tableFrom": "user_invite",
"tableTo": "user",
"columnsFrom": ["created_user_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_invite_invite_code_unique": {
"name": "user_invite_invite_code_unique",
"nullsNotDistinct": false,
"columns": ["invite_code"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user_passkey": {
"name": "user_passkey",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"public_key": {
"name": "public_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"counter": {
"name": "counter",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"device_name": {
"name": "device_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"last_used_at": {
"name": "last_used_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_passkey_user_idx": {
"name": "user_passkey_user_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_passkey_user_id_user_id_fk": {
"name": "user_passkey_user_id_user_id_fk",
"tableFrom": "user_passkey",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user_session": {
"name": "user_session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"session_token": {
"name": "session_token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"last_used_at": {
"name": "last_used_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {
"user_session_user_idx": {
"name": "user_session_user_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"user_session_token_idx": {
"name": "user_session_token_idx",
"columns": [
{
"expression": "session_token",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_session_user_id_user_id_fk": {
"name": "user_session_user_id_user_id_fk",
"tableFrom": "user_session",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_session_session_token_unique": {
"name": "user_session_session_token_unique",
"nullsNotDistinct": false,
"columns": ["session_token"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.config_type": {
"name": "config_type",
"schema": "public",
"values": ["BOOLEAN", "NUMBER", "STRING"]
},
"public.setup_state": {
"name": "setup_state",
"schema": "public",
"values": ["NEW", "SETTINGS_CREATED", "AGENTS_SET_UP", "FIRST_CHANNEL_CREATED"]
},
"public.user_role": {
"name": "user_role",
"schema": "public",
"values": ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+7
View File
@@ -15,6 +15,13 @@
"when": 1752668982273,
"tag": "0001_equal_wonder_man",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1758021433890,
"tag": "0002_fat_pete_wisdom",
"breakpoints": true
}
]
}
+25
View File
@@ -1,8 +1,33 @@
export const ERRORS = {
BACKEND: {
OBFUSCATED: "Internal server error",
},
VALIDATION: {
INVALID_REQUEST_BODY: "Invalid request body",
},
SECURITY: {
AUTHENTICATION_REQUIRED: "Authentication required",
AUTHORIZATION_FAILED: "Insufficient permissions",
INVALID_PASSKEY_REGISTRATION:
"Invalid passkey registration. Please request a new challenge first.",
EITHER_PASSKEY_OR_PHRASE: "Either passkey or passphrase must be provided",
BOTH_PASSKEY_AND_PHRASE: "Cannot provide both passkey and passphrase",
},
TENANTS: {
NAME_EXISTS: "A tenant with this short name already exists",
NO_TENANT_ID: "No tenant id given",
NOT_FOUND: "Tenant not found",
MISSING_TENANT_OR_AGENT_ID: "Missing tenant or agent ID",
},
CHANNELS: {
NOT_FOUND: "Channel not found",
},
USERS: {
ADMIN_EXISTS: "System was already initialized",
EMAIL_EXISTS: "E-Mail Address already exists",
FAILED_TO_UPDATE: "Failed to update user data",
},
AGENTS: {
NOT_FOUND: "Agent not found",
},
};
+1 -1
View File
@@ -126,7 +126,7 @@ describe("UniversalLogger Integration - Client Error Forwarding", () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: "Internal Server Error",
statusText: "Internal server error",
});
const { createLogger } = await import("../index");
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect } from "vitest";
import { AuthorizationService } from "../authorization-service";
import { AuthenticationError } from "$lib/server/utils/errors";
import { AuthenticationError, AuthorizationError } from "$lib/server/utils/errors";
import type { JWTPayload } from "jose";
const mockGlobalAdmin: JWTPayload = {
@@ -47,7 +47,7 @@ describe("AuthorizationService", () => {
it("should deny access for incorrect role", () => {
expect(() => {
AuthorizationService.requireRole(mockTenantAdmin, "GLOBAL_ADMIN");
}).toThrow(AuthenticationError);
}).toThrow(AuthorizationError);
});
it("should deny access for null user", () => {
@@ -71,7 +71,7 @@ describe("AuthorizationService", () => {
it("should deny access for disallowed role", () => {
expect(() => {
AuthorizationService.requireAnyRole(mockStaff, ["GLOBAL_ADMIN", "TENANT_ADMIN"]);
}).toThrow(AuthenticationError);
}).toThrow(AuthorizationError);
});
});
@@ -91,7 +91,7 @@ describe("AuthorizationService", () => {
it("should deny tenant admin access to other tenants", () => {
expect(() => {
AuthorizationService.requireTenantAccess(mockTenantAdmin, "tenant-456");
}).toThrow(AuthenticationError);
}).toThrow(AuthorizationError);
});
it("should allow staff access to their own tenant", () => {
@@ -103,7 +103,7 @@ describe("AuthorizationService", () => {
it("should deny staff access to other tenants", () => {
expect(() => {
AuthorizationService.requireTenantAccess(mockStaff, "tenant-456");
}).toThrow(AuthenticationError);
}).toThrow(AuthorizationError);
});
});
@@ -117,7 +117,7 @@ describe("AuthorizationService", () => {
it("should deny non-global admin access", () => {
expect(() => {
AuthorizationService.requireGlobalAdmin(mockTenantAdmin);
}).toThrow(AuthenticationError);
}).toThrow(AuthorizationError);
});
});
@@ -137,7 +137,7 @@ describe("AuthorizationService", () => {
it("should deny staff access", () => {
expect(() => {
AuthorizationService.requireTenantAdmin(mockStaff);
}).toThrow(AuthenticationError);
}).toThrow(AuthorizationError);
});
it("should check tenant access for tenant admin", () => {
@@ -147,7 +147,7 @@ describe("AuthorizationService", () => {
expect(() => {
AuthorizationService.requireTenantAdmin(mockTenantAdmin, "tenant-456");
}).toThrow(AuthenticationError);
}).toThrow(AuthorizationError);
});
});
@@ -173,7 +173,7 @@ describe("AuthorizationService", () => {
expect(() => {
AuthorizationService.requireStaffOrAbove(mockStaff, "tenant-456");
}).toThrow(AuthenticationError);
}).toThrow(AuthorizationError);
});
});
+9 -9
View File
@@ -1,6 +1,6 @@
import type { JWTPayload } from "jose";
import { UniversalLogger } from "$lib/logger";
import { AuthenticationError } from "$lib/server/utils/errors";
import { AuthorizationError, AuthenticationError } from "$lib/server/utils/errors";
const logger = new UniversalLogger().setContext("Authorization");
@@ -9,14 +9,14 @@ export type UserRole = "GLOBAL_ADMIN" | "TENANT_ADMIN" | "STAFF";
export class AuthorizationService {
static requireRole(user: JWTPayload, requiredRole: UserRole): void {
if (!user) {
throw new AuthenticationError("Authentication required");
throw new AuthenticationError();
}
if (user.role !== requiredRole) {
logger.warn(
`Access denied: User ${user.email} has role ${user.role}, required ${requiredRole}`,
);
throw new AuthenticationError("Insufficient permissions");
throw new AuthorizationError();
}
logger.debug(`Authorization granted: User ${user.email} has required role ${requiredRole}`);
@@ -24,14 +24,14 @@ export class AuthorizationService {
static requireAnyRole(user: JWTPayload, allowedRoles: UserRole[]): void {
if (!user) {
throw new AuthenticationError("Authentication required");
throw new AuthenticationError();
}
if (!allowedRoles.includes(user.role as UserRole)) {
logger.warn(
`Access denied: User ${user.email} has role ${user.role}, allowed roles: ${allowedRoles.join(", ")}`,
);
throw new AuthenticationError("Insufficient permissions");
throw new AuthorizationError();
}
logger.debug(`Authorization granted: User ${user.email} has allowed role ${user.role}`);
@@ -39,7 +39,7 @@ export class AuthorizationService {
static requireTenantAccess(user: JWTPayload, tenantId: string): void {
if (!user) {
throw new AuthenticationError("Authentication required");
throw new AuthenticationError();
}
if (user.role === "GLOBAL_ADMIN") {
@@ -52,14 +52,14 @@ export class AuthorizationService {
if (user.role === "TENANT_ADMIN" || user.role === "STAFF") {
if (!user.tenantId) {
logger.warn(`Access denied: User ${user.email} has no tenant assigned`);
throw new AuthenticationError("No tenant access");
throw new AuthorizationError("No tenant access");
}
if (user.tenantId !== tenantId) {
logger.warn(
`Access denied: User ${user.email} trying to access tenant ${tenantId}, but belongs to ${user.tenantId}`,
);
throw new AuthenticationError("Tenant access denied");
throw new AuthorizationError("Tenant access denied");
}
logger.debug(`Authorization granted: User ${user.email} accessing own tenant ${tenantId}`);
@@ -67,7 +67,7 @@ export class AuthorizationService {
}
logger.warn(`Access denied: User ${user.email} has invalid role ${user.role}`);
throw new AuthenticationError("Invalid role");
throw new AuthorizationError("Invalid role");
}
static requireGlobalAdmin(user: JWTPayload): void {
+35 -3
View File
@@ -1,5 +1,17 @@
import { ERRORS } from "$lib/errors";
import type { UniversalLogger } from "$lib/logger";
import { json } from "@sveltejs/kit";
export const logError =
(logger: UniversalLogger) =>
(message: string, error: unknown, requestedBy?: string, tenantId?: string) => {
logger.error(message, {
tenantId,
requestedBy,
error: JSON.stringify(error || "?"),
});
};
export class BackendError extends Error {
constructor(
message: string,
@@ -8,13 +20,23 @@ export class BackendError extends Error {
super(message);
}
public toJson = () => json({ message: this.message }, { status: this.code });
public toJson = () => json({ error: this.message, message: this.message }, { status: this.code });
}
export class AuthorizationError extends BackendError {
constructor(
message: string = ERRORS.SECURITY.AUTHORIZATION_FAILED,
public code = 403,
) {
super(message, code);
this.name = "AuthorizationError";
}
}
export class AuthenticationError extends BackendError {
constructor(
message: string,
public code = 403,
message: string = ERRORS.SECURITY.AUTHENTICATION_REQUIRED,
public code = 401,
) {
super(message, code);
this.name = "AuthenticationError";
@@ -50,3 +72,13 @@ export class ConflictError extends BackendError {
this.name = "ConflictError";
}
}
export class InternalError extends BackendError {
constructor(
message: string = ERRORS.BACKEND.OBFUSCATED,
public code = 500,
) {
super(message, code);
this.name = "InternalError";
}
}
+10 -7
View File
@@ -1,4 +1,4 @@
import { json } from "@sveltejs/kit";
import { AuthenticationError, AuthorizationError } from "./errors";
/**
* Check whether the user can access the data within a route.
@@ -10,29 +10,32 @@ export const checkPermission = (
locals: App.Locals,
tenantId: string | null,
administrative: boolean = false,
): Response | null => {
global: boolean = false,
): void => {
if (!locals.user) {
return json({ error: "Authentication required" }, { status: 401 });
throw new AuthenticationError();
}
if (locals.user.role === "GLOBAL_ADMIN") {
// Global admin can view absences for any tenant
return null;
return;
} else if (
!global &&
locals.user.role === "TENANT_ADMIN" &&
tenantId != null &&
locals.user.tenantId === tenantId
) {
// Tenant admin and staff can view absences for their own tenant
return null;
return;
} else if (
!global &&
!administrative &&
locals.user.role === "STAFF" &&
tenantId != null &&
locals.user.tenantId === tenantId
) {
// Tenant admin and staff can view absences for their own tenant
return null;
return;
} else {
return json({ error: "Insufficient permissions" }, { status: 403 });
throw new AuthorizationError();
}
};
+7 -2
View File
@@ -3,6 +3,7 @@ import { UserService } from "$lib/server/services/user-service";
import type { RequestHandler } from "./$types";
import { registerOpenAPIRoute } from "$lib/server/openapi";
import logger from "$lib/logger";
import { BackendError, InternalError, logError } from "$lib/server/utils/errors";
// Register OpenAPI documentation
registerOpenAPIRoute("/admin/exists", "GET", {
@@ -63,7 +64,11 @@ export const GET: RequestHandler = async () => {
count: adminCount,
});
} catch (error) {
log.error("Error checking admin existence:", String(error));
return json({ error: "Internal server error" }, { status: 500 });
logError(log)("Error checking admin existence", error);
if (error instanceof BackendError) {
return error.toJson();
}
return new InternalError().toJson();
}
};
+2 -6
View File
@@ -66,9 +66,7 @@ describe("GET /api/admin/exists", () => {
const data = await response.json();
expect(response.status).toBe(500);
expect(data).toEqual({
error: "Internal server error",
});
expect(data.error).toBe("Internal server error");
expect(UserService.adminExists).toHaveBeenCalledOnce();
expect(UserService.getAdminCount).not.toHaveBeenCalled();
});
@@ -81,9 +79,7 @@ describe("GET /api/admin/exists", () => {
const data = await response.json();
expect(response.status).toBe(500);
expect(data).toEqual({
error: "Internal server error",
});
expect(data.error).toBe("Internal server error");
expect(UserService.adminExists).toHaveBeenCalledOnce();
expect(UserService.getAdminCount).toHaveBeenCalledOnce();
});
+17 -13
View File
@@ -1,10 +1,17 @@
import { json } from "@sveltejs/kit";
import { UserService } from "$lib/server/services/user-service";
import { WebAuthnService } from "$lib/server/auth/webauthn-service";
import { ValidationError } from "$lib/server/utils/errors";
import {
BackendError,
ConflictError,
InternalError,
logError,
ValidationError,
} from "$lib/server/utils/errors";
import type { RequestHandler } from "./$types";
import { registerOpenAPIRoute } from "$lib/server/openapi";
import logger from "$lib/logger";
import { ERRORS } from "$lib/errors";
// Register OpenAPI documentation
registerOpenAPIRoute("/admin/init", "POST", {
@@ -113,7 +120,7 @@ export const POST: RequestHandler = async ({ request, cookies, url }) => {
const body = await request.json();
if (await UserService.adminExists()) {
return json({ error: "System was already initialized" }, { status: 409 });
throw new ConflictError(ERRORS.USERS.ADMIN_EXISTS);
}
// Validate that either passkey or passphrase is provided (but not both)
@@ -121,11 +128,11 @@ export const POST: RequestHandler = async ({ request, cookies, url }) => {
const hasPassphrase = !!body.passphrase;
if (!hasPasskey && !hasPassphrase) {
return json({ error: "Either passkey or passphrase must be provided" }, { status: 400 });
throw new ValidationError(ERRORS.SECURITY.EITHER_PASSKEY_OR_PHRASE);
}
if (hasPasskey && hasPassphrase) {
return json({ error: "Cannot provide both passkey and passphrase" }, { status: 400 });
throw new ValidationError(ERRORS.SECURITY.BOTH_PASSKEY_AND_PHRASE);
}
log.debug("Creating admin account", {
@@ -152,10 +159,7 @@ export const POST: RequestHandler = async ({ request, cookies, url }) => {
const registrationEmail = cookies.get("webauthn-registration-email");
if (!registrationEmail || registrationEmail !== body.email) {
return json(
{ error: "Invalid passkey registration. Please request a new challenge first." },
{ status: 400 },
);
throw new ValidationError(ERRORS.SECURITY.INVALID_PASSKEY_REGISTRATION);
}
// Clear the registration cookie after validation (challenge cookie is cleared by login route)
@@ -193,17 +197,17 @@ export const POST: RequestHandler = async ({ request, cookies, url }) => {
{ status: 201 },
);
} catch (error) {
log.error("Admin registration error:", JSON.stringify(error || "?"));
logError(log)("Admin registration error", error);
if (error instanceof ValidationError) {
return json({ error: error.message }, { status: 400 });
if (error instanceof BackendError) {
return error.toJson();
}
// Handle unique constraint violation (email already exists)
if (error instanceof Error && error.message.includes("unique constraint")) {
return json({ error: "An admin with this email already exists" }, { status: 409 });
return new ConflictError("An admin with this email already exists").toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
+17 -19
View File
@@ -1,14 +1,21 @@
import { error, json, type RequestHandler } from "@sveltejs/kit";
import { json, type RequestHandler } from "@sveltejs/kit";
import { z } from "zod";
import { centralDb } from "$lib/server/db";
import { tenant } from "$lib/server/db/central-schema";
import { eq } from "drizzle-orm";
import { ValidationError, NotFoundError } from "$lib/server/utils/errors";
import {
BackendError,
InternalError,
logError,
NotFoundError,
ValidationError,
} from "$lib/server/utils/errors";
import { UserService } from "$lib/server/services/user-service";
import { generateAccessToken } from "$lib/server/auth/jwt-utils";
import { UniversalLogger } from "$lib/logger";
import { registerOpenAPIRoute } from "$lib/server/openapi";
import { checkPermission } from "$lib/server/utils/permissions";
import { ERRORS } from "$lib/errors";
const logger = new UniversalLogger().setContext("AdminTenantSwitch");
@@ -23,10 +30,7 @@ const tenantSwitchSchema = z.object({
export const POST: RequestHandler = async ({ request, locals, cookies }) => {
try {
// Verify user is authenticated and is global admin
const permissionError = checkPermission(locals, null, true);
if (permissionError) {
return permissionError;
}
checkPermission(locals, null, true);
const body = await request.json();
const validation = tenantSwitchSchema.safeParse(body);
@@ -36,7 +40,7 @@ export const POST: RequestHandler = async ({ request, locals, cookies }) => {
userId: locals.user?.id,
errors: validation.error.errors,
});
throw error(400, "Invalid request body");
throw new ValidationError(ERRORS.VALIDATION.INVALID_REQUEST_BODY);
}
const { tenantId } = validation.data;
@@ -53,7 +57,7 @@ export const POST: RequestHandler = async ({ request, locals, cookies }) => {
userId: locals.user?.id,
tenantId,
});
throw error(404, "Tenant not found");
throw new NotFoundError(ERRORS.TENANTS.NOT_FOUND);
}
// Current user is already authenticated via authHandle
@@ -68,7 +72,7 @@ export const POST: RequestHandler = async ({ request, locals, cookies }) => {
userId: locals.user?.userId,
tenantId,
});
throw error(500, "Failed to update user data");
throw new InternalError(ERRORS.USERS.FAILED_TO_UPDATE);
}
// Generate new access token with updated tenant context
@@ -105,18 +109,12 @@ export const POST: RequestHandler = async ({ request, locals, cookies }) => {
},
});
} catch (err) {
if (err instanceof ValidationError) {
logger.warn("Validation error in tenant switch", { error: err.message });
throw error(400, err.message);
}
logError(logger)("Error in tenant switch", err, locals.user?.userId);
if (err instanceof NotFoundError) {
logger.warn("Not found error in tenant switch", { error: err.message });
throw error(404, err.message);
if (err instanceof BackendError) {
return err.toJson();
}
logger.error("Unexpected error in tenant switch", { error: String(err) });
throw error(500, "Internal server error");
return new InternalError().toJson();
}
};
+28 -25
View File
@@ -14,8 +14,9 @@ vi.mock("@sveltejs/kit", async () => {
(error as any).body = { message };
throw error;
}),
json: vi.fn((data: any) => {
return new Response(JSON.stringify(data), { status: 200 });
json: vi.fn((data: any, options: any = {}) => {
const status = options.status || 200;
return new Response(JSON.stringify(data), { status });
}),
};
});
@@ -83,12 +84,6 @@ vi.mock("drizzle-orm", () => ({
eq: vi.fn(() => "eq-condition"),
}));
// Mock error classes
vi.mock("$lib/server/utils/errors", () => ({
ValidationError: class ValidationError extends Error {},
NotFoundError: class NotFoundError extends Error {},
}));
// Mock permissions module
vi.mock("$lib/server/utils/permissions", () => ({
checkPermission: vi.fn(),
@@ -98,6 +93,7 @@ import { centralDb } from "$lib/server/db";
import { UserService } from "$lib/server/services/user-service";
import { generateAccessToken } from "$lib/server/auth/jwt-utils";
import { checkPermission } from "$lib/server/utils/permissions";
import { AuthenticationError, AuthorizationError } from "$lib/server/utils/errors";
describe("POST /api/admin/tenant", () => {
const mockUser = {
@@ -144,14 +140,17 @@ describe("POST /api/admin/tenant", () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset checkPermission mock to not throw by default
vi.mocked(checkPermission).mockImplementation(() => {
// Default implementation that doesn't throw
});
});
it("should successfully switch to a tenant", async () => {
const tenantId = "550e8400-e29b-41d4-a716-446655440000";
const requestEvent = createRequestEvent({ tenantId });
// Mock permission check to pass
vi.mocked(checkPermission).mockReturnValue(null);
// Permission check will use default mock (pass)
// Mock tenant exists query
const mockSelectQuery = {
@@ -197,11 +196,10 @@ describe("POST /api/admin/tenant", () => {
null,
);
// Mock permission check to return 401 error
const mockErrorResponse = new Response(JSON.stringify({ error: "Authentication required" }), {
status: 401,
// Mock permission check to throw authentication error
vi.mocked(checkPermission).mockImplementationOnce(() => {
throw new AuthenticationError("Authentication required");
});
vi.mocked(checkPermission).mockReturnValue(mockErrorResponse);
const response = await POST(requestEvent);
expect(response.status).toBe(401);
@@ -216,11 +214,10 @@ describe("POST /api/admin/tenant", () => {
tenantAdminUser,
);
// Mock permission check to return 403 error
const mockErrorResponse = new Response(JSON.stringify({ error: "Insufficient permissions" }), {
status: 403,
// Mock permission check to throw authorization error
vi.mocked(checkPermission).mockImplementationOnce(() => {
throw new AuthorizationError("Insufficient permissions");
});
vi.mocked(checkPermission).mockReturnValue(mockErrorResponse);
const response = await POST(requestEvent);
expect(response.status).toBe(403);
@@ -228,20 +225,22 @@ describe("POST /api/admin/tenant", () => {
expect(result.error).toBe("Insufficient permissions");
});
it("should return 400 when request body is invalid", async () => {
it("should return 422 when request body is invalid", async () => {
const requestEvent = createRequestEvent({ tenantId: "invalid-uuid" });
// Mock permission check to pass
vi.mocked(checkPermission).mockReturnValue(null);
// Permission check will use default mock (pass)
await expect(POST(requestEvent)).rejects.toThrow();
const response = await POST(requestEvent);
const result = await response.json();
expect(response.status).toBe(422);
expect(result.error).toBe("Invalid request body");
});
it("should return 404 when tenant does not exist", async () => {
const requestEvent = createRequestEvent({ tenantId: "550e8400-e29b-41d4-a716-446655440000" });
// Mock permission check to pass
vi.mocked(checkPermission).mockReturnValue(null);
// Permission check will use default mock (pass)
// Mock tenant doesn't exist
const mockSelectQuery = {
@@ -251,6 +250,10 @@ describe("POST /api/admin/tenant", () => {
};
vi.mocked(centralDb.select).mockReturnValue(mockSelectQuery as any);
await expect(POST(requestEvent)).rejects.toThrow();
const response = await POST(requestEvent);
const result = await response.json();
expect(response.status).toBe(404);
expect(result.error).toBe("Tenant not found");
});
});
+1 -1
View File
@@ -34,7 +34,7 @@ registerOpenAPIRoute("/health/services", "GET", {
},
},
"500": {
description: "Internal Server Error",
description: "Internal server error",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
+10 -20
View File
@@ -1,6 +1,6 @@
import { json } from "@sveltejs/kit";
import { TenantAdminService } from "$lib/server/services/tenant-admin-service";
import { BackendError } from "$lib/server/utils/errors";
import { BackendError, ConflictError, InternalError, logError } from "$lib/server/utils/errors";
import type { RequestHandler } from "@sveltejs/kit";
import { registerOpenAPIRoute } from "$lib/server/openapi";
import { db } from "$lib/server/db";
@@ -160,10 +160,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
hasInviteAdmin: !!body.inviteAdmin,
});
const error = checkPermission(locals, null, true);
if (error) {
return error;
}
checkPermission(locals, null, true);
const tenantService = await TenantAdminService.createTenant({
shortName: body.shortName,
@@ -184,18 +181,17 @@ export const POST: RequestHandler = async ({ locals, request }) => {
{ status: 201 },
);
} catch (error) {
log.error("Tenant creation error:", JSON.stringify(error || "?"));
logError(log)("Tenant creation error", error, locals.user?.userId);
if (error instanceof BackendError) {
return error.toJson();
}
// Handle unique constraint violation (shortName already exists)
if (error instanceof Error && error.message.includes("unique constraint")) {
return json({ error: ERRORS.TENANTS.NAME_EXISTS }, { status: 409 });
return new ConflictError(ERRORS.TENANTS.NAME_EXISTS).toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -204,16 +200,10 @@ export const GET: RequestHandler = async ({ locals }) => {
try {
// Check if user is authenticated and is a global admin
if (!locals.user) {
return json({ error: "Authentication required" }, { status: 401 });
}
if (locals.user.role !== "GLOBAL_ADMIN") {
return json({ error: "Global admin access required" }, { status: 403 });
}
checkPermission(locals, null, true, true);
log.debug("Getting all tenants", {
requestedBy: locals.user.userId,
requestedBy: locals.user?.userId,
});
// Get all tenants from database
@@ -230,14 +220,14 @@ export const GET: RequestHandler = async ({ locals }) => {
log.debug("Retrieved tenants successfully", {
tenantCount: tenants.length,
requestedBy: locals.user.userId,
requestedBy: locals.user?.userId,
});
return json({
tenants: tenants,
});
} catch (error) {
log.error("Failed to get tenants:", JSON.stringify(error || "?"));
return json({ error: "Internal server error" }, { status: 500 });
logError(log)("Error getting tenants", error, locals.user?.userId);
return new InternalError().toJson();
}
};
+31 -53
View File
@@ -1,7 +1,13 @@
import { json } from "@sveltejs/kit";
import { TenantAdminService } from "$lib/server/services/tenant-admin-service";
import { ValidationError, NotFoundError } from "$lib/server/utils/errors";
import { AuthorizationService } from "$lib/server/auth/authorization-service";
import {
ValidationError,
BackendError,
ConflictError,
InternalError,
logError,
NotFoundError,
} from "$lib/server/utils/errors";
import type { RequestHandler } from "@sveltejs/kit";
import { registerOpenAPIRoute } from "$lib/server/openapi";
import logger from "$lib/logger";
@@ -395,13 +401,10 @@ export const PUT: RequestHandler = async ({ locals, params, request }) => {
});
if (!tenantId) {
return json({ error: "No tenant id given" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID);
}
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
}
checkPermission(locals, tenantId, true);
const tenantService = await TenantAdminService.getTenantById(tenantId);
const updatedTenant = await tenantService.updateTenantData(body);
@@ -416,22 +419,18 @@ export const PUT: RequestHandler = async ({ locals, params, request }) => {
tenant: updatedTenant,
});
} catch (error) {
log.error("Error updating tenant metadata:", JSON.stringify(error || "?"));
logError(log)("Error updating tenant metadata", error, locals.user?.userId, params.id);
if (error instanceof ValidationError) {
return json({ error: error.message }, { status: 400 });
}
if (error instanceof NotFoundError) {
return json({ error: "Tenant not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
// Handle unique constraint violation (shortName already exists)
if (error instanceof Error && error.message.includes("unique constraint")) {
return json({ error: ERRORS.TENANTS.NAME_EXISTS }, { status: 409 });
return new ConflictError(ERRORS.TENANTS.NAME_EXISTS).toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -442,7 +441,7 @@ export const GET: RequestHandler = async ({ params, locals }) => {
const tenantId = params.id;
if (!tenantId) {
return json({ error: "No tenant id given" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID);
}
log.debug("Getting tenant details", {
@@ -450,16 +449,13 @@ export const GET: RequestHandler = async ({ params, locals }) => {
requestedBy: locals.user?.userId,
});
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
}
checkPermission(locals, tenantId, true);
const tenantService = await TenantAdminService.getTenantById(tenantId);
const tenantData = tenantService.tenantData;
if (!tenantData) {
return json({ error: "Tenant not found" }, { status: 404 });
throw new NotFoundError(ERRORS.TENANTS.NOT_FOUND);
}
log.debug("Tenant details retrieved successfully", {
@@ -471,13 +467,13 @@ export const GET: RequestHandler = async ({ params, locals }) => {
tenant: tenantData,
});
} catch (error) {
log.error("Error getting tenant details:", JSON.stringify(error || "?"));
logError(log)("Error getting tenant details", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: "Tenant not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -488,32 +484,18 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
const tenantId = params.id;
// Check if user is authenticated
if (!locals.user) {
return json({ error: "Authentication required" }, { status: 401 });
}
checkPermission(locals, tenantId ?? null, true, true);
if (!tenantId) {
return json({ error: "No tenant id given" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID);
}
log.info("Attempting tenant deletion", {
tenantId,
requestedBy: locals.user.userId,
userRole: locals.user.role,
requestedBy: locals.user?.userId,
userRole: locals.user?.role,
});
// Authorization: Only global admins can delete tenants
try {
AuthorizationService.requireGlobalAdmin(locals.user);
} catch {
log.warn("Tenant deletion denied: insufficient permissions", {
tenantId,
requestedBy: locals.user.userId,
userRole: locals.user.role,
});
return json({ error: "Insufficient permissions" }, { status: 403 });
}
// Get tenant service and perform deletion
const tenantService = await TenantAdminService.getTenantById(tenantId);
const result = await tenantService.deleteTenant();
@@ -524,7 +506,7 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
deletedUsersCount: result.deletedUsersCount,
updatedGlobalAdminsCount: result.updatedGlobalAdminsCount,
deletedConfigsCount: result.deletedConfigsCount,
requestedBy: locals.user.userId,
requestedBy: locals.user?.userId,
});
return json({
@@ -532,16 +514,12 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
result,
});
} catch (error) {
log.error("Error deleting tenant", {
tenantId: params.id,
requestedBy: locals.user?.userId,
error: JSON.stringify(error || "?"),
});
logError(log)("Error deleting tenant", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: "Tenant not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
+14 -23
View File
@@ -1,10 +1,11 @@
import { json } from "@sveltejs/kit";
import { AgentService } from "$lib/server/services/agent-service";
import { ValidationError, NotFoundError } from "$lib/server/utils/errors";
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 POST
registerOpenAPIRoute("/tenants/{id}/agents", "POST", {
@@ -204,13 +205,10 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
// Check if user is authenticated
if (!tenantId) {
return json({ error: "No tenant id given" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID);
}
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
}
checkPermission(locals, tenantId, true);
const body = await request.json();
@@ -237,17 +235,13 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
{ status: 201 },
);
} catch (error) {
log.error("Error creating agent:", JSON.stringify(error || "?"));
logError(log)("Error creating agent:", error, locals.user?.userId, params.id);
if (error instanceof ValidationError) {
return json({ error: error.message }, { status: 400 });
if (error instanceof BackendError) {
return error.toJson();
}
if (error instanceof NotFoundError) {
return json({ error: "Tenant not found" }, { status: 404 });
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -259,13 +253,10 @@ export const GET: RequestHandler = async ({ params, locals }) => {
// Check if user is authenticated
if (!tenantId) {
return json({ error: "No tenant id given" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID);
}
const error = checkPermission(locals, tenantId);
if (error) {
return error;
}
checkPermission(locals, tenantId);
log.debug("Getting all agents", {
tenantId,
@@ -285,12 +276,12 @@ export const GET: RequestHandler = async ({ params, locals }) => {
agents,
});
} catch (error) {
log.error("Error getting agents:", JSON.stringify(error || "?"));
logError(log)("Error getting agents:", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: "Tenant not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -1,10 +1,17 @@
import { json } from "@sveltejs/kit";
import { AgentService } from "$lib/server/services/agent-service";
import { ValidationError, NotFoundError } from "$lib/server/utils/errors";
import {
ValidationError,
NotFoundError,
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}/agents/{agentId}", "GET", {
@@ -287,13 +294,10 @@ export const GET: RequestHandler = async ({ params, locals }) => {
// Check if user is authenticated
if (!tenantId || !agentId) {
return json({ error: "Missing tenant or agent ID" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.MISSING_TENANT_OR_AGENT_ID);
}
const error = checkPermission(locals, tenantId);
if (error) {
return error;
}
checkPermission(locals, tenantId);
log.debug("Getting agent details", {
tenantId,
@@ -305,7 +309,7 @@ export const GET: RequestHandler = async ({ params, locals }) => {
const agent = await agentService.getAgentById(agentId);
if (!agent) {
return json({ error: "Agent not found" }, { status: 404 });
throw new NotFoundError("Agent not found");
}
log.debug("Agent details retrieved successfully", {
@@ -318,13 +322,13 @@ export const GET: RequestHandler = async ({ params, locals }) => {
agent,
});
} catch (error) {
log.error("Error getting agent details:", JSON.stringify(error || "?"));
logError(log)("Error getting agent details", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: "Agent not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -337,14 +341,10 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
// Check if user is authenticated
if (!tenantId || !agentId) {
return json({ error: "Missing tenant or agent ID" }, { status: 400 });
}
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
throw new ValidationError(ERRORS.TENANTS.MISSING_TENANT_OR_AGENT_ID);
}
checkPermission(locals, tenantId, true);
const body = await request.json();
log.debug("Updating agent", {
@@ -368,17 +368,13 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
agent: updatedAgent,
});
} catch (error) {
log.error("Error updating agent:", JSON.stringify(error || "?"));
logError(log)("Error updating agent", error, locals.user?.userId, params.id);
if (error instanceof ValidationError) {
return json({ error: error.message }, { status: 400 });
if (error instanceof BackendError) {
return error.toJson();
}
if (error instanceof NotFoundError) {
return json({ error: "Agent not found" }, { status: 404 });
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -390,13 +386,10 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
const agentId = params.agentId;
if (!tenantId || !agentId) {
return json({ error: "Missing tenant or agent ID" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.MISSING_TENANT_OR_AGENT_ID);
}
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
}
checkPermission(locals, tenantId, true);
log.debug("Deleting agent", {
tenantId,
@@ -408,7 +401,7 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
const deleted = await agentService.deleteAgent(agentId);
if (!deleted) {
return json({ error: "Agent not found" }, { status: 404 });
throw new NotFoundError(ERRORS.AGENTS.NOT_FOUND);
}
log.debug("Agent deleted successfully", {
@@ -421,12 +414,12 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
message: "Agent deleted successfully",
});
} catch (error) {
log.error("Error deleting agent:", JSON.stringify(error || "?"));
logError(log)("Error deleting agent", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: "Agent not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -250,7 +250,7 @@ describe("Agent Detail API Routes", () => {
const response = await PUT(event);
const data = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(data.error).toBe("Invalid agent data");
});
@@ -1,6 +1,6 @@
import { json } from "@sveltejs/kit";
import { AgentService } from "$lib/server/services/agent-service";
import { ValidationError, NotFoundError, ConflictError } from "$lib/server/utils/errors";
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";
@@ -258,13 +258,10 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
const agentId = params.agentId;
if (!tenantId || !agentId) {
return json({ error: "Missing tenant or agent ID" }, { status: 400 });
throw new ValidationError("Missing tenant or agent ID");
}
const error = checkPermission(locals, tenantId);
if (error) {
return error;
}
checkPermission(locals, tenantId);
const body = await request.json();
@@ -301,21 +298,13 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
{ status: 201 },
);
} catch (error) {
log.error("Error creating agent absence:", JSON.stringify(error || "?"));
logError(log)("Creating agent absence failed", error, locals.user?.userId, params.id);
if (error instanceof ValidationError) {
return json({ error: error.message }, { status: 400 });
if (error instanceof BackendError) {
return error.toJson();
}
if (error instanceof NotFoundError) {
return json({ error: "Agent not found" }, { status: 404 });
}
if (error instanceof ConflictError) {
return json({ error: error.message }, { status: 409 });
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -327,13 +316,10 @@ export const GET: RequestHandler = async ({ params, locals, url }) => {
const agentId = params.agentId;
if (!tenantId || !agentId) {
return json({ error: "Missing tenant or agent ID" }, { status: 400 });
throw new ValidationError("Missing tenant or agent ID");
}
const error = checkPermission(locals, tenantId);
if (error) {
return error;
}
checkPermission(locals, tenantId);
// Get optional query parameters for date filtering
const startDate = url.searchParams.get("startDate");
@@ -365,16 +351,12 @@ export const GET: RequestHandler = async ({ params, locals, url }) => {
absences,
});
} catch (error) {
log.error("Error getting agent absences:", JSON.stringify(error || "?"));
logError(log)("Getting agent absences failed", error, locals.user?.userId, params.id);
if (error instanceof ValidationError) {
return json({ error: error.message }, { status: 400 });
if (error instanceof BackendError) {
return error.toJson();
}
if (error instanceof NotFoundError) {
return json({ error: "Agent not found" }, { status: 404 });
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -1,6 +1,12 @@
import { json } from "@sveltejs/kit";
import { AgentService } from "$lib/server/services/agent-service";
import { ValidationError, NotFoundError, ConflictError } from "$lib/server/utils/errors";
import {
ValidationError,
NotFoundError,
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";
@@ -334,13 +340,10 @@ export const GET: RequestHandler = async ({ params, locals }) => {
// Check if user is authenticated
if (!tenantId || !agentId || !absenceId) {
return json({ error: "Missing tenant, agent, or absence ID" }, { status: 400 });
throw new ValidationError("Missing tenant, agent, or absence ID");
}
const error = checkPermission(locals, tenantId);
if (error) {
return error;
}
checkPermission(locals, tenantId);
log.debug("Getting absence details", {
tenantId,
@@ -353,12 +356,12 @@ export const GET: RequestHandler = async ({ params, locals }) => {
const absence = await agentService.getAbsenceById(absenceId);
if (!absence) {
return json({ error: "Absence not found" }, { status: 404 });
throw new NotFoundError("Absence not found");
}
// Verify the absence belongs to the requested agent
if (absence.agentId !== agentId) {
return json({ error: "Absence not found for this agent" }, { status: 404 });
throw new NotFoundError("Absence not found for this agent");
}
log.debug("Absence details retrieved successfully", {
@@ -372,13 +375,13 @@ export const GET: RequestHandler = async ({ params, locals }) => {
absence,
});
} catch (error) {
log.error("Error getting absence details:", JSON.stringify(error || "?"));
logError(log)("Error getting absence details", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: "Absence not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -392,13 +395,10 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
// Check if user is authenticated
if (!tenantId || !agentId || !absenceId) {
return json({ error: "Missing tenant, agent, or absence ID" }, { status: 400 });
throw new ValidationError("Missing tenant, agent, or absence ID");
}
const error = checkPermission(locals, tenantId);
if (error) {
return error;
}
checkPermission(locals, tenantId);
const body = await request.json();
@@ -415,11 +415,11 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
// First verify the absence exists and belongs to the agent
const existingAbsence = await agentService.getAbsenceById(absenceId);
if (!existingAbsence) {
return json({ error: "Absence not found" }, { status: 404 });
throw new NotFoundError("Absence not found");
}
if (existingAbsence.agentId !== agentId) {
return json({ error: "Absence not found for this agent" }, { status: 404 });
throw new NotFoundError("Absence not found for this agent");
}
const updatedAbsence = await agentService.updateAbsence(absenceId, body);
@@ -436,21 +436,13 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
absence: updatedAbsence,
});
} catch (error) {
log.error("Error updating agent absence:", JSON.stringify(error || "?"));
logError(log)("Error updating agent absence", error, locals.user?.userId, params.id);
if (error instanceof ValidationError) {
return json({ error: error.message }, { status: 400 });
if (error instanceof BackendError) {
return error.toJson();
}
if (error instanceof NotFoundError) {
return json({ error: "Absence not found" }, { status: 404 });
}
if (error instanceof ConflictError) {
return json({ error: error.message }, { status: 409 });
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -463,13 +455,10 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
const absenceId = params.absenceId;
if (!tenantId || !agentId || !absenceId) {
return json({ error: "Missing tenant, agent, or absence ID" }, { status: 400 });
throw new ValidationError("Missing tenant, agent, or absence ID");
}
const error = checkPermission(locals, tenantId);
if (error) {
return error;
}
checkPermission(locals, tenantId);
log.debug("Deleting agent absence", {
tenantId,
@@ -483,11 +472,11 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
// First verify the absence exists and belongs to the agent
const existingAbsence = await agentService.getAbsenceById(absenceId);
if (!existingAbsence) {
return json({ error: "Absence not found" }, { status: 404 });
throw new NotFoundError("Absence not found");
}
if (existingAbsence.agentId !== agentId) {
return json({ error: "Absence not found for this agent" }, { status: 404 });
throw new NotFoundError("Absence not found for this agent");
}
const deleted = await agentService.deleteAbsence(absenceId);
@@ -507,12 +496,12 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
message: "Absence deleted successfully",
});
} catch (error) {
log.error("Error deleting agent absence:", JSON.stringify(error || "?"));
logError(log)("Error deleting agent absence", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: "Absence not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -392,7 +392,7 @@ describe("Agent Absence Detail API Routes", () => {
const response = await PUT(event);
const data = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(data.error).toBe("Invalid date range");
});
@@ -196,7 +196,7 @@ describe("Agent Absence API Routes", () => {
const response = await POST(event);
const data = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(data.error).toBe("Invalid date range");
});
@@ -243,7 +243,7 @@ describe("Agent Absence API Routes", () => {
const response = await POST(event);
const data = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(data.error).toBe("Missing tenant or agent ID");
});
});
@@ -422,7 +422,7 @@ describe("Agent Absence API Routes", () => {
const response = await GET(event);
const data = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(data.error).toBe("Invalid date format");
});
@@ -134,19 +134,16 @@ describe("Agent API Routes", () => {
expect(data.agents).toEqual(mockAgents);
});
it("should reject unauthenticated requests", async () => {
const event = createMockRequestEvent({
locals: { user: null } as any,
});
it("should return 401 for unauthenticated requests", async () => {
const event = createMockRequestEvent({ locals: { user: null } as any });
const response = await GET(event);
const data = await response.json();
const result = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe("Authentication required");
expect(result.error).toBe("Authentication required");
});
it("should reject insufficient permissions", async () => {
it("should return 403 for insufficient permissions", async () => {
const event = createMockRequestEvent({
locals: {
user: {
@@ -156,12 +153,11 @@ describe("Agent API Routes", () => {
} as any,
},
});
const response = await GET(event);
const data = await response.json();
const result = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe("Insufficient permissions");
expect(result.error).toBe("Insufficient permissions");
});
it("should handle missing tenant ID", async () => {
@@ -172,7 +168,7 @@ describe("Agent API Routes", () => {
const response = await GET(event);
const data = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(data.error).toBe("No tenant id given");
});
@@ -251,7 +247,7 @@ describe("Agent API Routes", () => {
expect(data.agent).toEqual(mockAgent);
});
it("should reject staff users from creating agents", async () => {
it("should return 403 for staff users creating agents", async () => {
const event = createMockRequestEvent({
locals: {
user: {
@@ -261,25 +257,21 @@ describe("Agent API Routes", () => {
} as any,
},
});
const response = await POST(event);
const data = await response.json();
const result = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe("Insufficient permissions");
expect(result.error).toBe("Insufficient permissions");
expect(mockAgentService.createAgent).not.toHaveBeenCalled();
});
it("should reject unauthenticated requests", async () => {
const event = createMockRequestEvent({
locals: { user: null } as any,
});
it("should return 401 for unauthenticated requests", async () => {
const event = createMockRequestEvent({ locals: { user: null } as any });
const response = await POST(event);
const data = await response.json();
const result = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe("Authentication required");
expect(result.error).toBe("Authentication required");
});
it("should handle validation errors", async () => {
@@ -289,7 +281,7 @@ describe("Agent API Routes", () => {
const response = await POST(event);
const data = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(data.error).toBe("Invalid agent name");
});
@@ -323,7 +315,7 @@ describe("Agent API Routes", () => {
const response = await POST(event);
const data = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(data.error).toBe("No tenant id given");
});
});
@@ -1,10 +1,17 @@
import { json } from "@sveltejs/kit";
import { AppointmentService } from "$lib/server/services/appointment-service";
import { ValidationError, NotFoundError } from "$lib/server/utils/errors";
import {
BackendError,
ConflictError,
InternalError,
logError,
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";
import { ERRORS } from "$lib/errors";
// Register OpenAPI documentation for POST
registerOpenAPIRoute("/tenants/{id}/appointments", "POST", {
@@ -326,13 +333,10 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
const tenantId = params.id;
if (!tenantId) {
return json({ error: "No tenant id given" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID);
}
const error = checkPermission(locals, tenantId);
if (error) {
return error;
}
checkPermission(locals, tenantId);
const body = await request.json();
@@ -360,25 +364,19 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
{ status: 201 },
);
} catch (error) {
log.error("Error creating appointment:", JSON.stringify(error || "?"));
if (error instanceof ValidationError) {
return json({ error: error.message }, { status: 400 });
logError(log)("Error creating appointment", error, locals.user?.userId, params.id);
if (error instanceof BackendError) {
return error.toJson();
}
if (error instanceof NotFoundError) {
return json({ error: error.message }, { status: 404 });
}
// ConflictError from AppointmentService
if (
(error instanceof Error && error.message.includes("conflict")) ||
(error instanceof Error && error.message.includes("paused"))
) {
return json({ error: error.message }, { status: 409 });
return new ConflictError(error.message).toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -389,13 +387,10 @@ export const GET: RequestHandler = async ({ params, url, locals }) => {
const tenantId = params.id;
if (!tenantId) {
return json({ error: "No tenant id given" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID);
}
const error = checkPermission(locals, tenantId);
if (error) {
return error;
}
checkPermission(locals, tenantId);
// Extract query parameters
const startDate = url.searchParams.get("startDate");
@@ -405,7 +400,7 @@ export const GET: RequestHandler = async ({ params, url, locals }) => {
const status = url.searchParams.get("status");
if (!startDate || !endDate) {
return json({ error: "startDate and endDate are required" }, { status: 400 });
throw new ValidationError("startDate and endDate are required");
}
const query = {
@@ -435,16 +430,11 @@ export const GET: RequestHandler = async ({ params, url, locals }) => {
appointments,
});
} catch (error) {
log.error("Error getting appointments:", JSON.stringify(error || "?"));
if (error instanceof ValidationError) {
return json({ error: error.message }, { status: 400 });
logError(log)("Error getting appointments", error, locals.user?.userId, params.id);
if (error instanceof BackendError) {
return error.toJson();
}
if (error instanceof NotFoundError) {
return json({ error: "Tenant not found" }, { status: 404 });
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -1,6 +1,12 @@
import { json } from "@sveltejs/kit";
import { AppointmentService } from "$lib/server/services/appointment-service";
import { NotFoundError } 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";
@@ -204,13 +210,10 @@ export const GET: RequestHandler = async ({ params, locals }) => {
const appointmentId = params.appointmentId;
if (!tenantId || !appointmentId) {
return json({ error: "Tenant ID and appointment ID are required" }, { status: 400 });
throw new ValidationError("Tenant ID and appointment ID are required");
}
const error = checkPermission(locals, tenantId);
if (error) {
return error;
}
checkPermission(locals, tenantId);
log.debug("Getting appointment by ID", {
tenantId,
@@ -222,7 +225,7 @@ export const GET: RequestHandler = async ({ params, locals }) => {
const appointment = await appointmentService.getAppointmentById(appointmentId);
if (!appointment) {
return json({ error: "Appointment not found" }, { status: 404 });
throw new NotFoundError("Appointment not found");
}
log.debug("Appointment retrieved successfully", {
@@ -235,13 +238,13 @@ export const GET: RequestHandler = async ({ params, locals }) => {
appointment,
});
} catch (error) {
log.error("Error getting appointment:", JSON.stringify(error || "?"));
logError(log)("Error getting appointment", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: "Tenant not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -253,13 +256,10 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
const appointmentId = params.appointmentId;
if (!tenantId || !appointmentId) {
return json({ error: "Tenant ID and appointment ID are required" }, { status: 400 });
throw new ValidationError("Tenant ID and appointment ID are required");
}
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
}
checkPermission(locals, tenantId, true);
log.debug("Deleting appointment", {
tenantId,
@@ -284,12 +284,12 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
message: "Appointment deleted successfully",
});
} catch (error) {
log.error("Error deleting appointment:", JSON.stringify(error || "?"));
logError(log)("Error deleting appointment", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: "Tenant not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -77,7 +77,6 @@ describe("Appointment Detail API Routes", () => {
it("should return 404 if appointment not found", async () => {
mockAppointmentService.getAppointmentById.mockResolvedValue(null);
const event = createMockRequestEvent();
const response = await GET(event);
const result = await response.json();
@@ -87,10 +86,7 @@ describe("Appointment Detail API Routes", () => {
});
it("should return 401 if user is not authenticated", async () => {
const event = createMockRequestEvent({
locals: {},
});
const event = createMockRequestEvent({ locals: {} });
const response = await GET(event);
const result = await response.json();
@@ -109,7 +105,6 @@ describe("Appointment Detail API Routes", () => {
} as any,
},
});
const response = await GET(event);
const result = await response.json();
@@ -265,7 +260,7 @@ describe("Appointment Detail API Routes", () => {
expect(response.status).toBe(200);
});
it("should return 400 if tenant ID or appointment ID is missing", async () => {
it("should return 422 if tenant ID or appointment ID is missing", async () => {
const event = createMockRequestEvent({
params: { id: mockTenantId },
});
@@ -273,7 +268,7 @@ describe("Appointment Detail API Routes", () => {
const response = await DELETE(event);
const result = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(result.error).toBe("Tenant ID and appointment ID are required");
});
@@ -1,6 +1,6 @@
import { json } from "@sveltejs/kit";
import { AppointmentService } from "$lib/server/services/appointment-service";
import { NotFoundError } from "$lib/server/utils/errors";
import { BackendError, InternalError, logError, ValidationError } from "$lib/server/utils/errors";
import type { RequestHandler } from "@sveltejs/kit";
import { registerOpenAPIRoute } from "$lib/server/openapi";
import logger from "$lib/logger";
@@ -116,13 +116,10 @@ export const PUT: RequestHandler = async ({ params, locals }) => {
// Check if user is authenticated
if (!tenantId || !appointmentId) {
return json({ error: "Tenant ID and appointment ID are required" }, { status: 400 });
throw new ValidationError("Tenant ID and appointment ID are required");
}
const error = checkPermission(locals, tenantId);
if (error) {
return error;
}
checkPermission(locals, tenantId);
log.debug("Cancelling appointment", {
tenantId,
@@ -144,12 +141,12 @@ export const PUT: RequestHandler = async ({ params, locals }) => {
appointment: cancelledAppointment,
});
} catch (error) {
log.error("Error cancelling appointment:", JSON.stringify(error || "?"));
logError(log)("Error cancelling appointment", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: error.message }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -74,10 +74,7 @@ describe("Appointment Cancel API", () => {
});
it("should return 401 if user is not authenticated", async () => {
const event = createMockRequestEvent({
locals: {},
});
const event = createMockRequestEvent({ locals: {} });
const response = await PUT(event);
const result = await response.json();
@@ -96,7 +93,6 @@ describe("Appointment Cancel API", () => {
} as any,
},
});
const response = await PUT(event);
const result = await response.json();
@@ -150,7 +146,7 @@ describe("Appointment Cancel API", () => {
expect(response.status).toBe(200);
});
it("should return 400 if tenant ID or appointment ID is missing", async () => {
it("should return 422 if tenant ID or appointment ID is missing", async () => {
const event = createMockRequestEvent({
params: { id: mockTenantId },
});
@@ -158,7 +154,7 @@ describe("Appointment Cancel API", () => {
const response = await PUT(event);
const result = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(result.error).toBe("Tenant ID and appointment ID are required");
});
@@ -1,6 +1,6 @@
import { json } from "@sveltejs/kit";
import { AppointmentService } from "$lib/server/services/appointment-service";
import { NotFoundError } from "$lib/server/utils/errors";
import { BackendError, InternalError, logError, ValidationError } from "$lib/server/utils/errors";
import type { RequestHandler } from "@sveltejs/kit";
import { registerOpenAPIRoute } from "$lib/server/openapi";
import logger from "$lib/logger";
@@ -115,13 +115,10 @@ export const PUT: RequestHandler = async ({ params, locals }) => {
const appointmentId = params.appointmentId;
if (!tenantId || !appointmentId) {
return json({ error: "Tenant ID and appointment ID are required" }, { status: 400 });
throw new ValidationError("Tenant ID and appointment ID are required");
}
const error = checkPermission(locals, tenantId);
if (error) {
return error;
}
checkPermission(locals, tenantId);
log.debug("Confirming appointment", {
tenantId,
@@ -143,12 +140,10 @@ export const PUT: RequestHandler = async ({ params, locals }) => {
appointment: confirmedAppointment,
});
} catch (error) {
log.error("Error confirming appointment:", JSON.stringify(error || "?"));
if (error instanceof NotFoundError) {
return json({ error: error.message }, { status: 404 });
logError(log)("Error confirming appointment", error, locals.user?.userId, params.id);
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -74,10 +74,7 @@ describe("Appointment Confirm API", () => {
});
it("should return 401 if user is not authenticated", async () => {
const event = createMockRequestEvent({
locals: {},
});
const event = createMockRequestEvent({ locals: {} });
const response = await PUT(event);
const result = await response.json();
@@ -96,7 +93,6 @@ describe("Appointment Confirm API", () => {
} as any,
},
});
const response = await PUT(event);
const result = await response.json();
@@ -150,7 +146,7 @@ describe("Appointment Confirm API", () => {
expect(response.status).toBe(200);
});
it("should return 400 if tenant ID or appointment ID is missing", async () => {
it("should return 422 if tenant ID or appointment ID is missing", async () => {
const event = createMockRequestEvent({
params: { id: mockTenantId },
});
@@ -158,7 +154,7 @@ describe("Appointment Confirm API", () => {
const response = await PUT(event);
const result = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(result.error).toBe("Tenant ID and appointment ID are required");
});
@@ -92,10 +92,7 @@ describe("Appointment API Routes", () => {
});
it("should return 401 if user is not authenticated", async () => {
const event = createMockRequestEvent({
locals: {},
});
const event = createMockRequestEvent({ locals: {} });
const response = await POST(event);
const result = await response.json();
@@ -114,7 +111,6 @@ describe("Appointment API Routes", () => {
} as any,
},
});
const response = await POST(event);
const result = await response.json();
@@ -178,7 +174,7 @@ describe("Appointment API Routes", () => {
expect(response.status).toBe(201);
});
it("should return 400 for validation errors", async () => {
it("should return 422 for validation errors", async () => {
mockAppointmentService.createAppointment.mockRejectedValue(
new ValidationError("Invalid data"),
);
@@ -187,7 +183,7 @@ describe("Appointment API Routes", () => {
const response = await POST(event);
const result = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(result.error).toBe("Invalid data");
});
@@ -277,7 +273,7 @@ describe("Appointment API Routes", () => {
expect(result.error).toBe("Insufficient permissions");
});
it("should return 400 if startDate or endDate is missing", async () => {
it("should return 422 if startDate or endDate is missing", async () => {
const event = createMockRequestEvent({
url: new URL("http://localhost?startDate=2024-12-01T00:00:00Z"),
});
@@ -285,7 +281,7 @@ describe("Appointment API Routes", () => {
const response = await GET(event);
const result = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(result.error).toBe("startDate and endDate are required");
});
@@ -310,7 +306,7 @@ describe("Appointment API Routes", () => {
});
});
it("should return 400 for validation errors", async () => {
it("should return 422 for validation errors", async () => {
mockAppointmentService.queryAppointments.mockRejectedValue(
new ValidationError("Invalid query"),
);
@@ -319,7 +315,7 @@ describe("Appointment API Routes", () => {
const response = await GET(event);
const result = await response.json();
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(result.error).toBe("Invalid query");
});
});
+14 -24
View File
@@ -1,10 +1,11 @@
import { json } from "@sveltejs/kit";
import { ChannelService } from "$lib/server/services/channel-service";
import { ValidationError, NotFoundError } from "$lib/server/utils/errors";
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 POST
registerOpenAPIRoute("/tenants/{id}/channels", "POST", {
@@ -321,13 +322,10 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
const tenantId = params.id;
if (!tenantId) {
return json({ error: "No tenant id given" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID);
}
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
}
checkPermission(locals, tenantId, true);
const body = await request.json();
@@ -355,17 +353,13 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
{ status: 201 },
);
} catch (error) {
log.error("Error creating channel:", JSON.stringify(error || "?"));
logError(log)("Error creating channel", error, locals.user?.userId, params.id);
if (error instanceof ValidationError) {
return json({ error: error.message }, { status: 400 });
if (error instanceof BackendError) {
return error.toJson();
}
if (error instanceof NotFoundError) {
return json({ error: "Tenant not found" }, { status: 404 });
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -376,13 +370,10 @@ export const GET: RequestHandler = async ({ params, locals }) => {
const tenantId = params.id;
if (!tenantId) {
return json({ error: "No tenant id given" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID);
}
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
}
checkPermission(locals, tenantId, true);
log.debug("Getting all channels", {
tenantId,
@@ -402,12 +393,11 @@ export const GET: RequestHandler = async ({ params, locals }) => {
channels,
});
} catch (error) {
log.error("Error getting channels:", JSON.stringify(error || "?"));
logError(log)("Error in getting channels", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: "Tenant not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -1,10 +1,17 @@
import { json } from "@sveltejs/kit";
import { ChannelService } from "$lib/server/services/channel-service";
import { ValidationError, NotFoundError } from "$lib/server/utils/errors";
import {
ValidationError,
NotFoundError,
BackendError,
InternalError,
logError,
} 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}/channels/{channelId}", "GET", {
@@ -405,13 +412,10 @@ export const GET: RequestHandler = async ({ params, locals }) => {
const channelId = params.channelId;
if (!tenantId || !channelId) {
return json({ error: "Missing tenant or channel ID" }, { status: 400 });
throw new ValidationError("Missing tenant or channel ID");
}
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
}
checkPermission(locals, tenantId, true);
log.debug("Getting channel details", {
tenantId,
@@ -423,7 +427,7 @@ export const GET: RequestHandler = async ({ params, locals }) => {
const channel = await channelService.getChannelById(channelId);
if (!channel) {
return json({ error: "Channel not found" }, { status: 404 });
throw new NotFoundError(ERRORS.CHANNELS.NOT_FOUND);
}
log.debug("Channel details retrieved successfully", {
@@ -436,13 +440,12 @@ export const GET: RequestHandler = async ({ params, locals }) => {
channel,
});
} catch (error) {
log.error("Error getting channel details:", JSON.stringify(error || "?"));
logError(log)("Error getting channel details", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: "Channel not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -454,13 +457,10 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
const channelId = params.channelId;
if (!tenantId || !channelId) {
return json({ error: "Missing tenant or channel ID" }, { status: 400 });
throw new ValidationError("Missing tenant or channel ID");
}
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
}
checkPermission(locals, tenantId, true);
const body = await request.json();
@@ -485,17 +485,13 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
channel: updatedChannel,
});
} catch (error) {
log.error("Error updating channel:", JSON.stringify(error || "?"));
logError(log)("Error updating channel", error, locals.user?.userId, params.id);
if (error instanceof ValidationError) {
return json({ error: error.message }, { status: 400 });
if (error instanceof BackendError) {
return error.toJson();
}
if (error instanceof NotFoundError) {
return json({ error: "Channel not found" }, { status: 404 });
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -507,13 +503,10 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
const channelId = params.channelId;
if (!tenantId || !channelId) {
return json({ error: "Missing tenant or channel ID" }, { status: 400 });
throw new ValidationError("Missing tenant or channel ID");
}
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
}
checkPermission(locals, tenantId, true);
log.debug("Deleting channel", {
tenantId,
@@ -525,7 +518,7 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
const deleted = await channelService.deleteChannel(channelId);
if (!deleted) {
return json({ error: "Channel not found" }, { status: 404 });
throw new NotFoundError(ERRORS.CHANNELS.NOT_FOUND);
}
log.debug("Channel deleted successfully", {
@@ -538,12 +531,12 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
message: "Channel deleted successfully",
});
} catch (error) {
log.error("Error deleting channel:", JSON.stringify(error || "?"));
logError(log)("Error deleting channel", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: "Channel not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
+14 -23
View File
@@ -1,10 +1,11 @@
import { json } from "@sveltejs/kit";
import { TenantAdminService } from "$lib/server/services/tenant-admin-service";
import { ValidationError, NotFoundError } from "$lib/server/utils/errors";
import { BackendError, InternalError, logError, 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";
import { ERRORS } from "$lib/errors";
// Register OpenAPI documentation for GET
registerOpenAPIRoute("/tenants/{id}/config", "GET", {
@@ -185,13 +186,10 @@ export const GET: RequestHandler = async ({ locals, params }) => {
log.debug("Getting tenant configuration", { tenantId });
if (!tenantId) {
return json({ error: "No tenant id given" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID);
}
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
}
checkPermission(locals, tenantId, true);
const tenantService = await TenantAdminService.getTenantById(tenantId);
const config = await tenantService.configuration;
@@ -203,13 +201,13 @@ export const GET: RequestHandler = async ({ locals, params }) => {
return json(config);
} catch (error) {
log.error("Error getting tenant configuration:", JSON.stringify(error || "?"));
logError(log)("Error getting tenant configuration", error, locals.user?.userId, params.id);
if (error instanceof NotFoundError) {
return json({ error: "Tenant not found" }, { status: 404 });
if (error instanceof BackendError) {
return error.toJson();
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -226,13 +224,10 @@ export const PUT: RequestHandler = async ({ locals, params, request }) => {
});
if (!tenantId) {
return json({ error: "No tenant id given" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID);
}
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
}
checkPermission(locals, tenantId, true);
const tenantService = await TenantAdminService.getTenantById(tenantId);
await tenantService.updateTenantConfig(body);
@@ -247,16 +242,12 @@ export const PUT: RequestHandler = async ({ locals, params, request }) => {
updatedKeys: Object.keys(body),
});
} catch (error) {
log.error("Error updating tenant configuration:", JSON.stringify(error || "?"));
logError(log)("Error updating tenant configuration", error, locals.user?.userId, params.id);
if (error instanceof ValidationError) {
return json({ error: error.message }, { status: 400 });
if (error instanceof BackendError) {
return error.toJson();
}
if (error instanceof NotFoundError) {
return json({ error: "Tenant not found" }, { status: 404 });
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
@@ -1,11 +1,12 @@
import { json } from "@sveltejs/kit";
import { TenantAdminService } from "$lib/server/services/tenant-admin-service";
import { ValidationError, NotFoundError } from "$lib/server/utils/errors";
import { BackendError, InternalError, logError, ValidationError } from "$lib/server/utils/errors";
import type { RequestHandler } from "@sveltejs/kit";
import { registerOpenAPIRoute } from "$lib/server/openapi";
import logger from "$lib/logger";
import z from "zod/v4";
import { checkPermission } from "$lib/server/utils/permissions";
import { ERRORS } from "$lib/errors";
const setupStateSchema = z.object({
setupState: z.enum(["NEW", "SETTINGS_CREATED", "AGENTS_SET_UP", "FIRST_CHANNEL_CREATED"]),
@@ -114,13 +115,10 @@ export const PUT: RequestHandler = async ({ locals, params, request }) => {
});
if (!tenantId) {
return json({ error: "No tenant id given" }, { status: 400 });
throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID);
}
const error = checkPermission(locals, tenantId, true);
if (error) {
return error;
}
checkPermission(locals, tenantId, true);
const validation = setupStateSchema.safeParse(body);
if (!validation.success) {
@@ -140,16 +138,12 @@ export const PUT: RequestHandler = async ({ locals, params, request }) => {
tenant: updatedTenant,
});
} catch (error) {
log.error("Error updating tenant setup state:", JSON.stringify(error || "?"));
logError(log)("Error updating tenant setup state", error, locals.user?.userId, params.id);
if (error instanceof ValidationError) {
return json({ error: error.message }, { status: 400 });
if (error instanceof BackendError) {
return error.toJson();
}
if (error instanceof NotFoundError) {
return json({ error: "Tenant not found" }, { status: 404 });
}
return json({ error: "Internal server error" }, { status: 500 });
return new InternalError().toJson();
}
};
+7 -30
View File
@@ -43,21 +43,6 @@ vi.mock("$lib/logger", () => ({
})),
}));
vi.mock("$lib/server/utils/errors", () => ({
ValidationError: class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
},
NotFoundError: class NotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = "NotFoundError";
}
},
}));
vi.mock("$lib/server/auth/authorization-service", () => ({
AuthorizationService: {
requireGlobalAdmin: vi.fn(),
@@ -154,10 +139,8 @@ describe("Tenant API Routes", () => {
} as any);
const data = await response.json();
expect(data).toEqual({
error: "No tenant id given",
});
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(data.error).toBe("No tenant id given");
});
});
@@ -210,10 +193,8 @@ describe("Tenant API Routes", () => {
} as any);
const data = await response.json();
expect(data).toEqual({
error: "No tenant id given",
});
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(data.error).toBe("No tenant id given");
});
});
@@ -295,10 +276,8 @@ describe("Tenant API Routes", () => {
} as any);
const data = await response.json();
expect(data).toEqual({
error: "Invalid configuration data",
});
expect(response.status).toBe(400);
expect(response.status).toBe(422);
expect(data.error).toBe("Invalid configuration data");
});
it("should handle tenant not found", async () => {
@@ -330,10 +309,8 @@ describe("Tenant API Routes", () => {
} as any);
const data = await response.json();
expect(data).toEqual({
error: "Tenant not found",
});
expect(response.status).toBe(404);
expect(data.error).toBe("Tenant not found");
});
});
});
+4 -12
View File
@@ -184,9 +184,7 @@ describe("/api/tenants", () => {
} as any);
const data = await response.json();
expect(data).toEqual({
message: "Invalid tenant creation request",
});
expect(data.error).toEqual("Invalid tenant creation request");
expect(response.status).toBe(422);
});
@@ -226,9 +224,7 @@ describe("/api/tenants", () => {
} as any);
const data = await response.json();
expect(data).toEqual({
error: ERRORS.TENANTS.NAME_EXISTS,
});
expect(data.error).toEqual(ERRORS.TENANTS.NAME_EXISTS);
expect(response.status).toBe(409);
});
@@ -268,9 +264,7 @@ describe("/api/tenants", () => {
} as any);
const data = await response.json();
expect(data).toEqual({
error: "Internal server error",
});
expect(data.error).toEqual("Internal server error");
expect(response.status).toBe(500);
});
});
@@ -339,9 +333,7 @@ describe("/api/tenants", () => {
} as any);
const data = await response.json();
expect(data).toEqual({
error: "Internal server error",
});
expect(data.error).toEqual("Internal server error");
expect(response.status).toBe(500);
});
});
@@ -0,0 +1,23 @@
CREATE TABLE "agent_absence" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"agent_id" uuid NOT NULL,
"start_date" timestamp NOT NULL,
"end_date" timestamp NOT NULL,
"absence_type" text DEFAULT '' NOT NULL,
"description" text
);
--> statement-breakpoint
DROP TABLE "staff" CASCADE;--> statement-breakpoint
ALTER TABLE "agent" RENAME COLUMN "logo" TO "image";--> statement-breakpoint
ALTER TABLE "appointment" RENAME COLUMN "title" TO "name";--> statement-breakpoint
ALTER TABLE "appointment" RENAME COLUMN "description" TO "phone";--> statement-breakpoint
ALTER TABLE "channel" ADD COLUMN "names" json NOT NULL;--> statement-breakpoint
ALTER TABLE "channel" ADD COLUMN "paused" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "channel" ADD COLUMN "descriptions" json;--> statement-breakpoint
ALTER TABLE "channel" ADD COLUMN "languages" json NOT NULL;--> statement-breakpoint
ALTER TABLE "agent_absence" ADD CONSTRAINT "agent_absence_agent_id_agent_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agent"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "channel" DROP COLUMN "name";--> statement-breakpoint
ALTER TABLE "channel" DROP COLUMN "description";--> statement-breakpoint
ALTER TABLE "channel" DROP COLUMN "language";--> statement-breakpoint
ALTER TABLE "slotTemplate" DROP COLUMN "name";--> statement-breakpoint
DROP TYPE "public"."channel_type";
+458
View File
@@ -0,0 +1,458 @@
{
"id": "8afcc6a0-a00f-4f73-adb4-8c73b4b66986",
"prevId": "ae9961fd-c495-4680-98ed-595599978298",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.agent": {
"name": "agent",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"image": {
"name": "image",
"type": "bytea",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.agent_absence": {
"name": "agent_absence",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"agent_id": {
"name": "agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"start_date": {
"name": "start_date",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"end_date": {
"name": "end_date",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"absence_type": {
"name": "absence_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"agent_absence_agent_id_agent_id_fk": {
"name": "agent_absence_agent_id_agent_id_fk",
"tableFrom": "agent_absence",
"tableTo": "agent",
"columnsFrom": ["agent_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.appointment": {
"name": "appointment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"client_id": {
"name": "client_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"channel_id": {
"name": "channel_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"appointment_date": {
"name": "appointment_date",
"type": "date",
"primaryKey": false,
"notNull": true
},
"expiry_date": {
"name": "expiry_date",
"type": "date",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "appointment_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'NEW'"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"phone": {
"name": "phone",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"appointment_client_id_client_id_fk": {
"name": "appointment_client_id_client_id_fk",
"tableFrom": "appointment",
"tableTo": "client",
"columnsFrom": ["client_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"appointment_channel_id_channel_id_fk": {
"name": "appointment_channel_id_channel_id_fk",
"tableFrom": "appointment",
"tableTo": "channel",
"columnsFrom": ["channel_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.channel": {
"name": "channel",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"names": {
"name": "names",
"type": "json",
"primaryKey": false,
"notNull": true
},
"color": {
"name": "color",
"type": "text",
"primaryKey": false,
"notNull": false
},
"paused": {
"name": "paused",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"descriptions": {
"name": "descriptions",
"type": "json",
"primaryKey": false,
"notNull": false
},
"languages": {
"name": "languages",
"type": "json",
"primaryKey": false,
"notNull": true
},
"is_public": {
"name": "is_public",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"requires_confirmation": {
"name": "requires_confirmation",
"type": "boolean",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.channel_agent": {
"name": "channel_agent",
"schema": "",
"columns": {
"channel_id": {
"name": "channel_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"agent_id": {
"name": "agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"channel_agent_channel_id_channel_id_fk": {
"name": "channel_agent_channel_id_channel_id_fk",
"tableFrom": "channel_agent",
"tableTo": "channel",
"columnsFrom": ["channel_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"channel_agent_agent_id_agent_id_fk": {
"name": "channel_agent_agent_id_agent_id_fk",
"tableFrom": "channel_agent",
"tableTo": "agent",
"columnsFrom": ["agent_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.channel_slot_template": {
"name": "channel_slot_template",
"schema": "",
"columns": {
"channel_id": {
"name": "channel_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"slot_template_id": {
"name": "slot_template_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"channel_slot_template_channel_id_channel_id_fk": {
"name": "channel_slot_template_channel_id_channel_id_fk",
"tableFrom": "channel_slot_template",
"tableTo": "channel",
"columnsFrom": ["channel_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"channel_slot_template_slot_template_id_slotTemplate_id_fk": {
"name": "channel_slot_template_slot_template_id_slotTemplate_id_fk",
"tableFrom": "channel_slot_template",
"tableTo": "slotTemplate",
"columnsFrom": ["slot_template_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.client": {
"name": "client",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"hash_key": {
"name": "hash_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"public_key": {
"name": "public_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"private_key_share": {
"name": "private_key_share",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"client_hash_key_unique": {
"name": "client_hash_key_unique",
"nullsNotDistinct": false,
"columns": ["hash_key"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.slotTemplate": {
"name": "slotTemplate",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"weekdays": {
"name": "weekdays",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"from": {
"name": "from",
"type": "time",
"primaryKey": false,
"notNull": true
},
"to": {
"name": "to",
"type": "time",
"primaryKey": false,
"notNull": true
},
"duration": {
"name": "duration",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.appointment_status": {
"name": "appointment_status",
"schema": "public",
"values": ["NEW", "CONFIRMED", "HELD", "REJECTED", "NO_SHOW"]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+7
View File
@@ -8,6 +8,13 @@
"when": 1752671302514,
"tag": "0000_neat_nemesis",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1758021994040,
"tag": "0001_equal_king_bedlam",
"breakpoints": true
}
]
}