mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-09-07 09:47:39 +02:00
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:
@@ -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");
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
+1
-1
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
+3
-8
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
+3
-7
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
+3
-7
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user