diff --git a/.prettierignore b/.prettierignore
index 4f611ee..8151cc1 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -9,4 +9,5 @@ bun.lock
bun.lockb
# Build Artifacts
-src/i18n/**
\ No newline at end of file
+src/i18n/**
+project.inlang/**
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
index 1e5915a..7a9b6e3 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,15 +1,15 @@
{
- "useTabs": true,
- "singleQuote": false,
- "trailingComma": "none",
- "printWidth": 100,
- "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
- "overrides": [
- {
- "files": "*.svelte",
- "options": {
- "parser": "svelte"
- }
- }
- ]
+ "useTabs": false,
+ "singleQuote": false,
+ "trailingComma": "all",
+ "printWidth": 100,
+ "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
+ "overrides": [
+ {
+ "files": "*.svelte",
+ "options": {
+ "parser": "svelte"
+ }
+ }
+ ]
}
diff --git a/components.json b/components.json
index c5d91b4..298e869 100644
--- a/components.json
+++ b/components.json
@@ -1,16 +1,16 @@
{
- "$schema": "https://shadcn-svelte.com/schema.json",
- "tailwind": {
- "css": "src/app.css",
- "baseColor": "slate"
- },
- "aliases": {
- "components": "$lib/components",
- "utils": "$lib/utils",
- "ui": "$lib/components/ui",
- "hooks": "$lib/hooks",
- "lib": "$lib"
- },
- "typescript": true,
- "registry": "https://shadcn-svelte.com/registry"
+ "$schema": "https://shadcn-svelte.com/schema.json",
+ "tailwind": {
+ "css": "src/app.css",
+ "baseColor": "slate"
+ },
+ "aliases": {
+ "components": "$lib/components",
+ "utils": "$lib/utils",
+ "ui": "$lib/components/ui",
+ "hooks": "$lib/hooks",
+ "lib": "$lib"
+ },
+ "typescript": true,
+ "registry": "https://shadcn-svelte.com/registry"
}
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 5c5d2de..9e85a7d 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -20,7 +20,7 @@ services:
test:
[
"CMD-SHELL",
- "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-appointment_booking}"
+ "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-appointment_booking}",
]
interval: 10s
timeout: 5s
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index ff0f96f..65cda14 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -22,7 +22,7 @@ services:
test:
[
"CMD-SHELL",
- "pg_isready -U $$(cat /run/secrets/postgres_user) -d $$(cat /run/secrets/postgres_db)"
+ "pg_isready -U $$(cat /run/secrets/postgres_user) -d $$(cat /run/secrets/postgres_db)",
]
interval: 10s
timeout: 5s
diff --git a/docs/email-system.md b/docs/email-system.md
index 2dcd958..f95ee08 100644
--- a/docs/email-system.md
+++ b/docs/email-system.md
@@ -159,9 +159,9 @@ Tenants can customize three color values that are automatically applied to email
```typescript
interface TenantBranding {
- primaryColor: string; // Main brand color (buttons, links, logos)
- secondaryColor: string; // Accent color (success messages, highlights)
- backgroundColor: string; // Email background color
+ primaryColor: string; // Main brand color (buttons, links, logos)
+ secondaryColor: string; // Accent color (success messages, highlights)
+ backgroundColor: string; // Email background color
}
```
@@ -192,9 +192,9 @@ Tenant logos are stored as binary data in the database and automatically convert
```html
{{#if tenant.logo}}
{{/if}}
{{tenant.longName}}
@@ -255,16 +255,16 @@ The email system is designed with privacy in mind:
```typescript
// Client email creation (privacy-focused)
const clientRecipient = {
- email: user.email || "", // May be empty
- name: undefined, // Never stored for clients
- language: user.language || "de"
+ email: user.email || "", // May be empty
+ name: undefined, // Never stored for clients
+ language: user.language || "de",
};
// Staff email creation
const staffRecipient = {
- email: user.email, // Always required
- name: user.name || undefined, // Optional display name
- language: user.language || "de"
+ email: user.email, // Always required
+ name: user.name || undefined, // Optional display name
+ language: user.language || "de",
};
```
@@ -303,10 +303,10 @@ The system automatically uses test mode in development:
```typescript
// Add test case in email-system.test.ts
const result = await templateEngine.renderTemplate("user-created", {
- recipient: { email: "test@example.com", name: "Test User" },
- subject: "Test Subject",
- language: "de",
- tenant: mockTenant
+ recipient: { email: "test@example.com", name: "Test User" },
+ subject: "Test Subject",
+ language: "de",
+ tenant: mockTenant,
});
```
diff --git a/docs/infrastructure.md b/docs/infrastructure.md
index a8ab6b2..aeef6ca 100644
--- a/docs/infrastructure.md
+++ b/docs/infrastructure.md
@@ -170,29 +170,29 @@ import { readFileSync } from "fs";
import pg from "pg";
function getDatabaseConfig() {
- if (process.env.NODE_ENV === "production") {
- // Read from Docker secrets
- const user = readFileSync("/run/secrets/postgres_user", "utf8").trim();
- const password = readFileSync("/run/secrets/postgres_password", "utf8").trim();
- const database = readFileSync("/run/secrets/postgres_db", "utf8").trim();
+ if (process.env.NODE_ENV === "production") {
+ // Read from Docker secrets
+ const user = readFileSync("/run/secrets/postgres_user", "utf8").trim();
+ const password = readFileSync("/run/secrets/postgres_password", "utf8").trim();
+ const database = readFileSync("/run/secrets/postgres_db", "utf8").trim();
- return {
- host: "postgres",
- port: 5432,
- user,
- password,
- database
- };
- } else {
- // Development configuration
- return {
- host: "localhost",
- port: process.env.POSTGRES_PORT || 5432,
- user: process.env.POSTGRES_USER || "postgres",
- password: process.env.POSTGRES_PASSWORD,
- database: process.env.POSTGRES_DB || "appointment_booking"
- };
- }
+ return {
+ host: "postgres",
+ port: 5432,
+ user,
+ password,
+ database,
+ };
+ } else {
+ // Development configuration
+ return {
+ host: "localhost",
+ port: process.env.POSTGRES_PORT || 5432,
+ user: process.env.POSTGRES_USER || "postgres",
+ password: process.env.POSTGRES_PASSWORD,
+ database: process.env.POSTGRES_DB || "appointment_booking",
+ };
+ }
}
export const pool = new pg.Pool(getDatabaseConfig());
@@ -207,12 +207,12 @@ import { json } from "@sveltejs/kit";
import { pool } from "$lib/database.js";
export async function GET() {
- try {
- await pool.query("SELECT 1");
- return json({ status: "healthy", timestamp: new Date().toISOString() });
- } catch (error) {
- return json({ status: "unhealthy", error: error.message }, { status: 500 });
- }
+ try {
+ await pool.query("SELECT 1");
+ return json({ status: "healthy", timestamp: new Date().toISOString() });
+ } catch (error) {
+ return json({ status: "unhealthy", error: error.message }, { status: 500 });
+ }
}
```
@@ -225,11 +225,11 @@ import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
export default defineConfig({
- plugins: [sveltekit()],
- server: {
- host: "0.0.0.0",
- port: 5173
- }
+ plugins: [sveltekit()],
+ server: {
+ host: "0.0.0.0",
+ port: 5173,
+ },
});
```
diff --git a/docs/universal-logger.md b/docs/universal-logger.md
index cb23caf..585fa75 100644
--- a/docs/universal-logger.md
+++ b/docs/universal-logger.md
@@ -138,23 +138,23 @@ The server-side Winston logger is configured with:
import { logger } from "$lib/logger";
export const handle = async ({ event, resolve }) => {
- const start = Date.now();
- const requestLogger = logger.setContext("REQUEST");
+ const start = Date.now();
+ const requestLogger = logger.setContext("REQUEST");
- try {
- const response = await resolve(event);
- const duration = Date.now() - start;
+ try {
+ const response = await resolve(event);
+ const duration = Date.now() - start;
- requestLogger.info(`${event.request.method} ${event.url.pathname}`, {
- status: response.status,
- duration: `${duration}ms`
- });
+ requestLogger.info(`${event.request.method} ${event.url.pathname}`, {
+ status: response.status,
+ duration: `${duration}ms`,
+ });
- return response;
- } catch (error) {
- requestLogger.error("Request failed", { error });
- throw error;
- }
+ return response;
+ } catch (error) {
+ requestLogger.error("Request failed", { error });
+ throw error;
+ }
};
```
diff --git a/drizzle.config.ts b/drizzle.config.ts
index a1427d8..4d61959 100644
--- a/drizzle.config.ts
+++ b/drizzle.config.ts
@@ -11,10 +11,10 @@ if (!POSTGRES_PASSWORD) throw new Error("POSTGRES_PASSWORD is not set");
const DATABASE_URL = `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${POSTGRES_PORT}/${POSTGRES_DB}`;
export default defineConfig({
- schema: "./src/lib/server/db/central-schema.ts",
- dialect: "postgresql",
- dbCredentials: { url: DATABASE_URL },
- out: "./migrations",
- verbose: true,
- strict: true
+ schema: "./src/lib/server/db/central-schema.ts",
+ dialect: "postgresql",
+ dbCredentials: { url: DATABASE_URL },
+ out: "./migrations",
+ verbose: true,
+ strict: true,
});
diff --git a/drizzle.tenant.config.ts b/drizzle.tenant.config.ts
index eb81fe6..7e9120e 100644
--- a/drizzle.tenant.config.ts
+++ b/drizzle.tenant.config.ts
@@ -3,12 +3,12 @@ import { defineConfig } from "drizzle-kit";
// This config is used to generate migrations for tenant schemas
// It uses a placeholder database URL that will be replaced at runtime
export default defineConfig({
- schema: "./src/lib/server/db/tenant-schema.ts",
- dialect: "postgresql",
- dbCredentials: {
- url: "postgresql://placeholder:placeholder@localhost:5432/placeholder"
- },
- out: "./tenant-migrations",
- verbose: true,
- strict: true
+ schema: "./src/lib/server/db/tenant-schema.ts",
+ dialect: "postgresql",
+ dbCredentials: {
+ url: "postgresql://placeholder:placeholder@localhost:5432/placeholder",
+ },
+ out: "./tenant-migrations",
+ verbose: true,
+ strict: true,
});
diff --git a/e2e/demo.test.ts b/e2e/demo.test.ts
index 1f5a374..4e8ffcc 100644
--- a/e2e/demo.test.ts
+++ b/e2e/demo.test.ts
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
test("home page has expected h1", async ({ page }) => {
- await page.goto("/");
- await expect(page.locator("h1")).toBeVisible();
+ await page.goto("/");
+ await expect(page.locator("h1")).toBeVisible();
});
diff --git a/eslint.config.js b/eslint.config.js
index dedef03..ec83b6b 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -10,30 +10,30 @@ import svelteConfig from "./svelte.config.js";
const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url));
export default ts.config(
- includeIgnoreFile(gitignorePath),
- js.configs.recommended,
- ...ts.configs.recommended,
- ...svelte.configs.recommended,
- prettier,
- ...svelte.configs.prettier,
- {
- ignores: ["static/**"]
- },
- {
- languageOptions: {
- globals: { ...globals.browser, ...globals.node }
- },
- rules: { "no-undef": "off" }
- },
- {
- files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
- languageOptions: {
- parserOptions: {
- projectService: true,
- extraFileExtensions: [".svelte"],
- parser: ts.parser,
- svelteConfig
- }
- }
- }
+ includeIgnoreFile(gitignorePath),
+ js.configs.recommended,
+ ...ts.configs.recommended,
+ ...svelte.configs.recommended,
+ prettier,
+ ...svelte.configs.prettier,
+ {
+ ignores: ["static/**"],
+ },
+ {
+ languageOptions: {
+ globals: { ...globals.browser, ...globals.node },
+ },
+ rules: { "no-undef": "off" },
+ },
+ {
+ files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
+ languageOptions: {
+ parserOptions: {
+ projectService: true,
+ extraFileExtensions: [".svelte"],
+ parser: ts.parser,
+ svelteConfig,
+ },
+ },
+ },
);
diff --git a/migrations/meta/0000_snapshot.json b/migrations/meta/0000_snapshot.json
index 9ec0a3d..03f5698 100644
--- a/migrations/meta/0000_snapshot.json
+++ b/migrations/meta/0000_snapshot.json
@@ -1,551 +1,551 @@
{
- "id": "e70213b3-227a-4a40-bdbe-470310817fb3",
- "prevId": "00000000-0000-0000-0000-000000000000",
- "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
- },
- "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": "'GLOBAL_ADMIN'"
- },
- "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
- }
- },
- "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_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.user_role": {
- "name": "user_role",
- "schema": "public",
- "values": ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"]
- }
- },
- "schemas": {},
- "sequences": {},
- "roles": {},
- "policies": {},
- "views": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
+ "id": "e70213b3-227a-4a40-bdbe-470310817fb3",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "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
+ },
+ "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": "'GLOBAL_ADMIN'"
+ },
+ "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
+ }
+ },
+ "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_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.user_role": {
+ "name": "user_role",
+ "schema": "public",
+ "values": ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
}
diff --git a/migrations/meta/0001_snapshot.json b/migrations/meta/0001_snapshot.json
index cadc752..fa54d88 100644
--- a/migrations/meta/0001_snapshot.json
+++ b/migrations/meta/0001_snapshot.json
@@ -1,563 +1,563 @@
{
- "id": "bdfafee4-65d2-4381-b4e7-df8c9337d0c1",
- "prevId": "e70213b3-227a-4a40-bdbe-470310817fb3",
- "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
- },
- "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": "'GLOBAL_ADMIN'"
- },
- "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
- }
- },
- "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_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.user_role": {
- "name": "user_role",
- "schema": "public",
- "values": ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"]
- }
- },
- "schemas": {},
- "sequences": {},
- "roles": {},
- "policies": {},
- "views": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
+ "id": "bdfafee4-65d2-4381-b4e7-df8c9337d0c1",
+ "prevId": "e70213b3-227a-4a40-bdbe-470310817fb3",
+ "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
+ },
+ "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": "'GLOBAL_ADMIN'"
+ },
+ "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
+ }
+ },
+ "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_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.user_role": {
+ "name": "user_role",
+ "schema": "public",
+ "values": ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
}
diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json
index 1d95f67..8996898 100644
--- a/migrations/meta/_journal.json
+++ b/migrations/meta/_journal.json
@@ -1,20 +1,20 @@
{
- "version": "7",
- "dialect": "postgresql",
- "entries": [
- {
- "idx": 0,
- "version": "7",
- "when": 1752661377819,
- "tag": "0000_big_ultimatum",
- "breakpoints": true
- },
- {
- "idx": 1,
- "version": "7",
- "when": 1752668982273,
- "tag": "0001_equal_wonder_man",
- "breakpoints": true
- }
- ]
+ "version": "7",
+ "dialect": "postgresql",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "7",
+ "when": 1752661377819,
+ "tag": "0000_big_ultimatum",
+ "breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "7",
+ "when": 1752668982273,
+ "tag": "0001_equal_wonder_man",
+ "breakpoints": true
+ }
+ ]
}
diff --git a/package.json b/package.json
index de8ae7a..a58d611 100644
--- a/package.json
+++ b/package.json
@@ -1,100 +1,100 @@
{
- "name": "open-reception",
- "private": true,
- "version": "0.0.1",
- "type": "module",
- "description": "End-to-end encrypted appointment booking platform",
- "scripts": {
- "dev": "vite dev",
- "build": "vite build",
- "preview": "vite preview",
- "prepare": "svelte-kit sync && npm run i18n:compile || echo ''",
- "check": "npm run prepare && svelte-check --tsconfig ./tsconfig.json",
- "check:watch": "npm run prepare && svelte-check --tsconfig ./tsconfig.json --watch",
- "format": "prettier --write .",
- "lint": "prettier --check . && eslint .",
- "test:unit": "vitest",
- "test": "npm run test:unit -- --run && npm run test:e2e",
- "test:e2e": "playwright test",
- "db:push": "drizzle-kit push",
- "db:migrate": "drizzle-kit migrate",
- "db:studio": "drizzle-kit studio",
- "db:generate": "drizzle-kit generate",
- "db:tenant:generate": "drizzle-kit generate --config=drizzle.tenant.config.ts",
- "docker:dev:up": "docker compose -f docker-compose.dev.yml up -d",
- "docker:dev:down": "docker compose -f docker-compose.dev.yml down",
- "docker:dev:logs": "docker compose -f docker-compose.dev.yml logs -f",
- "docker:dev:clean": "docker compose -f docker-compose.dev.yml down -v --remove-orphans",
- "docker:build": "docker build -t openreception/open-reception:latest .",
- "docker:build:tag": "docker tag openreception/open-reception:latest openreception/open-reception:$npm_package_version",
- "docker:push": "docker push openreception/open-reception:$npm_package_version && docker push openreception/open-reception:latest",
- "docker:build-and-push": "npm run docker:build && npm run docker:build:tag && npm run docker:push",
- "docker:prod:up": "docker compose -f docker-compose.prod.yml up -d",
- "docker:prod:down": "docker compose -f docker-compose.prod.yml down",
- "docker:prod:logs": "docker compose -f docker-compose.prod.yml logs -f",
- "docker:prod:clean": "docker compose -f docker-compose.prod.yml down -v --remove-orphans",
- "i18n:compile": "npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/i18n"
- },
- "devDependencies": {
- "@eslint/compat": "^1.3.0",
- "@eslint/js": "^9.29.0",
- "@inlang/paraglide-js": "2.2.0",
- "@internationalized/date": "^3.8.2",
- "@lucide/svelte": "^0.542.0",
- "@playwright/test": "^1.53.1",
- "@sveltejs/adapter-auto": "^6.0.1",
- "@sveltejs/kit": "^2.22.0",
- "@sveltejs/vite-plugin-svelte": "^5.1.0",
- "@tailwindcss/typography": "^0.5.16",
- "@tailwindcss/vite": "^4.1.10",
- "@testing-library/jest-dom": "^6.6.3",
- "@testing-library/svelte": "^5.2.8",
- "@types/dotenv": "^6.1.1",
- "@types/node": "^24",
- "@types/nodemailer": "^6.4.17",
- "bits-ui": "^2.8.13",
- "clsx": "^2.1.1",
- "drizzle-kit": "^0.31.1",
- "eslint": "^9.29.0",
- "eslint-config-prettier": "^10.1.5",
- "eslint-plugin-svelte": "^3.9.3",
- "formsnap": "^2.0.1",
- "globals": "^16.2.0",
- "jsdom": "^26.1.0",
- "prettier": "^3.5.3",
- "prettier-plugin-svelte": "^3.4.0",
- "prettier-plugin-tailwindcss": "^0.6.13",
- "svelte": "^5.34.7",
- "svelte-check": "^4.2.2",
- "svelte-sonner": "^1.0.5",
- "sveltekit-superforms": "^2.27.1",
- "tailwind-merge": "^3.3.1",
- "tailwind-variants": "^1.0.0",
- "tailwindcss": "^4.1.10",
- "tw-animate-css": "^1.3.4",
- "typescript": "^5.8.3",
- "typescript-eslint": "^8.34.1",
- "vite": "^6.3.5",
- "vitest": "^3.2.4"
- },
- "dependencies": {
- "@noble/hashes": "^1.8.0",
- "@noble/post-quantum": "^0.4.1",
- "@sveltejs/adapter-node": "^5.2.12",
- "argon2": "^0.43.0",
- "argon2-browser": "^1.18.0",
- "date-fns": "^4.1.0",
- "dotenv": "^16.5.0",
- "dotenv-expand": "^12.0.2",
- "drizzle-orm": "^0.44.2",
- "jose": "^6.0.11",
- "mode-watcher": "^1.0.8",
- "nodemailer": "^7.0.3",
- "postgres": "^3.4.7",
- "secrets.js-34r7h": "^2.0.2",
- "uuidv7": "^1.0.2",
- "winston": "^3.17.0",
- "zod": "^3.25.76"
- },
- "license": "AGPL-3.0"
+ "name": "open-reception",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "description": "End-to-end encrypted appointment booking platform",
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "prepare": "svelte-kit sync && npm run i18n:compile || echo ''",
+ "check": "npm run prepare && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "npm run prepare && svelte-check --tsconfig ./tsconfig.json --watch",
+ "format": "prettier --write .",
+ "lint": "prettier --check . && eslint .",
+ "test:unit": "vitest",
+ "test": "npm run test:unit -- --run && npm run test:e2e",
+ "test:e2e": "playwright test",
+ "db:push": "drizzle-kit push",
+ "db:migrate": "drizzle-kit migrate",
+ "db:studio": "drizzle-kit studio",
+ "db:generate": "drizzle-kit generate",
+ "db:tenant:generate": "drizzle-kit generate --config=drizzle.tenant.config.ts",
+ "docker:dev:up": "docker compose -f docker-compose.dev.yml up -d",
+ "docker:dev:down": "docker compose -f docker-compose.dev.yml down",
+ "docker:dev:logs": "docker compose -f docker-compose.dev.yml logs -f",
+ "docker:dev:clean": "docker compose -f docker-compose.dev.yml down -v --remove-orphans",
+ "docker:build": "docker build -t openreception/open-reception:latest .",
+ "docker:build:tag": "docker tag openreception/open-reception:latest openreception/open-reception:$npm_package_version",
+ "docker:push": "docker push openreception/open-reception:$npm_package_version && docker push openreception/open-reception:latest",
+ "docker:build-and-push": "npm run docker:build && npm run docker:build:tag && npm run docker:push",
+ "docker:prod:up": "docker compose -f docker-compose.prod.yml up -d",
+ "docker:prod:down": "docker compose -f docker-compose.prod.yml down",
+ "docker:prod:logs": "docker compose -f docker-compose.prod.yml logs -f",
+ "docker:prod:clean": "docker compose -f docker-compose.prod.yml down -v --remove-orphans",
+ "i18n:compile": "npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/i18n"
+ },
+ "devDependencies": {
+ "@eslint/compat": "^1.3.0",
+ "@eslint/js": "^9.29.0",
+ "@inlang/paraglide-js": "2.2.0",
+ "@internationalized/date": "^3.8.2",
+ "@lucide/svelte": "^0.542.0",
+ "@playwright/test": "^1.53.1",
+ "@sveltejs/adapter-auto": "^6.0.1",
+ "@sveltejs/kit": "^2.22.0",
+ "@sveltejs/vite-plugin-svelte": "^5.1.0",
+ "@tailwindcss/typography": "^0.5.16",
+ "@tailwindcss/vite": "^4.1.10",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/svelte": "^5.2.8",
+ "@types/dotenv": "^6.1.1",
+ "@types/node": "^24",
+ "@types/nodemailer": "^6.4.17",
+ "bits-ui": "^2.8.13",
+ "clsx": "^2.1.1",
+ "drizzle-kit": "^0.31.1",
+ "eslint": "^9.29.0",
+ "eslint-config-prettier": "^10.1.5",
+ "eslint-plugin-svelte": "^3.9.3",
+ "formsnap": "^2.0.1",
+ "globals": "^16.2.0",
+ "jsdom": "^26.1.0",
+ "prettier": "^3.5.3",
+ "prettier-plugin-svelte": "^3.4.0",
+ "prettier-plugin-tailwindcss": "^0.6.13",
+ "svelte": "^5.34.7",
+ "svelte-check": "^4.2.2",
+ "svelte-sonner": "^1.0.5",
+ "sveltekit-superforms": "^2.27.1",
+ "tailwind-merge": "^3.3.1",
+ "tailwind-variants": "^1.0.0",
+ "tailwindcss": "^4.1.10",
+ "tw-animate-css": "^1.3.4",
+ "typescript": "^5.8.3",
+ "typescript-eslint": "^8.34.1",
+ "vite": "^6.3.5",
+ "vitest": "^3.2.4"
+ },
+ "dependencies": {
+ "@noble/hashes": "^1.8.0",
+ "@noble/post-quantum": "^0.4.1",
+ "@sveltejs/adapter-node": "^5.2.12",
+ "argon2": "^0.43.0",
+ "argon2-browser": "^1.18.0",
+ "date-fns": "^4.1.0",
+ "dotenv": "^16.5.0",
+ "dotenv-expand": "^12.0.2",
+ "drizzle-orm": "^0.44.2",
+ "jose": "^6.0.11",
+ "mode-watcher": "^1.0.8",
+ "nodemailer": "^7.0.3",
+ "postgres": "^3.4.7",
+ "secrets.js-34r7h": "^2.0.2",
+ "uuidv7": "^1.0.2",
+ "winston": "^3.17.0",
+ "zod": "^3.25.76"
+ },
+ "license": "AGPL-3.0"
}
diff --git a/playwright.config.ts b/playwright.config.ts
index 6449fa1..1e4262e 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -1,9 +1,9 @@
import { defineConfig } from "@playwright/test";
export default defineConfig({
- webServer: {
- command: "npm run build && npm run preview",
- port: 4173
- },
- testDir: "e2e"
+ webServer: {
+ command: "npm run build && npm run preview",
+ port: 4173,
+ },
+ testDir: "e2e",
});
diff --git a/src/app.css b/src/app.css
index a856a7a..5cc759f 100644
--- a/src/app.css
+++ b/src/app.css
@@ -2,143 +2,143 @@
@custom-variant dark (&:where(.dark, .dark *));
:root {
- --radius: 0.625rem;
- --background: oklch(1 0 0);
- --foreground: oklch(0.129 0.042 264.695);
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.129 0.042 264.695);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.129 0.042 264.695);
- --primary: oklch(0.208 0.042 265.755);
- --primary-foreground: oklch(0.984 0.003 247.858);
- --secondary: oklch(0.968 0.007 247.896);
- --secondary-foreground: oklch(0.208 0.042 265.755);
- --muted: oklch(0.968 0.007 247.896);
- --muted-foreground: oklch(0.554 0.046 257.417);
- --accent: oklch(0.968 0.007 247.896);
- --accent-foreground: oklch(0.208 0.042 265.755);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.929 0.013 255.508);
- --input: oklch(0.929 0.013 255.508);
- --ring: oklch(0.704 0.04 256.788);
- --chart-1: oklch(0.646 0.222 41.116);
- --chart-2: oklch(0.6 0.118 184.704);
- --chart-3: oklch(0.398 0.07 227.392);
- --chart-4: oklch(0.828 0.189 84.429);
- --chart-5: oklch(0.769 0.188 70.08);
- --sidebar: oklch(0.984 0.003 247.858);
- --sidebar-foreground: oklch(0.129 0.042 264.695);
- --sidebar-primary: oklch(0.208 0.042 265.755);
- --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
- --sidebar-accent: oklch(0.968 0.007 247.896);
- --sidebar-accent-foreground: oklch(0.208 0.042 265.755);
- --sidebar-border: oklch(0.929 0.013 255.508);
- --sidebar-ring: oklch(0.704 0.04 256.788);
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.129 0.042 264.695);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.129 0.042 264.695);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.129 0.042 264.695);
+ --primary: oklch(0.208 0.042 265.755);
+ --primary-foreground: oklch(0.984 0.003 247.858);
+ --secondary: oklch(0.968 0.007 247.896);
+ --secondary-foreground: oklch(0.208 0.042 265.755);
+ --muted: oklch(0.968 0.007 247.896);
+ --muted-foreground: oklch(0.554 0.046 257.417);
+ --accent: oklch(0.968 0.007 247.896);
+ --accent-foreground: oklch(0.208 0.042 265.755);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.929 0.013 255.508);
+ --input: oklch(0.929 0.013 255.508);
+ --ring: oklch(0.704 0.04 256.788);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.984 0.003 247.858);
+ --sidebar-foreground: oklch(0.129 0.042 264.695);
+ --sidebar-primary: oklch(0.208 0.042 265.755);
+ --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
+ --sidebar-accent: oklch(0.968 0.007 247.896);
+ --sidebar-accent-foreground: oklch(0.208 0.042 265.755);
+ --sidebar-border: oklch(0.929 0.013 255.508);
+ --sidebar-ring: oklch(0.704 0.04 256.788);
- /* custom colors */
- --lighter: oklch(77.983% 0.03673 254.95);
- --light: oklch(69.121% 0.03859 257.443);
- --medium: oklch(62.379% 0.01913 256.381);
- --dark: oklch(43.309% 0.00977 254.027);
- --darker: oklch(27.232% 0.00797 264.468);
+ /* custom colors */
+ --lighter: oklch(77.983% 0.03673 254.95);
+ --light: oklch(69.121% 0.03859 257.443);
+ --medium: oklch(62.379% 0.01913 256.381);
+ --dark: oklch(43.309% 0.00977 254.027);
+ --darker: oklch(27.232% 0.00797 264.468);
}
.dark {
- --background: oklch(0.129 0.042 264.695);
- --foreground: oklch(0.984 0.003 247.858);
- --card: oklch(0.208 0.042 265.755);
- --card-foreground: oklch(0.984 0.003 247.858);
- --popover: oklch(0.208 0.042 265.755);
- --popover-foreground: oklch(0.984 0.003 247.858);
- --primary: oklch(0.929 0.013 255.508);
- --primary-foreground: oklch(0.208 0.042 265.755);
- --secondary: oklch(0.279 0.041 260.031);
- --secondary-foreground: oklch(0.984 0.003 247.858);
- --muted: oklch(0.279 0.041 260.031);
- --muted-foreground: oklch(0.704 0.04 256.788);
- --accent: oklch(0.279 0.041 260.031);
- --accent-foreground: oklch(0.984 0.003 247.858);
- --destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.551 0.027 264.364);
- --chart-1: oklch(0.488 0.243 264.376);
- --chart-2: oklch(0.696 0.17 162.48);
- --chart-3: oklch(0.769 0.188 70.08);
- --chart-4: oklch(0.627 0.265 303.9);
- --chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.208 0.042 265.755);
- --sidebar-foreground: oklch(0.984 0.003 247.858);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
- --sidebar-accent: oklch(0.279 0.041 260.031);
- --sidebar-accent-foreground: oklch(0.984 0.003 247.858);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.551 0.027 264.364);
+ --background: oklch(0.129 0.042 264.695);
+ --foreground: oklch(0.984 0.003 247.858);
+ --card: oklch(0.208 0.042 265.755);
+ --card-foreground: oklch(0.984 0.003 247.858);
+ --popover: oklch(0.208 0.042 265.755);
+ --popover-foreground: oklch(0.984 0.003 247.858);
+ --primary: oklch(0.929 0.013 255.508);
+ --primary-foreground: oklch(0.208 0.042 265.755);
+ --secondary: oklch(0.279 0.041 260.031);
+ --secondary-foreground: oklch(0.984 0.003 247.858);
+ --muted: oklch(0.279 0.041 260.031);
+ --muted-foreground: oklch(0.704 0.04 256.788);
+ --accent: oklch(0.279 0.041 260.031);
+ --accent-foreground: oklch(0.984 0.003 247.858);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.551 0.027 264.364);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.208 0.042 265.755);
+ --sidebar-foreground: oklch(0.984 0.003 247.858);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
+ --sidebar-accent: oklch(0.279 0.041 260.031);
+ --sidebar-accent-foreground: oklch(0.984 0.003 247.858);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.551 0.027 264.364);
- /* custom colors */
- --lighter: oklch(27.232% 0.00797 264.468);
- --light: oklch(43.309% 0.00977 254.027);
- --medium: oklch(62.379% 0.01913 256.381);
- --dark: oklch(69.121% 0.03859 257.443);
- --darker: oklch(77.983% 0.03673 254.95);
+ /* custom colors */
+ --lighter: oklch(27.232% 0.00797 264.468);
+ --light: oklch(43.309% 0.00977 254.027);
+ --medium: oklch(62.379% 0.01913 256.381);
+ --dark: oklch(69.121% 0.03859 257.443);
+ --darker: oklch(77.983% 0.03673 254.95);
}
@theme {
- --radius-sm: calc(var(--radius) - 4px);
- --radius-md: calc(var(--radius) - 2px);
- --radius-lg: var(--radius);
- --radius-xl: calc(var(--radius) + 4px);
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --color-card: var(--card);
- --color-card-foreground: var(--card-foreground);
- --color-popover: var(--popover);
- --color-popover-foreground: var(--popover-foreground);
- --color-primary: var(--primary);
- --color-primary-foreground: var(--primary-foreground);
- --color-secondary: var(--secondary);
- --color-secondary-foreground: var(--secondary-foreground);
- --color-muted: var(--muted);
- --color-muted-foreground: var(--muted-foreground);
- --color-accent: var(--accent);
- --color-accent-foreground: var(--accent-foreground);
- --color-destructive: var(--destructive);
- --color-border: var(--border);
- --color-input: var(--input);
- --color-ring: var(--ring);
- --color-chart-1: var(--chart-1);
- --color-chart-2: var(--chart-2);
- --color-chart-3: var(--chart-3);
- --color-chart-4: var(--chart-4);
- --color-chart-5: var(--chart-5);
- --color-sidebar: var(--sidebar);
- --color-sidebar-foreground: var(--sidebar-foreground);
- --color-sidebar-primary: var(--sidebar-primary);
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
- --color-sidebar-accent: var(--sidebar-accent);
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
- --color-sidebar-border: var(--sidebar-border);
- --color-sidebar-ring: var(--sidebar-ring);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
- --color-lighter: var(--lighter);
- --color-light: var(--light);
- --color-medium: var(--medium);
- --color-dark: var(--dark);
- --color-darker: var(--darker);
+ --color-lighter: var(--lighter);
+ --color-light: var(--light);
+ --color-medium: var(--medium);
+ --color-dark: var(--dark);
+ --color-darker: var(--darker);
- --max-w-sm: 400px;
- --max-w-md: 640px;
+ --max-w-sm: 400px;
+ --max-w-md: 640px;
}
@layer base {
- * {
- border-color: var(--border);
- outline-color: color-mix(in oklab, var(--ring) 50%, transparent);
- }
+ * {
+ border-color: var(--border);
+ outline-color: color-mix(in oklab, var(--ring) 50%, transparent);
+ }
- body {
- background-color: var(--background);
- color: var(--foreground);
- }
+ body {
+ background-color: var(--background);
+ color: var(--foreground);
+ }
}
diff --git a/src/app.d.ts b/src/app.d.ts
index 8876c95..d48c210 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -4,18 +4,18 @@ import type { JWTPayload } from "jose";
// for information about these interfaces
declare global {
- namespace App {
- // interface Error {}
- interface Locals {
- user?: JWTPayload & { userId: string; sessionId: string };
- }
- // interface PageData {}
- // interface PageState {}
- // interface Platform {}
- }
- interface PageState {
- email?: string;
- }
+ namespace App {
+ // interface Error {}
+ interface Locals {
+ user?: JWTPayload & { userId: string; sessionId: string };
+ }
+ // interface PageData {}
+ // interface PageState {}
+ // interface Platform {}
+ }
+ interface PageState {
+ email?: string;
+ }
}
export {};
diff --git a/src/app.html b/src/app.html
index c6a98cd..54410cc 100644
--- a/src/app.html
+++ b/src/app.html
@@ -1,19 +1,19 @@
-
-
-
-
-
-
-
-
-
-
- %sveltekit.head%
-
+
+
+
+
+
+
+
+
+
+
+ %sveltekit.head%
+
-
- %sveltekit.body%
-
+
+ %sveltekit.body%
+
diff --git a/src/demo.spec.ts b/src/demo.spec.ts
index 808401e..dadc7e4 100644
--- a/src/demo.spec.ts
+++ b/src/demo.spec.ts
@@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest";
describe("sum test", () => {
- it("adds 1 + 2 to equal 3", () => {
- expect(1 + 2).toBe(3);
- });
+ it("adds 1 + 2 to equal 3", () => {
+ expect(1 + 2).toBe(3);
+ });
});
diff --git a/src/hooks.server.test.ts b/src/hooks.server.test.ts
index c064c57..d0ce7b3 100644
--- a/src/hooks.server.test.ts
+++ b/src/hooks.server.test.ts
@@ -4,224 +4,224 @@ import { handle } from "./hooks.server";
// Mock the startup service
vi.mock("$lib/server/services/startup-service", () => ({
- StartupService: {
- initialize: vi.fn(() => Promise.resolve())
- }
+ StartupService: {
+ initialize: vi.fn(() => Promise.resolve()),
+ },
}));
// Mock auth services
vi.mock("$lib/server/auth/session-service", () => ({
- SessionService: {
- validateSession: vi.fn()
- }
+ SessionService: {
+ validateSession: vi.fn(),
+ },
}));
vi.mock("$lib/server/auth/jwt-utils", () => ({
- verifyAccessToken: vi.fn()
+ verifyAccessToken: vi.fn(),
}));
vi.mock("$lib/server/auth/authorization-service", () => ({
- AuthorizationService: {
- hasRole: vi.fn(),
- hasAnyRole: vi.fn()
- }
+ AuthorizationService: {
+ hasRole: vi.fn(),
+ hasAnyRole: vi.fn(),
+ },
}));
// Mock the Date.now function for rate limiting tests
const mockDateNow = vi.fn();
const OriginalDate = Date;
vi.stubGlobal(
- "Date",
- class extends OriginalDate {
- static now = mockDateNow;
- }
+ "Date",
+ class extends OriginalDate {
+ static now = mockDateNow;
+ },
);
describe("hooks.server", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- mockDateNow.mockReturnValue(1000000); // Fixed timestamp for consistent testing
- });
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockDateNow.mockReturnValue(1000000); // Fixed timestamp for consistent testing
+ });
- afterEach(() => {
- // Reset any global state
- vi.clearAllTimers();
- });
+ afterEach(() => {
+ // Reset any global state
+ vi.clearAllTimers();
+ });
- describe("rate limiting", () => {
- const mockResolve = vi.fn();
- const createEvent = (ip: string = "192.168.1.1", method: string = "GET") => {
- const request = new Request("http://localhost/api/health", {
- method,
- headers: {
- "x-forwarded-for": ip
- }
- });
+ describe("rate limiting", () => {
+ const mockResolve = vi.fn();
+ const createEvent = (ip: string = "192.168.1.1", method: string = "GET") => {
+ const request = new Request("http://localhost/api/health", {
+ method,
+ headers: {
+ "x-forwarded-for": ip,
+ },
+ });
- return {
- url: new URL("http://localhost/api/health"),
- request,
- cookies: {} as any,
- fetch: {} as any,
- getClientAddress: () => ip,
- locals: {},
- params: {},
- route: { id: null },
- setHeaders: vi.fn(),
- isDataRequest: false,
- isSubRequest: false,
- platform: {} as any
- };
- };
+ return {
+ url: new URL("http://localhost/api/health"),
+ request,
+ cookies: {} as any,
+ fetch: {} as any,
+ getClientAddress: () => ip,
+ locals: {},
+ params: {},
+ route: { id: null },
+ setHeaders: vi.fn(),
+ isDataRequest: false,
+ isSubRequest: false,
+ platform: {} as any,
+ };
+ };
- beforeEach(() => {
- mockResolve.mockResolvedValue(new Response("OK"));
- });
+ beforeEach(() => {
+ mockResolve.mockResolvedValue(new Response("OK"));
+ });
- it("should allow requests within rate limit", async () => {
- const event = createEvent("192.168.1.100"); // Unique IP for this test
+ it("should allow requests within rate limit", async () => {
+ const event = createEvent("192.168.1.100"); // Unique IP for this test
- const response = await handle({ event, resolve: mockResolve });
+ const response = await handle({ event, resolve: mockResolve });
- expect(response.status).not.toBe(429);
- expect(mockResolve).toHaveBeenCalled();
- expect(mockResolve.mock.calls[0][0]).toEqual(event);
- });
+ expect(response.status).not.toBe(429);
+ expect(mockResolve).toHaveBeenCalled();
+ expect(mockResolve.mock.calls[0][0]).toEqual(event);
+ });
- it("should block requests when rate limit exceeded", async () => {
- const event = createEvent("192.168.1.101"); // Unique IP for this test
+ it("should block requests when rate limit exceeded", async () => {
+ const event = createEvent("192.168.1.101"); // Unique IP for this test
- // Make multiple requests to exceed rate limit
- for (let i = 0; i < 10; i++) {
- await handle({ event, resolve: mockResolve });
- }
+ // Make multiple requests to exceed rate limit
+ for (let i = 0; i < 10; i++) {
+ await handle({ event, resolve: mockResolve });
+ }
- // This request should be rate limited
- const response = await handle({ event, resolve: mockResolve });
+ // This request should be rate limited
+ const response = await handle({ event, resolve: mockResolve });
- expect(response.status).toBe(429);
- expect(await response.text()).toBe("Too Many Requests");
- });
+ expect(response.status).toBe(429);
+ expect(await response.text()).toBe("Too Many Requests");
+ });
- it("should reset rate limit after window expires", async () => {
- const event = createEvent("192.168.1.102"); // Unique IP for this test
+ it("should reset rate limit after window expires", async () => {
+ const event = createEvent("192.168.1.102"); // Unique IP for this test
- // Exceed rate limit
- for (let i = 0; i < 11; i++) {
- await handle({ event, resolve: mockResolve });
- }
+ // Exceed rate limit
+ for (let i = 0; i < 11; i++) {
+ await handle({ event, resolve: mockResolve });
+ }
- // Mock time passing (would need to mock Date.now in real implementation)
- // For now, we'll test that different IPs are treated separately
- const differentIPEvent = createEvent("192.168.1.2");
- const response = await handle({ event: differentIPEvent, resolve: mockResolve });
+ // Mock time passing (would need to mock Date.now in real implementation)
+ // For now, we'll test that different IPs are treated separately
+ const differentIPEvent = createEvent("192.168.1.2");
+ const response = await handle({ event: differentIPEvent, resolve: mockResolve });
- expect(response.status).not.toBe(429);
- });
- });
+ expect(response.status).not.toBe(429);
+ });
+ });
- describe("CORS handling", () => {
- const mockResolve = vi.fn();
+ describe("CORS handling", () => {
+ const mockResolve = vi.fn();
- const createCORSEvent = (method: string = "GET", path: string = "/api/health") => ({
- url: new URL(`http://localhost${path}`),
- request: new Request(`http://localhost${path}`, {
- method,
- headers: {
- "x-forwarded-for": "192.168.2.1" // Different IP for CORS tests
- }
- }),
- cookies: {} as any,
- fetch: {} as any,
- getClientAddress: () => "192.168.2.1",
- locals: {},
- params: {},
- route: { id: null },
- setHeaders: vi.fn(),
- isDataRequest: false,
- isSubRequest: false,
- platform: {} as any
- });
+ const createCORSEvent = (method: string = "GET", path: string = "/api/health") => ({
+ url: new URL(`http://localhost${path}`),
+ request: new Request(`http://localhost${path}`, {
+ method,
+ headers: {
+ "x-forwarded-for": "192.168.2.1", // Different IP for CORS tests
+ },
+ }),
+ cookies: {} as any,
+ fetch: {} as any,
+ getClientAddress: () => "192.168.2.1",
+ locals: {},
+ params: {},
+ route: { id: null },
+ setHeaders: vi.fn(),
+ isDataRequest: false,
+ isSubRequest: false,
+ platform: {} as any,
+ });
- beforeEach(() => {
- mockResolve.mockResolvedValue(new Response("OK"));
- });
+ beforeEach(() => {
+ mockResolve.mockResolvedValue(new Response("OK"));
+ });
- it("should handle OPTIONS preflight requests", async () => {
- const event = createCORSEvent("OPTIONS");
+ it("should handle OPTIONS preflight requests", async () => {
+ const event = createCORSEvent("OPTIONS");
- const response = await handle({ event, resolve: mockResolve });
+ const response = await handle({ event, resolve: mockResolve });
- expect(response.status).toBe(200);
- expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
- expect(response.headers.get("Access-Control-Allow-Methods")).toContain("GET");
- expect(mockResolve).not.toHaveBeenCalled();
- });
+ expect(response.status).toBe(200);
+ expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
+ expect(response.headers.get("Access-Control-Allow-Methods")).toContain("GET");
+ expect(mockResolve).not.toHaveBeenCalled();
+ });
- it("should add CORS headers to API routes", async () => {
- const event = createCORSEvent();
+ it("should add CORS headers to API routes", async () => {
+ const event = createCORSEvent();
- const response = await handle({ event, resolve: mockResolve });
+ const response = await handle({ event, resolve: mockResolve });
- expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
- expect(response.headers.get("Access-Control-Allow-Methods")).toContain("GET");
- });
- });
+ expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
+ expect(response.headers.get("Access-Control-Allow-Methods")).toContain("GET");
+ });
+ });
- describe("security headers", () => {
- const mockResolve = vi.fn();
+ describe("security headers", () => {
+ const mockResolve = vi.fn();
- const createSecurityEvent = (protocol: string = "http", path: string = "/test") => ({
- url: new URL(`${protocol}://localhost${path}`),
- request: new Request(`${protocol}://localhost${path}`, {
- headers: {
- "x-forwarded-for": "192.168.3.1" // Different IP for security tests
- }
- }),
- cookies: {} as any,
- fetch: {} as any,
- getClientAddress: () => "192.168.3.1",
- locals: {},
- params: {},
- route: { id: null },
- setHeaders: vi.fn(),
- isDataRequest: false,
- isSubRequest: false,
- platform: {} as any
- });
+ const createSecurityEvent = (protocol: string = "http", path: string = "/test") => ({
+ url: new URL(`${protocol}://localhost${path}`),
+ request: new Request(`${protocol}://localhost${path}`, {
+ headers: {
+ "x-forwarded-for": "192.168.3.1", // Different IP for security tests
+ },
+ }),
+ cookies: {} as any,
+ fetch: {} as any,
+ getClientAddress: () => "192.168.3.1",
+ locals: {},
+ params: {},
+ route: { id: null },
+ setHeaders: vi.fn(),
+ isDataRequest: false,
+ isSubRequest: false,
+ platform: {} as any,
+ });
- beforeEach(() => {
- mockResolve.mockResolvedValue(new Response("OK"));
- });
+ beforeEach(() => {
+ mockResolve.mockResolvedValue(new Response("OK"));
+ });
- it("should add security headers to all responses", async () => {
- const event = createSecurityEvent();
+ it("should add security headers to all responses", async () => {
+ const event = createSecurityEvent();
- const response = await handle({ event, resolve: mockResolve });
+ const response = await handle({ event, resolve: mockResolve });
- expect(response.headers.get("X-Frame-Options")).toBe("DENY");
- expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff");
- expect(response.headers.get("X-XSS-Protection")).toBe("1; mode=block");
- expect(response.headers.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin");
- expect(response.headers.get("Content-Security-Policy")).toContain("default-src 'self'");
- });
+ expect(response.headers.get("X-Frame-Options")).toBe("DENY");
+ expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff");
+ expect(response.headers.get("X-XSS-Protection")).toBe("1; mode=block");
+ expect(response.headers.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin");
+ expect(response.headers.get("Content-Security-Policy")).toContain("default-src 'self'");
+ });
- it("should add HSTS header for HTTPS requests", async () => {
- const event = createSecurityEvent("https");
+ it("should add HSTS header for HTTPS requests", async () => {
+ const event = createSecurityEvent("https");
- const response = await handle({ event, resolve: mockResolve });
+ const response = await handle({ event, resolve: mockResolve });
- expect(response.headers.get("Strict-Transport-Security")).toBe(
- "max-age=31536000; includeSubDomains; preload"
- );
- });
+ expect(response.headers.get("Strict-Transport-Security")).toBe(
+ "max-age=31536000; includeSubDomains; preload",
+ );
+ });
- it("should not add HSTS header for HTTP requests", async () => {
- const event = createSecurityEvent("http");
+ it("should not add HSTS header for HTTP requests", async () => {
+ const event = createSecurityEvent("http");
- const response = await handle({ event, resolve: mockResolve });
+ const response = await handle({ event, resolve: mockResolve });
- expect(response.headers.get("Strict-Transport-Security")).toBeNull();
- });
- });
+ expect(response.headers.get("Strict-Transport-Security")).toBeNull();
+ });
+ });
});
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
index 8d234ca..6c35a4b 100644
--- a/src/hooks.server.ts
+++ b/src/hooks.server.ts
@@ -11,36 +11,36 @@ import { i18nHandle } from "./server-hooks/i18nHandle";
import { building } from "$app/environment";
type Error = {
- message?: string;
- stack?: string;
+ message?: string;
+ stack?: string;
};
export async function handleError({ error, event, status, message }) {
- const errorLogger = logger.setContext("ERROR_HANDLER");
+ const errorLogger = logger.setContext("ERROR_HANDLER");
- errorLogger.error("Unhandled error occurred", {
- error: (error as Error).message,
- stack: (error as Error).stack,
- status,
- message,
- url: event.url.pathname,
- method: event.request.method,
- userAgent: event.request.headers.get("user-agent"),
- ip: !building ? event.getClientAddress() : "server"
- });
+ errorLogger.error("Unhandled error occurred", {
+ error: (error as Error).message,
+ stack: (error as Error).stack,
+ status,
+ message,
+ url: event.url.pathname,
+ method: event.request.method,
+ userAgent: event.request.headers.get("user-agent"),
+ ip: !building ? event.getClientAddress() : "server",
+ });
- return {
- message: "Internal server error occurred"
- };
+ return {
+ message: "Internal server error occurred",
+ };
}
export const handle = sequence(
- startupHandle,
- loggingHandle,
- i18nHandle,
- rateLimitHandle,
- corsHandle,
- secHeaderHandle,
- apiAuthHandle,
- authGuard
+ startupHandle,
+ loggingHandle,
+ i18nHandle,
+ rateLimitHandle,
+ corsHandle,
+ secHeaderHandle,
+ apiAuthHandle,
+ authGuard,
);
diff --git a/src/lib/components/layouts/centered-card/action-hint.svelte b/src/lib/components/layouts/centered-card/action-hint.svelte
index e053c8d..afdae93 100644
--- a/src/lib/components/layouts/centered-card/action-hint.svelte
+++ b/src/lib/components/layouts/centered-card/action-hint.svelte
@@ -1,10 +1,10 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/layouts/centered-card/action.svelte b/src/lib/components/layouts/centered-card/action.svelte
index eab531b..54003e9 100644
--- a/src/lib/components/layouts/centered-card/action.svelte
+++ b/src/lib/components/layouts/centered-card/action.svelte
@@ -1,9 +1,9 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/layouts/centered-card/description.svelte b/src/lib/components/layouts/centered-card/description.svelte
index df36383..13b76c0 100644
--- a/src/lib/components/layouts/centered-card/description.svelte
+++ b/src/lib/components/layouts/centered-card/description.svelte
@@ -1,11 +1,11 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/layouts/centered-card/header.svelte b/src/lib/components/layouts/centered-card/header.svelte
index 0f8d05d..4661704 100644
--- a/src/lib/components/layouts/centered-card/header.svelte
+++ b/src/lib/components/layouts/centered-card/header.svelte
@@ -1,9 +1,9 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/layouts/centered-card/main.svelte b/src/lib/components/layouts/centered-card/main.svelte
index 925987c..c6a3a35 100644
--- a/src/lib/components/layouts/centered-card/main.svelte
+++ b/src/lib/components/layouts/centered-card/main.svelte
@@ -1,10 +1,10 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/layouts/centered-card/root.svelte b/src/lib/components/layouts/centered-card/root.svelte
index 3715f1d..805cac2 100644
--- a/src/lib/components/layouts/centered-card/root.svelte
+++ b/src/lib/components/layouts/centered-card/root.svelte
@@ -1,21 +1,21 @@
-
- {@render children?.()}
-
+
+ {@render children?.()}
+
diff --git a/src/lib/components/layouts/centered-card/title.svelte b/src/lib/components/layouts/centered-card/title.svelte
index 9efb262..5f72de2 100644
--- a/src/lib/components/layouts/centered-card/title.svelte
+++ b/src/lib/components/layouts/centered-card/title.svelte
@@ -1,11 +1,11 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/layouts/empty-layout/action.svelte b/src/lib/components/layouts/empty-layout/action.svelte
index eac18d1..5a52c0b 100644
--- a/src/lib/components/layouts/empty-layout/action.svelte
+++ b/src/lib/components/layouts/empty-layout/action.svelte
@@ -1,7 +1,7 @@
{@render children?.()}
diff --git a/src/lib/components/layouts/empty-layout/main.svelte b/src/lib/components/layouts/empty-layout/main.svelte
index e8a5e3a..78aae87 100644
--- a/src/lib/components/layouts/empty-layout/main.svelte
+++ b/src/lib/components/layouts/empty-layout/main.svelte
@@ -1,16 +1,16 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/layouts/empty-layout/root.svelte b/src/lib/components/layouts/empty-layout/root.svelte
index c0150e2..449a868 100644
--- a/src/lib/components/layouts/empty-layout/root.svelte
+++ b/src/lib/components/layouts/empty-layout/root.svelte
@@ -1,18 +1,18 @@
-
- {@render children?.()}
-
+
+ {@render children?.()}
+
diff --git a/src/lib/components/layouts/sidebar-layout/components/app-sidebar.svelte b/src/lib/components/layouts/sidebar-layout/components/app-sidebar.svelte
index 1fc0379..7ddc892 100644
--- a/src/lib/components/layouts/sidebar-layout/components/app-sidebar.svelte
+++ b/src/lib/components/layouts/sidebar-layout/components/app-sidebar.svelte
@@ -1,27 +1,27 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/components/layouts/sidebar-layout/components/nav-primary.svelte b/src/lib/components/layouts/sidebar-layout/components/nav-primary.svelte
index 72def17..c5be0ea 100644
--- a/src/lib/components/layouts/sidebar-layout/components/nav-primary.svelte
+++ b/src/lib/components/layouts/sidebar-layout/components/nav-primary.svelte
@@ -1,70 +1,70 @@
- {#each items as item (item.title)}
- {#if item.isTenatOnly === false || $auth.user?.tenantId}
-
-
- {#snippet child({ props })}
-
-
- {item.title}
-
- {/snippet}
-
-
- {/if}
- {/each}
+ {#each items as item (item.title)}
+ {#if item.isTenatOnly === false || $auth.user?.tenantId}
+
+
+ {#snippet child({ props })}
+
+
+ {item.title}
+
+ {/snippet}
+
+
+ {/if}
+ {/each}
diff --git a/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte b/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte
index ddfd2dc..3b4f34f 100644
--- a/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte
+++ b/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte
@@ -1,57 +1,57 @@
- {#each items as item (item.title)}
- {#if item.isTenatOnly === false || $auth.user?.tenantId}
-
-
- {#snippet child({ props })}
-
-
- {item.title}
- {#if item.url.startsWith("http")}
-
- {/if}
-
- {/snippet}
-
-
- {/if}
- {/each}
+ {#each items as item (item.title)}
+ {#if item.isTenatOnly === false || $auth.user?.tenantId}
+
+
+ {#snippet child({ props })}
+
+
+ {item.title}
+ {#if item.url.startsWith("http")}
+
+ {/if}
+
+ {/snippet}
+
+
+ {/if}
+ {/each}
diff --git a/src/lib/components/layouts/sidebar-layout/components/nav-user.svelte b/src/lib/components/layouts/sidebar-layout/components/nav-user.svelte
index b5c075c..854c49b 100644
--- a/src/lib/components/layouts/sidebar-layout/components/nav-user.svelte
+++ b/src/lib/components/layouts/sidebar-layout/components/nav-user.svelte
@@ -1,77 +1,77 @@
-
-
-
- {#snippet child({ props })}
-
- {/snippet}
-
-
-
-
+
+
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+
+
diff --git a/src/lib/components/layouts/sidebar-layout/components/tenant-switcher.svelte b/src/lib/components/layouts/sidebar-layout/components/tenant-switcher.svelte
index 13be67b..b7ac05c 100644
--- a/src/lib/components/layouts/sidebar-layout/components/tenant-switcher.svelte
+++ b/src/lib/components/layouts/sidebar-layout/components/tenant-switcher.svelte
@@ -1,85 +1,85 @@
-
-
-
- {#snippet child({ props })}
-
- {/snippet}
-
-
-
-
+
+
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+
+
diff --git a/src/lib/components/layouts/sidebar-layout/root.svelte b/src/lib/components/layouts/sidebar-layout/root.svelte
index e26df38..ccd4485 100644
--- a/src/lib/components/layouts/sidebar-layout/root.svelte
+++ b/src/lib/components/layouts/sidebar-layout/root.svelte
@@ -1,56 +1,56 @@
sidebar.setOpen(open)}>
-
-
-
-
-
- {@render children?.()}
-
-
-
+
+
+
+
+
+ {@render children?.()}
+
+
+
diff --git a/src/lib/components/templates/empty-state/center-loading-state.svelte b/src/lib/components/templates/empty-state/center-loading-state.svelte
index 5f4cad6..fc3b146 100644
--- a/src/lib/components/templates/empty-state/center-loading-state.svelte
+++ b/src/lib/components/templates/empty-state/center-loading-state.svelte
@@ -1,14 +1,14 @@
-
-
-
-
+
+
+
+
diff --git a/src/lib/components/templates/empty-state/empty-state.svelte b/src/lib/components/templates/empty-state/empty-state.svelte
index e07ec90..a476adb 100644
--- a/src/lib/components/templates/empty-state/empty-state.svelte
+++ b/src/lib/components/templates/empty-state/empty-state.svelte
@@ -1,18 +1,18 @@
-
- {headline}
- {description}
+
+ {headline}
+ {description}
diff --git a/src/lib/components/templates/language-switch/language-switch.svelte b/src/lib/components/templates/language-switch/language-switch.svelte
index 7c7611e..6267f9d 100644
--- a/src/lib/components/templates/language-switch/language-switch.svelte
+++ b/src/lib/components/templates/language-switch/language-switch.svelte
@@ -1,41 +1,41 @@
- {
- setLocale(value as "de" | "en");
- }}
- labels={{
- placeholder: m["i18n.label"](),
- search: m["i18n.search"](),
- notFound: m["i18n.notFound"]()
- }}
- {triggerVariant}
- {triggerSize}
- {triggerClass}
- class={cn("w-auto")}
- />
+ {
+ setLocale(value as "de" | "en");
+ }}
+ labels={{
+ placeholder: m["i18n.label"](),
+ search: m["i18n.search"](),
+ notFound: m["i18n.notFound"](),
+ }}
+ {triggerVariant}
+ {triggerSize}
+ {triggerClass}
+ class={cn("w-auto")}
+ />
diff --git a/src/lib/components/ui/avatar/avatar-fallback.svelte b/src/lib/components/ui/avatar/avatar-fallback.svelte
index 249d4a4..fee7425 100644
--- a/src/lib/components/ui/avatar/avatar-fallback.svelte
+++ b/src/lib/components/ui/avatar/avatar-fallback.svelte
@@ -1,17 +1,17 @@
diff --git a/src/lib/components/ui/avatar/avatar-image.svelte b/src/lib/components/ui/avatar/avatar-image.svelte
index 2bb9db4..8f9ceea 100644
--- a/src/lib/components/ui/avatar/avatar-image.svelte
+++ b/src/lib/components/ui/avatar/avatar-image.svelte
@@ -1,17 +1,17 @@
diff --git a/src/lib/components/ui/avatar/avatar.svelte b/src/lib/components/ui/avatar/avatar.svelte
index e37214d..0832b53 100644
--- a/src/lib/components/ui/avatar/avatar.svelte
+++ b/src/lib/components/ui/avatar/avatar.svelte
@@ -1,19 +1,19 @@
diff --git a/src/lib/components/ui/avatar/index.ts b/src/lib/components/ui/avatar/index.ts
index b08c780..71f5b20 100644
--- a/src/lib/components/ui/avatar/index.ts
+++ b/src/lib/components/ui/avatar/index.ts
@@ -3,11 +3,11 @@ import Image from "./avatar-image.svelte";
import Fallback from "./avatar-fallback.svelte";
export {
- Root,
- Image,
- Fallback,
- //
- Root as Avatar,
- Image as AvatarImage,
- Fallback as AvatarFallback
+ Root,
+ Image,
+ Fallback,
+ //
+ Root as Avatar,
+ Image as AvatarImage,
+ Fallback as AvatarFallback,
};
diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte
index a178cf5..04a9a3c 100644
--- a/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte
+++ b/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte
@@ -1,23 +1,23 @@
-
- More
+
+ More
diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte
index 1a84c4c..4693cdb 100644
--- a/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte
+++ b/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte
@@ -1,20 +1,20 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte
index 382b6ab..0bdfc96 100644
--- a/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte
+++ b/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte
@@ -1,31 +1,31 @@
{#if child}
- {@render child({ props: attrs })}
+ {@render child({ props: attrs })}
{:else}
-
- {@render children?.()}
-
+
+ {@render children?.()}
+
{/if}
diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte
index 1272a37..01464a0 100644
--- a/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte
+++ b/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte
@@ -1,23 +1,23 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte
index 5fb6979..f9041cd 100644
--- a/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte
+++ b/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte
@@ -1,23 +1,23 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte
index 84106a1..37735d2 100644
--- a/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte
+++ b/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte
@@ -1,27 +1,27 @@
svg]:size-3.5", className)}
- {...restProps}
+ bind:this={ref}
+ data-slot="breadcrumb-separator"
+ role="presentation"
+ aria-hidden="true"
+ class={cn("[&>svg]:size-3.5", className)}
+ {...restProps}
>
- {#if children}
- {@render children?.()}
- {:else}
-
- {/if}
+ {#if children}
+ {@render children?.()}
+ {:else}
+
+ {/if}
diff --git a/src/lib/components/ui/breadcrumb/breadcrumb.svelte b/src/lib/components/ui/breadcrumb/breadcrumb.svelte
index 8f8a3e6..b80e3f3 100644
--- a/src/lib/components/ui/breadcrumb/breadcrumb.svelte
+++ b/src/lib/components/ui/breadcrumb/breadcrumb.svelte
@@ -1,21 +1,21 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/breadcrumb/index.ts b/src/lib/components/ui/breadcrumb/index.ts
index 2651956..72e5847 100644
--- a/src/lib/components/ui/breadcrumb/index.ts
+++ b/src/lib/components/ui/breadcrumb/index.ts
@@ -7,19 +7,19 @@ import List from "./breadcrumb-list.svelte";
import Page from "./breadcrumb-page.svelte";
export {
- Root,
- Ellipsis,
- Item,
- Separator,
- Link,
- List,
- Page,
- //
- Root as Breadcrumb,
- Ellipsis as BreadcrumbEllipsis,
- Item as BreadcrumbItem,
- Separator as BreadcrumbSeparator,
- Link as BreadcrumbLink,
- List as BreadcrumbList,
- Page as BreadcrumbPage
+ Root,
+ Ellipsis,
+ Item,
+ Separator,
+ Link,
+ List,
+ Page,
+ //
+ Root as Breadcrumb,
+ Ellipsis as BreadcrumbEllipsis,
+ Item as BreadcrumbItem,
+ Separator as BreadcrumbSeparator,
+ Link as BreadcrumbLink,
+ List as BreadcrumbList,
+ Page as BreadcrumbPage,
};
diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte
index d5bf4bc..1b15395 100644
--- a/src/lib/components/ui/button/button.svelte
+++ b/src/lib/components/ui/button/button.svelte
@@ -1,87 +1,87 @@
{#if href}
-
- {@render children?.()}
-
+
+ {@render children?.()}
+
{:else}
-
- {#if isLoading}
-
- {/if}
- {@render children?.()}
-
+
+ {#if isLoading}
+
+ {/if}
+ {@render children?.()}
+
{/if}
diff --git a/src/lib/components/ui/button/index.ts b/src/lib/components/ui/button/index.ts
index 068bfa2..872d97c 100644
--- a/src/lib/components/ui/button/index.ts
+++ b/src/lib/components/ui/button/index.ts
@@ -1,17 +1,17 @@
import Root, {
- type ButtonProps,
- type ButtonSize,
- type ButtonVariant,
- buttonVariants
+ type ButtonProps,
+ type ButtonSize,
+ type ButtonVariant,
+ buttonVariants,
} from "./button.svelte";
export {
- Root,
- type ButtonProps as Props,
- //
- Root as Button,
- buttonVariants,
- type ButtonProps,
- type ButtonSize,
- type ButtonVariant
+ Root,
+ type ButtonProps as Props,
+ //
+ Root as Button,
+ buttonVariants,
+ type ButtonProps,
+ type ButtonSize,
+ type ButtonVariant,
};
diff --git a/src/lib/components/ui/card/card-action.svelte b/src/lib/components/ui/card/card-action.svelte
index cc36c56..aea2eb9 100644
--- a/src/lib/components/ui/card/card-action.svelte
+++ b/src/lib/components/ui/card/card-action.svelte
@@ -1,20 +1,20 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/card/card-content.svelte b/src/lib/components/ui/card/card-content.svelte
index bc90b83..c55a8d4 100644
--- a/src/lib/components/ui/card/card-content.svelte
+++ b/src/lib/components/ui/card/card-content.svelte
@@ -1,15 +1,15 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/card/card-description.svelte b/src/lib/components/ui/card/card-description.svelte
index 9b20ac7..4ad41e5 100644
--- a/src/lib/components/ui/card/card-description.svelte
+++ b/src/lib/components/ui/card/card-description.svelte
@@ -1,20 +1,20 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/card/card-footer.svelte b/src/lib/components/ui/card/card-footer.svelte
index 2d4d0f2..65a1041 100644
--- a/src/lib/components/ui/card/card-footer.svelte
+++ b/src/lib/components/ui/card/card-footer.svelte
@@ -1,20 +1,20 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/card/card-header.svelte b/src/lib/components/ui/card/card-header.svelte
index 2501788..843fb3b 100644
--- a/src/lib/components/ui/card/card-header.svelte
+++ b/src/lib/components/ui/card/card-header.svelte
@@ -1,23 +1,23 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/card/card-title.svelte b/src/lib/components/ui/card/card-title.svelte
index 7447231..053db45 100644
--- a/src/lib/components/ui/card/card-title.svelte
+++ b/src/lib/components/ui/card/card-title.svelte
@@ -1,20 +1,20 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/card/card.svelte b/src/lib/components/ui/card/card.svelte
index f6cc3e5..2a494c2 100644
--- a/src/lib/components/ui/card/card.svelte
+++ b/src/lib/components/ui/card/card.svelte
@@ -1,23 +1,23 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/card/index.ts b/src/lib/components/ui/card/index.ts
index 10daffb..406a5ce 100644
--- a/src/lib/components/ui/card/index.ts
+++ b/src/lib/components/ui/card/index.ts
@@ -7,19 +7,19 @@ import Title from "./card-title.svelte";
import Action from "./card-action.svelte";
export {
- Root,
- Content,
- Description,
- Footer,
- Header,
- Title,
- Action,
- //
- Root as Card,
- Content as CardContent,
- Description as CardDescription,
- Footer as CardFooter,
- Header as CardHeader,
- Title as CardTitle,
- Action as CardAction
+ Root,
+ Content,
+ Description,
+ Footer,
+ Header,
+ Title,
+ Action,
+ //
+ Root as Card,
+ Content as CardContent,
+ Description as CardDescription,
+ Footer as CardFooter,
+ Header as CardHeader,
+ Title as CardTitle,
+ Action as CardAction,
};
diff --git a/src/lib/components/ui/collapsible/collapsible-content.svelte b/src/lib/components/ui/collapsible/collapsible-content.svelte
index bdabb55..81a6b0f 100644
--- a/src/lib/components/ui/collapsible/collapsible-content.svelte
+++ b/src/lib/components/ui/collapsible/collapsible-content.svelte
@@ -1,7 +1,7 @@
diff --git a/src/lib/components/ui/collapsible/collapsible-trigger.svelte b/src/lib/components/ui/collapsible/collapsible-trigger.svelte
index ece7ad6..f67035d 100644
--- a/src/lib/components/ui/collapsible/collapsible-trigger.svelte
+++ b/src/lib/components/ui/collapsible/collapsible-trigger.svelte
@@ -1,7 +1,7 @@
diff --git a/src/lib/components/ui/collapsible/collapsible.svelte b/src/lib/components/ui/collapsible/collapsible.svelte
index 39cdd4e..4e6fef5 100644
--- a/src/lib/components/ui/collapsible/collapsible.svelte
+++ b/src/lib/components/ui/collapsible/collapsible.svelte
@@ -1,11 +1,11 @@
diff --git a/src/lib/components/ui/collapsible/index.ts b/src/lib/components/ui/collapsible/index.ts
index d5db2aa..296a91b 100644
--- a/src/lib/components/ui/collapsible/index.ts
+++ b/src/lib/components/ui/collapsible/index.ts
@@ -3,11 +3,11 @@ import Trigger from "./collapsible-trigger.svelte";
import Content from "./collapsible-content.svelte";
export {
- Root,
- Content,
- Trigger,
- //
- Root as Collapsible,
- Content as CollapsibleContent,
- Trigger as CollapsibleTrigger
+ Root,
+ Content,
+ Trigger,
+ //
+ Root as Collapsible,
+ Content as CollapsibleContent,
+ Trigger as CollapsibleTrigger,
};
diff --git a/src/lib/components/ui/combobox/combobox.svelte b/src/lib/components/ui/combobox/combobox.svelte
index 9353567..51cd417 100644
--- a/src/lib/components/ui/combobox/combobox.svelte
+++ b/src/lib/components/ui/combobox/combobox.svelte
@@ -1,89 +1,89 @@
-
- {#snippet child({ props })}
-
- {selectedValue || labels.placeholder}
-
-
- {/snippet}
-
-
-
-
-
- {labels.notFound}
-
- {#each options as option (option.value)}
- {
- onChange(option.value);
- closeAndFocusTrigger();
- }}
- >
-
- {option.label}
-
- {/each}
-
-
-
-
+
+ {#snippet child({ props })}
+
+ {selectedValue || labels.placeholder}
+
+
+ {/snippet}
+
+
+
+
+
+ {labels.notFound}
+
+ {#each options as option (option.value)}
+ {
+ onChange(option.value);
+ closeAndFocusTrigger();
+ }}
+ >
+
+ {option.label}
+
+ {/each}
+
+
+
+
diff --git a/src/lib/components/ui/command/command-dialog.svelte b/src/lib/components/ui/command/command-dialog.svelte
index 4bdb740..5c97b28 100644
--- a/src/lib/components/ui/command/command-dialog.svelte
+++ b/src/lib/components/ui/command/command-dialog.svelte
@@ -1,40 +1,40 @@
-
- {title}
- {description}
-
-
-
-
+
+ {title}
+ {description}
+
+
+
+
diff --git a/src/lib/components/ui/command/command-empty.svelte b/src/lib/components/ui/command/command-empty.svelte
index 6726cd8..d1c8728 100644
--- a/src/lib/components/ui/command/command-empty.svelte
+++ b/src/lib/components/ui/command/command-empty.svelte
@@ -1,17 +1,17 @@
diff --git a/src/lib/components/ui/command/command-group.svelte b/src/lib/components/ui/command/command-group.svelte
index 9e3c3be..6c30c54 100644
--- a/src/lib/components/ui/command/command-group.svelte
+++ b/src/lib/components/ui/command/command-group.svelte
@@ -1,30 +1,30 @@
- {#if heading}
-
- {heading}
-
- {/if}
-
+ {#if heading}
+
+ {heading}
+
+ {/if}
+
diff --git a/src/lib/components/ui/command/command-input.svelte b/src/lib/components/ui/command/command-input.svelte
index 9e5cf7a..b3fc4a3 100644
--- a/src/lib/components/ui/command/command-input.svelte
+++ b/src/lib/components/ui/command/command-input.svelte
@@ -1,26 +1,26 @@
-
-
+
+
diff --git a/src/lib/components/ui/command/command-item.svelte b/src/lib/components/ui/command/command-item.svelte
index 5833416..5f44914 100644
--- a/src/lib/components/ui/command/command-item.svelte
+++ b/src/lib/components/ui/command/command-item.svelte
@@ -1,20 +1,20 @@
diff --git a/src/lib/components/ui/command/command-link-item.svelte b/src/lib/components/ui/command/command-link-item.svelte
index ada6d2c..12dbda6 100644
--- a/src/lib/components/ui/command/command-link-item.svelte
+++ b/src/lib/components/ui/command/command-link-item.svelte
@@ -1,20 +1,20 @@
diff --git a/src/lib/components/ui/command/command-list.svelte b/src/lib/components/ui/command/command-list.svelte
index 2d3a01a..cce4d5f 100644
--- a/src/lib/components/ui/command/command-list.svelte
+++ b/src/lib/components/ui/command/command-list.svelte
@@ -1,17 +1,17 @@
diff --git a/src/lib/components/ui/command/command-separator.svelte b/src/lib/components/ui/command/command-separator.svelte
index 35c4c95..9b27005 100644
--- a/src/lib/components/ui/command/command-separator.svelte
+++ b/src/lib/components/ui/command/command-separator.svelte
@@ -1,17 +1,17 @@
diff --git a/src/lib/components/ui/command/command-shortcut.svelte b/src/lib/components/ui/command/command-shortcut.svelte
index 3d68bc5..415609e 100644
--- a/src/lib/components/ui/command/command-shortcut.svelte
+++ b/src/lib/components/ui/command/command-shortcut.svelte
@@ -1,20 +1,20 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/command/command.svelte b/src/lib/components/ui/command/command.svelte
index c64a77e..92a8fe9 100644
--- a/src/lib/components/ui/command/command.svelte
+++ b/src/lib/components/ui/command/command.svelte
@@ -1,22 +1,22 @@
diff --git a/src/lib/components/ui/command/index.ts b/src/lib/components/ui/command/index.ts
index 14c2b32..b53a8ed 100644
--- a/src/lib/components/ui/command/index.ts
+++ b/src/lib/components/ui/command/index.ts
@@ -14,27 +14,27 @@ import LinkItem from "./command-link-item.svelte";
const Loading = CommandPrimitive.Loading;
export {
- Root,
- Dialog,
- Empty,
- Group,
- Item,
- LinkItem,
- Input,
- List,
- Separator,
- Shortcut,
- Loading,
- //
- Root as Command,
- Dialog as CommandDialog,
- Empty as CommandEmpty,
- Group as CommandGroup,
- Item as CommandItem,
- LinkItem as CommandLinkItem,
- Input as CommandInput,
- List as CommandList,
- Separator as CommandSeparator,
- Shortcut as CommandShortcut,
- Loading as CommandLoading
+ Root,
+ Dialog,
+ Empty,
+ Group,
+ Item,
+ LinkItem,
+ Input,
+ List,
+ Separator,
+ Shortcut,
+ Loading,
+ //
+ Root as Command,
+ Dialog as CommandDialog,
+ Empty as CommandEmpty,
+ Group as CommandGroup,
+ Item as CommandItem,
+ LinkItem as CommandLinkItem,
+ Input as CommandInput,
+ List as CommandList,
+ Separator as CommandSeparator,
+ Shortcut as CommandShortcut,
+ Loading as CommandLoading,
};
diff --git a/src/lib/components/ui/dialog/dialog-close.svelte b/src/lib/components/ui/dialog/dialog-close.svelte
index 840b2f6..7bcb438 100644
--- a/src/lib/components/ui/dialog/dialog-close.svelte
+++ b/src/lib/components/ui/dialog/dialog-close.svelte
@@ -1,7 +1,7 @@
diff --git a/src/lib/components/ui/dialog/dialog-content.svelte b/src/lib/components/ui/dialog/dialog-content.svelte
index 1610dcd..192fc2e 100644
--- a/src/lib/components/ui/dialog/dialog-content.svelte
+++ b/src/lib/components/ui/dialog/dialog-content.svelte
@@ -1,43 +1,43 @@
-
-
- {@render children?.()}
- {#if showCloseButton}
-
-
- Close
-
- {/if}
-
+
+
+ {@render children?.()}
+ {#if showCloseButton}
+
+
+ Close
+
+ {/if}
+
diff --git a/src/lib/components/ui/dialog/dialog-description.svelte b/src/lib/components/ui/dialog/dialog-description.svelte
index 3845023..5162cd3 100644
--- a/src/lib/components/ui/dialog/dialog-description.svelte
+++ b/src/lib/components/ui/dialog/dialog-description.svelte
@@ -1,17 +1,17 @@
diff --git a/src/lib/components/ui/dialog/dialog-footer.svelte b/src/lib/components/ui/dialog/dialog-footer.svelte
index e7ff446..fdb2ebf 100644
--- a/src/lib/components/ui/dialog/dialog-footer.svelte
+++ b/src/lib/components/ui/dialog/dialog-footer.svelte
@@ -1,20 +1,20 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/dialog/dialog-header.svelte b/src/lib/components/ui/dialog/dialog-header.svelte
index fc90cd9..be7438c 100644
--- a/src/lib/components/ui/dialog/dialog-header.svelte
+++ b/src/lib/components/ui/dialog/dialog-header.svelte
@@ -1,20 +1,20 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/dialog/dialog-overlay.svelte b/src/lib/components/ui/dialog/dialog-overlay.svelte
index f81ad83..21730ac 100644
--- a/src/lib/components/ui/dialog/dialog-overlay.svelte
+++ b/src/lib/components/ui/dialog/dialog-overlay.svelte
@@ -1,20 +1,20 @@
diff --git a/src/lib/components/ui/dialog/dialog-title.svelte b/src/lib/components/ui/dialog/dialog-title.svelte
index e4d4b34..5fb21c0 100644
--- a/src/lib/components/ui/dialog/dialog-title.svelte
+++ b/src/lib/components/ui/dialog/dialog-title.svelte
@@ -1,17 +1,17 @@
diff --git a/src/lib/components/ui/dialog/dialog-trigger.svelte b/src/lib/components/ui/dialog/dialog-trigger.svelte
index 9d1e801..92a8e3c 100644
--- a/src/lib/components/ui/dialog/dialog-trigger.svelte
+++ b/src/lib/components/ui/dialog/dialog-trigger.svelte
@@ -1,7 +1,7 @@
diff --git a/src/lib/components/ui/dialog/index.ts b/src/lib/components/ui/dialog/index.ts
index 790315c..07515e7 100644
--- a/src/lib/components/ui/dialog/index.ts
+++ b/src/lib/components/ui/dialog/index.ts
@@ -13,25 +13,25 @@ const Root = DialogPrimitive.Root;
const Portal = DialogPrimitive.Portal;
export {
- Root,
- Title,
- Portal,
- Footer,
- Header,
- Trigger,
- Overlay,
- Content,
- Description,
- Close,
- //
- Root as Dialog,
- Title as DialogTitle,
- Portal as DialogPortal,
- Footer as DialogFooter,
- Header as DialogHeader,
- Trigger as DialogTrigger,
- Overlay as DialogOverlay,
- Content as DialogContent,
- Description as DialogDescription,
- Close as DialogClose
+ Root,
+ Title,
+ Portal,
+ Footer,
+ Header,
+ Trigger,
+ Overlay,
+ Content,
+ Description,
+ Close,
+ //
+ Root as Dialog,
+ Title as DialogTitle,
+ Portal as DialogPortal,
+ Footer as DialogFooter,
+ Header as DialogHeader,
+ Trigger as DialogTrigger,
+ Overlay as DialogOverlay,
+ Content as DialogContent,
+ Description as DialogDescription,
+ Close as DialogClose,
};
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
index cf78621..b0825bb 100644
--- a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
@@ -1,41 +1,41 @@
- {#snippet children({ checked, indeterminate })}
-
- {#if indeterminate}
-
- {:else}
-
- {/if}
-
- {@render childrenProp?.()}
- {/snippet}
+ {#snippet children({ checked, indeterminate })}
+
+ {#if indeterminate}
+
+ {:else}
+
+ {/if}
+
+ {@render childrenProp?.()}
+ {/snippet}
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
index f1abd5d..685809b 100644
--- a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
@@ -1,27 +1,27 @@
-
+
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
index 48d14a9..e94a853 100644
--- a/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
@@ -1,22 +1,22 @@
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte
index aca1f7b..2172000 100644
--- a/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte
@@ -1,7 +1,7 @@
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
index 3f6159c..8a1a245 100644
--- a/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
@@ -1,27 +1,27 @@
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
index f72e477..14d11d7 100644
--- a/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
@@ -1,24 +1,24 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte
index 189aef4..d4bbe25 100644
--- a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte
@@ -1,16 +1,16 @@
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
index af8d5d7..9d8c9b9 100644
--- a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
@@ -1,31 +1,31 @@
- {#snippet children({ checked })}
-
- {#if checked}
-
- {/if}
-
- {@render childrenProp?.({ checked })}
- {/snippet}
+ {#snippet children({ checked })}
+
+ {#if checked}
+
+ {/if}
+
+ {@render childrenProp?.({ checked })}
+ {/snippet}
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
index 90f1b6f..7f2b04c 100644
--- a/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
@@ -1,17 +1,17 @@
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte
index 6974947..a283194 100644
--- a/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte
@@ -1,20 +1,20 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
index 7ea1437..3dc9b4c 100644
--- a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
@@ -1,20 +1,20 @@
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
index 32ccfd5..8d77bea 100644
--- a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
@@ -1,29 +1,29 @@
- {@render children?.()}
-
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte
index cb05344..bb88efd 100644
--- a/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte
@@ -1,7 +1,7 @@
diff --git a/src/lib/components/ui/dropdown-menu/index.ts b/src/lib/components/ui/dropdown-menu/index.ts
index 342f314..fd34142 100644
--- a/src/lib/components/ui/dropdown-menu/index.ts
+++ b/src/lib/components/ui/dropdown-menu/index.ts
@@ -16,34 +16,34 @@ const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
export {
- CheckboxItem,
- Content,
- Root as DropdownMenu,
- CheckboxItem as DropdownMenuCheckboxItem,
- Content as DropdownMenuContent,
- Group as DropdownMenuGroup,
- Item as DropdownMenuItem,
- Label as DropdownMenuLabel,
- RadioGroup as DropdownMenuRadioGroup,
- RadioItem as DropdownMenuRadioItem,
- Separator as DropdownMenuSeparator,
- Shortcut as DropdownMenuShortcut,
- Sub as DropdownMenuSub,
- SubContent as DropdownMenuSubContent,
- SubTrigger as DropdownMenuSubTrigger,
- Trigger as DropdownMenuTrigger,
- GroupHeading as DropdownMenuGroupHeading,
- Group,
- GroupHeading,
- Item,
- Label,
- RadioGroup,
- RadioItem,
- Root,
- Separator,
- Shortcut,
- Sub,
- SubContent,
- SubTrigger,
- Trigger
+ CheckboxItem,
+ Content,
+ Root as DropdownMenu,
+ CheckboxItem as DropdownMenuCheckboxItem,
+ Content as DropdownMenuContent,
+ Group as DropdownMenuGroup,
+ Item as DropdownMenuItem,
+ Label as DropdownMenuLabel,
+ RadioGroup as DropdownMenuRadioGroup,
+ RadioItem as DropdownMenuRadioItem,
+ Separator as DropdownMenuSeparator,
+ Shortcut as DropdownMenuShortcut,
+ Sub as DropdownMenuSub,
+ SubContent as DropdownMenuSubContent,
+ SubTrigger as DropdownMenuSubTrigger,
+ Trigger as DropdownMenuTrigger,
+ GroupHeading as DropdownMenuGroupHeading,
+ Group,
+ GroupHeading,
+ Item,
+ Label,
+ RadioGroup,
+ RadioItem,
+ Root,
+ Separator,
+ Shortcut,
+ Sub,
+ SubContent,
+ SubTrigger,
+ Trigger,
};
diff --git a/src/lib/components/ui/form/form-button.svelte b/src/lib/components/ui/form/form-button.svelte
index 12c6c85..59825a4 100644
--- a/src/lib/components/ui/form/form-button.svelte
+++ b/src/lib/components/ui/form/form-button.svelte
@@ -1,8 +1,8 @@
diff --git a/src/lib/components/ui/form/form-description.svelte b/src/lib/components/ui/form/form-description.svelte
index ed8719f..6ba1169 100644
--- a/src/lib/components/ui/form/form-description.svelte
+++ b/src/lib/components/ui/form/form-description.svelte
@@ -1,23 +1,23 @@
-
- {@render children?.()}
-
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/form/form-element-field.svelte b/src/lib/components/ui/form/form-element-field.svelte
index c3ba111..1fe1410 100644
--- a/src/lib/components/ui/form/form-element-field.svelte
+++ b/src/lib/components/ui/form/form-element-field.svelte
@@ -1,24 +1,24 @@
- {#snippet children({ constraints, errors, tainted, value })}
-
- {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })}
-
- {/snippet}
+ {#snippet children({ constraints, errors, tainted, value })}
+
+ {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })}
+
+ {/snippet}
diff --git a/src/lib/components/ui/form/form-field-errors.svelte b/src/lib/components/ui/form/form-field-errors.svelte
index 9d737a0..142515c 100644
--- a/src/lib/components/ui/form/form-field-errors.svelte
+++ b/src/lib/components/ui/form/form-field-errors.svelte
@@ -1,33 +1,33 @@
- {#snippet children({ errors, errorProps })}
- {#if childrenProp}
- {@render childrenProp({ errors, errorProps })}
- {:else}
-
- {#each errors as error (error)}
- {error}
- {/each}
-
- {/if}
- {/snippet}
+ {#snippet children({ errors, errorProps })}
+ {#if childrenProp}
+ {@render childrenProp({ errors, errorProps })}
+ {:else}
+
+ {#each errors as error (error)}
+ {error}
+ {/each}
+
+ {/if}
+ {/snippet}
diff --git a/src/lib/components/ui/form/form-field.svelte b/src/lib/components/ui/form/form-field.svelte
index 1398993..055c7b1 100644
--- a/src/lib/components/ui/form/form-field.svelte
+++ b/src/lib/components/ui/form/form-field.svelte
@@ -1,24 +1,24 @@
- {#snippet children({ constraints, errors, tainted, value })}
-
- {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })}
-
- {/snippet}
+ {#snippet children({ constraints, errors, tainted, value })}
+
+ {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })}
+
+ {/snippet}
diff --git a/src/lib/components/ui/form/form-fieldset.svelte b/src/lib/components/ui/form/form-fieldset.svelte
index 2c85857..cf0c8c2 100644
--- a/src/lib/components/ui/form/form-fieldset.svelte
+++ b/src/lib/components/ui/form/form-fieldset.svelte
@@ -1,15 +1,15 @@
diff --git a/src/lib/components/ui/form/form-label.svelte b/src/lib/components/ui/form/form-label.svelte
index 8749360..d4a14ef 100644
--- a/src/lib/components/ui/form/form-label.svelte
+++ b/src/lib/components/ui/form/form-label.svelte
@@ -1,24 +1,24 @@
- {#snippet child({ props })}
-
- {@render children?.()}
-
- {/snippet}
+ {#snippet child({ props })}
+
+ {@render children?.()}
+
+ {/snippet}
diff --git a/src/lib/components/ui/form/form-legend.svelte b/src/lib/components/ui/form/form-legend.svelte
index b3c9043..d81b2f7 100644
--- a/src/lib/components/ui/form/form-legend.svelte
+++ b/src/lib/components/ui/form/form-legend.svelte
@@ -1,16 +1,16 @@
diff --git a/src/lib/components/ui/form/form-root.svelte b/src/lib/components/ui/form/form-root.svelte
index 5462175..eaa8dc0 100644
--- a/src/lib/components/ui/form/form-root.svelte
+++ b/src/lib/components/ui/form/form-root.svelte
@@ -1,24 +1,24 @@
diff --git a/src/lib/components/ui/form/index.ts b/src/lib/components/ui/form/index.ts
index 1c07028..a429d46 100644
--- a/src/lib/components/ui/form/index.ts
+++ b/src/lib/components/ui/form/index.ts
@@ -12,24 +12,24 @@ import Button from "./form-button.svelte";
const Control = FormPrimitive.Control;
export {
- Root,
- Field,
- Control,
- Label,
- Button,
- FieldErrors,
- Description,
- Fieldset,
- Legend,
- ElementField,
- //
- Field as FormField,
- Control as FormControl,
- Description as FormDescription,
- Label as FormLabel,
- FieldErrors as FormFieldErrors,
- Fieldset as FormFieldset,
- Legend as FormLegend,
- ElementField as FormElementField,
- Button as FormButton
+ Root,
+ Field,
+ Control,
+ Label,
+ Button,
+ FieldErrors,
+ Description,
+ Fieldset,
+ Legend,
+ ElementField,
+ //
+ Field as FormField,
+ Control as FormControl,
+ Description as FormDescription,
+ Label as FormLabel,
+ FieldErrors as FormFieldErrors,
+ Fieldset as FormFieldset,
+ Legend as FormLegend,
+ ElementField as FormElementField,
+ Button as FormButton,
};
diff --git a/src/lib/components/ui/input/index.ts b/src/lib/components/ui/input/index.ts
index c9ffe28..ceb4b16 100644
--- a/src/lib/components/ui/input/index.ts
+++ b/src/lib/components/ui/input/index.ts
@@ -1,7 +1,7 @@
import Root from "./input.svelte";
export {
- Root,
- //
- Root as Input
+ Root,
+ //
+ Root as Input,
};
diff --git a/src/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte
index 29c91d8..7ae3572 100644
--- a/src/lib/components/ui/input/input.svelte
+++ b/src/lib/components/ui/input/input.svelte
@@ -1,51 +1,51 @@
{#if type === "file"}
-
+
{:else}
-
+
{/if}
diff --git a/src/lib/components/ui/label/index.ts b/src/lib/components/ui/label/index.ts
index 2c3128c..b0b23ce 100644
--- a/src/lib/components/ui/label/index.ts
+++ b/src/lib/components/ui/label/index.ts
@@ -1,7 +1,7 @@
import Root from "./label.svelte";
export {
- Root,
- //
- Root as Label
+ Root,
+ //
+ Root as Label,
};
diff --git a/src/lib/components/ui/label/label.svelte b/src/lib/components/ui/label/label.svelte
index d71afbc..b34ba65 100644
--- a/src/lib/components/ui/label/label.svelte
+++ b/src/lib/components/ui/label/label.svelte
@@ -1,20 +1,20 @@
diff --git a/src/lib/components/ui/page/horizontal-page-padding.svelte b/src/lib/components/ui/page/horizontal-page-padding.svelte
index 5da1b62..362a852 100644
--- a/src/lib/components/ui/page/horizontal-page-padding.svelte
+++ b/src/lib/components/ui/page/horizontal-page-padding.svelte
@@ -1,16 +1,16 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/page/page-with-claim.svelte b/src/lib/components/ui/page/page-with-claim.svelte
index b93c625..b72067d 100644
--- a/src/lib/components/ui/page/page-with-claim.svelte
+++ b/src/lib/components/ui/page/page-with-claim.svelte
@@ -1,53 +1,53 @@
- {#if isWithLanguageSwitch}
-
-
-
- {/if}
- {@render children?.()}
+ {#if isWithLanguageSwitch}
+
+
+
+ {/if}
+ {@render children?.()}
-
- {#if dev}
-
- Viewport:
- xs
- sm
- md
- lg
- xl
- 2xl
-
- {/if}
-
- {m.poweredBy()}
- OpenReception
-
- {#if dev}
-
- {mode?.current === "dark" ? "dark" : "light"}Mode
-
- {/if}
-
+
+ {#if dev}
+
+ Viewport:
+ xs
+ sm
+ md
+ lg
+ xl
+ 2xl
+
+ {/if}
+
+ {m.poweredBy()}
+ OpenReception
+
+ {#if dev}
+
+ {mode?.current === "dark" ? "dark" : "light"}Mode
+
+ {/if}
+
diff --git a/src/lib/components/ui/passkey/state.svelte b/src/lib/components/ui/passkey/state.svelte
index be07830..f00085d 100644
--- a/src/lib/components/ui/passkey/state.svelte
+++ b/src/lib/components/ui/passkey/state.svelte
@@ -1,55 +1,55 @@
- {#if state === "initial"}
-
- {m["passkey.add.initial"]()}
- {:else if state === "click"}
-
- {m["passkey.add.click"]()}
- {:else if state === "loading"}
-
- {m["passkey.add.loading"]()}
- {:else if state === "user"}
-
- {m["passkey.add.user"]()}
- {:else if state === "success"}
-
- {m["passkey.add.success"]()}
- {:else if state === "error"}
-
- {m["passkey.add.error"]()}
- {/if}
+ {#if state === "initial"}
+
+ {m["passkey.add.initial"]()}
+ {:else if state === "click"}
+
+ {m["passkey.add.click"]()}
+ {:else if state === "loading"}
+
+ {m["passkey.add.loading"]()}
+ {:else if state === "user"}
+
+ {m["passkey.add.user"]()}
+ {:else if state === "success"}
+
+ {m["passkey.add.success"]()}
+ {:else if state === "error"}
+
+ {m["passkey.add.error"]()}
+ {/if}
diff --git a/src/lib/components/ui/popover/index.ts b/src/lib/components/ui/popover/index.ts
index 1a8a309..5eba297 100644
--- a/src/lib/components/ui/popover/index.ts
+++ b/src/lib/components/ui/popover/index.ts
@@ -5,13 +5,13 @@ const Root = PopoverPrimitive.Root;
const Close = PopoverPrimitive.Close;
export {
- Root,
- Content,
- Trigger,
- Close,
- //
- Root as Popover,
- Content as PopoverContent,
- Trigger as PopoverTrigger,
- Close as PopoverClose
+ Root,
+ Content,
+ Trigger,
+ Close,
+ //
+ Root as Popover,
+ Content as PopoverContent,
+ Trigger as PopoverTrigger,
+ Close as PopoverClose,
};
diff --git a/src/lib/components/ui/popover/popover-content.svelte b/src/lib/components/ui/popover/popover-content.svelte
index a487233..815dfd9 100644
--- a/src/lib/components/ui/popover/popover-content.svelte
+++ b/src/lib/components/ui/popover/popover-content.svelte
@@ -1,29 +1,29 @@
-
+
diff --git a/src/lib/components/ui/popover/popover-trigger.svelte b/src/lib/components/ui/popover/popover-trigger.svelte
index 586323c..a58180b 100644
--- a/src/lib/components/ui/popover/popover-trigger.svelte
+++ b/src/lib/components/ui/popover/popover-trigger.svelte
@@ -1,17 +1,17 @@
diff --git a/src/lib/components/ui/separator/index.ts b/src/lib/components/ui/separator/index.ts
index dbfb139..d66644e 100644
--- a/src/lib/components/ui/separator/index.ts
+++ b/src/lib/components/ui/separator/index.ts
@@ -1,7 +1,7 @@
import Root from "./separator.svelte";
export {
- Root,
- //
- Root as Separator
+ Root,
+ //
+ Root as Separator,
};
diff --git a/src/lib/components/ui/separator/separator.svelte b/src/lib/components/ui/separator/separator.svelte
index 25a0662..eb0d628 100644
--- a/src/lib/components/ui/separator/separator.svelte
+++ b/src/lib/components/ui/separator/separator.svelte
@@ -1,20 +1,20 @@
diff --git a/src/lib/components/ui/sheet/index.ts b/src/lib/components/ui/sheet/index.ts
index 29e0d0c..ffa765d 100644
--- a/src/lib/components/ui/sheet/index.ts
+++ b/src/lib/components/ui/sheet/index.ts
@@ -12,25 +12,25 @@ const Root = SheetPrimitive.Root;
const Portal = SheetPrimitive.Portal;
export {
- Root,
- Close,
- Trigger,
- Portal,
- Overlay,
- Content,
- Header,
- Footer,
- Title,
- Description,
- //
- Root as Sheet,
- Close as SheetClose,
- Trigger as SheetTrigger,
- Portal as SheetPortal,
- Overlay as SheetOverlay,
- Content as SheetContent,
- Header as SheetHeader,
- Footer as SheetFooter,
- Title as SheetTitle,
- Description as SheetDescription
+ Root,
+ Close,
+ Trigger,
+ Portal,
+ Overlay,
+ Content,
+ Header,
+ Footer,
+ Title,
+ Description,
+ //
+ Root as Sheet,
+ Close as SheetClose,
+ Trigger as SheetTrigger,
+ Portal as SheetPortal,
+ Overlay as SheetOverlay,
+ Content as SheetContent,
+ Header as SheetHeader,
+ Footer as SheetFooter,
+ Title as SheetTitle,
+ Description as SheetDescription,
};
diff --git a/src/lib/components/ui/sheet/sheet-close.svelte b/src/lib/components/ui/sheet/sheet-close.svelte
index ae382c1..a198386 100644
--- a/src/lib/components/ui/sheet/sheet-close.svelte
+++ b/src/lib/components/ui/sheet/sheet-close.svelte
@@ -1,7 +1,7 @@
diff --git a/src/lib/components/ui/sheet/sheet-content.svelte b/src/lib/components/ui/sheet/sheet-content.svelte
index c4ce1d8..e034221 100644
--- a/src/lib/components/ui/sheet/sheet-content.svelte
+++ b/src/lib/components/ui/sheet/sheet-content.svelte
@@ -1,60 +1,60 @@
-
-
- {@render children?.()}
-
-
- Close
-
-
+
+
+ {@render children?.()}
+
+
+ Close
+
+
diff --git a/src/lib/components/ui/sheet/sheet-description.svelte b/src/lib/components/ui/sheet/sheet-description.svelte
index 333b17a..5066740 100644
--- a/src/lib/components/ui/sheet/sheet-description.svelte
+++ b/src/lib/components/ui/sheet/sheet-description.svelte
@@ -1,17 +1,17 @@
diff --git a/src/lib/components/ui/sheet/sheet-footer.svelte b/src/lib/components/ui/sheet/sheet-footer.svelte
index dd9ed84..12aba29 100644
--- a/src/lib/components/ui/sheet/sheet-footer.svelte
+++ b/src/lib/components/ui/sheet/sheet-footer.svelte
@@ -1,20 +1,20 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/sheet/sheet-header.svelte b/src/lib/components/ui/sheet/sheet-header.svelte
index 757a6a5..17e6bce 100644
--- a/src/lib/components/ui/sheet/sheet-header.svelte
+++ b/src/lib/components/ui/sheet/sheet-header.svelte
@@ -1,20 +1,20 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/sheet/sheet-overlay.svelte b/src/lib/components/ui/sheet/sheet-overlay.svelte
index 345e197..450f861 100644
--- a/src/lib/components/ui/sheet/sheet-overlay.svelte
+++ b/src/lib/components/ui/sheet/sheet-overlay.svelte
@@ -1,20 +1,20 @@
diff --git a/src/lib/components/ui/sheet/sheet-title.svelte b/src/lib/components/ui/sheet/sheet-title.svelte
index 9fda327..daf6ec1 100644
--- a/src/lib/components/ui/sheet/sheet-title.svelte
+++ b/src/lib/components/ui/sheet/sheet-title.svelte
@@ -1,17 +1,17 @@
diff --git a/src/lib/components/ui/sheet/sheet-trigger.svelte b/src/lib/components/ui/sheet/sheet-trigger.svelte
index e266975..dc9d59c 100644
--- a/src/lib/components/ui/sheet/sheet-trigger.svelte
+++ b/src/lib/components/ui/sheet/sheet-trigger.svelte
@@ -1,7 +1,7 @@
diff --git a/src/lib/components/ui/sidebar/context.svelte.ts b/src/lib/components/ui/sidebar/context.svelte.ts
index 6f9b4a9..7223a7a 100644
--- a/src/lib/components/ui/sidebar/context.svelte.ts
+++ b/src/lib/components/ui/sidebar/context.svelte.ts
@@ -5,56 +5,56 @@ import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js";
type Getter = () => T;
export type SidebarStateProps = {
- /**
- * A getter function that returns the current open state of the sidebar.
- * We use a getter function here to support `bind:open` on the `Sidebar.Provider`
- * component.
- */
- open: Getter;
+ /**
+ * A getter function that returns the current open state of the sidebar.
+ * We use a getter function here to support `bind:open` on the `Sidebar.Provider`
+ * component.
+ */
+ open: Getter;
- /**
- * A function that sets the open state of the sidebar. To support `bind:open`, we need
- * a source of truth for changing the open state to ensure it will be synced throughout
- * the sub-components and any `bind:` references.
- */
- setOpen: (open: boolean) => void;
+ /**
+ * A function that sets the open state of the sidebar. To support `bind:open`, we need
+ * a source of truth for changing the open state to ensure it will be synced throughout
+ * the sub-components and any `bind:` references.
+ */
+ setOpen: (open: boolean) => void;
};
class SidebarState {
- readonly props: SidebarStateProps;
- open = $derived.by(() => this.props.open());
- openMobile = $state(false);
- setOpen: SidebarStateProps["setOpen"];
- #isMobile: IsMobile;
- state = $derived.by(() => (this.open ? "expanded" : "collapsed"));
+ readonly props: SidebarStateProps;
+ open = $derived.by(() => this.props.open());
+ openMobile = $state(false);
+ setOpen: SidebarStateProps["setOpen"];
+ #isMobile: IsMobile;
+ state = $derived.by(() => (this.open ? "expanded" : "collapsed"));
- constructor(props: SidebarStateProps) {
- this.setOpen = props.setOpen;
- this.#isMobile = new IsMobile();
- this.props = props;
- }
+ constructor(props: SidebarStateProps) {
+ this.setOpen = props.setOpen;
+ this.#isMobile = new IsMobile();
+ this.props = props;
+ }
- // Convenience getter for checking if the sidebar is mobile
- // without this, we would need to use `sidebar.isMobile.current` everywhere
- get isMobile() {
- return this.#isMobile.current;
- }
+ // Convenience getter for checking if the sidebar is mobile
+ // without this, we would need to use `sidebar.isMobile.current` everywhere
+ get isMobile() {
+ return this.#isMobile.current;
+ }
- // Event handler to apply to the ``
- handleShortcutKeydown = (e: KeyboardEvent) => {
- if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
- e.preventDefault();
- this.toggle();
- }
- };
+ // Event handler to apply to the ``
+ handleShortcutKeydown = (e: KeyboardEvent) => {
+ if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ this.toggle();
+ }
+ };
- setOpenMobile = (value: boolean) => {
- this.openMobile = value;
- };
+ setOpenMobile = (value: boolean) => {
+ this.openMobile = value;
+ };
- toggle = () => {
- return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open);
- };
+ toggle = () => {
+ return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open);
+ };
}
const SYMBOL_KEY = "scn-sidebar";
@@ -66,7 +66,7 @@ const SYMBOL_KEY = "scn-sidebar";
* @returns The `SidebarState` instance.
*/
export function setSidebar(props: SidebarStateProps): SidebarState {
- return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));
+ return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));
}
/**
@@ -75,5 +75,5 @@ export function setSidebar(props: SidebarStateProps): SidebarState {
* @returns The `SidebarState` instance.
*/
export function useSidebar(): SidebarState {
- return getContext(Symbol.for(SYMBOL_KEY));
+ return getContext(Symbol.for(SYMBOL_KEY));
}
diff --git a/src/lib/components/ui/sidebar/index.ts b/src/lib/components/ui/sidebar/index.ts
index 5c2e6b8..42fe156 100644
--- a/src/lib/components/ui/sidebar/index.ts
+++ b/src/lib/components/ui/sidebar/index.ts
@@ -24,52 +24,52 @@ import Trigger from "./sidebar-trigger.svelte";
import Root from "./sidebar.svelte";
export {
- Content,
- Footer,
- Group,
- GroupAction,
- GroupContent,
- GroupLabel,
- Header,
- Input,
- Inset,
- Menu,
- MenuAction,
- MenuBadge,
- MenuButton,
- MenuItem,
- MenuSkeleton,
- MenuSub,
- MenuSubButton,
- MenuSubItem,
- Provider,
- Rail,
- Root,
- Separator,
- //
- Root as Sidebar,
- Content as SidebarContent,
- Footer as SidebarFooter,
- Group as SidebarGroup,
- GroupAction as SidebarGroupAction,
- GroupContent as SidebarGroupContent,
- GroupLabel as SidebarGroupLabel,
- Header as SidebarHeader,
- Input as SidebarInput,
- Inset as SidebarInset,
- Menu as SidebarMenu,
- MenuAction as SidebarMenuAction,
- MenuBadge as SidebarMenuBadge,
- MenuButton as SidebarMenuButton,
- MenuItem as SidebarMenuItem,
- MenuSkeleton as SidebarMenuSkeleton,
- MenuSub as SidebarMenuSub,
- MenuSubButton as SidebarMenuSubButton,
- MenuSubItem as SidebarMenuSubItem,
- Provider as SidebarProvider,
- Rail as SidebarRail,
- Separator as SidebarSeparator,
- Trigger as SidebarTrigger,
- Trigger,
- useSidebar
+ Content,
+ Footer,
+ Group,
+ GroupAction,
+ GroupContent,
+ GroupLabel,
+ Header,
+ Input,
+ Inset,
+ Menu,
+ MenuAction,
+ MenuBadge,
+ MenuButton,
+ MenuItem,
+ MenuSkeleton,
+ MenuSub,
+ MenuSubButton,
+ MenuSubItem,
+ Provider,
+ Rail,
+ Root,
+ Separator,
+ //
+ Root as Sidebar,
+ Content as SidebarContent,
+ Footer as SidebarFooter,
+ Group as SidebarGroup,
+ GroupAction as SidebarGroupAction,
+ GroupContent as SidebarGroupContent,
+ GroupLabel as SidebarGroupLabel,
+ Header as SidebarHeader,
+ Input as SidebarInput,
+ Inset as SidebarInset,
+ Menu as SidebarMenu,
+ MenuAction as SidebarMenuAction,
+ MenuBadge as SidebarMenuBadge,
+ MenuButton as SidebarMenuButton,
+ MenuItem as SidebarMenuItem,
+ MenuSkeleton as SidebarMenuSkeleton,
+ MenuSub as SidebarMenuSub,
+ MenuSubButton as SidebarMenuSubButton,
+ MenuSubItem as SidebarMenuSubItem,
+ Provider as SidebarProvider,
+ Rail as SidebarRail,
+ Separator as SidebarSeparator,
+ Trigger as SidebarTrigger,
+ Trigger,
+ useSidebar,
};
diff --git a/src/lib/components/ui/sidebar/sidebar-content.svelte b/src/lib/components/ui/sidebar/sidebar-content.svelte
index f121800..65c5330 100644
--- a/src/lib/components/ui/sidebar/sidebar-content.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-content.svelte
@@ -1,24 +1,24 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/sidebar/sidebar-footer.svelte b/src/lib/components/ui/sidebar/sidebar-footer.svelte
index 6259cb9..e528adb 100644
--- a/src/lib/components/ui/sidebar/sidebar-footer.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-footer.svelte
@@ -1,21 +1,21 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/sidebar/sidebar-group-action.svelte b/src/lib/components/ui/sidebar/sidebar-group-action.svelte
index d9579fd..a63d5e4 100644
--- a/src/lib/components/ui/sidebar/sidebar-group-action.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-group-action.svelte
@@ -1,36 +1,36 @@
{#if child}
- {@render child({ props: mergedProps })}
+ {@render child({ props: mergedProps })}
{:else}
-
- {@render children?.()}
-
+
+ {@render children?.()}
+
{/if}
diff --git a/src/lib/components/ui/sidebar/sidebar-group-content.svelte b/src/lib/components/ui/sidebar/sidebar-group-content.svelte
index 415255f..504bb3e 100644
--- a/src/lib/components/ui/sidebar/sidebar-group-content.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-group-content.svelte
@@ -1,21 +1,21 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/sidebar/sidebar-group-label.svelte b/src/lib/components/ui/sidebar/sidebar-group-label.svelte
index 2b6287a..a028060 100644
--- a/src/lib/components/ui/sidebar/sidebar-group-label.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-group-label.svelte
@@ -1,34 +1,34 @@
{#if child}
- {@render child({ props: mergedProps })}
+ {@render child({ props: mergedProps })}
{:else}
-
- {@render children?.()}
-
+
+ {@render children?.()}
+
{/if}
diff --git a/src/lib/components/ui/sidebar/sidebar-group.svelte b/src/lib/components/ui/sidebar/sidebar-group.svelte
index ec18a69..d858622 100644
--- a/src/lib/components/ui/sidebar/sidebar-group.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-group.svelte
@@ -1,21 +1,21 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/sidebar/sidebar-header.svelte b/src/lib/components/ui/sidebar/sidebar-header.svelte
index a1b2db1..cba453c 100644
--- a/src/lib/components/ui/sidebar/sidebar-header.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-header.svelte
@@ -1,21 +1,21 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/sidebar/sidebar-input.svelte b/src/lib/components/ui/sidebar/sidebar-input.svelte
index 19b3666..9fb07d2 100644
--- a/src/lib/components/ui/sidebar/sidebar-input.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-input.svelte
@@ -1,21 +1,21 @@
diff --git a/src/lib/components/ui/sidebar/sidebar-inset.svelte b/src/lib/components/ui/sidebar/sidebar-inset.svelte
index 140de4a..ad4740f 100644
--- a/src/lib/components/ui/sidebar/sidebar-inset.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-inset.svelte
@@ -1,24 +1,24 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-action.svelte b/src/lib/components/ui/sidebar/sidebar-menu-action.svelte
index fb386c3..d463388 100644
--- a/src/lib/components/ui/sidebar/sidebar-menu-action.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-menu-action.svelte
@@ -1,43 +1,43 @@
{#if child}
- {@render child({ props: mergedProps })}
+ {@render child({ props: mergedProps })}
{:else}
-
- {@render children?.()}
-
+
+ {@render children?.()}
+
{/if}
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
index 09d4a90..339fb56 100644
--- a/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
@@ -1,29 +1,29 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
index 57e2fa2..beda3d9 100644
--- a/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
@@ -1,101 +1,101 @@
{#snippet Button({ props }: { props?: Record })}
- {@const mergedProps = mergeProps(buttonProps, props)}
- {#if child}
- {@render child({ props: mergedProps })}
- {:else}
-
- {@render children?.()}
-
- {/if}
+ {@const mergedProps = mergeProps(buttonProps, props)}
+ {#if child}
+ {@render child({ props: mergedProps })}
+ {:else}
+
+ {@render children?.()}
+
+ {/if}
{/snippet}
{#if !tooltipContent}
- {@render Button({})}
+ {@render Button({})}
{:else}
-
-
- {#snippet child({ props })}
- {@render Button({ props })}
- {/snippet}
-
-
- {#if typeof tooltipContent === "string"}
- {tooltipContent}
- {:else if tooltipContent}
- {@render tooltipContent()}
- {/if}
-
-
+
+
+ {#snippet child({ props })}
+ {@render Button({ props })}
+ {/snippet}
+
+
+ {#if typeof tooltipContent === "string"}
+ {tooltipContent}
+ {:else if tooltipContent}
+ {@render tooltipContent()}
+ {/if}
+
+
{/if}
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-item.svelte
index d87587c..58eeb58 100644
--- a/src/lib/components/ui/sidebar/sidebar-menu-item.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-menu-item.svelte
@@ -1,21 +1,21 @@
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
index 68604e2..7d13a7b 100644
--- a/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
@@ -1,36 +1,36 @@
- {#if showIcon}
-
- {/if}
-
- {@render children?.()}
+ {#if showIcon}
+
+ {/if}
+
+ {@render children?.()}
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
index d221310..c234d96 100644
--- a/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
@@ -1,43 +1,43 @@
{#if child}
- {@render child({ props: mergedProps })}
+ {@render child({ props: mergedProps })}
{:else}
-
- {@render children?.()}
-
+
+ {@render children?.()}
+
{/if}
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
index 681d0f1..6065e7d 100644
--- a/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
@@ -1,21 +1,21 @@
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
index 8ab1111..8ea7996 100644
--- a/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
@@ -1,25 +1,25 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/sidebar/sidebar-menu.svelte b/src/lib/components/ui/sidebar/sidebar-menu.svelte
index 946ccce..9363848 100644
--- a/src/lib/components/ui/sidebar/sidebar-menu.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-menu.svelte
@@ -1,21 +1,21 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/sidebar/sidebar-provider.svelte b/src/lib/components/ui/sidebar/sidebar-provider.svelte
index b11d893..29a251d 100644
--- a/src/lib/components/ui/sidebar/sidebar-provider.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-provider.svelte
@@ -1,45 +1,45 @@
-
- {@render children?.()}
-
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar-rail.svelte b/src/lib/components/ui/sidebar/sidebar-rail.svelte
index 3d58ed6..81890ad 100644
--- a/src/lib/components/ui/sidebar/sidebar-rail.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-rail.svelte
@@ -1,36 +1,36 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/sidebar/sidebar-separator.svelte b/src/lib/components/ui/sidebar/sidebar-separator.svelte
index 5a7deda..7edb9ca 100644
--- a/src/lib/components/ui/sidebar/sidebar-separator.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-separator.svelte
@@ -1,19 +1,19 @@
diff --git a/src/lib/components/ui/sidebar/sidebar-trigger.svelte b/src/lib/components/ui/sidebar/sidebar-trigger.svelte
index 1825182..e0b02cb 100644
--- a/src/lib/components/ui/sidebar/sidebar-trigger.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-trigger.svelte
@@ -1,35 +1,35 @@
{
- onclick?.(e);
- sidebar.toggle();
- }}
- {...restProps}
+ data-sidebar="trigger"
+ data-slot="sidebar-trigger"
+ variant="ghost"
+ size="icon"
+ class={cn("size-7", className)}
+ type="button"
+ onclick={(e) => {
+ onclick?.(e);
+ sidebar.toggle();
+ }}
+ {...restProps}
>
-
- Toggle Sidebar
+
+ Toggle Sidebar
diff --git a/src/lib/components/ui/sidebar/sidebar.svelte b/src/lib/components/ui/sidebar/sidebar.svelte
index 854796e..ccdf338 100644
--- a/src/lib/components/ui/sidebar/sidebar.svelte
+++ b/src/lib/components/ui/sidebar/sidebar.svelte
@@ -1,101 +1,101 @@
{#if collapsible === "none"}
-
- {@render children?.()}
-
+
+ {@render children?.()}
+
{:else if sidebar.isMobile}
- sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} {...restProps}>
-
-
+ sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} {...restProps}>
+
+
{:else}
-
+
{/if}
diff --git a/src/lib/components/ui/skeleton/index.ts b/src/lib/components/ui/skeleton/index.ts
index 2be5c50..cb26b2c 100644
--- a/src/lib/components/ui/skeleton/index.ts
+++ b/src/lib/components/ui/skeleton/index.ts
@@ -1,7 +1,7 @@
import Root from "./skeleton.svelte";
export {
- Root,
- //
- Root as Skeleton
+ Root,
+ //
+ Root as Skeleton,
};
diff --git a/src/lib/components/ui/skeleton/skeleton.svelte b/src/lib/components/ui/skeleton/skeleton.svelte
index b21d230..e0e5062 100644
--- a/src/lib/components/ui/skeleton/skeleton.svelte
+++ b/src/lib/components/ui/skeleton/skeleton.svelte
@@ -1,17 +1,17 @@
diff --git a/src/lib/components/ui/sonner/sonner.svelte b/src/lib/components/ui/sonner/sonner.svelte
index 1f50e1e..fea32fc 100644
--- a/src/lib/components/ui/sonner/sonner.svelte
+++ b/src/lib/components/ui/sonner/sonner.svelte
@@ -1,13 +1,13 @@
diff --git a/src/lib/components/ui/tooltip/index.ts b/src/lib/components/ui/tooltip/index.ts
index f8dc9df..85d7744 100644
--- a/src/lib/components/ui/tooltip/index.ts
+++ b/src/lib/components/ui/tooltip/index.ts
@@ -7,15 +7,15 @@ const Provider = TooltipPrimitive.Provider;
const Portal = TooltipPrimitive.Portal;
export {
- Root,
- Trigger,
- Content,
- Provider,
- Portal,
- //
- Root as Tooltip,
- Content as TooltipContent,
- Trigger as TooltipTrigger,
- Provider as TooltipProvider,
- Portal as TooltipPortal
+ Root,
+ Trigger,
+ Content,
+ Provider,
+ Portal,
+ //
+ Root as Tooltip,
+ Content as TooltipContent,
+ Trigger as TooltipTrigger,
+ Provider as TooltipProvider,
+ Portal as TooltipPortal,
};
diff --git a/src/lib/components/ui/tooltip/tooltip-content.svelte b/src/lib/components/ui/tooltip/tooltip-content.svelte
index b1e783e..7c61148 100644
--- a/src/lib/components/ui/tooltip/tooltip-content.svelte
+++ b/src/lib/components/ui/tooltip/tooltip-content.svelte
@@ -1,47 +1,47 @@
-
- {@render children?.()}
-
- {#snippet child({ props })}
-
- {/snippet}
-
-
+
+ {@render children?.()}
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
diff --git a/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/src/lib/components/ui/tooltip/tooltip-trigger.svelte
index 1acdaa4..d7b41d3 100644
--- a/src/lib/components/ui/tooltip/tooltip-trigger.svelte
+++ b/src/lib/components/ui/tooltip/tooltip-trigger.svelte
@@ -1,7 +1,7 @@
diff --git a/src/lib/components/ui/typography/headline.svelte b/src/lib/components/ui/typography/headline.svelte
index 7ecf40c..e8a7de4 100644
--- a/src/lib/components/ui/typography/headline.svelte
+++ b/src/lib/components/ui/typography/headline.svelte
@@ -1,38 +1,38 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/components/ui/typography/text.svelte b/src/lib/components/ui/typography/text.svelte
index 9cd3365..58da46c 100644
--- a/src/lib/components/ui/typography/text.svelte
+++ b/src/lib/components/ui/typography/text.svelte
@@ -1,44 +1,44 @@
- {@render children?.()}
+ {@render children?.()}
diff --git a/src/lib/const/routes.ts b/src/lib/const/routes.ts
index f82bf6f..0932258 100644
--- a/src/lib/const/routes.ts
+++ b/src/lib/const/routes.ts
@@ -1,21 +1,21 @@
export const ROUTES = {
- SETUP: {
- MAIN: "/setup",
- CREATE_ADMIN_ACCOUNT: "/setup/create-admin-account",
- CHECK_EMAIL: "/setup/check-email"
- },
- RESEND_CONFIRMATION: "/confirm/resend",
- LOGIN: "/login",
- LOGOUT: "/logout",
- DASHBOARD: {
- MAIN: "/dashboard",
- TENANTS: "/dashboard/tenants",
- CALENDAR: "/dashboard/calendar",
- STAFF: "/dashboard/staff",
- AGENTS: "/dashboard/agents",
- CHANNELS: "/dashboard/channels",
- ABSENCES: "/dashboard/absences",
- SETTINGS: "/dashboard/settings",
- ACCOUNT: "/dashboard/account"
- }
+ SETUP: {
+ MAIN: "/setup",
+ CREATE_ADMIN_ACCOUNT: "/setup/create-admin-account",
+ CHECK_EMAIL: "/setup/check-email",
+ },
+ RESEND_CONFIRMATION: "/confirm/resend",
+ LOGIN: "/login",
+ LOGOUT: "/logout",
+ DASHBOARD: {
+ MAIN: "/dashboard",
+ TENANTS: "/dashboard/tenants",
+ CALENDAR: "/dashboard/calendar",
+ STAFF: "/dashboard/staff",
+ AGENTS: "/dashboard/agents",
+ CHANNELS: "/dashboard/channels",
+ ABSENCES: "/dashboard/absences",
+ SETTINGS: "/dashboard/settings",
+ ACCOUNT: "/dashboard/account",
+ },
};
diff --git a/src/lib/crypto/__tests__/hashing.test.ts b/src/lib/crypto/__tests__/hashing.test.ts
index 6ada051..bb4fb43 100644
--- a/src/lib/crypto/__tests__/hashing.test.ts
+++ b/src/lib/crypto/__tests__/hashing.test.ts
@@ -4,292 +4,292 @@ import type { Argon2Options } from "../hashing";
// Mock the logger
vi.mock("$lib/logger", () => ({
- logger: {
- setContext: vi.fn(),
- debug: vi.fn(),
- warn: vi.fn(),
- error: vi.fn()
- }
+ logger: {
+ setContext: vi.fn(),
+ debug: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
+ },
}));
describe("OptimizedArgon2", () => {
- // Test data
- const testPin = "123456";
- const testClientId = "test-client-id";
- const testEmail = "test@example.com";
+ // Test data
+ const testPin = "123456";
+ const testClientId = "test-client-id";
+ const testEmail = "test@example.com";
- beforeEach(() => {
- vi.clearAllMocks();
- });
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
- afterEach(() => {
- vi.restoreAllMocks();
- });
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
- describe("deriveKeyFromPIN()", () => {
- it("should derive a key with default options", async () => {
- const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId);
+ describe("deriveKeyFromPIN()", () => {
+ it("should derive a key with default options", async () => {
+ const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId);
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(32); // Default hash length
- });
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(32); // Default hash length
+ });
- it("should derive a key with custom options", async () => {
- const options: Argon2Options = {
- memoryCost: 32768,
- timeCost: 5,
- parallelism: 2,
- hashLength: 64
- };
+ it("should derive a key with custom options", async () => {
+ const options: Argon2Options = {
+ memoryCost: 32768,
+ timeCost: 5,
+ parallelism: 2,
+ hashLength: 64,
+ };
- const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
+ const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(64);
- });
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(64);
+ });
- it("should produce consistent results for same inputs", async () => {
- const result1 = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId);
- const result2 = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId);
+ it("should produce consistent results for same inputs", async () => {
+ const result1 = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId);
+ const result2 = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId);
- expect(result1).toEqual(result2);
- });
+ expect(result1).toEqual(result2);
+ });
- it("should produce different results for different PINs", async () => {
- const result1 = await OptimizedArgon2.deriveKeyFromPIN("123456", testClientId);
- const result2 = await OptimizedArgon2.deriveKeyFromPIN("654321", testClientId);
+ it("should produce different results for different PINs", async () => {
+ const result1 = await OptimizedArgon2.deriveKeyFromPIN("123456", testClientId);
+ const result2 = await OptimizedArgon2.deriveKeyFromPIN("654321", testClientId);
- expect(result1).not.toEqual(result2);
- });
-
- it("should produce different results for different client IDs", async () => {
- const result1 = await OptimizedArgon2.deriveKeyFromPIN(testPin, "client1");
- const result2 = await OptimizedArgon2.deriveKeyFromPIN(testPin, "client2");
-
- expect(result1).not.toEqual(result2);
- });
-
- it("should handle empty PIN", async () => {
- const result = await OptimizedArgon2.deriveKeyFromPIN("", testClientId);
-
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(32);
- });
-
- it("should handle empty client ID", async () => {
- const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, "");
-
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(32);
- });
-
- it("should handle unicode characters in PIN", async () => {
- const unicodePin = "тест🔐äöü";
- const result = await OptimizedArgon2.deriveKeyFromPIN(unicodePin, testClientId);
-
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(32);
- });
-
- it("should handle unicode characters in client ID", async () => {
- const unicodeClientId = "клиент🆔äöü";
- const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, unicodeClientId);
-
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(32);
- });
- });
-
- describe("createClientIdAsync()", () => {
- it("should create consistent client ID for same email", async () => {
- const result1 = await OptimizedArgon2.createClientIdAsync(testEmail);
- const result2 = await OptimizedArgon2.createClientIdAsync(testEmail);
-
- expect(result1).toBe(result2);
- expect(typeof result1).toBe("string");
- expect(result1.length).toBe(64); // SHA-256 hex = 64 chars
- });
-
- it("should create different client IDs for different emails", async () => {
- const result1 = await OptimizedArgon2.createClientIdAsync("user1@example.com");
- const result2 = await OptimizedArgon2.createClientIdAsync("user2@example.com");
-
- expect(result1).not.toBe(result2);
- });
+ expect(result1).not.toEqual(result2);
+ });
+
+ it("should produce different results for different client IDs", async () => {
+ const result1 = await OptimizedArgon2.deriveKeyFromPIN(testPin, "client1");
+ const result2 = await OptimizedArgon2.deriveKeyFromPIN(testPin, "client2");
+
+ expect(result1).not.toEqual(result2);
+ });
+
+ it("should handle empty PIN", async () => {
+ const result = await OptimizedArgon2.deriveKeyFromPIN("", testClientId);
+
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(32);
+ });
+
+ it("should handle empty client ID", async () => {
+ const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, "");
+
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(32);
+ });
+
+ it("should handle unicode characters in PIN", async () => {
+ const unicodePin = "тест🔐äöü";
+ const result = await OptimizedArgon2.deriveKeyFromPIN(unicodePin, testClientId);
+
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(32);
+ });
+
+ it("should handle unicode characters in client ID", async () => {
+ const unicodeClientId = "клиент🆔äöü";
+ const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, unicodeClientId);
+
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(32);
+ });
+ });
+
+ describe("createClientIdAsync()", () => {
+ it("should create consistent client ID for same email", async () => {
+ const result1 = await OptimizedArgon2.createClientIdAsync(testEmail);
+ const result2 = await OptimizedArgon2.createClientIdAsync(testEmail);
+
+ expect(result1).toBe(result2);
+ expect(typeof result1).toBe("string");
+ expect(result1.length).toBe(64); // SHA-256 hex = 64 chars
+ });
+
+ it("should create different client IDs for different emails", async () => {
+ const result1 = await OptimizedArgon2.createClientIdAsync("user1@example.com");
+ const result2 = await OptimizedArgon2.createClientIdAsync("user2@example.com");
+
+ expect(result1).not.toBe(result2);
+ });
- it("should handle email case insensitivity", async () => {
- const result1 = await OptimizedArgon2.createClientIdAsync("Test@Example.Com");
- const result2 = await OptimizedArgon2.createClientIdAsync("test@example.com");
+ it("should handle email case insensitivity", async () => {
+ const result1 = await OptimizedArgon2.createClientIdAsync("Test@Example.Com");
+ const result2 = await OptimizedArgon2.createClientIdAsync("test@example.com");
- expect(result1).toBe(result2);
- });
+ expect(result1).toBe(result2);
+ });
- it("should handle empty email", async () => {
- const result = await OptimizedArgon2.createClientIdAsync("");
+ it("should handle empty email", async () => {
+ const result = await OptimizedArgon2.createClientIdAsync("");
- expect(typeof result).toBe("string");
- expect(result.length).toBe(64);
- });
-
- it("should handle unicode in email", async () => {
- const unicodeEmail = "тест@example.com";
- const result = await OptimizedArgon2.createClientIdAsync(unicodeEmail);
+ expect(typeof result).toBe("string");
+ expect(result.length).toBe(64);
+ });
+
+ it("should handle unicode in email", async () => {
+ const unicodeEmail = "тест@example.com";
+ const result = await OptimizedArgon2.createClientIdAsync(unicodeEmail);
- expect(typeof result).toBe("string");
- expect(result.length).toBe(64);
- });
+ expect(typeof result).toBe("string");
+ expect(result.length).toBe(64);
+ });
- it("should produce valid hex string", async () => {
- const result = await OptimizedArgon2.createClientIdAsync(testEmail);
+ it("should produce valid hex string", async () => {
+ const result = await OptimizedArgon2.createClientIdAsync(testEmail);
- expect(result).toMatch(/^[0-9a-f]{64}$/);
- });
- });
+ expect(result).toMatch(/^[0-9a-f]{64}$/);
+ });
+ });
- describe("createClientId() - Browser only", () => {
- it("should throw error in Node.js environment", async () => {
- // This test runs in Node.js environment by default
- await expect(OptimizedArgon2.createClientId(testEmail)).rejects.toThrow(
- "createClientId should be called asynchronously in Node.js environment"
- );
- });
- });
+ describe("createClientId() - Browser only", () => {
+ it("should throw error in Node.js environment", async () => {
+ // This test runs in Node.js environment by default
+ await expect(OptimizedArgon2.createClientId(testEmail)).rejects.toThrow(
+ "createClientId should be called asynchronously in Node.js environment",
+ );
+ });
+ });
- describe("getImplementationInfo()", () => {
- it("should return implementation information", async () => {
- const info = await OptimizedArgon2.getImplementationInfo();
+ describe("getImplementationInfo()", () => {
+ it("should return implementation information", async () => {
+ const info = await OptimizedArgon2.getImplementationInfo();
- expect(typeof info).toBe("string");
- expect(info.length).toBeGreaterThan(0);
- // In Node.js environment, it should try native argon2 first
- expect(info).toMatch(/(Native Node\.js argon2|@noble\/hashes)/);
- });
- });
+ expect(typeof info).toBe("string");
+ expect(info.length).toBeGreaterThan(0);
+ // In Node.js environment, it should try native argon2 first
+ expect(info).toMatch(/(Native Node\.js argon2|@noble\/hashes)/);
+ });
+ });
- describe("Edge cases and error handling", () => {
- it("should handle very long PIN", async () => {
- const longPin = "a".repeat(1000);
- const result = await OptimizedArgon2.deriveKeyFromPIN(longPin, testClientId);
-
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(32);
- });
-
- it("should handle very long client ID", async () => {
- const longClientId = "b".repeat(1000);
- const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, longClientId);
-
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(32);
- });
-
- it("should handle minimum hash length", async () => {
- const options: Argon2Options = {
- hashLength: 4 // Noble hashes requires minimum 4 bytes
- };
-
- const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
-
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(4);
- });
-
- it("should handle large hash length", async () => {
- const options: Argon2Options = {
- hashLength: 128
- };
-
- const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
-
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(128);
- });
-
- it("should handle minimum memory cost", async () => {
- const options: Argon2Options = {
- memoryCost: 8 // 8 KB minimum
- };
-
- const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
-
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(32);
- });
-
- it("should handle minimum time cost", async () => {
- const options: Argon2Options = {
- timeCost: 1
- };
-
- const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
-
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(32);
- });
-
- it("should handle different parallelism values", async () => {
- const options: Argon2Options = {
- parallelism: 4
- };
-
- const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
-
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(32);
- });
- });
-
- describe("Performance characteristics", () => {
- it("should complete within reasonable time with default options", async () => {
- const startTime = Date.now();
-
- await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId);
-
- const endTime = Date.now();
- const duration = endTime - startTime;
-
- // Should complete within 5 seconds even on slow systems
- expect(duration).toBeLessThan(5000);
- });
-
- it("should handle concurrent derivations", async () => {
- const promises = Array.from({ length: 5 }, (_, i) =>
- OptimizedArgon2.deriveKeyFromPIN(`pin${i}`, `client${i}`)
- );
-
- const results = await Promise.all(promises);
-
- expect(results).toHaveLength(5);
- results.forEach((result, i) => {
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(32);
-
- // Each result should be different
- results.slice(i + 1).forEach((otherResult) => {
- expect(result).not.toEqual(otherResult);
- });
- });
- });
- });
-
- describe("Consistency across implementations", () => {
- it("should produce consistent results regardless of implementation fallback", async () => {
- // Test that the fallback implementation produces consistent results
- const options: Argon2Options = {
- memoryCost: 1024, // Low memory to ensure fallback might be used
- timeCost: 1,
- parallelism: 1,
- hashLength: 32
- };
-
- const results = await Promise.all([
- OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options),
- OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options),
- OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options)
- ]);
-
- expect(results[0]).toEqual(results[1]);
- expect(results[1]).toEqual(results[2]);
- });
- });
+ describe("Edge cases and error handling", () => {
+ it("should handle very long PIN", async () => {
+ const longPin = "a".repeat(1000);
+ const result = await OptimizedArgon2.deriveKeyFromPIN(longPin, testClientId);
+
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(32);
+ });
+
+ it("should handle very long client ID", async () => {
+ const longClientId = "b".repeat(1000);
+ const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, longClientId);
+
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(32);
+ });
+
+ it("should handle minimum hash length", async () => {
+ const options: Argon2Options = {
+ hashLength: 4, // Noble hashes requires minimum 4 bytes
+ };
+
+ const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
+
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(4);
+ });
+
+ it("should handle large hash length", async () => {
+ const options: Argon2Options = {
+ hashLength: 128,
+ };
+
+ const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
+
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(128);
+ });
+
+ it("should handle minimum memory cost", async () => {
+ const options: Argon2Options = {
+ memoryCost: 8, // 8 KB minimum
+ };
+
+ const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
+
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(32);
+ });
+
+ it("should handle minimum time cost", async () => {
+ const options: Argon2Options = {
+ timeCost: 1,
+ };
+
+ const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
+
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(32);
+ });
+
+ it("should handle different parallelism values", async () => {
+ const options: Argon2Options = {
+ parallelism: 4,
+ };
+
+ const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
+
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(32);
+ });
+ });
+
+ describe("Performance characteristics", () => {
+ it("should complete within reasonable time with default options", async () => {
+ const startTime = Date.now();
+
+ await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId);
+
+ const endTime = Date.now();
+ const duration = endTime - startTime;
+
+ // Should complete within 5 seconds even on slow systems
+ expect(duration).toBeLessThan(5000);
+ });
+
+ it("should handle concurrent derivations", async () => {
+ const promises = Array.from({ length: 5 }, (_, i) =>
+ OptimizedArgon2.deriveKeyFromPIN(`pin${i}`, `client${i}`),
+ );
+
+ const results = await Promise.all(promises);
+
+ expect(results).toHaveLength(5);
+ results.forEach((result, i) => {
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(32);
+
+ // Each result should be different
+ results.slice(i + 1).forEach((otherResult) => {
+ expect(result).not.toEqual(otherResult);
+ });
+ });
+ });
+ });
+
+ describe("Consistency across implementations", () => {
+ it("should produce consistent results regardless of implementation fallback", async () => {
+ // Test that the fallback implementation produces consistent results
+ const options: Argon2Options = {
+ memoryCost: 1024, // Low memory to ensure fallback might be used
+ timeCost: 1,
+ parallelism: 1,
+ hashLength: 32,
+ };
+
+ const results = await Promise.all([
+ OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options),
+ OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options),
+ OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options),
+ ]);
+
+ expect(results[0]).toEqual(results[1]);
+ expect(results[1]).toEqual(results[2]);
+ });
+ });
});
diff --git a/src/lib/crypto/__tests__/utils.test.ts b/src/lib/crypto/__tests__/utils.test.ts
index 1e1b4a1..47e331b 100644
--- a/src/lib/crypto/__tests__/utils.test.ts
+++ b/src/lib/crypto/__tests__/utils.test.ts
@@ -3,422 +3,422 @@ import { BufferUtils, KyberCrypto, AESCrypto, ShamirSecretSharing } from "../uti
import type { CryptoBuffer, KyberKeyPair, ShamirShare } from "../utils";
describe("BufferUtils", () => {
- describe("from()", () => {
- it("should convert string to CryptoBuffer with utf8 encoding", () => {
- const input = "Hello World";
- const result = BufferUtils.from(input);
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result).toEqual(new TextEncoder().encode(input));
- });
+ describe("from()", () => {
+ it("should convert string to CryptoBuffer with utf8 encoding", () => {
+ const input = "Hello World";
+ const result = BufferUtils.from(input);
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result).toEqual(new TextEncoder().encode(input));
+ });
- it("should convert hex string to CryptoBuffer", () => {
- const input = "48656c6c6f";
- const result = BufferUtils.from(input, "hex");
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result).toEqual(new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]));
- });
+ it("should convert hex string to CryptoBuffer", () => {
+ const input = "48656c6c6f";
+ const result = BufferUtils.from(input, "hex");
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result).toEqual(new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]));
+ });
- it("should convert number array to CryptoBuffer", () => {
- const input = [72, 101, 108, 108, 111];
- const result = BufferUtils.from(input);
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result).toEqual(new Uint8Array(input));
- });
+ it("should convert number array to CryptoBuffer", () => {
+ const input = [72, 101, 108, 108, 111];
+ const result = BufferUtils.from(input);
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result).toEqual(new Uint8Array(input));
+ });
- it("should convert ArrayBuffer to CryptoBuffer", () => {
- const input = new ArrayBuffer(5);
- const view = new Uint8Array(input);
- view.set([72, 101, 108, 108, 111]);
- const result = BufferUtils.from(input);
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result).toEqual(new Uint8Array([72, 101, 108, 108, 111]));
- });
+ it("should convert ArrayBuffer to CryptoBuffer", () => {
+ const input = new ArrayBuffer(5);
+ const view = new Uint8Array(input);
+ view.set([72, 101, 108, 108, 111]);
+ const result = BufferUtils.from(input);
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result).toEqual(new Uint8Array([72, 101, 108, 108, 111]));
+ });
- it("should handle empty hex string", () => {
- const result = BufferUtils.from("", "hex");
- expect(result).toEqual(new Uint8Array([]));
- });
+ it("should handle empty hex string", () => {
+ const result = BufferUtils.from("", "hex");
+ expect(result).toEqual(new Uint8Array([]));
+ });
- it("should handle invalid hex string", () => {
- const result = BufferUtils.from("invalid", "hex");
- // Invalid hex characters will be parsed as NaN, resulting in non-empty array
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBeGreaterThan(0);
- });
- });
+ it("should handle invalid hex string", () => {
+ const result = BufferUtils.from("invalid", "hex");
+ // Invalid hex characters will be parsed as NaN, resulting in non-empty array
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBeGreaterThan(0);
+ });
+ });
- describe("toString()", () => {
- it("should convert CryptoBuffer to utf8 string", () => {
- const input = new Uint8Array([72, 101, 108, 108, 111]);
- const result = BufferUtils.toString(input);
- expect(result).toBe("Hello");
- });
+ describe("toString()", () => {
+ it("should convert CryptoBuffer to utf8 string", () => {
+ const input = new Uint8Array([72, 101, 108, 108, 111]);
+ const result = BufferUtils.toString(input);
+ expect(result).toBe("Hello");
+ });
- it("should convert CryptoBuffer to hex string", () => {
- const input = new Uint8Array([72, 101, 108, 108, 111]);
- const result = BufferUtils.toString(input, "hex");
- expect(result).toBe("48656c6c6f");
- });
+ it("should convert CryptoBuffer to hex string", () => {
+ const input = new Uint8Array([72, 101, 108, 108, 111]);
+ const result = BufferUtils.toString(input, "hex");
+ expect(result).toBe("48656c6c6f");
+ });
- it("should handle empty buffer", () => {
- const input = new Uint8Array([]);
- expect(BufferUtils.toString(input)).toBe("");
- expect(BufferUtils.toString(input, "hex")).toBe("");
- });
+ it("should handle empty buffer", () => {
+ const input = new Uint8Array([]);
+ expect(BufferUtils.toString(input)).toBe("");
+ expect(BufferUtils.toString(input, "hex")).toBe("");
+ });
- it("should pad single digit hex values", () => {
- const input = new Uint8Array([0, 15, 255]);
- const result = BufferUtils.toString(input, "hex");
- expect(result).toBe("000fff");
- });
- });
+ it("should pad single digit hex values", () => {
+ const input = new Uint8Array([0, 15, 255]);
+ const result = BufferUtils.toString(input, "hex");
+ expect(result).toBe("000fff");
+ });
+ });
- describe("concat()", () => {
- it("should concatenate multiple buffers", () => {
- const buf1 = new Uint8Array([1, 2, 3]);
- const buf2 = new Uint8Array([4, 5]);
- const buf3 = new Uint8Array([6, 7, 8, 9]);
- const result = BufferUtils.concat([buf1, buf2, buf3]);
- expect(result).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]));
- });
+ describe("concat()", () => {
+ it("should concatenate multiple buffers", () => {
+ const buf1 = new Uint8Array([1, 2, 3]);
+ const buf2 = new Uint8Array([4, 5]);
+ const buf3 = new Uint8Array([6, 7, 8, 9]);
+ const result = BufferUtils.concat([buf1, buf2, buf3]);
+ expect(result).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]));
+ });
- it("should handle empty buffers", () => {
- const buf1 = new Uint8Array([1, 2]);
- const buf2 = new Uint8Array([]);
- const buf3 = new Uint8Array([3, 4]);
- const result = BufferUtils.concat([buf1, buf2, buf3]);
- expect(result).toEqual(new Uint8Array([1, 2, 3, 4]));
- });
+ it("should handle empty buffers", () => {
+ const buf1 = new Uint8Array([1, 2]);
+ const buf2 = new Uint8Array([]);
+ const buf3 = new Uint8Array([3, 4]);
+ const result = BufferUtils.concat([buf1, buf2, buf3]);
+ expect(result).toEqual(new Uint8Array([1, 2, 3, 4]));
+ });
- it("should handle single buffer", () => {
- const buf = new Uint8Array([1, 2, 3]);
- const result = BufferUtils.concat([buf]);
- expect(result).toEqual(buf);
- });
+ it("should handle single buffer", () => {
+ const buf = new Uint8Array([1, 2, 3]);
+ const result = BufferUtils.concat([buf]);
+ expect(result).toEqual(buf);
+ });
- it("should handle empty array", () => {
- const result = BufferUtils.concat([]);
- expect(result).toEqual(new Uint8Array([]));
- });
- });
+ it("should handle empty array", () => {
+ const result = BufferUtils.concat([]);
+ expect(result).toEqual(new Uint8Array([]));
+ });
+ });
- describe("randomBytes()", () => {
- it("should generate random bytes of specified length", () => {
- const length = 32;
- const result = BufferUtils.randomBytes(length);
- expect(result).toBeInstanceOf(Uint8Array);
- expect(result.length).toBe(length);
- });
+ describe("randomBytes()", () => {
+ it("should generate random bytes of specified length", () => {
+ const length = 32;
+ const result = BufferUtils.randomBytes(length);
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(length);
+ });
- it("should generate different random bytes on each call", () => {
- const result1 = BufferUtils.randomBytes(16);
- const result2 = BufferUtils.randomBytes(16);
- expect(result1).not.toEqual(result2);
- });
+ it("should generate different random bytes on each call", () => {
+ const result1 = BufferUtils.randomBytes(16);
+ const result2 = BufferUtils.randomBytes(16);
+ expect(result1).not.toEqual(result2);
+ });
- it("should handle zero length", () => {
- const result = BufferUtils.randomBytes(0);
- expect(result).toEqual(new Uint8Array([]));
- });
- });
+ it("should handle zero length", () => {
+ const result = BufferUtils.randomBytes(0);
+ expect(result).toEqual(new Uint8Array([]));
+ });
+ });
- describe("xor()", () => {
- it("should perform XOR operation on equal length buffers", () => {
- const a = new Uint8Array([0xff, 0x00, 0xaa]);
- const b = new Uint8Array([0x00, 0xff, 0x55]);
- const result = BufferUtils.xor(a, b);
- expect(result).toEqual(new Uint8Array([0xff, 0xff, 0xff]));
- });
+ describe("xor()", () => {
+ it("should perform XOR operation on equal length buffers", () => {
+ const a = new Uint8Array([0xff, 0x00, 0xaa]);
+ const b = new Uint8Array([0x00, 0xff, 0x55]);
+ const result = BufferUtils.xor(a, b);
+ expect(result).toEqual(new Uint8Array([0xff, 0xff, 0xff]));
+ });
- it("should handle different length buffers", () => {
- const a = new Uint8Array([0xff, 0x00]);
- const b = new Uint8Array([0x00, 0xff, 0x55]);
- const result = BufferUtils.xor(a, b);
- expect(result).toEqual(new Uint8Array([0xff, 0xff, 0x55]));
- });
+ it("should handle different length buffers", () => {
+ const a = new Uint8Array([0xff, 0x00]);
+ const b = new Uint8Array([0x00, 0xff, 0x55]);
+ const result = BufferUtils.xor(a, b);
+ expect(result).toEqual(new Uint8Array([0xff, 0xff, 0x55]));
+ });
- it("should handle empty buffers", () => {
- const a = new Uint8Array([]);
- const b = new Uint8Array([1, 2, 3]);
- const result = BufferUtils.xor(a, b);
- expect(result).toEqual(new Uint8Array([1, 2, 3]));
- });
- });
+ it("should handle empty buffers", () => {
+ const a = new Uint8Array([]);
+ const b = new Uint8Array([1, 2, 3]);
+ const result = BufferUtils.xor(a, b);
+ expect(result).toEqual(new Uint8Array([1, 2, 3]));
+ });
+ });
- describe("equals()", () => {
- it("should return true for equal buffers", () => {
- const a = new Uint8Array([1, 2, 3, 4]);
- const b = new Uint8Array([1, 2, 3, 4]);
- expect(BufferUtils.equals(a, b)).toBe(true);
- });
+ describe("equals()", () => {
+ it("should return true for equal buffers", () => {
+ const a = new Uint8Array([1, 2, 3, 4]);
+ const b = new Uint8Array([1, 2, 3, 4]);
+ expect(BufferUtils.equals(a, b)).toBe(true);
+ });
- it("should return false for different buffers", () => {
- const a = new Uint8Array([1, 2, 3, 4]);
- const b = new Uint8Array([1, 2, 3, 5]);
- expect(BufferUtils.equals(a, b)).toBe(false);
- });
+ it("should return false for different buffers", () => {
+ const a = new Uint8Array([1, 2, 3, 4]);
+ const b = new Uint8Array([1, 2, 3, 5]);
+ expect(BufferUtils.equals(a, b)).toBe(false);
+ });
- it("should return false for different length buffers", () => {
- const a = new Uint8Array([1, 2, 3]);
- const b = new Uint8Array([1, 2, 3, 4]);
- expect(BufferUtils.equals(a, b)).toBe(false);
- });
+ it("should return false for different length buffers", () => {
+ const a = new Uint8Array([1, 2, 3]);
+ const b = new Uint8Array([1, 2, 3, 4]);
+ expect(BufferUtils.equals(a, b)).toBe(false);
+ });
- it("should return true for empty buffers", () => {
- const a = new Uint8Array([]);
- const b = new Uint8Array([]);
- expect(BufferUtils.equals(a, b)).toBe(true);
- });
- });
+ it("should return true for empty buffers", () => {
+ const a = new Uint8Array([]);
+ const b = new Uint8Array([]);
+ expect(BufferUtils.equals(a, b)).toBe(true);
+ });
+ });
});
describe("KyberCrypto", () => {
- let keyPair: KyberKeyPair;
+ let keyPair: KyberKeyPair;
- beforeEach(() => {
- keyPair = KyberCrypto.generateKeyPair();
- });
+ beforeEach(() => {
+ keyPair = KyberCrypto.generateKeyPair();
+ });
- describe("generateKeyPair()", () => {
- it("should generate a valid key pair", () => {
- expect(keyPair.publicKey).toBeInstanceOf(Uint8Array);
- expect(keyPair.privateKey).toBeInstanceOf(Uint8Array);
- expect(keyPair.publicKey.length).toBeGreaterThan(0);
- expect(keyPair.privateKey.length).toBeGreaterThan(0);
- });
+ describe("generateKeyPair()", () => {
+ it("should generate a valid key pair", () => {
+ expect(keyPair.publicKey).toBeInstanceOf(Uint8Array);
+ expect(keyPair.privateKey).toBeInstanceOf(Uint8Array);
+ expect(keyPair.publicKey.length).toBeGreaterThan(0);
+ expect(keyPair.privateKey.length).toBeGreaterThan(0);
+ });
- it("should generate different key pairs on each call", () => {
- const keyPair2 = KyberCrypto.generateKeyPair();
- expect(keyPair.publicKey).not.toEqual(keyPair2.publicKey);
- expect(keyPair.privateKey).not.toEqual(keyPair2.privateKey);
- });
- });
+ it("should generate different key pairs on each call", () => {
+ const keyPair2 = KyberCrypto.generateKeyPair();
+ expect(keyPair.publicKey).not.toEqual(keyPair2.publicKey);
+ expect(keyPair.privateKey).not.toEqual(keyPair2.privateKey);
+ });
+ });
- describe("encapsulate()", () => {
- it("should encapsulate a shared secret", () => {
- const result = KyberCrypto.encapsulate(keyPair.publicKey);
- expect(result.sharedSecret).toBeInstanceOf(Uint8Array);
- expect(result.encapsulatedSecret).toBeInstanceOf(Uint8Array);
- expect(result.sharedSecret.length).toBeGreaterThan(0);
- expect(result.encapsulatedSecret.length).toBeGreaterThan(0);
- });
+ describe("encapsulate()", () => {
+ it("should encapsulate a shared secret", () => {
+ const result = KyberCrypto.encapsulate(keyPair.publicKey);
+ expect(result.sharedSecret).toBeInstanceOf(Uint8Array);
+ expect(result.encapsulatedSecret).toBeInstanceOf(Uint8Array);
+ expect(result.sharedSecret.length).toBeGreaterThan(0);
+ expect(result.encapsulatedSecret.length).toBeGreaterThan(0);
+ });
- it("should generate different shared secrets on each call", () => {
- const result1 = KyberCrypto.encapsulate(keyPair.publicKey);
- const result2 = KyberCrypto.encapsulate(keyPair.publicKey);
- expect(result1.sharedSecret).not.toEqual(result2.sharedSecret);
- expect(result1.encapsulatedSecret).not.toEqual(result2.encapsulatedSecret);
- });
- });
+ it("should generate different shared secrets on each call", () => {
+ const result1 = KyberCrypto.encapsulate(keyPair.publicKey);
+ const result2 = KyberCrypto.encapsulate(keyPair.publicKey);
+ expect(result1.sharedSecret).not.toEqual(result2.sharedSecret);
+ expect(result1.encapsulatedSecret).not.toEqual(result2.encapsulatedSecret);
+ });
+ });
- describe("decapsulate()", () => {
- it("should decapsulate the shared secret correctly", () => {
- const encapsulated = KyberCrypto.encapsulate(keyPair.publicKey);
- const decapsulated = KyberCrypto.decapsulate(
- keyPair.privateKey,
- encapsulated.encapsulatedSecret
- );
- expect(decapsulated).toEqual(encapsulated.sharedSecret);
- });
+ describe("decapsulate()", () => {
+ it("should decapsulate the shared secret correctly", () => {
+ const encapsulated = KyberCrypto.encapsulate(keyPair.publicKey);
+ const decapsulated = KyberCrypto.decapsulate(
+ keyPair.privateKey,
+ encapsulated.encapsulatedSecret,
+ );
+ expect(decapsulated).toEqual(encapsulated.sharedSecret);
+ });
- it("should return different results for different private keys", () => {
- const keyPair2 = KyberCrypto.generateKeyPair();
- const encapsulated = KyberCrypto.encapsulate(keyPair.publicKey);
- const decapsulated1 = KyberCrypto.decapsulate(
- keyPair.privateKey,
- encapsulated.encapsulatedSecret
- );
- const decapsulated2 = KyberCrypto.decapsulate(
- keyPair2.privateKey,
- encapsulated.encapsulatedSecret
- );
- expect(decapsulated1).not.toEqual(decapsulated2);
- });
- });
+ it("should return different results for different private keys", () => {
+ const keyPair2 = KyberCrypto.generateKeyPair();
+ const encapsulated = KyberCrypto.encapsulate(keyPair.publicKey);
+ const decapsulated1 = KyberCrypto.decapsulate(
+ keyPair.privateKey,
+ encapsulated.encapsulatedSecret,
+ );
+ const decapsulated2 = KyberCrypto.decapsulate(
+ keyPair2.privateKey,
+ encapsulated.encapsulatedSecret,
+ );
+ expect(decapsulated1).not.toEqual(decapsulated2);
+ });
+ });
});
describe("AESCrypto", () => {
- describe("generateSessionKey()", () => {
- it("should generate a 32-byte key", () => {
- const key = AESCrypto.generateSessionKey();
- expect(key).toBeInstanceOf(Uint8Array);
- expect(key.length).toBe(32);
- });
+ describe("generateSessionKey()", () => {
+ it("should generate a 32-byte key", () => {
+ const key = AESCrypto.generateSessionKey();
+ expect(key).toBeInstanceOf(Uint8Array);
+ expect(key.length).toBe(32);
+ });
- it("should generate different keys on each call", () => {
- const key1 = AESCrypto.generateSessionKey();
- const key2 = AESCrypto.generateSessionKey();
- expect(key1).not.toEqual(key2);
- });
- });
+ it("should generate different keys on each call", () => {
+ const key1 = AESCrypto.generateSessionKey();
+ const key2 = AESCrypto.generateSessionKey();
+ expect(key1).not.toEqual(key2);
+ });
+ });
- describe("encrypt() and decrypt()", () => {
- let key: CryptoBuffer;
+ describe("encrypt() and decrypt()", () => {
+ let key: CryptoBuffer;
- beforeEach(() => {
- key = AESCrypto.generateSessionKey();
- });
+ beforeEach(() => {
+ key = AESCrypto.generateSessionKey();
+ });
- it("should encrypt and decrypt data correctly", async () => {
- const plaintext = "Hello, World!";
- const encrypted = await AESCrypto.encrypt(plaintext, key);
+ it("should encrypt and decrypt data correctly", async () => {
+ const plaintext = "Hello, World!";
+ const encrypted = await AESCrypto.encrypt(plaintext, key);
- expect(encrypted.encrypted).toBeInstanceOf(Uint8Array);
- expect(encrypted.iv).toBeInstanceOf(Uint8Array);
- expect(encrypted.tag).toBeInstanceOf(Uint8Array);
- expect(encrypted.iv.length).toBe(16);
- expect(encrypted.tag.length).toBe(16);
+ expect(encrypted.encrypted).toBeInstanceOf(Uint8Array);
+ expect(encrypted.iv).toBeInstanceOf(Uint8Array);
+ expect(encrypted.tag).toBeInstanceOf(Uint8Array);
+ expect(encrypted.iv.length).toBe(16);
+ expect(encrypted.tag.length).toBe(16);
- const decrypted = await AESCrypto.decrypt(
- encrypted.encrypted,
- key,
- encrypted.iv,
- encrypted.tag
- );
- expect(decrypted).toBe(plaintext);
- });
+ const decrypted = await AESCrypto.decrypt(
+ encrypted.encrypted,
+ key,
+ encrypted.iv,
+ encrypted.tag,
+ );
+ expect(decrypted).toBe(plaintext);
+ });
- it("should handle empty string", async () => {
- const plaintext = "";
- const encrypted = await AESCrypto.encrypt(plaintext, key);
- const decrypted = await AESCrypto.decrypt(
- encrypted.encrypted,
- key,
- encrypted.iv,
- encrypted.tag
- );
- expect(decrypted).toBe(plaintext);
- });
+ it("should handle empty string", async () => {
+ const plaintext = "";
+ const encrypted = await AESCrypto.encrypt(plaintext, key);
+ const decrypted = await AESCrypto.decrypt(
+ encrypted.encrypted,
+ key,
+ encrypted.iv,
+ encrypted.tag,
+ );
+ expect(decrypted).toBe(plaintext);
+ });
- it("should handle unicode characters", async () => {
- const plaintext = "🔐 Verschlüsselter Text mit Umlauten: äöü";
- const encrypted = await AESCrypto.encrypt(plaintext, key);
- const decrypted = await AESCrypto.decrypt(
- encrypted.encrypted,
- key,
- encrypted.iv,
- encrypted.tag
- );
- expect(decrypted).toBe(plaintext);
- });
+ it("should handle unicode characters", async () => {
+ const plaintext = "🔐 Verschlüsselter Text mit Umlauten: äöü";
+ const encrypted = await AESCrypto.encrypt(plaintext, key);
+ const decrypted = await AESCrypto.decrypt(
+ encrypted.encrypted,
+ key,
+ encrypted.iv,
+ encrypted.tag,
+ );
+ expect(decrypted).toBe(plaintext);
+ });
- it("should generate different IV and tag for same plaintext", async () => {
- const plaintext = "Same text";
- const encrypted1 = await AESCrypto.encrypt(plaintext, key);
- const encrypted2 = await AESCrypto.encrypt(plaintext, key);
+ it("should generate different IV and tag for same plaintext", async () => {
+ const plaintext = "Same text";
+ const encrypted1 = await AESCrypto.encrypt(plaintext, key);
+ const encrypted2 = await AESCrypto.encrypt(plaintext, key);
- expect(encrypted1.iv).not.toEqual(encrypted2.iv);
- expect(encrypted1.encrypted).not.toEqual(encrypted2.encrypted);
- expect(encrypted1.tag).not.toEqual(encrypted2.tag);
- });
+ expect(encrypted1.iv).not.toEqual(encrypted2.iv);
+ expect(encrypted1.encrypted).not.toEqual(encrypted2.encrypted);
+ expect(encrypted1.tag).not.toEqual(encrypted2.tag);
+ });
- it("should fail with wrong key", async () => {
- const plaintext = "Secret message";
- const wrongKey = AESCrypto.generateSessionKey();
- const encrypted = await AESCrypto.encrypt(plaintext, key);
+ it("should fail with wrong key", async () => {
+ const plaintext = "Secret message";
+ const wrongKey = AESCrypto.generateSessionKey();
+ const encrypted = await AESCrypto.encrypt(plaintext, key);
- await expect(
- AESCrypto.decrypt(encrypted.encrypted, wrongKey, encrypted.iv, encrypted.tag)
- ).rejects.toThrow();
- });
+ await expect(
+ AESCrypto.decrypt(encrypted.encrypted, wrongKey, encrypted.iv, encrypted.tag),
+ ).rejects.toThrow();
+ });
- it("should fail with wrong IV", async () => {
- const plaintext = "Secret message";
- const wrongIV = BufferUtils.randomBytes(16);
- const encrypted = await AESCrypto.encrypt(plaintext, key);
+ it("should fail with wrong IV", async () => {
+ const plaintext = "Secret message";
+ const wrongIV = BufferUtils.randomBytes(16);
+ const encrypted = await AESCrypto.encrypt(plaintext, key);
- await expect(
- AESCrypto.decrypt(encrypted.encrypted, key, wrongIV, encrypted.tag)
- ).rejects.toThrow();
- });
+ await expect(
+ AESCrypto.decrypt(encrypted.encrypted, key, wrongIV, encrypted.tag),
+ ).rejects.toThrow();
+ });
- it("should fail with wrong tag", async () => {
- const plaintext = "Secret message";
- const wrongTag = BufferUtils.randomBytes(16);
- const encrypted = await AESCrypto.encrypt(plaintext, key);
+ it("should fail with wrong tag", async () => {
+ const plaintext = "Secret message";
+ const wrongTag = BufferUtils.randomBytes(16);
+ const encrypted = await AESCrypto.encrypt(plaintext, key);
- await expect(
- AESCrypto.decrypt(encrypted.encrypted, key, encrypted.iv, wrongTag)
- ).rejects.toThrow();
- });
- });
+ await expect(
+ AESCrypto.decrypt(encrypted.encrypted, key, encrypted.iv, wrongTag),
+ ).rejects.toThrow();
+ });
+ });
});
describe("ShamirSecretSharing", () => {
- describe("splitSecret()", () => {
- it("should split secret into shares", () => {
- const secret = BufferUtils.from("test secret");
- const shares = ShamirSecretSharing.splitSecret(secret, 2, 3);
+ describe("splitSecret()", () => {
+ it("should split secret into shares", () => {
+ const secret = BufferUtils.from("test secret");
+ const shares = ShamirSecretSharing.splitSecret(secret, 2, 3);
- expect(shares).toHaveLength(3);
- shares.forEach((share, index) => {
- expect(share.x).toBe(index + 1);
- expect(share.y).toBeInstanceOf(Uint8Array);
- expect(share.y.length).toBe(4 + secret.length); // 4 bytes for length + secret
- });
- });
+ expect(shares).toHaveLength(3);
+ shares.forEach((share, index) => {
+ expect(share.x).toBe(index + 1);
+ expect(share.y).toBeInstanceOf(Uint8Array);
+ expect(share.y.length).toBe(4 + secret.length); // 4 bytes for length + secret
+ });
+ });
- it("should throw error for empty secret", () => {
- const secret = new Uint8Array([]);
- expect(() => ShamirSecretSharing.splitSecret(secret, 2, 3)).toThrow(
- "Secret cannot be empty for Shamir secret sharing"
- );
- });
+ it("should throw error for empty secret", () => {
+ const secret = new Uint8Array([]);
+ expect(() => ShamirSecretSharing.splitSecret(secret, 2, 3)).toThrow(
+ "Secret cannot be empty for Shamir secret sharing",
+ );
+ });
- it("should handle large secrets", () => {
- const secret = BufferUtils.randomBytes(1000);
- const shares = ShamirSecretSharing.splitSecret(secret, 3, 5);
+ it("should handle large secrets", () => {
+ const secret = BufferUtils.randomBytes(1000);
+ const shares = ShamirSecretSharing.splitSecret(secret, 3, 5);
- expect(shares).toHaveLength(5);
- shares.forEach((share) => {
- expect(share.y.length).toBe(4 + secret.length);
- });
- });
- });
+ expect(shares).toHaveLength(5);
+ shares.forEach((share) => {
+ expect(share.y.length).toBe(4 + secret.length);
+ });
+ });
+ });
- describe("reconstructSecret()", () => {
- it("should reconstruct secret from shares", () => {
- const originalSecret = BufferUtils.from("test secret for reconstruction");
- const shares = ShamirSecretSharing.splitSecret(originalSecret, 2, 5);
+ describe("reconstructSecret()", () => {
+ it("should reconstruct secret from shares", () => {
+ const originalSecret = BufferUtils.from("test secret for reconstruction");
+ const shares = ShamirSecretSharing.splitSecret(originalSecret, 2, 5);
- // Use first 2 shares (minimum threshold)
- const reconstructed = ShamirSecretSharing.reconstructSecret(shares.slice(0, 2));
- expect(reconstructed).toEqual(originalSecret);
- });
+ // Use first 2 shares (minimum threshold)
+ const reconstructed = ShamirSecretSharing.reconstructSecret(shares.slice(0, 2));
+ expect(reconstructed).toEqual(originalSecret);
+ });
- it("should reconstruct secret from any subset of shares", () => {
- const originalSecret = BufferUtils.from("another test secret");
- const shares = ShamirSecretSharing.splitSecret(originalSecret, 3, 5);
+ it("should reconstruct secret from any subset of shares", () => {
+ const originalSecret = BufferUtils.from("another test secret");
+ const shares = ShamirSecretSharing.splitSecret(originalSecret, 3, 5);
- // Test different combinations
- const reconstructed1 = ShamirSecretSharing.reconstructSecret(shares.slice(0, 3));
- const reconstructed2 = ShamirSecretSharing.reconstructSecret(shares.slice(1, 4));
- const reconstructed3 = ShamirSecretSharing.reconstructSecret(shares.slice(2, 5));
+ // Test different combinations
+ const reconstructed1 = ShamirSecretSharing.reconstructSecret(shares.slice(0, 3));
+ const reconstructed2 = ShamirSecretSharing.reconstructSecret(shares.slice(1, 4));
+ const reconstructed3 = ShamirSecretSharing.reconstructSecret(shares.slice(2, 5));
- expect(reconstructed1).toEqual(originalSecret);
- expect(reconstructed2).toEqual(originalSecret);
- expect(reconstructed3).toEqual(originalSecret);
- });
+ expect(reconstructed1).toEqual(originalSecret);
+ expect(reconstructed2).toEqual(originalSecret);
+ expect(reconstructed3).toEqual(originalSecret);
+ });
- it("should throw error with insufficient shares", () => {
- const shares: ShamirShare[] = [{ x: 1, y: new Uint8Array([1, 2, 3]) }];
- expect(() => ShamirSecretSharing.reconstructSecret(shares)).toThrow(
- "Need at least 2 shares to reconstruct the secret"
- );
- });
+ it("should throw error with insufficient shares", () => {
+ const shares: ShamirShare[] = [{ x: 1, y: new Uint8Array([1, 2, 3]) }];
+ expect(() => ShamirSecretSharing.reconstructSecret(shares)).toThrow(
+ "Need at least 2 shares to reconstruct the secret",
+ );
+ });
- it("should handle secrets with different lengths", () => {
- const secrets = [
- BufferUtils.from("short"),
- BufferUtils.from("medium length secret"),
- BufferUtils.randomBytes(100)
- ];
+ it("should handle secrets with different lengths", () => {
+ const secrets = [
+ BufferUtils.from("short"),
+ BufferUtils.from("medium length secret"),
+ BufferUtils.randomBytes(100),
+ ];
- secrets.forEach((secret) => {
- const shares = ShamirSecretSharing.splitSecret(secret, 2, 3);
- const reconstructed = ShamirSecretSharing.reconstructSecret(shares.slice(0, 2));
- expect(reconstructed).toEqual(secret);
- });
- });
- });
+ secrets.forEach((secret) => {
+ const shares = ShamirSecretSharing.splitSecret(secret, 2, 3);
+ const reconstructed = ShamirSecretSharing.reconstructSecret(shares.slice(0, 2));
+ expect(reconstructed).toEqual(secret);
+ });
+ });
+ });
});
diff --git a/src/lib/crypto/hashing.ts b/src/lib/crypto/hashing.ts
index c65e1cf..3fa6777 100644
--- a/src/lib/crypto/hashing.ts
+++ b/src/lib/crypto/hashing.ts
@@ -14,14 +14,14 @@ import { logger } from "$lib/logger";
* Configuration options for Argon2 hashing
*/
export interface Argon2Options {
- /** Memory cost parameter (default: 65536) */
- memoryCost?: number;
- /** Time cost parameter (default: 10) */
- timeCost?: number;
- /** Parallelism parameter (default: 1) */
- parallelism?: number;
- /** Hash output length in bytes (default: 32) */
- hashLength?: number;
+ /** Memory cost parameter (default: 65536) */
+ memoryCost?: number;
+ /** Time cost parameter (default: 10) */
+ timeCost?: number;
+ /** Parallelism parameter (default: 1) */
+ parallelism?: number;
+ /** Hash output length in bytes (default: 32) */
+ hashLength?: number;
}
/**
@@ -31,212 +31,212 @@ export interface Argon2Options {
* Final fallback to @noble/hashes if neither is available
*/
export class OptimizedArgon2 {
- /**
- * Derives a cryptographic key from a PIN and client ID using Argon2
- * @param pin - The PIN to hash
- * @param clientId - The client identifier used as salt material
- * @param options - Optional Argon2 parameters
- * @returns Promise resolving to the derived key
- */
- static async deriveKeyFromPIN(
- pin: string,
- clientId: string,
- options: Argon2Options = {}
- ): Promise {
- const defaultOptions = {
- memoryCost: 65536,
- timeCost: 10,
- parallelism: 1,
- hashLength: 32
- };
+ /**
+ * Derives a cryptographic key from a PIN and client ID using Argon2
+ * @param pin - The PIN to hash
+ * @param clientId - The client identifier used as salt material
+ * @param options - Optional Argon2 parameters
+ * @returns Promise resolving to the derived key
+ */
+ static async deriveKeyFromPIN(
+ pin: string,
+ clientId: string,
+ options: Argon2Options = {},
+ ): Promise {
+ const defaultOptions = {
+ memoryCost: 65536,
+ timeCost: 10,
+ parallelism: 1,
+ hashLength: 32,
+ };
- const opts = { ...defaultOptions, ...options };
+ const opts = { ...defaultOptions, ...options };
- if (typeof window !== "undefined") {
- return await this.#deriveKeyBrowser(pin, clientId, opts);
- } else {
- return await this.#deriveKeyNode(pin, clientId, opts);
- }
- }
+ if (typeof window !== "undefined") {
+ return await this.#deriveKeyBrowser(pin, clientId, opts);
+ } else {
+ return await this.#deriveKeyNode(pin, clientId, opts);
+ }
+ }
- /**
- * Node.js-specific key derivation using native argon2 or fallback
- * @param pin - The PIN to hash
- * @param clientId - The client identifier
- * @param options - Complete Argon2 options
- * @returns Promise resolving to the derived key
- * @private
- */
- static async #deriveKeyNode(
- pin: string,
- clientId: string,
- options: Required
- ): Promise {
- try {
- const crypto = await import("crypto");
- const salt = crypto.createHash("sha256").update(clientId).digest();
+ /**
+ * Node.js-specific key derivation using native argon2 or fallback
+ * @param pin - The PIN to hash
+ * @param clientId - The client identifier
+ * @param options - Complete Argon2 options
+ * @returns Promise resolving to the derived key
+ * @private
+ */
+ static async #deriveKeyNode(
+ pin: string,
+ clientId: string,
+ options: Required,
+ ): Promise {
+ try {
+ const crypto = await import("crypto");
+ const salt = crypto.createHash("sha256").update(clientId).digest();
- try {
- const argon2 = await import("argon2");
- const hash = await argon2.hash(pin, {
- type: argon2.argon2id,
- memoryCost: options.memoryCost,
- timeCost: options.timeCost,
- parallelism: options.parallelism,
- salt: salt,
- raw: true,
- hashLength: options.hashLength
- });
- return new Uint8Array(hash);
- } catch {
- logger.setContext("Hashing");
- logger.warn("Native argon2 unavailable, falling back to @noble/hashes");
- return await this.#deriveKeyFallback(pin, salt, options);
- }
- } catch (error) {
- throw new Error(`Failed to derive key in Node.js: ${error}`);
- }
- }
+ try {
+ const argon2 = await import("argon2");
+ const hash = await argon2.hash(pin, {
+ type: argon2.argon2id,
+ memoryCost: options.memoryCost,
+ timeCost: options.timeCost,
+ parallelism: options.parallelism,
+ salt: salt,
+ raw: true,
+ hashLength: options.hashLength,
+ });
+ return new Uint8Array(hash);
+ } catch {
+ logger.setContext("Hashing");
+ logger.warn("Native argon2 unavailable, falling back to @noble/hashes");
+ return await this.#deriveKeyFallback(pin, salt, options);
+ }
+ } catch (error) {
+ throw new Error(`Failed to derive key in Node.js: ${error}`);
+ }
+ }
- /**
- * Browser-specific key derivation using argon2-browser WASM or fallback
- * @param pin - The PIN to hash
- * @param clientId - The client identifier
- * @param options - Complete Argon2 options
- * @returns Promise resolving to the derived key
- * @private
- */
- static async #deriveKeyBrowser(
- pin: string,
- clientId: string,
- options: Required
- ): Promise {
- try {
- // Create salt from client ID using Web Crypto API
- const encoder = new TextEncoder();
- const saltData = await crypto.subtle.digest("SHA-256", encoder.encode(clientId));
- const salt = new Uint8Array(saltData);
+ /**
+ * Browser-specific key derivation using argon2-browser WASM or fallback
+ * @param pin - The PIN to hash
+ * @param clientId - The client identifier
+ * @param options - Complete Argon2 options
+ * @returns Promise resolving to the derived key
+ * @private
+ */
+ static async #deriveKeyBrowser(
+ pin: string,
+ clientId: string,
+ options: Required,
+ ): Promise {
+ try {
+ // Create salt from client ID using Web Crypto API
+ const encoder = new TextEncoder();
+ const saltData = await crypto.subtle.digest("SHA-256", encoder.encode(clientId));
+ const salt = new Uint8Array(saltData);
- // Try to use argon2-browser WASM (from static files)
- try {
- logger.setContext("Hashing");
- logger.debug("🔍 Checking for argon2-browser from static files...");
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const argon2 = (window as any).argon2;
+ // Try to use argon2-browser WASM (from static files)
+ try {
+ logger.setContext("Hashing");
+ logger.debug("🔍 Checking for argon2-browser from static files...");
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const argon2 = (window as any).argon2;
- if (argon2) {
- logger.debug("✅ argon2-browser found from static files");
+ if (argon2) {
+ logger.debug("✅ argon2-browser found from static files");
- const result = await argon2.hash({
- pass: pin,
- salt: Array.from(salt),
- type: argon2.ArgonType.Argon2id,
- mem: options.memoryCost,
- time: options.timeCost,
- parallelism: options.parallelism,
- hashLen: options.hashLength
- });
+ const result = await argon2.hash({
+ pass: pin,
+ salt: Array.from(salt),
+ type: argon2.ArgonType.Argon2id,
+ mem: options.memoryCost,
+ time: options.timeCost,
+ parallelism: options.parallelism,
+ hashLen: options.hashLength,
+ });
- logger.debug("✅ Argon2 WASM hash successful");
- return new Uint8Array(result.hash);
- } else {
- logger.warn("❌ argon2-browser not available on window");
- throw new Error("argon2-browser not available");
- }
- } catch (error) {
- logger.warn(
- "❌ argon2-browser static files unavailable, falling back to @noble/hashes:",
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- error as any
- );
- return await this.#deriveKeyFallback(pin, salt, options);
- }
- } catch (error) {
- throw new Error(`Failed to derive key in browser: ${error}`);
- }
- }
+ logger.debug("✅ Argon2 WASM hash successful");
+ return new Uint8Array(result.hash);
+ } else {
+ logger.warn("❌ argon2-browser not available on window");
+ throw new Error("argon2-browser not available");
+ }
+ } catch (error) {
+ logger.warn(
+ "❌ argon2-browser static files unavailable, falling back to @noble/hashes:",
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ error as any,
+ );
+ return await this.#deriveKeyFallback(pin, salt, options);
+ }
+ } catch (error) {
+ throw new Error(`Failed to derive key in browser: ${error}`);
+ }
+ }
- /**
- * Fallback key derivation using @noble/hashes
- * @param pin - The PIN to hash
- * @param salt - The salt for hashing
- * @param options - Complete Argon2 options
- * @returns Promise resolving to the derived key
- * @private
- */
- static async #deriveKeyFallback(
- pin: string,
- salt: CryptoBuffer,
- options: Required
- ): Promise {
- const { argon2id } = await import("@noble/hashes/argon2");
+ /**
+ * Fallback key derivation using @noble/hashes
+ * @param pin - The PIN to hash
+ * @param salt - The salt for hashing
+ * @param options - Complete Argon2 options
+ * @returns Promise resolving to the derived key
+ * @private
+ */
+ static async #deriveKeyFallback(
+ pin: string,
+ salt: CryptoBuffer,
+ options: Required,
+ ): Promise {
+ const { argon2id } = await import("@noble/hashes/argon2");
- return argon2id(BufferUtils.from(pin), new Uint8Array(salt), {
- m: options.memoryCost,
- t: options.timeCost,
- p: options.parallelism,
- dkLen: options.hashLength
- });
- }
+ return argon2id(BufferUtils.from(pin), new Uint8Array(salt), {
+ m: options.memoryCost,
+ t: options.timeCost,
+ p: options.parallelism,
+ dkLen: options.hashLength,
+ });
+ }
- /**
- * Creates a client ID from an email address (browser-only)
- * @param email - The email address to hash
- * @returns Promise resolving to hex-encoded client ID
- * @throws Error in Node.js environment
- */
- static async createClientId(email: string): Promise {
- if (typeof window !== "undefined") {
- // Browser environment - use Web Crypto API
- const hash = await crypto.subtle.digest(
- "SHA-256",
- new TextEncoder().encode(email.toLowerCase())
- );
- return BufferUtils.toString(new Uint8Array(hash), "hex");
- } else {
- // Node.js environment - this will be handled at runtime
- throw new Error("createClientId should be called asynchronously in Node.js environment");
- }
- }
+ /**
+ * Creates a client ID from an email address (browser-only)
+ * @param email - The email address to hash
+ * @returns Promise resolving to hex-encoded client ID
+ * @throws Error in Node.js environment
+ */
+ static async createClientId(email: string): Promise {
+ if (typeof window !== "undefined") {
+ // Browser environment - use Web Crypto API
+ const hash = await crypto.subtle.digest(
+ "SHA-256",
+ new TextEncoder().encode(email.toLowerCase()),
+ );
+ return BufferUtils.toString(new Uint8Array(hash), "hex");
+ } else {
+ // Node.js environment - this will be handled at runtime
+ throw new Error("createClientId should be called asynchronously in Node.js environment");
+ }
+ }
- /**
- * Creates a client ID from an email address (cross-platform)
- * @param email - The email address to hash
- * @returns Promise resolving to hex-encoded client ID
- */
- static async createClientIdAsync(email: string): Promise {
- if (typeof window !== "undefined") {
- // Browser environment
- const hash = await crypto.subtle.digest(
- "SHA-256",
- new TextEncoder().encode(email.toLowerCase())
- );
- return BufferUtils.toString(new Uint8Array(hash), "hex");
- } else {
- // Node.js environment
- const crypto = await import("crypto");
- return crypto.createHash("sha256").update(email.toLowerCase()).digest("hex");
- }
- }
+ /**
+ * Creates a client ID from an email address (cross-platform)
+ * @param email - The email address to hash
+ * @returns Promise resolving to hex-encoded client ID
+ */
+ static async createClientIdAsync(email: string): Promise {
+ if (typeof window !== "undefined") {
+ // Browser environment
+ const hash = await crypto.subtle.digest(
+ "SHA-256",
+ new TextEncoder().encode(email.toLowerCase()),
+ );
+ return BufferUtils.toString(new Uint8Array(hash), "hex");
+ } else {
+ // Node.js environment
+ const crypto = await import("crypto");
+ return crypto.createHash("sha256").update(email.toLowerCase()).digest("hex");
+ }
+ }
- /**
- * Gets information about the current Argon2 implementation being used
- * @returns Promise resolving to implementation description
- */
- static async getImplementationInfo(): Promise {
- if (typeof window !== "undefined") {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- if ((window as any).argon2) {
- return "argon2-browser WASM (static files)";
- }
- return "@noble/hashes (JavaScript fallback)";
- } else {
- try {
- await import("argon2");
- return "Native Node.js argon2 (C++ addon)";
- } catch {
- return "@noble/hashes (JavaScript fallback)";
- }
- }
- }
+ /**
+ * Gets information about the current Argon2 implementation being used
+ * @returns Promise resolving to implementation description
+ */
+ static async getImplementationInfo(): Promise {
+ if (typeof window !== "undefined") {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if ((window as any).argon2) {
+ return "argon2-browser WASM (static files)";
+ }
+ return "@noble/hashes (JavaScript fallback)";
+ } else {
+ try {
+ await import("argon2");
+ return "Native Node.js argon2 (C++ addon)";
+ } catch {
+ return "@noble/hashes (JavaScript fallback)";
+ }
+ }
+ }
}
diff --git a/src/lib/crypto/utils.ts b/src/lib/crypto/utils.ts
index 4b2117c..c79c3e7 100644
--- a/src/lib/crypto/utils.ts
+++ b/src/lib/crypto/utils.ts
@@ -10,105 +10,105 @@ export type CryptoBuffer = Uint8Array;
* Utility class for buffer operations and conversions
*/
export class BufferUtils {
- /**
- * Converts various data types to CryptoBuffer
- * @param data - The data to convert (string, number array, Uint8Array, or ArrayBuffer)
- * @param encoding - The encoding to use for string conversion ('hex' or 'utf8')
- * @returns A CryptoBuffer representation of the input data
- */
- static from(
- data: string | number[] | Uint8Array | ArrayBuffer,
- encoding?: "hex" | "utf8"
- ): CryptoBuffer {
- if (typeof data === "string") {
- if (encoding === "hex") {
- return new Uint8Array(data.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) || []);
- }
- return new TextEncoder().encode(data);
- }
- if (Array.isArray(data)) {
- return new Uint8Array(data);
- }
- return new Uint8Array(data);
- }
+ /**
+ * Converts various data types to CryptoBuffer
+ * @param data - The data to convert (string, number array, Uint8Array, or ArrayBuffer)
+ * @param encoding - The encoding to use for string conversion ('hex' or 'utf8')
+ * @returns A CryptoBuffer representation of the input data
+ */
+ static from(
+ data: string | number[] | Uint8Array | ArrayBuffer,
+ encoding?: "hex" | "utf8",
+ ): CryptoBuffer {
+ if (typeof data === "string") {
+ if (encoding === "hex") {
+ return new Uint8Array(data.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) || []);
+ }
+ return new TextEncoder().encode(data);
+ }
+ if (Array.isArray(data)) {
+ return new Uint8Array(data);
+ }
+ return new Uint8Array(data);
+ }
- /**
- * Converts a CryptoBuffer to string
- * @param buffer - The buffer to convert
- * @param encoding - The encoding to use ('hex' or 'utf8', defaults to 'utf8')
- * @returns String representation of the buffer
- */
- static toString(buffer: CryptoBuffer, encoding: "hex" | "utf8" = "utf8"): string {
- if (encoding === "hex") {
- return Array.from(buffer)
- .map((b) => b.toString(16).padStart(2, "0"))
- .join("");
- }
- return new TextDecoder().decode(buffer);
- }
+ /**
+ * Converts a CryptoBuffer to string
+ * @param buffer - The buffer to convert
+ * @param encoding - The encoding to use ('hex' or 'utf8', defaults to 'utf8')
+ * @returns String representation of the buffer
+ */
+ static toString(buffer: CryptoBuffer, encoding: "hex" | "utf8" = "utf8"): string {
+ if (encoding === "hex") {
+ return Array.from(buffer)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("");
+ }
+ return new TextDecoder().decode(buffer);
+ }
- /**
- * Concatenates multiple CryptoBuffers into a single buffer
- * @param buffers - Array of buffers to concatenate
- * @returns A single CryptoBuffer containing all input buffers
- */
- static concat(buffers: CryptoBuffer[]): CryptoBuffer {
- const totalLength = buffers.reduce((sum, buf) => sum + buf.length, 0);
- const result = new Uint8Array(totalLength);
- let offset = 0;
- for (const buffer of buffers) {
- result.set(buffer, offset);
- offset += buffer.length;
- }
- return result;
- }
+ /**
+ * Concatenates multiple CryptoBuffers into a single buffer
+ * @param buffers - Array of buffers to concatenate
+ * @returns A single CryptoBuffer containing all input buffers
+ */
+ static concat(buffers: CryptoBuffer[]): CryptoBuffer {
+ const totalLength = buffers.reduce((sum, buf) => sum + buf.length, 0);
+ const result = new Uint8Array(totalLength);
+ let offset = 0;
+ for (const buffer of buffers) {
+ result.set(buffer, offset);
+ offset += buffer.length;
+ }
+ return result;
+ }
- /**
- * Generates cryptographically secure random bytes
- * @param length - The number of random bytes to generate
- * @returns A CryptoBuffer containing random bytes
- */
- static randomBytes(length: number): CryptoBuffer {
- return randomBytes(length);
- }
+ /**
+ * Generates cryptographically secure random bytes
+ * @param length - The number of random bytes to generate
+ * @returns A CryptoBuffer containing random bytes
+ */
+ static randomBytes(length: number): CryptoBuffer {
+ return randomBytes(length);
+ }
- /**
- * Performs XOR operation on two CryptoBuffers
- * @param a - First buffer
- * @param b - Second buffer
- * @returns XOR result as CryptoBuffer
- */
- static xor(a: CryptoBuffer, b: CryptoBuffer): CryptoBuffer {
- const result = new Uint8Array(Math.max(a.length, b.length));
- for (let i = 0; i < result.length; i++) {
- result[i] = (a[i] || 0) ^ (b[i] || 0);
- }
- return result;
- }
+ /**
+ * Performs XOR operation on two CryptoBuffers
+ * @param a - First buffer
+ * @param b - Second buffer
+ * @returns XOR result as CryptoBuffer
+ */
+ static xor(a: CryptoBuffer, b: CryptoBuffer): CryptoBuffer {
+ const result = new Uint8Array(Math.max(a.length, b.length));
+ for (let i = 0; i < result.length; i++) {
+ result[i] = (a[i] || 0) ^ (b[i] || 0);
+ }
+ return result;
+ }
- /**
- * Compares two CryptoBuffers for equality
- * @param a - First buffer
- * @param b - Second buffer
- * @returns True if buffers are equal, false otherwise
- */
- static equals(a: CryptoBuffer, b: CryptoBuffer): boolean {
- if (a.length !== b.length) return false;
- for (let i = 0; i < a.length; i++) {
- if (a[i] !== b[i]) return false;
- }
- return true;
- }
+ /**
+ * Compares two CryptoBuffers for equality
+ * @param a - First buffer
+ * @param b - Second buffer
+ * @returns True if buffers are equal, false otherwise
+ */
+ static equals(a: CryptoBuffer, b: CryptoBuffer): boolean {
+ if (a.length !== b.length) return false;
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) return false;
+ }
+ return true;
+ }
}
/**
* Interface for Kyber (ML-KEM-768) key pairs
*/
export interface KyberKeyPair {
- /** The public key for encryption */
- publicKey: CryptoBuffer;
- /** The private key for decryption */
- privateKey: CryptoBuffer;
+ /** The public key for encryption */
+ publicKey: CryptoBuffer;
+ /** The private key for decryption */
+ privateKey: CryptoBuffer;
}
/**
@@ -116,43 +116,43 @@ export interface KyberKeyPair {
* Provides key generation, encapsulation, and decapsulation
*/
export class KyberCrypto {
- /**
- * Generates a new Kyber key pair
- * @returns A new KyberKeyPair containing public and private keys
- */
- static generateKeyPair(): KyberKeyPair {
- const keys = ml_kem768.keygen();
- return {
- publicKey: new Uint8Array(keys.publicKey),
- privateKey: new Uint8Array(keys.secretKey)
- };
- }
+ /**
+ * Generates a new Kyber key pair
+ * @returns A new KyberKeyPair containing public and private keys
+ */
+ static generateKeyPair(): KyberKeyPair {
+ const keys = ml_kem768.keygen();
+ return {
+ publicKey: new Uint8Array(keys.publicKey),
+ privateKey: new Uint8Array(keys.secretKey),
+ };
+ }
- /**
- * Encapsulates a shared secret using the recipient's public key
- * @param publicKey - The recipient's public key
- * @returns Object containing the shared secret and encapsulated secret
- */
- static encapsulate(publicKey: CryptoBuffer): {
- sharedSecret: CryptoBuffer;
- encapsulatedSecret: CryptoBuffer;
- } {
- const result = ml_kem768.encapsulate(publicKey);
- return {
- sharedSecret: new Uint8Array(result.sharedSecret),
- encapsulatedSecret: new Uint8Array(result.cipherText)
- };
- }
+ /**
+ * Encapsulates a shared secret using the recipient's public key
+ * @param publicKey - The recipient's public key
+ * @returns Object containing the shared secret and encapsulated secret
+ */
+ static encapsulate(publicKey: CryptoBuffer): {
+ sharedSecret: CryptoBuffer;
+ encapsulatedSecret: CryptoBuffer;
+ } {
+ const result = ml_kem768.encapsulate(publicKey);
+ return {
+ sharedSecret: new Uint8Array(result.sharedSecret),
+ encapsulatedSecret: new Uint8Array(result.cipherText),
+ };
+ }
- /**
- * Decapsulates a shared secret using the private key
- * @param privateKey - The private key for decapsulation
- * @param encapsulatedSecret - The encapsulated secret from the sender
- * @returns The shared secret
- */
- static decapsulate(privateKey: CryptoBuffer, encapsulatedSecret: CryptoBuffer): CryptoBuffer {
- return new Uint8Array(ml_kem768.decapsulate(encapsulatedSecret, privateKey));
- }
+ /**
+ * Decapsulates a shared secret using the private key
+ * @param privateKey - The private key for decapsulation
+ * @param encapsulatedSecret - The encapsulated secret from the sender
+ * @returns The shared secret
+ */
+ static decapsulate(privateKey: CryptoBuffer, encapsulatedSecret: CryptoBuffer): CryptoBuffer {
+ return new Uint8Array(ml_kem768.decapsulate(encapsulatedSecret, privateKey));
+ }
}
/**
@@ -160,123 +160,123 @@ export class KyberCrypto {
* Uses Web Crypto API in browsers and Node.js crypto in server environments
*/
export class AESCrypto {
- /**
- * Generates a random 256-bit AES session key
- * @returns A 32-byte CryptoBuffer for use as AES-256 key
- */
- static generateSessionKey(): CryptoBuffer {
- return BufferUtils.randomBytes(32);
- }
+ /**
+ * Generates a random 256-bit AES session key
+ * @returns A 32-byte CryptoBuffer for use as AES-256 key
+ */
+ static generateSessionKey(): CryptoBuffer {
+ return BufferUtils.randomBytes(32);
+ }
- /**
- * Encrypts data using AES-256-GCM
- * @param data - The plaintext string to encrypt
- * @param key - The 32-byte AES key
- * @returns Promise resolving to encrypted data, IV, and authentication tag
- */
- static async encrypt(
- data: string,
- key: CryptoBuffer
- ): Promise<{ encrypted: CryptoBuffer; iv: CryptoBuffer; tag: CryptoBuffer }> {
- const iv = BufferUtils.randomBytes(16);
+ /**
+ * Encrypts data using AES-256-GCM
+ * @param data - The plaintext string to encrypt
+ * @param key - The 32-byte AES key
+ * @returns Promise resolving to encrypted data, IV, and authentication tag
+ */
+ static async encrypt(
+ data: string,
+ key: CryptoBuffer,
+ ): Promise<{ encrypted: CryptoBuffer; iv: CryptoBuffer; tag: CryptoBuffer }> {
+ const iv = BufferUtils.randomBytes(16);
- // Use Web Crypto API if available (browser), otherwise fall back to Node.js
- if (typeof crypto !== "undefined" && crypto.subtle) {
- // Browser implementation using Web Crypto API
- const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
- "encrypt"
- ]);
+ // Use Web Crypto API if available (browser), otherwise fall back to Node.js
+ if (typeof crypto !== "undefined" && crypto.subtle) {
+ // Browser implementation using Web Crypto API
+ const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
+ "encrypt",
+ ]);
- const additionalData = BufferUtils.from("appointment-data");
- const encrypted = await crypto.subtle.encrypt(
- { name: "AES-GCM", iv, additionalData },
- cryptoKey,
- BufferUtils.from(data)
- );
+ const additionalData = BufferUtils.from("appointment-data");
+ const encrypted = await crypto.subtle.encrypt(
+ { name: "AES-GCM", iv, additionalData },
+ cryptoKey,
+ BufferUtils.from(data),
+ );
- // Extract tag from encrypted data (last 16 bytes)
- const encryptedArray = new Uint8Array(encrypted);
- const tag = encryptedArray.slice(-16);
- const ciphertext = encryptedArray.slice(0, -16);
+ // Extract tag from encrypted data (last 16 bytes)
+ const encryptedArray = new Uint8Array(encrypted);
+ const tag = encryptedArray.slice(-16);
+ const ciphertext = encryptedArray.slice(0, -16);
- return {
- encrypted: ciphertext,
- iv,
- tag
- };
- } else {
- // Node.js fallback using built-in crypto
- const nodeCrypto = await import("crypto");
- const cipher = nodeCrypto.createCipheriv("aes-256-gcm", key, iv);
- cipher.setAAD(BufferUtils.from("appointment-data"));
+ return {
+ encrypted: ciphertext,
+ iv,
+ tag,
+ };
+ } else {
+ // Node.js fallback using built-in crypto
+ const nodeCrypto = await import("crypto");
+ const cipher = nodeCrypto.createCipheriv("aes-256-gcm", key, iv);
+ cipher.setAAD(BufferUtils.from("appointment-data"));
- let encrypted = cipher.update(data, "utf8");
- encrypted = Buffer.concat([encrypted, cipher.final()]);
- const tag = cipher.getAuthTag();
+ let encrypted = cipher.update(data, "utf8");
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
+ const tag = cipher.getAuthTag();
- return {
- encrypted: new Uint8Array(encrypted),
- iv,
- tag: new Uint8Array(tag)
- };
- }
- }
+ return {
+ encrypted: new Uint8Array(encrypted),
+ iv,
+ tag: new Uint8Array(tag),
+ };
+ }
+ }
- /**
- * Decrypts AES-256-GCM encrypted data
- * @param encrypted - The encrypted data
- * @param key - The 32-byte AES key
- * @param iv - The initialization vector
- * @param tag - The authentication tag
- * @returns Promise resolving to the decrypted plaintext string
- */
- static async decrypt(
- encrypted: CryptoBuffer,
- key: CryptoBuffer,
- iv: CryptoBuffer,
- tag: CryptoBuffer
- ): Promise {
- if (typeof crypto !== "undefined" && crypto.subtle) {
- // Browser implementation using Web Crypto API
- const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
- "decrypt"
- ]);
+ /**
+ * Decrypts AES-256-GCM encrypted data
+ * @param encrypted - The encrypted data
+ * @param key - The 32-byte AES key
+ * @param iv - The initialization vector
+ * @param tag - The authentication tag
+ * @returns Promise resolving to the decrypted plaintext string
+ */
+ static async decrypt(
+ encrypted: CryptoBuffer,
+ key: CryptoBuffer,
+ iv: CryptoBuffer,
+ tag: CryptoBuffer,
+ ): Promise {
+ if (typeof crypto !== "undefined" && crypto.subtle) {
+ // Browser implementation using Web Crypto API
+ const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
+ "decrypt",
+ ]);
- const additionalData = BufferUtils.from("appointment-data");
+ const additionalData = BufferUtils.from("appointment-data");
- // Combine encrypted data and tag for Web Crypto API
- const encryptedWithTag = BufferUtils.concat([encrypted, tag]);
+ // Combine encrypted data and tag for Web Crypto API
+ const encryptedWithTag = BufferUtils.concat([encrypted, tag]);
- const decrypted = await crypto.subtle.decrypt(
- { name: "AES-GCM", iv, additionalData },
- cryptoKey,
- encryptedWithTag
- );
+ const decrypted = await crypto.subtle.decrypt(
+ { name: "AES-GCM", iv, additionalData },
+ cryptoKey,
+ encryptedWithTag,
+ );
- return BufferUtils.toString(new Uint8Array(decrypted));
- } else {
- // Node.js fallback using built-in crypto
- const nodeCrypto = await import("crypto");
- const decipher = nodeCrypto.createDecipheriv("aes-256-gcm", key, iv);
- decipher.setAAD(BufferUtils.from("appointment-data"));
- decipher.setAuthTag(tag);
+ return BufferUtils.toString(new Uint8Array(decrypted));
+ } else {
+ // Node.js fallback using built-in crypto
+ const nodeCrypto = await import("crypto");
+ const decipher = nodeCrypto.createDecipheriv("aes-256-gcm", key, iv);
+ decipher.setAAD(BufferUtils.from("appointment-data"));
+ decipher.setAuthTag(tag);
- let decrypted = decipher.update(encrypted);
- decrypted = Buffer.concat([decrypted, decipher.final()]);
+ let decrypted = decipher.update(encrypted);
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
- return decrypted.toString("utf8");
- }
- }
+ return decrypted.toString("utf8");
+ }
+ }
}
/**
* Interface for Shamir secret sharing shares
*/
export interface ShamirShare {
- /** The x-coordinate of the share */
- x: number;
- /** The y-coordinate containing the share data */
- y: CryptoBuffer;
+ /** The x-coordinate of the share */
+ x: number;
+ /** The y-coordinate containing the share data */
+ y: CryptoBuffer;
}
/**
@@ -284,55 +284,55 @@ export interface ShamirShare {
* Note: This is a basic implementation for demonstration purposes
*/
export class ShamirSecretSharing {
- /**
- * Splits a secret into multiple shares
- * @param secret - The secret to split
- * @param threshold - Minimum number of shares needed to reconstruct
- * @param totalShares - Total number of shares to create
- * @returns Array of ShamirShare objects
- */
- static splitSecret(secret: CryptoBuffer, threshold: number, totalShares: number): ShamirShare[] {
- if (!secret || secret.length === 0) {
- throw new Error("Secret cannot be empty for Shamir secret sharing");
- }
+ /**
+ * Splits a secret into multiple shares
+ * @param secret - The secret to split
+ * @param threshold - Minimum number of shares needed to reconstruct
+ * @param totalShares - Total number of shares to create
+ * @returns Array of ShamirShare objects
+ */
+ static splitSecret(secret: CryptoBuffer, threshold: number, totalShares: number): ShamirShare[] {
+ if (!secret || secret.length === 0) {
+ throw new Error("Secret cannot be empty for Shamir secret sharing");
+ }
- const originalLength = secret.length;
- const lengthBytes = new Uint8Array(4);
- new DataView(lengthBytes.buffer).setUint32(0, originalLength, true);
+ const originalLength = secret.length;
+ const lengthBytes = new Uint8Array(4);
+ new DataView(lengthBytes.buffer).setUint32(0, originalLength, true);
- const shares: ShamirShare[] = [];
+ const shares: ShamirShare[] = [];
- for (let i = 0; i < totalShares; i++) {
- const shareData = new Uint8Array(4 + secret.length);
- shareData.set(lengthBytes, 0);
- shareData.set(secret, 4);
+ for (let i = 0; i < totalShares; i++) {
+ const shareData = new Uint8Array(4 + secret.length);
+ shareData.set(lengthBytes, 0);
+ shareData.set(secret, 4);
- shares.push({
- x: i + 1,
- y: shareData
- });
- }
+ shares.push({
+ x: i + 1,
+ y: shareData,
+ });
+ }
- return shares;
- }
+ return shares;
+ }
- /**
- * Reconstructs a secret from shares
- * @param shareArray - Array of shares to reconstruct from
- * @returns The reconstructed secret
- */
- static reconstructSecret(shareArray: ShamirShare[]): CryptoBuffer {
- if (shareArray.length < 2) {
- throw new Error("Need at least 2 shares to reconstruct the secret");
- }
+ /**
+ * Reconstructs a secret from shares
+ * @param shareArray - Array of shares to reconstruct from
+ * @returns The reconstructed secret
+ */
+ static reconstructSecret(shareArray: ShamirShare[]): CryptoBuffer {
+ if (shareArray.length < 2) {
+ throw new Error("Need at least 2 shares to reconstruct the secret");
+ }
- const firstShare = shareArray[0];
+ const firstShare = shareArray[0];
- const lengthBytes = firstShare.y.slice(0, 4);
- const originalLength = new DataView(lengthBytes.buffer).getUint32(0, true);
+ const lengthBytes = firstShare.y.slice(0, 4);
+ const originalLength = new DataView(lengthBytes.buffer).getUint32(0, true);
- const secret = firstShare.y.slice(4, 4 + originalLength);
+ const secret = firstShare.y.slice(4, 4 + originalLength);
- return secret;
- }
+ return secret;
+ }
}
diff --git a/src/lib/hooks/is-mobile.svelte.ts b/src/lib/hooks/is-mobile.svelte.ts
index 4829c00..ef34be7 100644
--- a/src/lib/hooks/is-mobile.svelte.ts
+++ b/src/lib/hooks/is-mobile.svelte.ts
@@ -3,7 +3,7 @@ import { MediaQuery } from "svelte/reactivity";
const DEFAULT_MOBILE_BREAKPOINT = 768;
export class IsMobile extends MediaQuery {
- constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) {
- super(`max-width: ${breakpoint - 1}px`);
- }
+ constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) {
+ super(`max-width: ${breakpoint - 1}px`);
+ }
}
diff --git a/src/lib/logger/__tests__/api-endpoint.test.ts b/src/lib/logger/__tests__/api-endpoint.test.ts
index 598d8ab..2c7db25 100644
--- a/src/lib/logger/__tests__/api-endpoint.test.ts
+++ b/src/lib/logger/__tests__/api-endpoint.test.ts
@@ -4,68 +4,68 @@ import { describe, it, expect } from "vitest";
// The actual server endpoint at /routes/api/log/+server.ts is tested via integration
describe("API Log Endpoint Logic", () => {
- describe("Request Processing", () => {
- it("should handle different log levels correctly", () => {
- // Test that our endpoint logic handles all log levels
- const logLevels = ["debug", "info", "warn", "error", "unknown"];
+ describe("Request Processing", () => {
+ it("should handle different log levels correctly", () => {
+ // Test that our endpoint logic handles all log levels
+ const logLevels = ["debug", "info", "warn", "error", "unknown"];
- logLevels.forEach((level) => {
- expect(level).toBeTruthy(); // Basic test that levels exist
- });
- });
+ logLevels.forEach((level) => {
+ expect(level).toBeTruthy(); // Basic test that levels exist
+ });
+ });
- it("should validate request structure", () => {
- // Test basic request validation concepts
- const validRequest = {
- level: "info",
- message: "test message",
- meta: { key: "value" }
- };
+ it("should validate request structure", () => {
+ // Test basic request validation concepts
+ const validRequest = {
+ level: "info",
+ message: "test message",
+ meta: { key: "value" },
+ };
- expect(validRequest.level).toBe("info");
- expect(validRequest.message).toBe("test message");
- expect(validRequest.meta).toEqual({ key: "value" });
- });
+ expect(validRequest.level).toBe("info");
+ expect(validRequest.message).toBe("test message");
+ expect(validRequest.meta).toEqual({ key: "value" });
+ });
- it("should handle missing or malformed data", () => {
- // Test edge cases
- const edgeCases = [
- { level: null, message: "test" },
- { level: "info", message: null },
- { level: "info", message: "test", meta: null },
- {}
- ];
+ it("should handle missing or malformed data", () => {
+ // Test edge cases
+ const edgeCases = [
+ { level: null, message: "test" },
+ { level: "info", message: null },
+ { level: "info", message: "test", meta: null },
+ {},
+ ];
- edgeCases.forEach((testCase) => {
- // Basic validation that we can handle these cases
- expect(typeof testCase).toBe("object");
- });
- });
- });
+ edgeCases.forEach((testCase) => {
+ // Basic validation that we can handle these cases
+ expect(typeof testCase).toBe("object");
+ });
+ });
+ });
- describe("Response Format", () => {
- it("should return success response format", () => {
- const successResponse = { success: true };
- expect(successResponse.success).toBe(true);
- });
+ describe("Response Format", () => {
+ it("should return success response format", () => {
+ const successResponse = { success: true };
+ expect(successResponse.success).toBe(true);
+ });
- it("should return error response format", () => {
- const errorResponse = { success: false };
- expect(errorResponse.success).toBe(false);
- });
- });
+ it("should return error response format", () => {
+ const errorResponse = { success: false };
+ expect(errorResponse.success).toBe(false);
+ });
+ });
- describe("Error Handling", () => {
- it("should handle JSON parsing errors", () => {
- const error = new Error("Invalid JSON");
- expect(error.message).toBe("Invalid JSON");
- });
+ describe("Error Handling", () => {
+ it("should handle JSON parsing errors", () => {
+ const error = new Error("Invalid JSON");
+ expect(error.message).toBe("Invalid JSON");
+ });
- it("should handle unknown errors", () => {
- const unknownError = "string error";
- expect(typeof unknownError).toBe("string");
- });
- });
+ it("should handle unknown errors", () => {
+ const unknownError = "string error";
+ expect(typeof unknownError).toBe("string");
+ });
+ });
});
// Note: The actual API endpoint is tested through integration tests
diff --git a/src/lib/logger/__tests__/index.server.test.ts b/src/lib/logger/__tests__/index.server.test.ts
index d8cc2ff..6af9513 100644
--- a/src/lib/logger/__tests__/index.server.test.ts
+++ b/src/lib/logger/__tests__/index.server.test.ts
@@ -3,389 +3,389 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock $app/environment for server environment
vi.mock("$app/environment", () => ({
- browser: false,
- dev: true
+ browser: false,
+ dev: true,
}));
// Mock winston logger
const mockWinstonLogger = {
- debug: vi.fn(),
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn()
+ debug: vi.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
};
// Mock winston module
vi.mock("../winston", () => ({
- default: mockWinstonLogger
+ default: mockWinstonLogger,
}));
describe("UniversalLogger - Server Side", () => {
- let logger: any;
- let createLogger: any;
+ let logger: any;
+ let createLogger: any;
- beforeEach(async () => {
- // Clear all mocks
- vi.clearAllMocks();
+ beforeEach(async () => {
+ // Clear all mocks
+ vi.clearAllMocks();
- // Dynamic import after mocks are set up
- const loggerModule = await import("../index");
- logger = loggerModule.logger;
- createLogger = loggerModule.createLogger;
- });
+ // Dynamic import after mocks are set up
+ const loggerModule = await import("../index");
+ logger = loggerModule.logger;
+ createLogger = loggerModule.createLogger;
+ });
- afterEach(() => {
- vi.restoreAllMocks();
- });
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
- describe("Context Management", () => {
- it("should set and use context correctly", () => {
- const contextLogger = createLogger("ServerContext");
- contextLogger.info("Server message", { data: "test" });
+ describe("Context Management", () => {
+ it("should set and use context correctly", () => {
+ const contextLogger = createLogger("ServerContext");
+ contextLogger.info("Server message", { data: "test" });
- expect(mockWinstonLogger.info).toHaveBeenCalledWith({
- message: "[ServerContext] Server message",
- data: "test",
- source: "server",
- userAgent: undefined,
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
- });
- });
+ expect(mockWinstonLogger.info).toHaveBeenCalledWith({
+ message: "[ServerContext] Server message",
+ data: "test",
+ source: "server",
+ userAgent: undefined,
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
+ });
+ });
- it("should work without context", () => {
- logger.info("Server message without context");
+ it("should work without context", () => {
+ logger.info("Server message without context");
- expect(mockWinstonLogger.info).toHaveBeenCalledWith({
- message: "Server message without context",
- source: "server",
- userAgent: undefined,
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
- });
- });
+ expect(mockWinstonLogger.info).toHaveBeenCalledWith({
+ message: "Server message without context",
+ source: "server",
+ userAgent: undefined,
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
+ });
+ });
- it("should return logger instance when setting context", () => {
- const result = logger.setContext("TestContext");
- expect(result).toBe(logger);
- });
- });
+ it("should return logger instance when setting context", () => {
+ const result = logger.setContext("TestContext");
+ expect(result).toBe(logger);
+ });
+ });
- describe("Message Formatting", () => {
- it("should format messages with context prefix", () => {
- const testLogger = createLogger("API");
- testLogger.debug("Processing request", { requestId: "123" });
+ describe("Message Formatting", () => {
+ it("should format messages with context prefix", () => {
+ const testLogger = createLogger("API");
+ testLogger.debug("Processing request", { requestId: "123" });
- expect(mockWinstonLogger.debug).toHaveBeenCalledWith({
- message: "[API] Processing request",
- requestId: "123",
- source: "server",
- userAgent: undefined,
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
- });
- });
+ expect(mockWinstonLogger.debug).toHaveBeenCalledWith({
+ message: "[API] Processing request",
+ requestId: "123",
+ source: "server",
+ userAgent: undefined,
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
+ });
+ });
- it("should format messages without context prefix when no context set", async () => {
- // Create a fresh logger without context
- const { UniversalLogger } = await import("../index");
- const freshLogger = new UniversalLogger();
- freshLogger.debug("Processing request", { requestId: "456" });
+ it("should format messages without context prefix when no context set", async () => {
+ // Create a fresh logger without context
+ const { UniversalLogger } = await import("../index");
+ const freshLogger = new UniversalLogger();
+ freshLogger.debug("Processing request", { requestId: "456" });
- expect(mockWinstonLogger.debug).toHaveBeenCalledWith({
- message: "Processing request",
- requestId: "456",
- source: "server",
- userAgent: undefined,
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
- });
- });
+ expect(mockWinstonLogger.debug).toHaveBeenCalledWith({
+ message: "Processing request",
+ requestId: "456",
+ source: "server",
+ userAgent: undefined,
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
+ });
+ });
- it("should include source as server", () => {
- logger.info("Test message");
+ it("should include source as server", () => {
+ logger.info("Test message");
- expect(mockWinstonLogger.info).toHaveBeenCalledWith(
- expect.objectContaining({
- source: "server"
- })
- );
- });
+ expect(mockWinstonLogger.info).toHaveBeenCalledWith(
+ expect.objectContaining({
+ source: "server",
+ }),
+ );
+ });
- it("should set userAgent as undefined for server", () => {
- logger.info("Test message");
+ it("should set userAgent as undefined for server", () => {
+ logger.info("Test message");
- expect(mockWinstonLogger.info).toHaveBeenCalledWith(
- expect.objectContaining({
- userAgent: undefined
- })
- );
- });
+ expect(mockWinstonLogger.info).toHaveBeenCalledWith(
+ expect.objectContaining({
+ userAgent: undefined,
+ }),
+ );
+ });
- it("should add timestamp to all messages", () => {
- const beforeTime = Date.now();
- logger.info("Test message");
- const afterTime = Date.now();
+ it("should add timestamp to all messages", () => {
+ const beforeTime = Date.now();
+ logger.info("Test message");
+ const afterTime = Date.now();
- expect(mockWinstonLogger.info).toHaveBeenCalledWith(
- expect.objectContaining({
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
- })
- );
+ expect(mockWinstonLogger.info).toHaveBeenCalledWith(
+ expect.objectContaining({
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
+ }),
+ );
- const loggedTimestamp = new Date(mockWinstonLogger.info.mock.calls[0][0].timestamp).getTime();
- expect(loggedTimestamp).toBeGreaterThanOrEqual(beforeTime);
- expect(loggedTimestamp).toBeLessThanOrEqual(afterTime);
- });
- });
+ const loggedTimestamp = new Date(mockWinstonLogger.info.mock.calls[0][0].timestamp).getTime();
+ expect(loggedTimestamp).toBeGreaterThanOrEqual(beforeTime);
+ expect(loggedTimestamp).toBeLessThanOrEqual(afterTime);
+ });
+ });
- describe("Logging Methods", () => {
- it("should call winston debug with formatted message", () => {
- const testLogger = createLogger("Debug");
- const meta = { debug: true, level: 1 };
+ describe("Logging Methods", () => {
+ it("should call winston debug with formatted message", () => {
+ const testLogger = createLogger("Debug");
+ const meta = { debug: true, level: 1 };
- testLogger.debug("Debug message", meta);
+ testLogger.debug("Debug message", meta);
- expect(mockWinstonLogger.debug).toHaveBeenCalledWith({
- message: "[Debug] Debug message",
- debug: true,
- level: 1,
- source: "server",
- userAgent: undefined,
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
- });
- });
+ expect(mockWinstonLogger.debug).toHaveBeenCalledWith({
+ message: "[Debug] Debug message",
+ debug: true,
+ level: 1,
+ source: "server",
+ userAgent: undefined,
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
+ });
+ });
- it("should call winston info with formatted message", () => {
- const testLogger = createLogger("Info");
- const meta = { userId: 123, action: "login" };
+ it("should call winston info with formatted message", () => {
+ const testLogger = createLogger("Info");
+ const meta = { userId: 123, action: "login" };
- testLogger.info("User logged in", meta);
+ testLogger.info("User logged in", meta);
- expect(mockWinstonLogger.info).toHaveBeenCalledWith({
- message: "[Info] User logged in",
- userId: 123,
- action: "login",
- source: "server",
- userAgent: undefined,
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
- });
- });
+ expect(mockWinstonLogger.info).toHaveBeenCalledWith({
+ message: "[Info] User logged in",
+ userId: 123,
+ action: "login",
+ source: "server",
+ userAgent: undefined,
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
+ });
+ });
- it("should call winston warn with formatted message", () => {
- const testLogger = createLogger("Warn");
- const meta = { warning: "deprecated", api: "v1" };
+ it("should call winston warn with formatted message", () => {
+ const testLogger = createLogger("Warn");
+ const meta = { warning: "deprecated", api: "v1" };
- testLogger.warn("API deprecated", meta);
+ testLogger.warn("API deprecated", meta);
- expect(mockWinstonLogger.warn).toHaveBeenCalledWith({
- message: "[Warn] API deprecated",
- warning: "deprecated",
- api: "v1",
- source: "server",
- userAgent: undefined,
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
- });
- });
+ expect(mockWinstonLogger.warn).toHaveBeenCalledWith({
+ message: "[Warn] API deprecated",
+ warning: "deprecated",
+ api: "v1",
+ source: "server",
+ userAgent: undefined,
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
+ });
+ });
- it("should call winston error with formatted message", () => {
- const testLogger = createLogger("Error");
- const meta = { error: "database_connection", code: 500 };
+ it("should call winston error with formatted message", () => {
+ const testLogger = createLogger("Error");
+ const meta = { error: "database_connection", code: 500 };
- testLogger.error("Database connection failed", meta);
+ testLogger.error("Database connection failed", meta);
- expect(mockWinstonLogger.error).toHaveBeenCalledWith({
- message: "[Error] Database connection failed",
- error: "database_connection",
- code: 500,
- source: "server",
- userAgent: undefined,
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
- });
- });
+ expect(mockWinstonLogger.error).toHaveBeenCalledWith({
+ message: "[Error] Database connection failed",
+ error: "database_connection",
+ code: 500,
+ source: "server",
+ userAgent: undefined,
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
+ });
+ });
- it("should handle empty meta objects", async () => {
- // Create a fresh logger without context
- const { UniversalLogger } = await import("../index");
- const freshLogger = new UniversalLogger();
- freshLogger.info("Message without meta");
+ it("should handle empty meta objects", async () => {
+ // Create a fresh logger without context
+ const { UniversalLogger } = await import("../index");
+ const freshLogger = new UniversalLogger();
+ freshLogger.info("Message without meta");
- expect(mockWinstonLogger.info).toHaveBeenCalledWith({
- message: "Message without meta",
- source: "server",
- userAgent: undefined,
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
- });
- });
+ expect(mockWinstonLogger.info).toHaveBeenCalledWith({
+ message: "Message without meta",
+ source: "server",
+ userAgent: undefined,
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
+ });
+ });
- it("should preserve existing meta properties", async () => {
- const meta = {
- source: "override-attempt",
- timestamp: "override-attempt",
- userAgent: "override-attempt",
- customProp: "should-be-preserved"
- };
+ it("should preserve existing meta properties", async () => {
+ const meta = {
+ source: "override-attempt",
+ timestamp: "override-attempt",
+ userAgent: "override-attempt",
+ customProp: "should-be-preserved",
+ };
- // Create a fresh logger without context
- const { UniversalLogger } = await import("../index");
- const freshLogger = new UniversalLogger();
- freshLogger.info("Test message", meta);
+ // Create a fresh logger without context
+ const { UniversalLogger } = await import("../index");
+ const freshLogger = new UniversalLogger();
+ freshLogger.info("Test message", meta);
- expect(mockWinstonLogger.info).toHaveBeenCalledWith({
- message: "Test message",
- source: "server", // Should override
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/), // Should override
- userAgent: undefined, // Should override
- customProp: "should-be-preserved" // Should preserve
- });
- });
- });
+ expect(mockWinstonLogger.info).toHaveBeenCalledWith({
+ message: "Test message",
+ source: "server", // Should override
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/), // Should override
+ userAgent: undefined, // Should override
+ customProp: "should-be-preserved", // Should preserve
+ });
+ });
+ });
- describe("Request Logging", () => {
- it("should log successful requests as info", async () => {
- const mockRequest = {
- method: "GET",
- url: "https://api.example.com/users"
- } as Request;
+ describe("Request Logging", () => {
+ it("should log successful requests as info", async () => {
+ const mockRequest = {
+ method: "GET",
+ url: "https://api.example.com/users",
+ } as Request;
- // Create a fresh logger without context
- const { UniversalLogger } = await import("../index");
- const freshLogger = new UniversalLogger();
- freshLogger.logRequest(mockRequest, 150, 200);
+ // Create a fresh logger without context
+ const { UniversalLogger } = await import("../index");
+ const freshLogger = new UniversalLogger();
+ freshLogger.logRequest(mockRequest, 150, 200);
- expect(mockWinstonLogger.info).toHaveBeenCalledWith({
- message: "GET https://api.example.com/users - 200 (150ms)",
- method: "GET",
- url: "https://api.example.com/users",
- statusCode: 200,
- responseTime: 150,
- source: "server",
- userAgent: undefined,
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
- });
- });
+ expect(mockWinstonLogger.info).toHaveBeenCalledWith({
+ message: "GET https://api.example.com/users - 200 (150ms)",
+ method: "GET",
+ url: "https://api.example.com/users",
+ statusCode: 200,
+ responseTime: 150,
+ source: "server",
+ userAgent: undefined,
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
+ });
+ });
- it("should log client errors (4xx) as errors", async () => {
- const mockRequest = {
- method: "POST",
- url: "https://api.example.com/users"
- } as Request;
+ it("should log client errors (4xx) as errors", async () => {
+ const mockRequest = {
+ method: "POST",
+ url: "https://api.example.com/users",
+ } as Request;
- // Create a fresh logger without context
- const { UniversalLogger } = await import("../index");
- const freshLogger = new UniversalLogger();
- freshLogger.logRequest(mockRequest, 200, 404);
+ // Create a fresh logger without context
+ const { UniversalLogger } = await import("../index");
+ const freshLogger = new UniversalLogger();
+ freshLogger.logRequest(mockRequest, 200, 404);
- expect(mockWinstonLogger.error).toHaveBeenCalledWith({
- message: "POST https://api.example.com/users - 404 (200ms)",
- method: "POST",
- url: "https://api.example.com/users",
- statusCode: 404,
- responseTime: 200,
- source: "server",
- userAgent: undefined,
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
- });
- });
+ expect(mockWinstonLogger.error).toHaveBeenCalledWith({
+ message: "POST https://api.example.com/users - 404 (200ms)",
+ method: "POST",
+ url: "https://api.example.com/users",
+ statusCode: 404,
+ responseTime: 200,
+ source: "server",
+ userAgent: undefined,
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
+ });
+ });
- it("should log server errors (5xx) as errors", async () => {
- const mockRequest = {
- method: "PUT",
- url: "https://api.example.com/data/123"
- } as Request;
+ it("should log server errors (5xx) as errors", async () => {
+ const mockRequest = {
+ method: "PUT",
+ url: "https://api.example.com/data/123",
+ } as Request;
- // Create a fresh logger without context
- const { UniversalLogger } = await import("../index");
- const freshLogger = new UniversalLogger();
- freshLogger.logRequest(mockRequest, 5000, 500);
+ // Create a fresh logger without context
+ const { UniversalLogger } = await import("../index");
+ const freshLogger = new UniversalLogger();
+ freshLogger.logRequest(mockRequest, 5000, 500);
- expect(mockWinstonLogger.error).toHaveBeenCalledWith({
- message: "PUT https://api.example.com/data/123 - 500 (5000ms)",
- method: "PUT",
- url: "https://api.example.com/data/123",
- statusCode: 500,
- responseTime: 5000,
- source: "server",
- userAgent: undefined,
- timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
- });
- });
+ expect(mockWinstonLogger.error).toHaveBeenCalledWith({
+ message: "PUT https://api.example.com/data/123 - 500 (5000ms)",
+ method: "PUT",
+ url: "https://api.example.com/data/123",
+ statusCode: 500,
+ responseTime: 5000,
+ source: "server",
+ userAgent: undefined,
+ timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
+ });
+ });
- it("should handle edge case status codes correctly", async () => {
- const mockRequest = {
- method: "GET",
- url: "https://api.example.com/test"
- } as Request;
+ it("should handle edge case status codes correctly", async () => {
+ const mockRequest = {
+ method: "GET",
+ url: "https://api.example.com/test",
+ } as Request;
- // Create fresh loggers without context
- const { UniversalLogger } = await import("../index");
- const freshLogger1 = new UniversalLogger();
- const freshLogger2 = new UniversalLogger();
+ // Create fresh loggers without context
+ const { UniversalLogger } = await import("../index");
+ const freshLogger1 = new UniversalLogger();
+ const freshLogger2 = new UniversalLogger();
- // Test boundary cases
- freshLogger1.logRequest(mockRequest, 100, 399); // Should be info
- expect(mockWinstonLogger.info).toHaveBeenCalled();
+ // Test boundary cases
+ freshLogger1.logRequest(mockRequest, 100, 399); // Should be info
+ expect(mockWinstonLogger.info).toHaveBeenCalled();
- vi.clearAllMocks();
+ vi.clearAllMocks();
- freshLogger2.logRequest(mockRequest, 100, 400); // Should be error
- expect(mockWinstonLogger.error).toHaveBeenCalled();
- });
- });
+ freshLogger2.logRequest(mockRequest, 100, 400); // Should be error
+ expect(mockWinstonLogger.error).toHaveBeenCalled();
+ });
+ });
- describe("Factory Function", () => {
- it("should create new logger instances with different contexts", () => {
- const logger1 = createLogger("Service1");
- const logger2 = createLogger("Service2");
+ describe("Factory Function", () => {
+ it("should create new logger instances with different contexts", () => {
+ const logger1 = createLogger("Service1");
+ const logger2 = createLogger("Service2");
- logger1.info("Message from service 1");
- logger2.info("Message from service 2");
+ logger1.info("Message from service 1");
+ logger2.info("Message from service 2");
- expect(mockWinstonLogger.info).toHaveBeenNthCalledWith(
- 1,
- expect.objectContaining({
- message: "[Service1] Message from service 1"
- })
- );
- expect(mockWinstonLogger.info).toHaveBeenNthCalledWith(
- 2,
- expect.objectContaining({
- message: "[Service2] Message from service 2"
- })
- );
- });
+ expect(mockWinstonLogger.info).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ message: "[Service1] Message from service 1",
+ }),
+ );
+ expect(mockWinstonLogger.info).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ message: "[Service2] Message from service 2",
+ }),
+ );
+ });
- it("should create independent logger instances", () => {
- const logger1 = createLogger("Original");
- const logger2 = createLogger("Independent");
+ it("should create independent logger instances", () => {
+ const logger1 = createLogger("Original");
+ const logger2 = createLogger("Independent");
- // Modify one logger's context
- logger1.setContext("Modified");
+ // Modify one logger's context
+ logger1.setContext("Modified");
- logger1.info("Message 1");
- logger2.info("Message 2");
+ logger1.info("Message 1");
+ logger2.info("Message 2");
- expect(mockWinstonLogger.info).toHaveBeenNthCalledWith(
- 1,
- expect.objectContaining({
- message: "[Modified] Message 1"
- })
- );
- expect(mockWinstonLogger.info).toHaveBeenNthCalledWith(
- 2,
- expect.objectContaining({
- message: "[Independent] Message 2"
- })
- );
- });
- });
+ expect(mockWinstonLogger.info).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ message: "[Modified] Message 1",
+ }),
+ );
+ expect(mockWinstonLogger.info).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ message: "[Independent] Message 2",
+ }),
+ );
+ });
+ });
- describe("No Client Error Forwarding on Server", () => {
- it("should not attempt to send error to server when in server environment", () => {
- // Mock fetch to verify it's not called
- const mockFetch = vi.fn();
- global.fetch = mockFetch;
+ describe("No Client Error Forwarding on Server", () => {
+ it("should not attempt to send error to server when in server environment", () => {
+ // Mock fetch to verify it's not called
+ const mockFetch = vi.fn();
+ global.fetch = mockFetch;
- logger.error("Server error", { code: 500 });
+ logger.error("Server error", { code: 500 });
- expect(mockWinstonLogger.error).toHaveBeenCalled();
- expect(mockFetch).not.toHaveBeenCalled();
- });
- });
+ expect(mockWinstonLogger.error).toHaveBeenCalled();
+ expect(mockFetch).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/src/lib/logger/__tests__/index.test.ts b/src/lib/logger/__tests__/index.test.ts
index 811f931..d2fa188 100644
--- a/src/lib/logger/__tests__/index.test.ts
+++ b/src/lib/logger/__tests__/index.test.ts
@@ -3,32 +3,32 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock $app/environment before importing the logger
vi.mock("$app/environment", () => ({
- browser: true
+ browser: true,
}));
// Mock global objects that exist in browser
Object.defineProperty(global, "navigator", {
- value: {
- userAgent: "Test Browser/1.0"
- },
- writable: true
+ value: {
+ userAgent: "Test Browser/1.0",
+ },
+ writable: true,
});
Object.defineProperty(global, "window", {
- value: {
- location: {
- href: "https://test.example.com/page"
- }
- },
- writable: true
+ value: {
+ location: {
+ href: "https://test.example.com/page",
+ },
+ },
+ writable: true,
});
// Mock console methods
const mockConsole = {
- debug: vi.fn(),
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn()
+ debug: vi.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
};
// Mock fetch
@@ -36,264 +36,264 @@ const mockFetch = vi.fn();
global.fetch = mockFetch;
describe("UniversalLogger - Client Side", () => {
- let logger: any;
- let createLogger: any;
+ let logger: any;
+ let createLogger: any;
- beforeEach(async () => {
- // Clear all mocks
- vi.clearAllMocks();
+ beforeEach(async () => {
+ // Clear all mocks
+ vi.clearAllMocks();
- // Mock console methods
- vi.spyOn(console, "debug").mockImplementation(mockConsole.debug);
- vi.spyOn(console, "info").mockImplementation(mockConsole.info);
- vi.spyOn(console, "warn").mockImplementation(mockConsole.warn);
- vi.spyOn(console, "error").mockImplementation(mockConsole.error);
+ // Mock console methods
+ vi.spyOn(console, "debug").mockImplementation(mockConsole.debug);
+ vi.spyOn(console, "info").mockImplementation(mockConsole.info);
+ vi.spyOn(console, "warn").mockImplementation(mockConsole.warn);
+ vi.spyOn(console, "error").mockImplementation(mockConsole.error);
- // Dynamic import after mocks are set up
- const loggerModule = await import("../index");
- logger = loggerModule.logger;
- createLogger = loggerModule.createLogger;
- });
+ // Dynamic import after mocks are set up
+ const loggerModule = await import("../index");
+ logger = loggerModule.logger;
+ createLogger = loggerModule.createLogger;
+ });
- afterEach(() => {
- vi.restoreAllMocks();
- });
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
- describe("Context Management", () => {
- it("should set and use context correctly", () => {
- const contextLogger = createLogger("TestContext");
- contextLogger.info("Test message");
+ describe("Context Management", () => {
+ it("should set and use context correctly", () => {
+ const contextLogger = createLogger("TestContext");
+ contextLogger.info("Test message");
- expect(mockConsole.info).toHaveBeenCalledWith("ℹ️ [TestContext] Test message", {});
- });
+ expect(mockConsole.info).toHaveBeenCalledWith("ℹ️ [TestContext] Test message", {});
+ });
- it("should work without context", () => {
- logger.info("Test message");
+ it("should work without context", () => {
+ logger.info("Test message");
- expect(mockConsole.info).toHaveBeenCalledWith("ℹ️ Test message", {});
- });
+ expect(mockConsole.info).toHaveBeenCalledWith("ℹ️ Test message", {});
+ });
- it("should return logger instance when setting context", () => {
- const result = logger.setContext("TestContext");
- expect(result).toBe(logger);
- });
- });
+ it("should return logger instance when setting context", () => {
+ const result = logger.setContext("TestContext");
+ expect(result).toBe(logger);
+ });
+ });
- describe("Logging Methods", () => {
- it("should log debug messages with correct format", () => {
- const testLogger = createLogger("Debug");
- const meta = { extra: "data" };
+ describe("Logging Methods", () => {
+ it("should log debug messages with correct format", () => {
+ const testLogger = createLogger("Debug");
+ const meta = { extra: "data" };
- testLogger.debug("Debug message", meta);
+ testLogger.debug("Debug message", meta);
- expect(mockConsole.debug).toHaveBeenCalledWith("🐛 [Debug] Debug message", meta);
- });
+ expect(mockConsole.debug).toHaveBeenCalledWith("🐛 [Debug] Debug message", meta);
+ });
- it("should log info messages with correct format", () => {
- const testLogger = createLogger("Info");
- const meta = { userId: 123 };
+ it("should log info messages with correct format", () => {
+ const testLogger = createLogger("Info");
+ const meta = { userId: 123 };
- testLogger.info("Info message", meta);
+ testLogger.info("Info message", meta);
- expect(mockConsole.info).toHaveBeenCalledWith("ℹ️ [Info] Info message", meta);
- });
+ expect(mockConsole.info).toHaveBeenCalledWith("ℹ️ [Info] Info message", meta);
+ });
- it("should log warn messages with correct format", () => {
- const testLogger = createLogger("Warn");
- const meta = { warning: "deprecated" };
+ it("should log warn messages with correct format", () => {
+ const testLogger = createLogger("Warn");
+ const meta = { warning: "deprecated" };
- testLogger.warn("Warning message", meta);
+ testLogger.warn("Warning message", meta);
- expect(mockConsole.warn).toHaveBeenCalledWith("⚠️ [Warn] Warning message", meta);
- });
+ expect(mockConsole.warn).toHaveBeenCalledWith("⚠️ [Warn] Warning message", meta);
+ });
- it("should log error messages with correct format", () => {
- const testLogger = createLogger("Error");
- const meta = { error: "failed" };
+ it("should log error messages with correct format", () => {
+ const testLogger = createLogger("Error");
+ const meta = { error: "failed" };
- testLogger.error("Error message", meta);
+ testLogger.error("Error message", meta);
- expect(mockConsole.error).toHaveBeenCalledWith("❌ [Error] Error message", meta);
- });
+ expect(mockConsole.error).toHaveBeenCalledWith("❌ [Error] Error message", meta);
+ });
- it("should handle empty meta objects", async () => {
- // Create a fresh logger without context
- const { UniversalLogger } = await import("../index");
- const freshLogger = new UniversalLogger();
- freshLogger.info("Message without meta");
+ it("should handle empty meta objects", async () => {
+ // Create a fresh logger without context
+ const { UniversalLogger } = await import("../index");
+ const freshLogger = new UniversalLogger();
+ freshLogger.info("Message without meta");
- expect(mockConsole.info).toHaveBeenCalledWith("ℹ️ Message without meta", {});
- });
- });
+ expect(mockConsole.info).toHaveBeenCalledWith("ℹ️ Message without meta", {});
+ });
+ });
- describe("Client Error Forwarding", () => {
- it("should send error to server when logging error", async () => {
- mockFetch.mockResolvedValueOnce({
- ok: true,
- json: async () => ({ success: true })
- });
+ describe("Client Error Forwarding", () => {
+ it("should send error to server when logging error", async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ success: true }),
+ });
- const testLogger = createLogger("ErrorTest");
- const meta = { errorCode: 500 };
+ const testLogger = createLogger("ErrorTest");
+ const meta = { errorCode: 500 };
- testLogger.error("Server error", meta);
+ testLogger.error("Server error", meta);
- // Wait for async operation
- await new Promise((resolve) => setTimeout(resolve, 0));
+ // Wait for async operation
+ await new Promise((resolve) => setTimeout(resolve, 0));
- expect(mockFetch).toHaveBeenCalledWith(
- "/api/log",
- expect.objectContaining({
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: expect.stringContaining('"level":"error"')
- })
- );
+ expect(mockFetch).toHaveBeenCalledWith(
+ "/api/log",
+ expect.objectContaining({
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: expect.stringContaining('"level":"error"'),
+ }),
+ );
- // Verify the sent data contains expected properties
- const sentData = JSON.parse(mockFetch.mock.calls[0][1].body);
- expect(sentData.level).toBe("error");
- expect(sentData.message).toBe("Server error");
- expect(sentData.meta.context).toBe("ErrorTest");
- expect(sentData.meta.url).toBe("https://test.example.com/page");
- expect(sentData.meta.userAgent).toBe("Test Browser/1.0");
- expect(sentData.meta.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
- });
+ // Verify the sent data contains expected properties
+ const sentData = JSON.parse(mockFetch.mock.calls[0][1].body);
+ expect(sentData.level).toBe("error");
+ expect(sentData.message).toBe("Server error");
+ expect(sentData.meta.context).toBe("ErrorTest");
+ expect(sentData.meta.url).toBe("https://test.example.com/page");
+ expect(sentData.meta.userAgent).toBe("Test Browser/1.0");
+ expect(sentData.meta.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
+ });
- it("should handle fetch errors gracefully", async () => {
- mockFetch.mockRejectedValueOnce(new Error("Network error"));
+ it("should handle fetch errors gracefully", async () => {
+ mockFetch.mockRejectedValueOnce(new Error("Network error"));
- const testLogger = createLogger("ErrorTest");
- testLogger.error("Test error");
+ const testLogger = createLogger("ErrorTest");
+ testLogger.error("Test error");
- // Wait for async operation
- await new Promise((resolve) => setTimeout(resolve, 0));
+ // Wait for async operation
+ await new Promise((resolve) => setTimeout(resolve, 0));
- expect(mockConsole.error).toHaveBeenCalledWith(
- "Failed to send error to server:",
- expect.any(Error)
- );
- });
+ expect(mockConsole.error).toHaveBeenCalledWith(
+ "Failed to send error to server:",
+ expect.any(Error),
+ );
+ });
- it("should not send non-error logs to server", () => {
- logger.info("Info message");
- logger.warn("Warning message");
- logger.debug("Debug message");
+ it("should not send non-error logs to server", () => {
+ logger.info("Info message");
+ logger.warn("Warning message");
+ logger.debug("Debug message");
- expect(mockFetch).not.toHaveBeenCalled();
- });
- });
+ expect(mockFetch).not.toHaveBeenCalled();
+ });
+ });
- describe("Request Logging", () => {
- it("should log successful requests as info", async () => {
- const mockRequest = {
- method: "GET",
- url: "https://api.example.com/users"
- } as Request;
+ describe("Request Logging", () => {
+ it("should log successful requests as info", async () => {
+ const mockRequest = {
+ method: "GET",
+ url: "https://api.example.com/users",
+ } as Request;
- // Create a fresh logger without context
- const { UniversalLogger } = await import("../index");
- const freshLogger = new UniversalLogger();
- freshLogger.logRequest(mockRequest, 150, 200);
+ // Create a fresh logger without context
+ const { UniversalLogger } = await import("../index");
+ const freshLogger = new UniversalLogger();
+ freshLogger.logRequest(mockRequest, 150, 200);
- expect(mockConsole.info).toHaveBeenCalledWith(
- "ℹ️ GET https://api.example.com/users - 200 (150ms)",
- {
- method: "GET",
- url: "https://api.example.com/users",
- statusCode: 200,
- responseTime: 150
- }
- );
- });
+ expect(mockConsole.info).toHaveBeenCalledWith(
+ "ℹ️ GET https://api.example.com/users - 200 (150ms)",
+ {
+ method: "GET",
+ url: "https://api.example.com/users",
+ statusCode: 200,
+ responseTime: 150,
+ },
+ );
+ });
- it("should log client errors (4xx) as errors", async () => {
- mockFetch.mockResolvedValueOnce({
- ok: true,
- json: async () => ({ success: true })
- });
+ it("should log client errors (4xx) as errors", async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ success: true }),
+ });
- const mockRequest = {
- method: "POST",
- url: "https://api.example.com/users"
- } as Request;
+ const mockRequest = {
+ method: "POST",
+ url: "https://api.example.com/users",
+ } as Request;
- // Create a fresh logger without context
- const { UniversalLogger } = await import("../index");
- const freshLogger = new UniversalLogger();
- freshLogger.logRequest(mockRequest, 200, 404);
+ // Create a fresh logger without context
+ const { UniversalLogger } = await import("../index");
+ const freshLogger = new UniversalLogger();
+ freshLogger.logRequest(mockRequest, 200, 404);
- expect(mockConsole.error).toHaveBeenCalledWith(
- "❌ POST https://api.example.com/users - 404 (200ms)",
- {
- method: "POST",
- url: "https://api.example.com/users",
- statusCode: 404,
- responseTime: 200
- }
- );
+ expect(mockConsole.error).toHaveBeenCalledWith(
+ "❌ POST https://api.example.com/users - 404 (200ms)",
+ {
+ method: "POST",
+ url: "https://api.example.com/users",
+ statusCode: 404,
+ responseTime: 200,
+ },
+ );
- // Should also send to server since it's an error
- await new Promise((resolve) => setTimeout(resolve, 0));
- expect(mockFetch).toHaveBeenCalled();
- });
+ // Should also send to server since it's an error
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ expect(mockFetch).toHaveBeenCalled();
+ });
- it("should log server errors (5xx) as errors", async () => {
- mockFetch.mockResolvedValueOnce({
- ok: true,
- json: async () => ({ success: true })
- });
+ it("should log server errors (5xx) as errors", async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ success: true }),
+ });
- const mockRequest = {
- method: "GET",
- url: "https://api.example.com/data"
- } as Request;
+ const mockRequest = {
+ method: "GET",
+ url: "https://api.example.com/data",
+ } as Request;
- // Create a fresh logger without context
- const { UniversalLogger } = await import("../index");
- const freshLogger = new UniversalLogger();
- freshLogger.logRequest(mockRequest, 1000, 500);
+ // Create a fresh logger without context
+ const { UniversalLogger } = await import("../index");
+ const freshLogger = new UniversalLogger();
+ freshLogger.logRequest(mockRequest, 1000, 500);
- expect(mockConsole.error).toHaveBeenCalledWith(
- "❌ GET https://api.example.com/data - 500 (1000ms)",
- {
- method: "GET",
- url: "https://api.example.com/data",
- statusCode: 500,
- responseTime: 1000
- }
- );
+ expect(mockConsole.error).toHaveBeenCalledWith(
+ "❌ GET https://api.example.com/data - 500 (1000ms)",
+ {
+ method: "GET",
+ url: "https://api.example.com/data",
+ statusCode: 500,
+ responseTime: 1000,
+ },
+ );
- // Should also send to server since it's an error
- await new Promise((resolve) => setTimeout(resolve, 0));
- expect(mockFetch).toHaveBeenCalled();
- });
- });
+ // Should also send to server since it's an error
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ expect(mockFetch).toHaveBeenCalled();
+ });
+ });
- describe("Factory Function", () => {
- it("should create new logger instances with context", () => {
- const logger1 = createLogger("Context1");
- const logger2 = createLogger("Context2");
+ describe("Factory Function", () => {
+ it("should create new logger instances with context", () => {
+ const logger1 = createLogger("Context1");
+ const logger2 = createLogger("Context2");
- logger1.info("Message 1");
- logger2.info("Message 2");
+ logger1.info("Message 1");
+ logger2.info("Message 2");
- expect(mockConsole.info).toHaveBeenNthCalledWith(1, "ℹ️ [Context1] Message 1", {});
- expect(mockConsole.info).toHaveBeenNthCalledWith(2, "ℹ️ [Context2] Message 2", {});
- });
+ expect(mockConsole.info).toHaveBeenNthCalledWith(1, "ℹ️ [Context1] Message 1", {});
+ expect(mockConsole.info).toHaveBeenNthCalledWith(2, "ℹ️ [Context2] Message 2", {});
+ });
- it("should create independent logger instances", () => {
- const logger1 = createLogger("Context1");
- const logger2 = createLogger("Context2");
+ it("should create independent logger instances", () => {
+ const logger1 = createLogger("Context1");
+ const logger2 = createLogger("Context2");
- // Modify one logger's context
- logger1.setContext("ModifiedContext");
+ // Modify one logger's context
+ logger1.setContext("ModifiedContext");
- logger1.info("Message 1");
- logger2.info("Message 2");
+ logger1.info("Message 1");
+ logger2.info("Message 2");
- expect(mockConsole.info).toHaveBeenNthCalledWith(1, "ℹ️ [ModifiedContext] Message 1", {});
- expect(mockConsole.info).toHaveBeenNthCalledWith(2, "ℹ️ [Context2] Message 2", {});
- });
- });
+ expect(mockConsole.info).toHaveBeenNthCalledWith(1, "ℹ️ [ModifiedContext] Message 1", {});
+ expect(mockConsole.info).toHaveBeenNthCalledWith(2, "ℹ️ [Context2] Message 2", {});
+ });
+ });
});
diff --git a/src/lib/logger/__tests__/integration.test.ts b/src/lib/logger/__tests__/integration.test.ts
index 5f82d06..73e823f 100644
--- a/src/lib/logger/__tests__/integration.test.ts
+++ b/src/lib/logger/__tests__/integration.test.ts
@@ -3,300 +3,300 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Integration test for the complete client error forwarding flow
describe("UniversalLogger Integration - Client Error Forwarding", () => {
- let mockFetch: any;
- let mockConsole: any;
- let originalFetch: any;
+ let mockFetch: any;
+ let mockConsole: any;
+ let originalFetch: any;
- beforeEach(() => {
- // Store original fetch
- originalFetch = global.fetch;
+ beforeEach(() => {
+ // Store original fetch
+ originalFetch = global.fetch;
- // Setup fetch mock
- mockFetch = vi.fn();
- global.fetch = mockFetch;
+ // Setup fetch mock
+ mockFetch = vi.fn();
+ global.fetch = mockFetch;
- // Setup console mocks
- mockConsole = {
- debug: vi.fn(),
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn()
- };
+ // Setup console mocks
+ mockConsole = {
+ debug: vi.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
+ };
- vi.spyOn(console, "debug").mockImplementation(mockConsole.debug);
- vi.spyOn(console, "info").mockImplementation(mockConsole.info);
- vi.spyOn(console, "warn").mockImplementation(mockConsole.warn);
- vi.spyOn(console, "error").mockImplementation(mockConsole.error);
+ vi.spyOn(console, "debug").mockImplementation(mockConsole.debug);
+ vi.spyOn(console, "info").mockImplementation(mockConsole.info);
+ vi.spyOn(console, "warn").mockImplementation(mockConsole.warn);
+ vi.spyOn(console, "error").mockImplementation(mockConsole.error);
- // Mock browser environment
- vi.doMock("$app/environment", () => ({
- browser: true
- }));
+ // Mock browser environment
+ vi.doMock("$app/environment", () => ({
+ browser: true,
+ }));
- // Mock browser globals
- Object.defineProperty(global, "navigator", {
- value: { userAgent: "Test Browser/1.0" },
- writable: true
- });
+ // Mock browser globals
+ Object.defineProperty(global, "navigator", {
+ value: { userAgent: "Test Browser/1.0" },
+ writable: true,
+ });
- Object.defineProperty(global, "window", {
- value: { location: { href: "https://test.example.com/page" } },
- writable: true
- });
- });
+ Object.defineProperty(global, "window", {
+ value: { location: { href: "https://test.example.com/page" } },
+ writable: true,
+ });
+ });
- afterEach(() => {
- // Restore original fetch
- global.fetch = originalFetch;
- vi.restoreAllMocks();
- vi.resetModules();
- });
+ afterEach(() => {
+ // Restore original fetch
+ global.fetch = originalFetch;
+ vi.restoreAllMocks();
+ vi.resetModules();
+ });
- describe("End-to-End Error Forwarding", () => {
- it("should forward client error to server and handle successful response", async () => {
- // Mock successful server response
- mockFetch.mockResolvedValueOnce({
- ok: true,
- json: async () => ({ success: true })
- });
+ describe("End-to-End Error Forwarding", () => {
+ it("should forward client error to server and handle successful response", async () => {
+ // Mock successful server response
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ success: true }),
+ });
- // Import logger after mocks are set up
- const { createLogger } = await import("../index");
- const logger = createLogger("IntegrationTest");
+ // Import logger after mocks are set up
+ const { createLogger } = await import("../index");
+ const logger = createLogger("IntegrationTest");
- // Trigger client error
- const errorMessage = "Integration test error";
- const errorMeta = { errorCode: "INT001", component: "TestComponent" };
+ // Trigger client error
+ const errorMessage = "Integration test error";
+ const errorMeta = { errorCode: "INT001", component: "TestComponent" };
- logger.error(errorMessage, errorMeta);
+ logger.error(errorMessage, errorMeta);
- // Wait for async operation
- await new Promise((resolve) => setTimeout(resolve, 10));
+ // Wait for async operation
+ await new Promise((resolve) => setTimeout(resolve, 10));
- // Verify client-side logging
- expect(mockConsole.error).toHaveBeenCalledWith(
- "❌ [IntegrationTest] Integration test error",
- errorMeta
- );
+ // Verify client-side logging
+ expect(mockConsole.error).toHaveBeenCalledWith(
+ "❌ [IntegrationTest] Integration test error",
+ errorMeta,
+ );
- // Verify server request
- expect(mockFetch).toHaveBeenCalledWith(
- "/api/log",
- expect.objectContaining({
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: expect.stringContaining('"level":"error"')
- })
- );
+ // Verify server request
+ expect(mockFetch).toHaveBeenCalledWith(
+ "/api/log",
+ expect.objectContaining({
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: expect.stringContaining('"level":"error"'),
+ }),
+ );
- // Verify the sent data contains expected properties
- const sentData = JSON.parse(mockFetch.mock.calls[0][1].body);
- expect(sentData.level).toBe("error");
- expect(sentData.message).toBe(errorMessage);
- expect(sentData.meta.context).toBe("IntegrationTest");
- expect(sentData.meta.url).toBe("https://test.example.com/page");
- expect(sentData.meta.userAgent).toBe("Test Browser/1.0");
- expect(sentData.meta.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
- });
+ // Verify the sent data contains expected properties
+ const sentData = JSON.parse(mockFetch.mock.calls[0][1].body);
+ expect(sentData.level).toBe("error");
+ expect(sentData.message).toBe(errorMessage);
+ expect(sentData.meta.context).toBe("IntegrationTest");
+ expect(sentData.meta.url).toBe("https://test.example.com/page");
+ expect(sentData.meta.userAgent).toBe("Test Browser/1.0");
+ expect(sentData.meta.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
+ });
- it("should handle server API error gracefully", async () => {
- // Mock server error response
- mockFetch.mockRejectedValueOnce(new Error("Network error"));
+ it("should handle server API error gracefully", async () => {
+ // Mock server error response
+ mockFetch.mockRejectedValueOnce(new Error("Network error"));
- const { createLogger } = await import("../index");
- const logger = createLogger("ErrorHandling");
+ const { createLogger } = await import("../index");
+ const logger = createLogger("ErrorHandling");
- logger.error("Test error");
+ logger.error("Test error");
- // Wait for async operation
- await new Promise((resolve) => setTimeout(resolve, 10));
+ // Wait for async operation
+ await new Promise((resolve) => setTimeout(resolve, 10));
- // Verify client error is logged locally
- expect(mockConsole.error).toHaveBeenCalledWith("❌ [ErrorHandling] Test error", {});
+ // Verify client error is logged locally
+ expect(mockConsole.error).toHaveBeenCalledWith("❌ [ErrorHandling] Test error", {});
- // Verify fallback error logging
- expect(mockConsole.error).toHaveBeenCalledWith(
- "Failed to send error to server:",
- expect.any(Error)
- );
- });
+ // Verify fallback error logging
+ expect(mockConsole.error).toHaveBeenCalledWith(
+ "Failed to send error to server:",
+ expect.any(Error),
+ );
+ });
- it("should handle server returning error status", async () => {
- // Mock server error status
- mockFetch.mockResolvedValueOnce({
- ok: false,
- status: 500,
- statusText: "Internal Server Error"
- });
+ it("should handle server returning error status", async () => {
+ // Mock server error status
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ statusText: "Internal Server Error",
+ });
- const { createLogger } = await import("../index");
- const logger = createLogger("ServerError");
+ const { createLogger } = await import("../index");
+ const logger = createLogger("ServerError");
- logger.error("Server unavailable");
+ logger.error("Server unavailable");
- // Wait for async operation
- await new Promise((resolve) => setTimeout(resolve, 10));
+ // Wait for async operation
+ await new Promise((resolve) => setTimeout(resolve, 10));
- // Should still attempt to send to server
- expect(mockFetch).toHaveBeenCalled();
+ // Should still attempt to send to server
+ expect(mockFetch).toHaveBeenCalled();
- // Local error should still be logged
- expect(mockConsole.error).toHaveBeenCalledWith("❌ [ServerError] Server unavailable", {});
- });
- });
+ // Local error should still be logged
+ expect(mockConsole.error).toHaveBeenCalledWith("❌ [ServerError] Server unavailable", {});
+ });
+ });
- describe("Server-side API Integration", () => {
- it("should process forwarded client errors correctly", async () => {
- // Mock server environment
- vi.doMock("$app/environment", () => ({
- browser: false
- }));
+ describe("Server-side API Integration", () => {
+ it("should process forwarded client errors correctly", async () => {
+ // Mock server environment
+ vi.doMock("$app/environment", () => ({
+ browser: false,
+ }));
- // Mock winston logger
- const mockWinstonLogger = {
- debug: vi.fn(),
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn()
- };
+ // Mock winston logger
+ const mockWinstonLogger = {
+ debug: vi.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
+ };
- vi.doMock("./winston", () => ({
- default: mockWinstonLogger
- }));
+ vi.doMock("./winston", () => ({
+ default: mockWinstonLogger,
+ }));
- // Mock SvelteKit json function
- const mockJson = vi.fn().mockImplementation((data, options) => ({ data, options }));
- vi.doMock("@sveltejs/kit", () => ({
- json: mockJson
- }));
+ // Mock SvelteKit json function
+ const mockJson = vi.fn().mockImplementation((data, options) => ({ data, options }));
+ vi.doMock("@sveltejs/kit", () => ({
+ json: mockJson,
+ }));
- // This test is simplified since we can't easily test the actual server endpoint
- // in this integration test due to mocking complexities. The server endpoint
- // is tested separately in its own test file.
+ // This test is simplified since we can't easily test the actual server endpoint
+ // in this integration test due to mocking complexities. The server endpoint
+ // is tested separately in its own test file.
- // Just verify that the mock setup would work
- expect(mockWinstonLogger.error).toBeDefined();
- expect(mockJson).toBeDefined();
+ // Just verify that the mock setup would work
+ expect(mockWinstonLogger.error).toBeDefined();
+ expect(mockJson).toBeDefined();
- // The actual server endpoint integration is tested in the dedicated server.test.ts file
- });
- });
+ // The actual server endpoint integration is tested in the dedicated server.test.ts file
+ });
+ });
- describe("Cross-Environment Behavior", () => {
- it("should not forward non-error logs from client", async () => {
- mockFetch.mockResolvedValueOnce({
- ok: true,
- json: async () => ({ success: true })
- });
+ describe("Cross-Environment Behavior", () => {
+ it("should not forward non-error logs from client", async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ success: true }),
+ });
- const { createLogger } = await import("../index");
- const logger = createLogger("NoForwarding");
+ const { createLogger } = await import("../index");
+ const logger = createLogger("NoForwarding");
- // Log different levels
- logger.debug("Debug message");
- logger.info("Info message");
- logger.warn("Warning message");
+ // Log different levels
+ logger.debug("Debug message");
+ logger.info("Info message");
+ logger.warn("Warning message");
- // Wait for any potential async operations
- await new Promise((resolve) => setTimeout(resolve, 10));
+ // Wait for any potential async operations
+ await new Promise((resolve) => setTimeout(resolve, 10));
- // Verify only console methods were called, no fetch
- expect(mockConsole.debug).toHaveBeenCalled();
- expect(mockConsole.info).toHaveBeenCalled();
- expect(mockConsole.warn).toHaveBeenCalled();
- expect(mockFetch).not.toHaveBeenCalled();
- });
+ // Verify only console methods were called, no fetch
+ expect(mockConsole.debug).toHaveBeenCalled();
+ expect(mockConsole.info).toHaveBeenCalled();
+ expect(mockConsole.warn).toHaveBeenCalled();
+ expect(mockFetch).not.toHaveBeenCalled();
+ });
- it("should handle multiple concurrent error forwarding requests", async () => {
- // Mock successful responses for all requests
- mockFetch.mockResolvedValue({
- ok: true,
- json: async () => ({ success: true })
- });
+ it("should handle multiple concurrent error forwarding requests", async () => {
+ // Mock successful responses for all requests
+ mockFetch.mockResolvedValue({
+ ok: true,
+ json: async () => ({ success: true }),
+ });
- const { createLogger } = await import("../index");
- const logger1 = createLogger("Concurrent1");
- const logger2 = createLogger("Concurrent2");
+ const { createLogger } = await import("../index");
+ const logger1 = createLogger("Concurrent1");
+ const logger2 = createLogger("Concurrent2");
- // Trigger multiple errors simultaneously
- const promises = [
- logger1.error("Error 1", { id: 1 }),
- logger2.error("Error 2", { id: 2 }),
- logger1.error("Error 3", { id: 3 })
- ];
+ // Trigger multiple errors simultaneously
+ const promises = [
+ logger1.error("Error 1", { id: 1 }),
+ logger2.error("Error 2", { id: 2 }),
+ logger1.error("Error 3", { id: 3 }),
+ ];
- await Promise.all(promises);
+ await Promise.all(promises);
- // Wait for all async operations
- await new Promise((resolve) => setTimeout(resolve, 20));
+ // Wait for all async operations
+ await new Promise((resolve) => setTimeout(resolve, 20));
- // Verify all requests were made
- expect(mockFetch).toHaveBeenCalledTimes(3);
+ // Verify all requests were made
+ expect(mockFetch).toHaveBeenCalledTimes(3);
- // Verify each request has correct context
- const calls = mockFetch.mock.calls;
- expect(calls[0][1].body).toContain('"context":"Concurrent1"');
- expect(calls[1][1].body).toContain('"context":"Concurrent2"');
- expect(calls[2][1].body).toContain('"context":"Concurrent1"');
- });
- });
+ // Verify each request has correct context
+ const calls = mockFetch.mock.calls;
+ expect(calls[0][1].body).toContain('"context":"Concurrent1"');
+ expect(calls[1][1].body).toContain('"context":"Concurrent2"');
+ expect(calls[2][1].body).toContain('"context":"Concurrent1"');
+ });
+ });
- describe("Data Integrity", () => {
- it("should preserve complex metadata through forwarding", async () => {
- mockFetch.mockResolvedValueOnce({
- ok: true,
- json: async () => ({ success: true })
- });
+ describe("Data Integrity", () => {
+ it("should preserve complex metadata through forwarding", async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ success: true }),
+ });
- const { createLogger } = await import("../index");
- const logger = createLogger("DataIntegrity");
+ const { createLogger } = await import("../index");
+ const logger = createLogger("DataIntegrity");
- const complexMeta = {
- user: { id: 123, name: "Test User" },
- error: { stack: "Error stack trace...", code: 500 },
- array: [1, 2, { nested: true }],
- boolean: true,
- null: null,
- timestamp: new Date().toISOString()
- };
+ const complexMeta = {
+ user: { id: 123, name: "Test User" },
+ error: { stack: "Error stack trace...", code: 500 },
+ array: [1, 2, { nested: true }],
+ boolean: true,
+ null: null,
+ timestamp: new Date().toISOString(),
+ };
- logger.error("Complex error", complexMeta);
+ logger.error("Complex error", complexMeta);
- await new Promise((resolve) => setTimeout(resolve, 10));
+ await new Promise((resolve) => setTimeout(resolve, 10));
- const sentData = JSON.parse(mockFetch.mock.calls[0][1].body);
+ const sentData = JSON.parse(mockFetch.mock.calls[0][1].body);
- // Verify complex meta is preserved
- expect(sentData.meta.user).toEqual({ id: 123, name: "Test User" });
- expect(sentData.meta.error).toEqual({ stack: "Error stack trace...", code: 500 });
- expect(sentData.meta.array).toEqual([1, 2, { nested: true }]);
- expect(sentData.meta.boolean).toBe(true);
- expect(sentData.meta.null).toBe(null);
+ // Verify complex meta is preserved
+ expect(sentData.meta.user).toEqual({ id: 123, name: "Test User" });
+ expect(sentData.meta.error).toEqual({ stack: "Error stack trace...", code: 500 });
+ expect(sentData.meta.array).toEqual([1, 2, { nested: true }]);
+ expect(sentData.meta.boolean).toBe(true);
+ expect(sentData.meta.null).toBe(null);
- // Verify additional context is added
- expect(sentData.meta.context).toBe("DataIntegrity");
- expect(sentData.meta.url).toBe("https://test.example.com/page");
- expect(sentData.meta.userAgent).toBe("Test Browser/1.0");
- });
+ // Verify additional context is added
+ expect(sentData.meta.context).toBe("DataIntegrity");
+ expect(sentData.meta.url).toBe("https://test.example.com/page");
+ expect(sentData.meta.userAgent).toBe("Test Browser/1.0");
+ });
- it("should maintain message and level integrity", async () => {
- mockFetch.mockResolvedValueOnce({
- ok: true,
- json: async () => ({ success: true })
- });
+ it("should maintain message and level integrity", async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ success: true }),
+ });
- const { createLogger } = await import("../index");
- const logger = createLogger("MessageIntegrity");
+ const { createLogger } = await import("../index");
+ const logger = createLogger("MessageIntegrity");
- const originalMessage = "Original error message with special chars: áéíóú 中文 🚀";
- logger.error(originalMessage);
+ const originalMessage = "Original error message with special chars: áéíóú 中文 🚀";
+ logger.error(originalMessage);
- await new Promise((resolve) => setTimeout(resolve, 10));
+ await new Promise((resolve) => setTimeout(resolve, 10));
- const sentData = JSON.parse(mockFetch.mock.calls[0][1].body);
+ const sentData = JSON.parse(mockFetch.mock.calls[0][1].body);
- expect(sentData.level).toBe("error");
- expect(sentData.message).toBe(originalMessage);
- });
- });
+ expect(sentData.level).toBe("error");
+ expect(sentData.message).toBe(originalMessage);
+ });
+ });
});
diff --git a/src/lib/logger/index.ts b/src/lib/logger/index.ts
index d45d6b7..e4efbd6 100644
--- a/src/lib/logger/index.ts
+++ b/src/lib/logger/index.ts
@@ -3,110 +3,110 @@ import { browser } from "$app/environment";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let winstonLogger: any = undefined;
if (!browser) {
- const { default: winston } = await import("./winston");
- winstonLogger = winston;
+ const { default: winston } = await import("./winston");
+ winstonLogger = winston;
}
class UniversalLogger {
- #context: string = "";
+ #context: string = "";
- setContext(context: string) {
- this.#context = context;
- return this;
- }
+ setContext(context: string) {
+ this.#context = context;
+ return this;
+ }
- #formatMessage(message: string, meta = {}) {
- const contextPrefix = this.#context ? `[${this.#context}] ` : "";
- return {
- message: `${contextPrefix}${message}`,
- ...meta,
- source: browser ? "client" : "server",
- userAgent: browser ? navigator.userAgent : undefined,
- timestamp: new Date().toISOString()
- };
- }
+ #formatMessage(message: string, meta = {}) {
+ const contextPrefix = this.#context ? `[${this.#context}] ` : "";
+ return {
+ message: `${contextPrefix}${message}`,
+ ...meta,
+ source: browser ? "client" : "server",
+ userAgent: browser ? navigator.userAgent : undefined,
+ timestamp: new Date().toISOString(),
+ };
+ }
- debug(message: string, meta = {}) {
- if (browser) {
- console.debug(`🐛 ${this.#context ? `[${this.#context}] ` : ""}${message}`, meta);
- } else {
- winstonLogger.debug(this.#formatMessage(message, meta));
- }
- }
+ debug(message: string, meta = {}) {
+ if (browser) {
+ console.debug(`🐛 ${this.#context ? `[${this.#context}] ` : ""}${message}`, meta);
+ } else {
+ winstonLogger.debug(this.#formatMessage(message, meta));
+ }
+ }
- info(message: string, meta = {}) {
- if (browser) {
- console.info(`ℹ️ ${this.#context ? `[${this.#context}] ` : ""}${message}`, meta);
- } else {
- winstonLogger.info(this.#formatMessage(message, meta));
- }
- }
+ info(message: string, meta = {}) {
+ if (browser) {
+ console.info(`ℹ️ ${this.#context ? `[${this.#context}] ` : ""}${message}`, meta);
+ } else {
+ winstonLogger.info(this.#formatMessage(message, meta));
+ }
+ }
- warn(message: string, meta = {}) {
- if (browser) {
- console.warn(`⚠️ ${this.#context ? `[${this.#context}] ` : ""}${message}`, meta);
- } else {
- winstonLogger.warn(this.#formatMessage(message, meta));
- }
- }
+ warn(message: string, meta = {}) {
+ if (browser) {
+ console.warn(`⚠️ ${this.#context ? `[${this.#context}] ` : ""}${message}`, meta);
+ } else {
+ winstonLogger.warn(this.#formatMessage(message, meta));
+ }
+ }
- error(message: string, meta = {}) {
- if (browser) {
- console.error(`❌ ${this.#context ? `[${this.#context}] ` : ""}${message}`, meta);
+ error(message: string, meta = {}) {
+ if (browser) {
+ console.error(`❌ ${this.#context ? `[${this.#context}] ` : ""}${message}`, meta);
- this.#sendErrorToServer(message, meta);
- } else {
- winstonLogger.error(this.#formatMessage(message, meta));
- }
- }
+ this.#sendErrorToServer(message, meta);
+ } else {
+ winstonLogger.error(this.#formatMessage(message, meta));
+ }
+ }
- async #sendErrorToServer(message: string, meta = {}) {
- try {
- await fetch("/api/log", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- level: "error",
- message,
- meta: {
- ...meta,
- context: this.#context,
- url: window.location.href,
- userAgent: navigator.userAgent,
- timestamp: new Date().toISOString()
- }
- })
- });
- } catch (err) {
- console.error("Failed to send error to server:", err);
- }
- }
+ async #sendErrorToServer(message: string, meta = {}) {
+ try {
+ await fetch("/api/log", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ level: "error",
+ message,
+ meta: {
+ ...meta,
+ context: this.#context,
+ url: window.location.href,
+ userAgent: navigator.userAgent,
+ timestamp: new Date().toISOString(),
+ },
+ }),
+ });
+ } catch (err) {
+ console.error("Failed to send error to server:", err);
+ }
+ }
- logRequest(request: Request, responseTime: number, statusCode: number) {
- const message = `${request.method} ${request.url} - ${statusCode} (${responseTime}ms)`;
+ logRequest(request: Request, responseTime: number, statusCode: number) {
+ const message = `${request.method} ${request.url} - ${statusCode} (${responseTime}ms)`;
- if (statusCode >= 400) {
- this.error(message, {
- method: request.method,
- url: request.url,
- statusCode,
- responseTime
- });
- } else {
- this.info(message, {
- method: request.method,
- url: request.url,
- statusCode,
- responseTime
- });
- }
- }
+ if (statusCode >= 400) {
+ this.error(message, {
+ method: request.method,
+ url: request.url,
+ statusCode,
+ responseTime,
+ });
+ } else {
+ this.info(message, {
+ method: request.method,
+ url: request.url,
+ statusCode,
+ responseTime,
+ });
+ }
+ }
}
export const logger = new UniversalLogger();
export const createLogger = (context: string) => {
- return new UniversalLogger().setContext(context);
+ return new UniversalLogger().setContext(context);
};
export { UniversalLogger };
diff --git a/src/lib/logger/winston.ts b/src/lib/logger/winston.ts
index 948e206..4d16b86 100644
--- a/src/lib/logger/winston.ts
+++ b/src/lib/logger/winston.ts
@@ -2,31 +2,31 @@ import winston from "winston";
import { dev } from "$app/environment";
const customFormat = winston.format.combine(
- winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
- winston.format.errors({ stack: true }),
- winston.format.colorize(),
- winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
- let log = `${timestamp} [${level}]: ${message}`;
- if (stack) {
- log += `\n${stack}`;
- }
+ winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
+ winston.format.errors({ stack: true }),
+ winston.format.colorize(),
+ winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
+ let log = `${timestamp} [${level}]: ${message}`;
+ if (stack) {
+ log += `\n${stack}`;
+ }
- if (Object.keys(meta).length > 0) {
- log += `\n${JSON.stringify(meta, null, 2)}`;
- }
+ if (Object.keys(meta).length > 0) {
+ log += `\n${JSON.stringify(meta, null, 2)}`;
+ }
- return log;
- })
+ return log;
+ }),
);
const logger = winston.createLogger({
- level: dev ? "debug" : "info",
- format: customFormat,
- transports: [
- new winston.transports.Console({
- format: winston.format.combine(customFormat)
- })
- ]
+ level: dev ? "debug" : "info",
+ format: customFormat,
+ transports: [
+ new winston.transports.Console({
+ format: winston.format.combine(customFormat),
+ }),
+ ],
});
export default logger;
diff --git a/src/lib/server/auth/__tests__/authorization.test.ts b/src/lib/server/auth/__tests__/authorization.test.ts
index bc81d06..cf68011 100644
--- a/src/lib/server/auth/__tests__/authorization.test.ts
+++ b/src/lib/server/auth/__tests__/authorization.test.ts
@@ -5,212 +5,212 @@ import { AuthenticationError } from "$lib/server/utils/errors";
import type { JWTPayload } from "jose";
const mockGlobalAdmin: JWTPayload = {
- userId: "global-admin-id",
- email: "global@example.com",
- name: "Global Admin",
- role: "GLOBAL_ADMIN",
- sessionId: "session-1",
- iat: Date.now(),
- exp: Date.now() + 3600
+ userId: "global-admin-id",
+ email: "global@example.com",
+ name: "Global Admin",
+ role: "GLOBAL_ADMIN",
+ sessionId: "session-1",
+ iat: Date.now(),
+ exp: Date.now() + 3600,
};
const mockTenantAdmin: JWTPayload = {
- userId: "tenant-admin-id",
- email: "tenant@example.com",
- name: "Tenant Admin",
- role: "TENANT_ADMIN",
- tenantId: "tenant-123",
- sessionId: "session-2",
- iat: Date.now(),
- exp: Date.now() + 3600
+ userId: "tenant-admin-id",
+ email: "tenant@example.com",
+ name: "Tenant Admin",
+ role: "TENANT_ADMIN",
+ tenantId: "tenant-123",
+ sessionId: "session-2",
+ iat: Date.now(),
+ exp: Date.now() + 3600,
};
const mockStaff: JWTPayload = {
- userId: "staff-id",
- email: "staff@example.com",
- name: "Staff Member",
- role: "STAFF",
- tenantId: "tenant-123",
- sessionId: "session-3",
- iat: Date.now(),
- exp: Date.now() + 3600
+ userId: "staff-id",
+ email: "staff@example.com",
+ name: "Staff Member",
+ role: "STAFF",
+ tenantId: "tenant-123",
+ sessionId: "session-3",
+ iat: Date.now(),
+ exp: Date.now() + 3600,
};
describe("AuthorizationService", () => {
- describe("requireRole", () => {
- it("should allow access for correct role", () => {
- expect(() => {
- AuthorizationService.requireRole(mockGlobalAdmin, "GLOBAL_ADMIN");
- }).not.toThrow();
- });
+ describe("requireRole", () => {
+ it("should allow access for correct role", () => {
+ expect(() => {
+ AuthorizationService.requireRole(mockGlobalAdmin, "GLOBAL_ADMIN");
+ }).not.toThrow();
+ });
- it("should deny access for incorrect role", () => {
- expect(() => {
- AuthorizationService.requireRole(mockTenantAdmin, "GLOBAL_ADMIN");
- }).toThrow(AuthenticationError);
- });
+ it("should deny access for incorrect role", () => {
+ expect(() => {
+ AuthorizationService.requireRole(mockTenantAdmin, "GLOBAL_ADMIN");
+ }).toThrow(AuthenticationError);
+ });
- it("should deny access for null user", () => {
- expect(() => {
- AuthorizationService.requireRole(null as any, "GLOBAL_ADMIN");
- }).toThrow(AuthenticationError);
- });
- });
+ it("should deny access for null user", () => {
+ expect(() => {
+ AuthorizationService.requireRole(null as any, "GLOBAL_ADMIN");
+ }).toThrow(AuthenticationError);
+ });
+ });
- describe("requireAnyRole", () => {
- it("should allow access for any allowed role", () => {
- expect(() => {
- AuthorizationService.requireAnyRole(mockTenantAdmin, ["GLOBAL_ADMIN", "TENANT_ADMIN"]);
- }).not.toThrow();
+ describe("requireAnyRole", () => {
+ it("should allow access for any allowed role", () => {
+ expect(() => {
+ AuthorizationService.requireAnyRole(mockTenantAdmin, ["GLOBAL_ADMIN", "TENANT_ADMIN"]);
+ }).not.toThrow();
- expect(() => {
- AuthorizationService.requireAnyRole(mockStaff, ["TENANT_ADMIN", "STAFF"]);
- }).not.toThrow();
- });
+ expect(() => {
+ AuthorizationService.requireAnyRole(mockStaff, ["TENANT_ADMIN", "STAFF"]);
+ }).not.toThrow();
+ });
- it("should deny access for disallowed role", () => {
- expect(() => {
- AuthorizationService.requireAnyRole(mockStaff, ["GLOBAL_ADMIN", "TENANT_ADMIN"]);
- }).toThrow(AuthenticationError);
- });
- });
+ it("should deny access for disallowed role", () => {
+ expect(() => {
+ AuthorizationService.requireAnyRole(mockStaff, ["GLOBAL_ADMIN", "TENANT_ADMIN"]);
+ }).toThrow(AuthenticationError);
+ });
+ });
- describe("requireTenantAccess", () => {
- it("should allow global admin access to any tenant", () => {
- expect(() => {
- AuthorizationService.requireTenantAccess(mockGlobalAdmin, "any-tenant-id");
- }).not.toThrow();
- });
+ describe("requireTenantAccess", () => {
+ it("should allow global admin access to any tenant", () => {
+ expect(() => {
+ AuthorizationService.requireTenantAccess(mockGlobalAdmin, "any-tenant-id");
+ }).not.toThrow();
+ });
- it("should allow tenant admin access to their own tenant", () => {
- expect(() => {
- AuthorizationService.requireTenantAccess(mockTenantAdmin, "tenant-123");
- }).not.toThrow();
- });
+ it("should allow tenant admin access to their own tenant", () => {
+ expect(() => {
+ AuthorizationService.requireTenantAccess(mockTenantAdmin, "tenant-123");
+ }).not.toThrow();
+ });
- it("should deny tenant admin access to other tenants", () => {
- expect(() => {
- AuthorizationService.requireTenantAccess(mockTenantAdmin, "tenant-456");
- }).toThrow(AuthenticationError);
- });
+ it("should deny tenant admin access to other tenants", () => {
+ expect(() => {
+ AuthorizationService.requireTenantAccess(mockTenantAdmin, "tenant-456");
+ }).toThrow(AuthenticationError);
+ });
- it("should allow staff access to their own tenant", () => {
- expect(() => {
- AuthorizationService.requireTenantAccess(mockStaff, "tenant-123");
- }).not.toThrow();
- });
+ it("should allow staff access to their own tenant", () => {
+ expect(() => {
+ AuthorizationService.requireTenantAccess(mockStaff, "tenant-123");
+ }).not.toThrow();
+ });
- it("should deny staff access to other tenants", () => {
- expect(() => {
- AuthorizationService.requireTenantAccess(mockStaff, "tenant-456");
- }).toThrow(AuthenticationError);
- });
- });
+ it("should deny staff access to other tenants", () => {
+ expect(() => {
+ AuthorizationService.requireTenantAccess(mockStaff, "tenant-456");
+ }).toThrow(AuthenticationError);
+ });
+ });
- describe("requireGlobalAdmin", () => {
- it("should allow global admin access", () => {
- expect(() => {
- AuthorizationService.requireGlobalAdmin(mockGlobalAdmin);
- }).not.toThrow();
- });
+ describe("requireGlobalAdmin", () => {
+ it("should allow global admin access", () => {
+ expect(() => {
+ AuthorizationService.requireGlobalAdmin(mockGlobalAdmin);
+ }).not.toThrow();
+ });
- it("should deny non-global admin access", () => {
- expect(() => {
- AuthorizationService.requireGlobalAdmin(mockTenantAdmin);
- }).toThrow(AuthenticationError);
- });
- });
+ it("should deny non-global admin access", () => {
+ expect(() => {
+ AuthorizationService.requireGlobalAdmin(mockTenantAdmin);
+ }).toThrow(AuthenticationError);
+ });
+ });
- describe("requireTenantAdmin", () => {
- it("should allow global admin access", () => {
- expect(() => {
- AuthorizationService.requireTenantAdmin(mockGlobalAdmin);
- }).not.toThrow();
- });
+ describe("requireTenantAdmin", () => {
+ it("should allow global admin access", () => {
+ expect(() => {
+ AuthorizationService.requireTenantAdmin(mockGlobalAdmin);
+ }).not.toThrow();
+ });
- it("should allow tenant admin access", () => {
- expect(() => {
- AuthorizationService.requireTenantAdmin(mockTenantAdmin);
- }).not.toThrow();
- });
+ it("should allow tenant admin access", () => {
+ expect(() => {
+ AuthorizationService.requireTenantAdmin(mockTenantAdmin);
+ }).not.toThrow();
+ });
- it("should deny staff access", () => {
- expect(() => {
- AuthorizationService.requireTenantAdmin(mockStaff);
- }).toThrow(AuthenticationError);
- });
+ it("should deny staff access", () => {
+ expect(() => {
+ AuthorizationService.requireTenantAdmin(mockStaff);
+ }).toThrow(AuthenticationError);
+ });
- it("should check tenant access for tenant admin", () => {
- expect(() => {
- AuthorizationService.requireTenantAdmin(mockTenantAdmin, "tenant-123");
- }).not.toThrow();
+ it("should check tenant access for tenant admin", () => {
+ expect(() => {
+ AuthorizationService.requireTenantAdmin(mockTenantAdmin, "tenant-123");
+ }).not.toThrow();
- expect(() => {
- AuthorizationService.requireTenantAdmin(mockTenantAdmin, "tenant-456");
- }).toThrow(AuthenticationError);
- });
- });
+ expect(() => {
+ AuthorizationService.requireTenantAdmin(mockTenantAdmin, "tenant-456");
+ }).toThrow(AuthenticationError);
+ });
+ });
- describe("requireStaffOrAbove", () => {
- it("should allow all roles access", () => {
- expect(() => {
- AuthorizationService.requireStaffOrAbove(mockGlobalAdmin);
- }).not.toThrow();
+ describe("requireStaffOrAbove", () => {
+ it("should allow all roles access", () => {
+ expect(() => {
+ AuthorizationService.requireStaffOrAbove(mockGlobalAdmin);
+ }).not.toThrow();
- expect(() => {
- AuthorizationService.requireStaffOrAbove(mockTenantAdmin);
- }).not.toThrow();
+ expect(() => {
+ AuthorizationService.requireStaffOrAbove(mockTenantAdmin);
+ }).not.toThrow();
- expect(() => {
- AuthorizationService.requireStaffOrAbove(mockStaff);
- }).not.toThrow();
- });
+ expect(() => {
+ AuthorizationService.requireStaffOrAbove(mockStaff);
+ }).not.toThrow();
+ });
- it("should check tenant access for tenant-specific roles", () => {
- expect(() => {
- AuthorizationService.requireStaffOrAbove(mockStaff, "tenant-123");
- }).not.toThrow();
+ it("should check tenant access for tenant-specific roles", () => {
+ expect(() => {
+ AuthorizationService.requireStaffOrAbove(mockStaff, "tenant-123");
+ }).not.toThrow();
- expect(() => {
- AuthorizationService.requireStaffOrAbove(mockStaff, "tenant-456");
- }).toThrow(AuthenticationError);
- });
- });
+ expect(() => {
+ AuthorizationService.requireStaffOrAbove(mockStaff, "tenant-456");
+ }).toThrow(AuthenticationError);
+ });
+ });
- describe("utility functions", () => {
- it("should correctly identify user roles", () => {
- expect(AuthorizationService.isGlobalAdmin(mockGlobalAdmin)).toBe(true);
- expect(AuthorizationService.isGlobalAdmin(mockTenantAdmin)).toBe(false);
+ describe("utility functions", () => {
+ it("should correctly identify user roles", () => {
+ expect(AuthorizationService.isGlobalAdmin(mockGlobalAdmin)).toBe(true);
+ expect(AuthorizationService.isGlobalAdmin(mockTenantAdmin)).toBe(false);
- expect(AuthorizationService.isTenantAdmin(mockTenantAdmin)).toBe(true);
- expect(AuthorizationService.isTenantAdmin(mockStaff)).toBe(false);
+ expect(AuthorizationService.isTenantAdmin(mockTenantAdmin)).toBe(true);
+ expect(AuthorizationService.isTenantAdmin(mockStaff)).toBe(false);
- expect(AuthorizationService.isStaff(mockStaff)).toBe(true);
- expect(AuthorizationService.isStaff(mockTenantAdmin)).toBe(false);
- });
+ expect(AuthorizationService.isStaff(mockStaff)).toBe(true);
+ expect(AuthorizationService.isStaff(mockTenantAdmin)).toBe(false);
+ });
- it("should correctly check role membership", () => {
- expect(AuthorizationService.hasRole(mockGlobalAdmin, "GLOBAL_ADMIN")).toBe(true);
- expect(AuthorizationService.hasRole(mockGlobalAdmin, "TENANT_ADMIN")).toBe(false);
+ it("should correctly check role membership", () => {
+ expect(AuthorizationService.hasRole(mockGlobalAdmin, "GLOBAL_ADMIN")).toBe(true);
+ expect(AuthorizationService.hasRole(mockGlobalAdmin, "TENANT_ADMIN")).toBe(false);
- expect(
- AuthorizationService.hasAnyRole(mockTenantAdmin, ["GLOBAL_ADMIN", "TENANT_ADMIN"])
- ).toBe(true);
- expect(AuthorizationService.hasAnyRole(mockStaff, ["GLOBAL_ADMIN", "TENANT_ADMIN"])).toBe(
- false
- );
- });
+ expect(
+ AuthorizationService.hasAnyRole(mockTenantAdmin, ["GLOBAL_ADMIN", "TENANT_ADMIN"]),
+ ).toBe(true);
+ expect(AuthorizationService.hasAnyRole(mockStaff, ["GLOBAL_ADMIN", "TENANT_ADMIN"])).toBe(
+ false,
+ );
+ });
- it("should correctly check tenant access", () => {
- expect(AuthorizationService.canAccessTenant(mockGlobalAdmin, "any-tenant")).toBe(true);
- expect(AuthorizationService.canAccessTenant(mockTenantAdmin, "tenant-123")).toBe(true);
- expect(AuthorizationService.canAccessTenant(mockTenantAdmin, "tenant-456")).toBe(false);
- });
+ it("should correctly check tenant access", () => {
+ expect(AuthorizationService.canAccessTenant(mockGlobalAdmin, "any-tenant")).toBe(true);
+ expect(AuthorizationService.canAccessTenant(mockTenantAdmin, "tenant-123")).toBe(true);
+ expect(AuthorizationService.canAccessTenant(mockTenantAdmin, "tenant-456")).toBe(false);
+ });
- it("should correctly get user tenant ID", () => {
- expect(AuthorizationService.getUserTenantId(mockGlobalAdmin)).toBe(null);
- expect(AuthorizationService.getUserTenantId(mockTenantAdmin)).toBe("tenant-123");
- expect(AuthorizationService.getUserTenantId(mockStaff)).toBe("tenant-123");
- });
- });
+ it("should correctly get user tenant ID", () => {
+ expect(AuthorizationService.getUserTenantId(mockGlobalAdmin)).toBe(null);
+ expect(AuthorizationService.getUserTenantId(mockTenantAdmin)).toBe("tenant-123");
+ expect(AuthorizationService.getUserTenantId(mockStaff)).toBe("tenant-123");
+ });
+ });
});
diff --git a/src/lib/server/auth/__tests__/jwt-utils.test.ts b/src/lib/server/auth/__tests__/jwt-utils.test.ts
index 580b912..48c4bb9 100644
--- a/src/lib/server/auth/__tests__/jwt-utils.test.ts
+++ b/src/lib/server/auth/__tests__/jwt-utils.test.ts
@@ -1,163 +1,163 @@
import { describe, it, expect } from "vitest";
import {
- generateAccessToken,
- generateRefreshToken,
- verifyAccessToken,
- verifyRefreshToken,
- generateTokens,
- isTokenExpired
+ generateAccessToken,
+ generateRefreshToken,
+ verifyAccessToken,
+ verifyRefreshToken,
+ generateTokens,
+ isTokenExpired,
} from "../jwt-utils";
import type { SelectUser } from "$lib/server/db/central-schema";
const mockUser: SelectUser = {
- id: "test-user-id",
- email: "test@example.com",
- name: "Test User",
- role: "GLOBAL_ADMIN",
- tenantId: null,
- createdAt: new Date(),
- updatedAt: new Date(),
- lastLoginAt: new Date(),
- isActive: true,
- confirmed: true,
- token: null,
- tokenValidUntil: null,
- passphraseHash: null,
- recoveryPassphrase: null,
- language: "de"
+ id: "test-user-id",
+ email: "test@example.com",
+ name: "Test User",
+ role: "GLOBAL_ADMIN",
+ tenantId: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ lastLoginAt: new Date(),
+ isActive: true,
+ confirmed: true,
+ token: null,
+ tokenValidUntil: null,
+ passphraseHash: null,
+ recoveryPassphrase: null,
+ language: "de",
};
describe("JWT Utils", () => {
- const sessionId = "test-session-id";
+ const sessionId = "test-session-id";
- describe("generateAccessToken", () => {
- it("should generate a valid access token", async () => {
- const token = await generateAccessToken(mockUser, sessionId);
- expect(token).toBeDefined();
- expect(typeof token).toBe("string");
- expect(token.split(".")).toHaveLength(3);
- });
+ describe("generateAccessToken", () => {
+ it("should generate a valid access token", async () => {
+ const token = await generateAccessToken(mockUser, sessionId);
+ expect(token).toBeDefined();
+ expect(typeof token).toBe("string");
+ expect(token.split(".")).toHaveLength(3);
+ });
- it("should include user data in token payload", async () => {
- const token = await generateAccessToken(mockUser, sessionId);
- const payload = await verifyAccessToken(token);
+ it("should include user data in token payload", async () => {
+ const token = await generateAccessToken(mockUser, sessionId);
+ const payload = await verifyAccessToken(token);
- expect(payload).toBeDefined();
- expect(payload!.userId).toBe(mockUser.id);
- expect(payload!.email).toBe(mockUser.email);
- expect(payload!.name).toBe(mockUser.name);
- expect(payload!.role).toBe(mockUser.role);
- expect(payload!.sessionId).toBe(sessionId);
- });
- });
+ expect(payload).toBeDefined();
+ expect(payload!.userId).toBe(mockUser.id);
+ expect(payload!.email).toBe(mockUser.email);
+ expect(payload!.name).toBe(mockUser.name);
+ expect(payload!.role).toBe(mockUser.role);
+ expect(payload!.sessionId).toBe(sessionId);
+ });
+ });
- describe("generateRefreshToken", () => {
- it("should generate a valid refresh token", async () => {
- const token = await generateRefreshToken(mockUser.id, sessionId);
- expect(token).toBeDefined();
- expect(typeof token).toBe("string");
- expect(token.split(".")).toHaveLength(3);
- });
+ describe("generateRefreshToken", () => {
+ it("should generate a valid refresh token", async () => {
+ const token = await generateRefreshToken(mockUser.id, sessionId);
+ expect(token).toBeDefined();
+ expect(typeof token).toBe("string");
+ expect(token.split(".")).toHaveLength(3);
+ });
- it("should include user and session data in token payload", async () => {
- const token = await generateRefreshToken(mockUser.id, sessionId);
- const payload = await verifyRefreshToken(token);
+ it("should include user and session data in token payload", async () => {
+ const token = await generateRefreshToken(mockUser.id, sessionId);
+ const payload = await verifyRefreshToken(token);
- expect(payload).toBeDefined();
- expect(payload!.userId).toBe(mockUser.id);
- expect(payload!.sessionId).toBe(sessionId);
- });
- });
+ expect(payload).toBeDefined();
+ expect(payload!.userId).toBe(mockUser.id);
+ expect(payload!.sessionId).toBe(sessionId);
+ });
+ });
- describe("verifyAccessToken", () => {
- it("should verify a valid access token", async () => {
- const token = await generateAccessToken(mockUser, sessionId);
- const payload = await verifyAccessToken(token);
+ describe("verifyAccessToken", () => {
+ it("should verify a valid access token", async () => {
+ const token = await generateAccessToken(mockUser, sessionId);
+ const payload = await verifyAccessToken(token);
- expect(payload).toBeDefined();
- expect(payload!.userId).toBe(mockUser.id);
- });
+ expect(payload).toBeDefined();
+ expect(payload!.userId).toBe(mockUser.id);
+ });
- it("should reject invalid tokens", async () => {
- const payload = await verifyAccessToken("invalid.token.here");
- expect(payload).toBeNull();
- });
+ it("should reject invalid tokens", async () => {
+ const payload = await verifyAccessToken("invalid.token.here");
+ expect(payload).toBeNull();
+ });
- it("should reject malformed tokens", async () => {
- const payload = await verifyAccessToken("invalid-token");
- expect(payload).toBeNull();
- });
- });
+ it("should reject malformed tokens", async () => {
+ const payload = await verifyAccessToken("invalid-token");
+ expect(payload).toBeNull();
+ });
+ });
- describe("verifyRefreshToken", () => {
- it("should verify a valid refresh token", async () => {
- const token = await generateRefreshToken(mockUser.id, sessionId);
- const payload = await verifyRefreshToken(token);
+ describe("verifyRefreshToken", () => {
+ it("should verify a valid refresh token", async () => {
+ const token = await generateRefreshToken(mockUser.id, sessionId);
+ const payload = await verifyRefreshToken(token);
- expect(payload).toBeDefined();
- expect(payload!.userId).toBe(mockUser.id);
- expect(payload!.sessionId).toBe(sessionId);
- });
+ expect(payload).toBeDefined();
+ expect(payload!.userId).toBe(mockUser.id);
+ expect(payload!.sessionId).toBe(sessionId);
+ });
- it("should reject invalid tokens", async () => {
- const payload = await verifyRefreshToken("invalid.token.here");
- expect(payload).toBeNull();
- });
+ it("should reject invalid tokens", async () => {
+ const payload = await verifyRefreshToken("invalid.token.here");
+ expect(payload).toBeNull();
+ });
- it("should reject access tokens as refresh tokens", async () => {
- const accessToken = await generateAccessToken(mockUser, sessionId);
- const payload = await verifyRefreshToken(accessToken);
- expect(payload).toBeNull();
- });
- });
+ it("should reject access tokens as refresh tokens", async () => {
+ const accessToken = await generateAccessToken(mockUser, sessionId);
+ const payload = await verifyRefreshToken(accessToken);
+ expect(payload).toBeNull();
+ });
+ });
- describe("generateTokens", () => {
- it("should generate both access and refresh tokens", async () => {
- const tokens = await generateTokens(mockUser, sessionId);
+ describe("generateTokens", () => {
+ it("should generate both access and refresh tokens", async () => {
+ const tokens = await generateTokens(mockUser, sessionId);
- expect(tokens.accessToken).toBeDefined();
- expect(tokens.refreshToken).toBeDefined();
- expect(typeof tokens.accessToken).toBe("string");
- expect(typeof tokens.refreshToken).toBe("string");
- });
+ expect(tokens.accessToken).toBeDefined();
+ expect(tokens.refreshToken).toBeDefined();
+ expect(typeof tokens.accessToken).toBe("string");
+ expect(typeof tokens.refreshToken).toBe("string");
+ });
- it("should generate verifiable tokens", async () => {
- const tokens = await generateTokens(mockUser, sessionId);
+ it("should generate verifiable tokens", async () => {
+ const tokens = await generateTokens(mockUser, sessionId);
- const accessPayload = await verifyAccessToken(tokens.accessToken);
- const refreshPayload = await verifyRefreshToken(tokens.refreshToken);
+ const accessPayload = await verifyAccessToken(tokens.accessToken);
+ const refreshPayload = await verifyRefreshToken(tokens.refreshToken);
- expect(accessPayload).toBeDefined();
- expect(refreshPayload).toBeDefined();
- expect(accessPayload!.userId).toBe(mockUser.id);
- expect(refreshPayload!.userId).toBe(mockUser.id);
- });
- });
+ expect(accessPayload).toBeDefined();
+ expect(refreshPayload).toBeDefined();
+ expect(accessPayload!.userId).toBe(mockUser.id);
+ expect(refreshPayload!.userId).toBe(mockUser.id);
+ });
+ });
- describe("isTokenExpired", () => {
- it("should return false for valid tokens", async () => {
- const token = await generateAccessToken(mockUser, sessionId);
- expect(await isTokenExpired(token)).toBe(false);
- });
+ describe("isTokenExpired", () => {
+ it("should return false for valid tokens", async () => {
+ const token = await generateAccessToken(mockUser, sessionId);
+ expect(await isTokenExpired(token)).toBe(false);
+ });
- it("should return true for malformed tokens", async () => {
- expect(await isTokenExpired("invalid-token")).toBe(true);
- });
+ it("should return true for malformed tokens", async () => {
+ expect(await isTokenExpired("invalid-token")).toBe(true);
+ });
- it("should return true for empty tokens", async () => {
- expect(await isTokenExpired("")).toBe(true);
- });
- });
+ it("should return true for empty tokens", async () => {
+ expect(await isTokenExpired("")).toBe(true);
+ });
+ });
- describe("Token expiration", () => {
- it("should respect token expiration", async () => {
- // Test with an obviously expired token (JWT with past exp claim)
- // This is a manually crafted expired JWT for testing purposes
- const expiredToken =
- "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJ0ZXN0LXVzZXItaWQiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20iLCJuYW1lIjoiVGVzdCBVc2VyIiwicm9sZSI6IkdMT0JBTF9BRE1JTiIsInNlc3Npb25JZCI6InRlc3Qtc2Vzc2lvbi1pZCIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwOTAwfQ.";
+ describe("Token expiration", () => {
+ it("should respect token expiration", async () => {
+ // Test with an obviously expired token (JWT with past exp claim)
+ // This is a manually crafted expired JWT for testing purposes
+ const expiredToken =
+ "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJ0ZXN0LXVzZXItaWQiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20iLCJuYW1lIjoiVGVzdCBVc2VyIiwicm9sZSI6IkdMT0JBTF9BRE1JTiIsInNlc3Npb25JZCI6InRlc3Qtc2Vzc2lvbi1pZCIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwOTAwfQ.";
- const payload = await verifyAccessToken(expiredToken);
- expect(payload).toBeNull();
- });
- });
+ const payload = await verifyAccessToken(expiredToken);
+ expect(payload).toBeNull();
+ });
+ });
});
diff --git a/src/lib/server/auth/__tests__/session-service.test.ts b/src/lib/server/auth/__tests__/session-service.test.ts
index c843f36..3dd44bf 100644
--- a/src/lib/server/auth/__tests__/session-service.test.ts
+++ b/src/lib/server/auth/__tests__/session-service.test.ts
@@ -5,205 +5,205 @@ import * as jwtUtils from "../jwt-utils";
// Mock dependencies
vi.mock("$lib/server/db", () => ({
- db: {
- select: vi.fn(),
- update: vi.fn()
- }
+ db: {
+ select: vi.fn(),
+ update: vi.fn(),
+ },
}));
vi.mock("../jwt-utils");
const mockUser = {
- id: "test-user-id",
- email: "test@example.com",
- name: "Test User",
- role: "STAFF" as const,
- tenantId: "tenant-123",
- isActive: true,
- confirmed: true,
- createdAt: new Date(),
- updatedAt: new Date(),
- lastLoginAt: null
+ id: "test-user-id",
+ email: "test@example.com",
+ name: "Test User",
+ role: "STAFF" as const,
+ tenantId: "tenant-123",
+ isActive: true,
+ confirmed: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ lastLoginAt: null,
};
const mockSession = {
- id: "session-id",
- userId: "test-user-id",
- sessionToken: "session-token",
- accessToken: "access-token",
- refreshToken: "refresh-token",
- expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now
- lastUsedAt: new Date(),
- ipAddress: "127.0.0.1",
- userAgent: "test-agent",
- createdAt: new Date(),
- updatedAt: new Date()
+ id: "session-id",
+ userId: "test-user-id",
+ sessionToken: "session-token",
+ accessToken: "access-token",
+ refreshToken: "refresh-token",
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now
+ lastUsedAt: new Date(),
+ ipAddress: "127.0.0.1",
+ userAgent: "test-agent",
+ createdAt: new Date(),
+ updatedAt: new Date(),
};
describe("SessionService.validateTokenWithDB", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
- it("should return user, sessionId and exp for valid token", async () => {
- const { db } = await import("$lib/server/db");
+ it("should return user, sessionId and exp for valid token", async () => {
+ const { db } = await import("$lib/server/db");
- // Mock JWT verification
- const mockTokenData = {
- sessionId: "session-id",
- userId: "test-user-id",
- exp: Math.floor(Date.now() / 1000) + 3600
- };
- vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(mockTokenData);
+ // Mock JWT verification
+ const mockTokenData = {
+ sessionId: "session-id",
+ userId: "test-user-id",
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ };
+ vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(mockTokenData);
- // Mock successful database query
- const mockQuery = vi.fn().mockResolvedValue([
- {
- user: mockUser,
- user_session: mockSession
- }
- ]);
+ // Mock successful database query
+ const mockQuery = vi.fn().mockResolvedValue([
+ {
+ user: mockUser,
+ user_session: mockSession,
+ },
+ ]);
- vi.mocked(db.select).mockReturnValue({
- from: vi.fn().mockReturnValue({
- innerJoin: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: mockQuery
- })
- })
- })
- } as any);
+ vi.mocked(db.select).mockReturnValue({
+ from: vi.fn().mockReturnValue({
+ innerJoin: vi.fn().mockReturnValue({
+ where: vi.fn().mockReturnValue({
+ limit: mockQuery,
+ }),
+ }),
+ }),
+ } as any);
- vi.mocked(db.update).mockReturnValue({
- set: vi.fn().mockReturnValue({
- where: vi.fn().mockResolvedValue([])
- })
- } as any);
+ vi.mocked(db.update).mockReturnValue({
+ set: vi.fn().mockReturnValue({
+ where: vi.fn().mockResolvedValue([]),
+ }),
+ } as any);
- const result = await SessionService.validateTokenWithDB("valid-token");
+ const result = await SessionService.validateTokenWithDB("valid-token");
- expect(result).toEqual({
- user: mockUser,
- sessionId: "session-id",
- exp: mockSession.expiresAt
- });
- expect(jwtUtils.verifyAccessToken).toHaveBeenCalledWith("valid-token");
- });
+ expect(result).toEqual({
+ user: mockUser,
+ sessionId: "session-id",
+ exp: mockSession.expiresAt,
+ });
+ expect(jwtUtils.verifyAccessToken).toHaveBeenCalledWith("valid-token");
+ });
- it("should return null for invalid JWT token", async () => {
- vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(null);
+ it("should return null for invalid JWT token", async () => {
+ vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(null);
- const result = await SessionService.validateTokenWithDB("invalid-token");
+ const result = await SessionService.validateTokenWithDB("invalid-token");
- expect(result).toBeNull();
- expect(jwtUtils.verifyAccessToken).toHaveBeenCalledWith("invalid-token");
- });
+ expect(result).toBeNull();
+ expect(jwtUtils.verifyAccessToken).toHaveBeenCalledWith("invalid-token");
+ });
- it("should return null for non-existent session", async () => {
- const { db } = await import("$lib/server/db");
+ it("should return null for non-existent session", async () => {
+ const { db } = await import("$lib/server/db");
- const mockTokenData = {
- sessionId: "non-existent-session",
- userId: "test-user-id",
- exp: Math.floor(Date.now() / 1000) + 3600
- };
- vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(mockTokenData);
+ const mockTokenData = {
+ sessionId: "non-existent-session",
+ userId: "test-user-id",
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ };
+ vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(mockTokenData);
- // Mock empty database result
- const mockQuery = vi.fn().mockResolvedValue([]);
- vi.mocked(db.select).mockReturnValue({
- from: vi.fn().mockReturnValue({
- innerJoin: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: mockQuery
- })
- })
- })
- } as any);
+ // Mock empty database result
+ const mockQuery = vi.fn().mockResolvedValue([]);
+ vi.mocked(db.select).mockReturnValue({
+ from: vi.fn().mockReturnValue({
+ innerJoin: vi.fn().mockReturnValue({
+ where: vi.fn().mockReturnValue({
+ limit: mockQuery,
+ }),
+ }),
+ }),
+ } as any);
- const result = await SessionService.validateTokenWithDB("valid-token");
+ const result = await SessionService.validateTokenWithDB("valid-token");
- expect(result).toBeNull();
- });
+ expect(result).toBeNull();
+ });
- it("should return null for inactive user", async () => {
- const { db } = await import("$lib/server/db");
+ it("should return null for inactive user", async () => {
+ const { db } = await import("$lib/server/db");
- const mockTokenData = {
- sessionId: "session-id",
- userId: "test-user-id",
- exp: Math.floor(Date.now() / 1000) + 3600
- };
- vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(mockTokenData);
+ const mockTokenData = {
+ sessionId: "session-id",
+ userId: "test-user-id",
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ };
+ vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(mockTokenData);
- const inactiveUser = {
- ...mockUser,
- isActive: false
- };
+ const inactiveUser = {
+ ...mockUser,
+ isActive: false,
+ };
- const mockQuery = vi.fn().mockResolvedValue([
- {
- user: inactiveUser,
- user_session: mockSession
- }
- ]);
+ const mockQuery = vi.fn().mockResolvedValue([
+ {
+ user: inactiveUser,
+ user_session: mockSession,
+ },
+ ]);
- vi.mocked(db.select).mockReturnValue({
- from: vi.fn().mockReturnValue({
- innerJoin: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: mockQuery
- })
- })
- })
- } as any);
+ vi.mocked(db.select).mockReturnValue({
+ from: vi.fn().mockReturnValue({
+ innerJoin: vi.fn().mockReturnValue({
+ where: vi.fn().mockReturnValue({
+ limit: mockQuery,
+ }),
+ }),
+ }),
+ } as any);
- const result = await SessionService.validateTokenWithDB("valid-token");
+ const result = await SessionService.validateTokenWithDB("valid-token");
- expect(result).toBeNull();
- });
+ expect(result).toBeNull();
+ });
- it("should return null for unconfirmed user", async () => {
- const { db } = await import("$lib/server/db");
+ it("should return null for unconfirmed user", async () => {
+ const { db } = await import("$lib/server/db");
- const mockTokenData = {
- sessionId: "session-id",
- userId: "test-user-id",
- exp: Math.floor(Date.now() / 1000) + 3600
- };
- vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(mockTokenData);
+ const mockTokenData = {
+ sessionId: "session-id",
+ userId: "test-user-id",
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ };
+ vi.mocked(jwtUtils.verifyAccessToken).mockResolvedValue(mockTokenData);
- const unconfirmedUser = {
- ...mockUser,
- confirmed: false
- };
+ const unconfirmedUser = {
+ ...mockUser,
+ confirmed: false,
+ };
- const mockQuery = vi.fn().mockResolvedValue([
- {
- user: unconfirmedUser,
- user_session: mockSession
- }
- ]);
+ const mockQuery = vi.fn().mockResolvedValue([
+ {
+ user: unconfirmedUser,
+ user_session: mockSession,
+ },
+ ]);
- vi.mocked(db.select).mockReturnValue({
- from: vi.fn().mockReturnValue({
- innerJoin: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: mockQuery
- })
- })
- })
- } as any);
+ vi.mocked(db.select).mockReturnValue({
+ from: vi.fn().mockReturnValue({
+ innerJoin: vi.fn().mockReturnValue({
+ where: vi.fn().mockReturnValue({
+ limit: mockQuery,
+ }),
+ }),
+ }),
+ } as any);
- const result = await SessionService.validateTokenWithDB("valid-token");
+ const result = await SessionService.validateTokenWithDB("valid-token");
- expect(result).toBeNull();
- });
+ expect(result).toBeNull();
+ });
- it("should return null and handle errors gracefully", async () => {
- vi.mocked(jwtUtils.verifyAccessToken).mockRejectedValue(new Error("JWT error"));
+ it("should return null and handle errors gracefully", async () => {
+ vi.mocked(jwtUtils.verifyAccessToken).mockRejectedValue(new Error("JWT error"));
- const result = await SessionService.validateTokenWithDB("token");
+ const result = await SessionService.validateTokenWithDB("token");
- expect(result).toBeNull();
- });
+ expect(result).toBeNull();
+ });
});
diff --git a/src/lib/server/auth/authorization-service.ts b/src/lib/server/auth/authorization-service.ts
index f0a5468..0cad355 100644
--- a/src/lib/server/auth/authorization-service.ts
+++ b/src/lib/server/auth/authorization-service.ts
@@ -7,137 +7,137 @@ const logger = new UniversalLogger().setContext("Authorization");
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");
- }
+ static requireRole(user: JWTPayload, requiredRole: UserRole): void {
+ if (!user) {
+ throw new AuthenticationError("Authentication required");
+ }
- if (user.role !== requiredRole) {
- logger.warn(
- `Access denied: User ${user.email} has role ${user.role}, required ${requiredRole}`
- );
- throw new AuthenticationError("Insufficient permissions");
- }
+ if (user.role !== requiredRole) {
+ logger.warn(
+ `Access denied: User ${user.email} has role ${user.role}, required ${requiredRole}`,
+ );
+ throw new AuthenticationError("Insufficient permissions");
+ }
- logger.debug(`Authorization granted: User ${user.email} has required role ${requiredRole}`);
- }
+ logger.debug(`Authorization granted: User ${user.email} has required role ${requiredRole}`);
+ }
- static requireAnyRole(user: JWTPayload, allowedRoles: UserRole[]): void {
- if (!user) {
- throw new AuthenticationError("Authentication required");
- }
+ static requireAnyRole(user: JWTPayload, allowedRoles: UserRole[]): void {
+ if (!user) {
+ throw new AuthenticationError("Authentication required");
+ }
- 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");
- }
+ 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");
+ }
- logger.debug(`Authorization granted: User ${user.email} has allowed role ${user.role}`);
- }
+ logger.debug(`Authorization granted: User ${user.email} has allowed role ${user.role}`);
+ }
- static requireTenantAccess(user: JWTPayload, tenantId: string): void {
- if (!user) {
- throw new AuthenticationError("Authentication required");
- }
+ static requireTenantAccess(user: JWTPayload, tenantId: string): void {
+ if (!user) {
+ throw new AuthenticationError("Authentication required");
+ }
- if (user.role === "GLOBAL_ADMIN") {
- logger.debug(
- `Authorization granted: Global admin ${user.email} accessing tenant ${tenantId}`
- );
- return;
- }
+ if (user.role === "GLOBAL_ADMIN") {
+ logger.debug(
+ `Authorization granted: Global admin ${user.email} accessing tenant ${tenantId}`,
+ );
+ return;
+ }
- 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");
- }
+ 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");
+ }
- 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");
- }
+ 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");
+ }
- logger.debug(`Authorization granted: User ${user.email} accessing own tenant ${tenantId}`);
- return;
- }
+ logger.debug(`Authorization granted: User ${user.email} accessing own tenant ${tenantId}`);
+ return;
+ }
- logger.warn(`Access denied: User ${user.email} has invalid role ${user.role}`);
- throw new AuthenticationError("Invalid role");
- }
+ logger.warn(`Access denied: User ${user.email} has invalid role ${user.role}`);
+ throw new AuthenticationError("Invalid role");
+ }
- static requireGlobalAdmin(user: JWTPayload): void {
- this.requireRole(user, "GLOBAL_ADMIN");
- }
+ static requireGlobalAdmin(user: JWTPayload): void {
+ this.requireRole(user, "GLOBAL_ADMIN");
+ }
- static requireTenantAdmin(user: JWTPayload, tenantId?: string): void {
- this.requireAnyRole(user, ["GLOBAL_ADMIN", "TENANT_ADMIN"]);
+ static requireTenantAdmin(user: JWTPayload, tenantId?: string): void {
+ this.requireAnyRole(user, ["GLOBAL_ADMIN", "TENANT_ADMIN"]);
- if (tenantId && user.role === "TENANT_ADMIN") {
- this.requireTenantAccess(user, tenantId);
- }
- }
+ if (tenantId && user.role === "TENANT_ADMIN") {
+ this.requireTenantAccess(user, tenantId);
+ }
+ }
- static requireStaffOrAbove(user: JWTPayload, tenantId?: string): void {
- this.requireAnyRole(user, ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"]);
+ static requireStaffOrAbove(user: JWTPayload, tenantId?: string): void {
+ this.requireAnyRole(user, ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"]);
- if (tenantId && (user.role === "TENANT_ADMIN" || user.role === "STAFF")) {
- this.requireTenantAccess(user, tenantId);
- }
- }
+ if (tenantId && (user.role === "TENANT_ADMIN" || user.role === "STAFF")) {
+ this.requireTenantAccess(user, tenantId);
+ }
+ }
- static canAccessTenant(user: JWTPayload, tenantId: string): boolean {
- try {
- this.requireTenantAccess(user, tenantId);
- return true;
- } catch {
- return false;
- }
- }
+ static canAccessTenant(user: JWTPayload, tenantId: string): boolean {
+ try {
+ this.requireTenantAccess(user, tenantId);
+ return true;
+ } catch {
+ return false;
+ }
+ }
- static isGlobalAdmin(user: JWTPayload): boolean {
- return user.role === "GLOBAL_ADMIN";
- }
+ static isGlobalAdmin(user: JWTPayload): boolean {
+ return user.role === "GLOBAL_ADMIN";
+ }
- static isTenantAdmin(user: JWTPayload): boolean {
- return user.role === "TENANT_ADMIN";
- }
+ static isTenantAdmin(user: JWTPayload): boolean {
+ return user.role === "TENANT_ADMIN";
+ }
- static isStaff(user: JWTPayload): boolean {
- return user.role === "STAFF";
- }
+ static isStaff(user: JWTPayload): boolean {
+ return user.role === "STAFF";
+ }
- static hasRole(user: JWTPayload, role: UserRole): boolean {
- return user.role === role;
- }
+ static hasRole(user: JWTPayload, role: UserRole): boolean {
+ return user.role === role;
+ }
- static hasAnyRole(user: JWTPayload, roles: UserRole[]): boolean {
- return roles.includes(user.role as UserRole);
- }
+ static hasAnyRole(user: JWTPayload, roles: UserRole[]): boolean {
+ return roles.includes(user.role as UserRole);
+ }
- static getUserTenantId(user: JWTPayload): string | null {
- return (user.tenantId as string) || null;
- }
+ static getUserTenantId(user: JWTPayload): string | null {
+ return (user.tenantId as string) || null;
+ }
}
export function withAuthorization(
- user: JWTPayload,
- requiredRole?: UserRole,
- allowedRoles?: UserRole[]
+ user: JWTPayload,
+ requiredRole?: UserRole,
+ allowedRoles?: UserRole[],
) {
- if (!user) {
- throw new AuthenticationError("Authentication required");
- }
+ if (!user) {
+ throw new AuthenticationError("Authentication required");
+ }
- if (requiredRole) {
- AuthorizationService.requireRole(user, requiredRole);
- }
+ if (requiredRole) {
+ AuthorizationService.requireRole(user, requiredRole);
+ }
- if (allowedRoles) {
- AuthorizationService.requireAnyRole(user, allowedRoles);
- }
+ if (allowedRoles) {
+ AuthorizationService.requireAnyRole(user, allowedRoles);
+ }
}
diff --git a/src/lib/server/auth/jwt-utils.ts b/src/lib/server/auth/jwt-utils.ts
index 12870bf..156984c 100644
--- a/src/lib/server/auth/jwt-utils.ts
+++ b/src/lib/server/auth/jwt-utils.ts
@@ -6,12 +6,12 @@ import { UniversalLogger } from "$lib/logger";
const logger = new UniversalLogger().setContext("JWT");
export interface JWTTokens {
- accessToken: string;
- refreshToken: string;
+ accessToken: string;
+ refreshToken: string;
}
if (!env.JWT_SECRET) {
- throw Error("Mandatory ENV variable JWT_SECRET is missing!");
+ throw Error("Mandatory ENV variable JWT_SECRET is missing!");
}
const JWT_SECRET = new TextEncoder().encode(env.JWT_SECRET);
@@ -19,108 +19,108 @@ const ACCESS_TOKEN_EXPIRES = "15m"; // 15 minutes
const REFRESH_TOKEN_EXPIRES = "7d"; // 7 days
export async function generateAccessToken(user: SelectUser, sessionId: string): Promise {
- const now = Math.floor(Date.now() / 1000);
+ const now = Math.floor(Date.now() / 1000);
- const payload: Omit = {
- userId: user.id,
- email: user.email,
- name: user.name,
- role: user.role,
- tenantId: user.tenantId || undefined,
- sessionId
- };
+ const payload: Omit = {
+ userId: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.role,
+ tenantId: user.tenantId || undefined,
+ sessionId,
+ };
- const jwt = await new SignJWT(payload)
- .setProtectedHeader({ alg: "HS256" })
- .setIssuedAt(now)
- .setExpirationTime(ACCESS_TOKEN_EXPIRES)
- .sign(JWT_SECRET);
+ const jwt = await new SignJWT(payload)
+ .setProtectedHeader({ alg: "HS256" })
+ .setIssuedAt(now)
+ .setExpirationTime(ACCESS_TOKEN_EXPIRES)
+ .sign(JWT_SECRET);
- return jwt;
+ return jwt;
}
export async function generateRefreshToken(userId: string, sessionId: string): Promise {
- const now = Math.floor(Date.now() / 1000);
+ const now = Math.floor(Date.now() / 1000);
- const payload = {
- userId,
- sessionId,
- type: "refresh"
- };
+ const payload = {
+ userId,
+ sessionId,
+ type: "refresh",
+ };
- const jwt = await new SignJWT(payload)
- .setProtectedHeader({ alg: "HS256" })
- .setIssuedAt(now)
- .setExpirationTime(REFRESH_TOKEN_EXPIRES)
- .sign(JWT_SECRET);
+ const jwt = await new SignJWT(payload)
+ .setProtectedHeader({ alg: "HS256" })
+ .setIssuedAt(now)
+ .setExpirationTime(REFRESH_TOKEN_EXPIRES)
+ .sign(JWT_SECRET);
- return jwt;
+ return jwt;
}
export async function verifyAccessToken(
- token: string
+ token: string,
): Promise<(JWTPayload & { userId: string; sessionId: string }) | null> {
- try {
- const { payload } = await jwtVerify(token, JWT_SECRET);
+ try {
+ const { payload } = await jwtVerify(token, JWT_SECRET);
- return {
- userId: payload.userId as string,
- email: payload.email,
- name: payload.name,
- role: payload.role as "GLOBAL_ADMIN" | "TENANT_ADMIN" | "STAFF",
- tenantId: payload.tenantId as string | undefined,
- sessionId: payload.sessionId as string,
- iat: payload.iat,
- exp: payload.exp
- };
- } catch (error) {
- logger.warn("JWT verification failed:", { error: String(error) });
- return null;
- }
+ return {
+ userId: payload.userId as string,
+ email: payload.email,
+ name: payload.name,
+ role: payload.role as "GLOBAL_ADMIN" | "TENANT_ADMIN" | "STAFF",
+ tenantId: payload.tenantId as string | undefined,
+ sessionId: payload.sessionId as string,
+ iat: payload.iat,
+ exp: payload.exp,
+ };
+ } catch (error) {
+ logger.warn("JWT verification failed:", { error: String(error) });
+ return null;
+ }
}
export async function verifyRefreshToken(
- token: string
+ token: string,
): Promise<{ userId: string; sessionId: string } | null> {
- try {
- const { payload } = await jwtVerify(token, JWT_SECRET);
+ try {
+ const { payload } = await jwtVerify(token, JWT_SECRET);
- if (
- typeof payload.userId === "string" &&
- typeof payload.sessionId === "string" &&
- payload.type === "refresh"
- ) {
- return {
- userId: payload.userId,
- sessionId: payload.sessionId
- };
- }
+ if (
+ typeof payload.userId === "string" &&
+ typeof payload.sessionId === "string" &&
+ payload.type === "refresh"
+ ) {
+ return {
+ userId: payload.userId,
+ sessionId: payload.sessionId,
+ };
+ }
- logger.warn("Invalid refresh token payload");
- return null;
- } catch (error) {
- logger.warn("Refresh token verification failed:", { error: String(error) });
- return null;
- }
+ logger.warn("Invalid refresh token payload");
+ return null;
+ } catch (error) {
+ logger.warn("Refresh token verification failed:", { error: String(error) });
+ return null;
+ }
}
export async function generateTokens(user: SelectUser, sessionId: string): Promise {
- const [accessToken, refreshToken] = await Promise.all([
- generateAccessToken(user, sessionId),
- generateRefreshToken(user.id, sessionId)
- ]);
+ const [accessToken, refreshToken] = await Promise.all([
+ generateAccessToken(user, sessionId),
+ generateRefreshToken(user.id, sessionId),
+ ]);
- return {
- accessToken,
- refreshToken
- };
+ return {
+ accessToken,
+ refreshToken,
+ };
}
export async function isTokenExpired(token: string): Promise {
- try {
- await jwtVerify(token, JWT_SECRET);
- return false;
- } catch {
- return true;
- }
+ try {
+ await jwtVerify(token, JWT_SECRET);
+ return false;
+ } catch {
+ return true;
+ }
}
diff --git a/src/lib/server/auth/session-service.ts b/src/lib/server/auth/session-service.ts
index cb90a61..7233b50 100644
--- a/src/lib/server/auth/session-service.ts
+++ b/src/lib/server/auth/session-service.ts
@@ -2,9 +2,9 @@ import { eq, and, gt, lt } from "drizzle-orm";
import { db } from "$lib/server/db";
import { user, userSession } from "$lib/server/db/central-schema";
import type {
- SelectUser,
- InsertUserSession,
- SelectUserSession
+ SelectUser,
+ InsertUserSession,
+ SelectUserSession,
} from "$lib/server/db/central-schema";
import { generateTokens, verifyRefreshToken, verifyAccessToken } from "./jwt-utils";
import { UniversalLogger } from "$lib/logger";
@@ -13,300 +13,300 @@ import { ValidationError, NotFoundError } from "$lib/server/utils/errors";
const logger = new UniversalLogger().setContext("AuthService");
export interface SessionData {
- sessionToken: string;
- accessToken: string;
- refreshToken: string;
- user: SelectUser;
- expiresAt: Date;
+ sessionToken: string;
+ accessToken: string;
+ refreshToken: string;
+ user: SelectUser;
+ expiresAt: Date;
}
export interface LoginResult {
- sessionToken: string;
- accessToken: string;
- refreshToken: string;
- user: SelectUser;
- expiresAt: Date;
+ sessionToken: string;
+ accessToken: string;
+ refreshToken: string;
+ user: SelectUser;
+ expiresAt: Date;
}
export interface RefreshResult {
- accessToken: string;
- refreshToken: string;
- expiresAt: Date;
+ accessToken: string;
+ refreshToken: string;
+ expiresAt: Date;
}
export class SessionService {
- private static readonly SESSION_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
+ private static readonly SESSION_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
- static async createSession(
- userId: string,
- ipAddress?: string,
- userAgent?: string
- ): Promise {
- logger.info(`Creating session for user: ${userId}`);
+ static async createSession(
+ userId: string,
+ ipAddress?: string,
+ userAgent?: string,
+ ): Promise {
+ logger.info(`Creating session for user: ${userId}`);
- const existingUser = await db.select().from(user).where(eq(user.id, userId)).limit(1);
+ const existingUser = await db.select().from(user).where(eq(user.id, userId)).limit(1);
- if (existingUser.length === 0) {
- throw new NotFoundError(`User with ID ${userId} not found`);
- }
+ if (existingUser.length === 0) {
+ throw new NotFoundError(`User with ID ${userId} not found`);
+ }
- const userData = existingUser[0];
+ const userData = existingUser[0];
- if (!userData.isActive) {
- throw new ValidationError("User account is inactive");
- }
+ if (!userData.isActive) {
+ throw new ValidationError("User account is inactive");
+ }
- if (!userData.confirmed) {
- throw new ValidationError("User account is not confirmed");
- }
+ if (!userData.confirmed) {
+ throw new ValidationError("User account is not confirmed");
+ }
- // Create session entry first to get the ID
- const sessionData: InsertUserSession = {
- userId: userData.id,
- sessionToken: "", // Will be updated with actual token
- accessToken: "", // Will be updated with actual token
- refreshToken: "", // Will be updated with actual token
- ipAddress,
- userAgent,
- expiresAt: new Date(Date.now() + this.SESSION_DURATION),
- lastUsedAt: new Date()
- };
+ // Create session entry first to get the ID
+ const sessionData: InsertUserSession = {
+ userId: userData.id,
+ sessionToken: "", // Will be updated with actual token
+ accessToken: "", // Will be updated with actual token
+ refreshToken: "", // Will be updated with actual token
+ ipAddress,
+ userAgent,
+ expiresAt: new Date(Date.now() + this.SESSION_DURATION),
+ lastUsedAt: new Date(),
+ };
- const [createdSession] = await db.insert(userSession).values(sessionData).returning();
+ const [createdSession] = await db.insert(userSession).values(sessionData).returning();
- // Generate tokens with the actual session ID
- const tokens = await generateTokens(userData, createdSession.id);
+ // Generate tokens with the actual session ID
+ const tokens = await generateTokens(userData, createdSession.id);
- // Update session with actual tokens
- const [updatedSession] = await db
- .update(userSession)
- .set({
- sessionToken: tokens.accessToken, // Use access token as session token
- accessToken: tokens.accessToken,
- refreshToken: tokens.refreshToken
- })
- .where(eq(userSession.id, createdSession.id))
- .returning();
+ // Update session with actual tokens
+ const [updatedSession] = await db
+ .update(userSession)
+ .set({
+ sessionToken: tokens.accessToken, // Use access token as session token
+ accessToken: tokens.accessToken,
+ refreshToken: tokens.refreshToken,
+ })
+ .where(eq(userSession.id, createdSession.id))
+ .returning();
- await db.update(user).set({ lastLoginAt: new Date() }).where(eq(user.id, userId));
+ await db.update(user).set({ lastLoginAt: new Date() }).where(eq(user.id, userId));
- logger.info(`Session created successfully for user: ${userId}`);
+ logger.info(`Session created successfully for user: ${userId}`);
- return {
- sessionToken: updatedSession.sessionToken,
- accessToken: updatedSession.accessToken,
- refreshToken: updatedSession.refreshToken,
- user: userData,
- expiresAt: updatedSession.expiresAt
- };
- }
+ return {
+ sessionToken: updatedSession.sessionToken,
+ accessToken: updatedSession.accessToken,
+ refreshToken: updatedSession.refreshToken,
+ user: userData,
+ expiresAt: updatedSession.expiresAt,
+ };
+ }
- /**
- * Validates an access token by checking if it exists in the database and is not expired
- */
- static async validateTokenWithDB(
- accessToken: string
- ): Promise<{ user: SelectUser; sessionId: string; exp: Date } | null> {
- logger.debug("Validating access token with database");
+ /**
+ * Validates an access token by checking if it exists in the database and is not expired
+ */
+ static async validateTokenWithDB(
+ accessToken: string,
+ ): Promise<{ user: SelectUser; sessionId: string; exp: Date } | null> {
+ logger.debug("Validating access token with database");
- try {
- // First verify the JWT token structure
- const tokenData = await verifyAccessToken(accessToken);
- if (!tokenData) {
- logger.debug("Access token is invalid or expired");
- return null;
- }
+ try {
+ // First verify the JWT token structure
+ const tokenData = await verifyAccessToken(accessToken);
+ if (!tokenData) {
+ logger.debug("Access token is invalid or expired");
+ return null;
+ }
- // Check if session exists in database and is not expired
- const sessions = await db
- .select()
- .from(userSession)
- .innerJoin(user, eq(userSession.userId, user.id))
- .where(
- and(
- eq(userSession.id, tokenData.sessionId),
- eq(userSession.accessToken, accessToken),
- gt(userSession.expiresAt, new Date())
- )
- )
- .limit(1);
+ // Check if session exists in database and is not expired
+ const sessions = await db
+ .select()
+ .from(userSession)
+ .innerJoin(user, eq(userSession.userId, user.id))
+ .where(
+ and(
+ eq(userSession.id, tokenData.sessionId),
+ eq(userSession.accessToken, accessToken),
+ gt(userSession.expiresAt, new Date()),
+ ),
+ )
+ .limit(1);
- if (sessions.length === 0) {
- logger.debug("Session not found in database or expired");
- return null;
- }
+ if (sessions.length === 0) {
+ logger.debug("Session not found in database or expired");
+ return null;
+ }
- const session = sessions[0];
+ const session = sessions[0];
- if (!session.user.isActive) {
- logger.debug("User account is inactive");
- return null;
- }
+ if (!session.user.isActive) {
+ logger.debug("User account is inactive");
+ return null;
+ }
- if (!session.user.confirmed) {
- logger.debug("User account is not confirmed");
- return null;
- }
+ if (!session.user.confirmed) {
+ logger.debug("User account is not confirmed");
+ return null;
+ }
- // Update last used time
- await db
- .update(userSession)
- .set({ lastUsedAt: new Date() })
- .where(eq(userSession.id, session.user_session.id));
+ // Update last used time
+ await db
+ .update(userSession)
+ .set({ lastUsedAt: new Date() })
+ .where(eq(userSession.id, session.user_session.id));
- logger.debug("Access token validated successfully");
+ logger.debug("Access token validated successfully");
- return {
- user: session.user,
- exp: session.user_session.expiresAt,
- sessionId: session.user_session.id
- };
- } catch (error) {
- logger.error("Error validating token with database:", { error: String(error) });
- return null;
- }
- }
+ return {
+ user: session.user,
+ exp: session.user_session.expiresAt,
+ sessionId: session.user_session.id,
+ };
+ } catch (error) {
+ logger.error("Error validating token with database:", { error: String(error) });
+ return null;
+ }
+ }
- static async validateSession(sessionToken: string): Promise {
- logger.debug(`Validating session: ${sessionToken}`);
+ static async validateSession(sessionToken: string): Promise {
+ logger.debug(`Validating session: ${sessionToken}`);
- const sessions = await db
- .select()
- .from(userSession)
- .innerJoin(user, eq(userSession.userId, user.id))
- .where(and(eq(userSession.sessionToken, sessionToken), gt(userSession.expiresAt, new Date())))
- .limit(1);
+ const sessions = await db
+ .select()
+ .from(userSession)
+ .innerJoin(user, eq(userSession.userId, user.id))
+ .where(and(eq(userSession.sessionToken, sessionToken), gt(userSession.expiresAt, new Date())))
+ .limit(1);
- if (sessions.length === 0) {
- logger.warn(`Session not found or expired: ${sessionToken}`);
- return null;
- }
+ if (sessions.length === 0) {
+ logger.warn(`Session not found or expired: ${sessionToken}`);
+ return null;
+ }
- const session = sessions[0];
+ const session = sessions[0];
- if (!session.user.isActive) {
- logger.warn(`User account inactive for session: ${sessionToken}`);
- return null;
- }
+ if (!session.user.isActive) {
+ logger.warn(`User account inactive for session: ${sessionToken}`);
+ return null;
+ }
- await db
- .update(userSession)
- .set({ lastUsedAt: new Date() })
- .where(eq(userSession.id, session.user_session.id));
+ await db
+ .update(userSession)
+ .set({ lastUsedAt: new Date() })
+ .where(eq(userSession.id, session.user_session.id));
- logger.debug(`Session validated successfully: ${sessionToken}`);
+ logger.debug(`Session validated successfully: ${sessionToken}`);
- return {
- sessionToken: session.user_session.sessionToken,
- accessToken: session.user_session.accessToken,
- refreshToken: session.user_session.refreshToken,
- user: session.user,
- expiresAt: session.user_session.expiresAt
- };
- }
+ return {
+ sessionToken: session.user_session.sessionToken,
+ accessToken: session.user_session.accessToken,
+ refreshToken: session.user_session.refreshToken,
+ user: session.user,
+ expiresAt: session.user_session.expiresAt,
+ };
+ }
- static async refreshSession(refreshToken: string): Promise {
- logger.debug("Refreshing session tokens");
+ static async refreshSession(refreshToken: string): Promise {
+ logger.debug("Refreshing session tokens");
- const tokenData = await verifyRefreshToken(refreshToken);
- if (!tokenData) {
- logger.warn("Invalid refresh token");
- return null;
- }
+ const tokenData = await verifyRefreshToken(refreshToken);
+ if (!tokenData) {
+ logger.warn("Invalid refresh token");
+ return null;
+ }
- const sessions = await db
- .select()
- .from(userSession)
- .innerJoin(user, eq(userSession.userId, user.id))
- .where(
- and(
- eq(userSession.id, tokenData.sessionId),
- eq(userSession.refreshToken, refreshToken),
- gt(userSession.expiresAt, new Date())
- )
- )
- .limit(1);
+ const sessions = await db
+ .select()
+ .from(userSession)
+ .innerJoin(user, eq(userSession.userId, user.id))
+ .where(
+ and(
+ eq(userSession.id, tokenData.sessionId),
+ eq(userSession.refreshToken, refreshToken),
+ gt(userSession.expiresAt, new Date()),
+ ),
+ )
+ .limit(1);
- if (sessions.length === 0) {
- logger.warn("Session not found for refresh token");
- return null;
- }
+ if (sessions.length === 0) {
+ logger.warn("Session not found for refresh token");
+ return null;
+ }
- const session = sessions[0];
+ const session = sessions[0];
- if (!session.user.isActive) {
- logger.warn("User account inactive for refresh");
- return null;
- }
+ if (!session.user.isActive) {
+ logger.warn("User account inactive for refresh");
+ return null;
+ }
- const newTokens = await generateTokens(session.user, session.user_session.id);
- const newExpiresAt = new Date(Date.now() + this.SESSION_DURATION);
+ const newTokens = await generateTokens(session.user, session.user_session.id);
+ const newExpiresAt = new Date(Date.now() + this.SESSION_DURATION);
- await db
- .update(userSession)
- .set({
- accessToken: newTokens.accessToken,
- refreshToken: newTokens.refreshToken,
- expiresAt: newExpiresAt,
- lastUsedAt: new Date()
- })
- .where(eq(userSession.id, session.user_session.id));
+ await db
+ .update(userSession)
+ .set({
+ accessToken: newTokens.accessToken,
+ refreshToken: newTokens.refreshToken,
+ expiresAt: newExpiresAt,
+ lastUsedAt: new Date(),
+ })
+ .where(eq(userSession.id, session.user_session.id));
- logger.info("Session tokens refreshed successfully");
+ logger.info("Session tokens refreshed successfully");
- return {
- accessToken: newTokens.accessToken,
- refreshToken: newTokens.refreshToken,
- expiresAt: newExpiresAt
- };
- }
+ return {
+ accessToken: newTokens.accessToken,
+ refreshToken: newTokens.refreshToken,
+ expiresAt: newExpiresAt,
+ };
+ }
- static async logout(sessionToken: string): Promise {
- logger.info(`Logging out session: ${sessionToken}`);
+ static async logout(sessionToken: string): Promise {
+ logger.info(`Logging out session: ${sessionToken}`);
- await db.delete(userSession).where(eq(userSession.sessionToken, sessionToken));
+ await db.delete(userSession).where(eq(userSession.sessionToken, sessionToken));
- logger.info(`Session logged out successfully: ${sessionToken}`);
- }
+ logger.info(`Session logged out successfully: ${sessionToken}`);
+ }
- static async logoutAllSessions(userId: string): Promise {
- logger.info(`Logging out all sessions for user: ${userId}`);
+ static async logoutAllSessions(userId: string): Promise {
+ logger.info(`Logging out all sessions for user: ${userId}`);
- await db.delete(userSession).where(eq(userSession.userId, userId));
+ await db.delete(userSession).where(eq(userSession.userId, userId));
- logger.info(`All sessions logged out for user: ${userId}`);
- }
+ logger.info(`All sessions logged out for user: ${userId}`);
+ }
- static async cleanupExpiredSessions(): Promise {
- logger.info("Cleaning up expired sessions");
+ static async cleanupExpiredSessions(): Promise {
+ logger.info("Cleaning up expired sessions");
- await db.delete(userSession).where(lt(userSession.expiresAt, new Date()));
+ await db.delete(userSession).where(lt(userSession.expiresAt, new Date()));
- logger.info("Expired sessions cleaned up");
- }
+ logger.info("Expired sessions cleaned up");
+ }
- static async getActiveSessions(userId: string): Promise {
- logger.debug(`Getting active sessions for user: ${userId}`);
+ static async getActiveSessions(userId: string): Promise {
+ logger.debug(`Getting active sessions for user: ${userId}`);
- const sessions = await db
- .select()
- .from(userSession)
- .where(and(eq(userSession.userId, userId), gt(userSession.expiresAt, new Date())))
- .orderBy(userSession.lastUsedAt);
+ const sessions = await db
+ .select()
+ .from(userSession)
+ .where(and(eq(userSession.userId, userId), gt(userSession.expiresAt, new Date())))
+ .orderBy(userSession.lastUsedAt);
- return sessions;
- }
+ return sessions;
+ }
- static async getUserFromSession(sessionToken: string): Promise {
- const sessionData = await this.validateSession(sessionToken);
- return sessionData ? sessionData.user : null;
- }
+ static async getUserFromSession(sessionToken: string): Promise {
+ const sessionData = await this.validateSession(sessionToken);
+ return sessionData ? sessionData.user : null;
+ }
- static async revokeSession(sessionId: string): Promise {
- logger.info(`Revoking session: ${sessionId}`);
+ static async revokeSession(sessionId: string): Promise {
+ logger.info(`Revoking session: ${sessionId}`);
- await db.delete(userSession).where(eq(userSession.id, sessionId));
+ await db.delete(userSession).where(eq(userSession.id, sessionId));
- logger.info(`Session revoked: ${sessionId}`);
- }
+ logger.info(`Session revoked: ${sessionId}`);
+ }
}
diff --git a/src/lib/server/auth/webauthn-service.ts b/src/lib/server/auth/webauthn-service.ts
index 97aa2a8..020173d 100644
--- a/src/lib/server/auth/webauthn-service.ts
+++ b/src/lib/server/auth/webauthn-service.ts
@@ -7,392 +7,392 @@ import { UniversalLogger } from "$lib/logger";
const logger = new UniversalLogger().setContext("WebAuthnService");
export interface WebAuthnCredential {
- id: string;
- response: {
- authenticatorData: string;
- signature: string;
- userHandle?: string;
- clientDataJSON: string;
- };
+ id: string;
+ response: {
+ authenticatorData: string;
+ signature: string;
+ userHandle?: string;
+ clientDataJSON: string;
+ };
}
export interface WebAuthnVerificationResult {
- verified: boolean;
- userId?: string;
- newCounter?: number;
- passkeyId?: string;
+ verified: boolean;
+ userId?: string;
+ newCounter?: number;
+ passkeyId?: string;
}
export class WebAuthnService {
- /**
- * Verify a WebAuthn authentication assertion
- * @param credential - The WebAuthn credential from the client
- * @param challengeFromSession - The challenge that was sent to the client (should be stored in session)
- * @returns Verification result with user ID if successful
- */
- static async verifyAuthentication(
- credential: WebAuthnCredential,
- challengeFromSession: string
- ): Promise {
- try {
- logger.debug("Verifying WebAuthn authentication", {
- credentialId: credential.id,
- hasChallenge: !!challengeFromSession
- });
+ /**
+ * Verify a WebAuthn authentication assertion
+ * @param credential - The WebAuthn credential from the client
+ * @param challengeFromSession - The challenge that was sent to the client (should be stored in session)
+ * @returns Verification result with user ID if successful
+ */
+ static async verifyAuthentication(
+ credential: WebAuthnCredential,
+ challengeFromSession: string,
+ ): Promise {
+ try {
+ logger.debug("Verifying WebAuthn authentication", {
+ credentialId: credential.id,
+ hasChallenge: !!challengeFromSession,
+ });
- // Get the passkey from database
- const passkeyResults = await db
- .select()
- .from(userPasskey)
- .where(eq(userPasskey.id, credential.id))
- .limit(1);
+ // Get the passkey from database
+ const passkeyResults = await db
+ .select()
+ .from(userPasskey)
+ .where(eq(userPasskey.id, credential.id))
+ .limit(1);
- if (passkeyResults.length === 0) {
- logger.warn("Passkey not found", { credentialId: credential.id });
- return { verified: false };
- }
+ if (passkeyResults.length === 0) {
+ logger.warn("Passkey not found", { credentialId: credential.id });
+ return { verified: false };
+ }
- const passkey = passkeyResults[0];
+ const passkey = passkeyResults[0];
- // Parse client data JSON
- const clientDataJSON = JSON.parse(
- Buffer.from(credential.response.clientDataJSON, "base64").toString()
- );
+ // Parse client data JSON
+ const clientDataJSON = JSON.parse(
+ Buffer.from(credential.response.clientDataJSON, "base64").toString(),
+ );
- // Verify the challenge (convert base64url to base64 for comparison)
- const expectedChallenge = challengeFromSession.replace(/-/g, "+").replace(/_/g, "/");
- const receivedChallenge = clientDataJSON.challenge.replace(/-/g, "+").replace(/_/g, "/");
+ // Verify the challenge (convert base64url to base64 for comparison)
+ const expectedChallenge = challengeFromSession.replace(/-/g, "+").replace(/_/g, "/");
+ const receivedChallenge = clientDataJSON.challenge.replace(/-/g, "+").replace(/_/g, "/");
- if (expectedChallenge !== receivedChallenge) {
- logger.warn("Challenge mismatch", {
- credentialId: credential.id,
- expectedChallenge,
- receivedChallenge
- });
- return { verified: false };
- }
+ if (expectedChallenge !== receivedChallenge) {
+ logger.warn("Challenge mismatch", {
+ credentialId: credential.id,
+ expectedChallenge,
+ receivedChallenge,
+ });
+ return { verified: false };
+ }
- // Verify the origin matches expected origin (critical for security)
- const allowedOrigins = WebAuthnService.getAllowedOrigins();
- if (!allowedOrigins.includes(clientDataJSON.origin)) {
- logger.warn("Origin verification failed", {
- credentialId: credential.id,
- receivedOrigin: clientDataJSON.origin,
- allowedOrigins
- });
- return { verified: false };
- }
+ // Verify the origin matches expected origin (critical for security)
+ const allowedOrigins = WebAuthnService.getAllowedOrigins();
+ if (!allowedOrigins.includes(clientDataJSON.origin)) {
+ logger.warn("Origin verification failed", {
+ credentialId: credential.id,
+ receivedOrigin: clientDataJSON.origin,
+ allowedOrigins,
+ });
+ return { verified: false };
+ }
- logger.debug("Origin verification successful", {
- origin: clientDataJSON.origin
- });
+ logger.debug("Origin verification successful", {
+ origin: clientDataJSON.origin,
+ });
- // Parse authenticator data
- const authenticatorDataBuffer = Buffer.from(credential.response.authenticatorData, "base64");
+ // Parse authenticator data
+ const authenticatorDataBuffer = Buffer.from(credential.response.authenticatorData, "base64");
- // Parse authenticator data for detailed debugging
- const parsedAuthData = WebAuthnService.parseAuthenticatorData(authenticatorDataBuffer);
+ // Parse authenticator data for detailed debugging
+ const parsedAuthData = WebAuthnService.parseAuthenticatorData(authenticatorDataBuffer);
- logger.debug("Authenticator data analysis", {
- credentialId: credential.id,
- bufferLength: authenticatorDataBuffer.length,
- bufferHex: authenticatorDataBuffer.toString("hex"),
- parsed: parsedAuthData
- });
+ logger.debug("Authenticator data analysis", {
+ credentialId: credential.id,
+ bufferLength: authenticatorDataBuffer.length,
+ bufferHex: authenticatorDataBuffer.toString("hex"),
+ parsed: parsedAuthData,
+ });
- // Extract counter from authenticator data
- // Format: rpIdHash(32) + flags(1) + counter(4) + attestedCredentialData(variable)
- // Counter is at bytes 33-36 (0-indexed)
- if (authenticatorDataBuffer.length < 37) {
- logger.warn("Authenticator data too short for counter extraction", {
- credentialId: credential.id,
- bufferLength: authenticatorDataBuffer.length
- });
- return { verified: false };
- }
+ // Extract counter from authenticator data
+ // Format: rpIdHash(32) + flags(1) + counter(4) + attestedCredentialData(variable)
+ // Counter is at bytes 33-36 (0-indexed)
+ if (authenticatorDataBuffer.length < 37) {
+ logger.warn("Authenticator data too short for counter extraction", {
+ credentialId: credential.id,
+ bufferLength: authenticatorDataBuffer.length,
+ });
+ return { verified: false };
+ }
- const newCounter = authenticatorDataBuffer.readUInt32BE(33);
+ const newCounter = authenticatorDataBuffer.readUInt32BE(33);
- logger.debug("Counter extraction", {
- credentialId: credential.id,
- storedCounter: passkey.counter,
- newCounter,
- counterBytes: authenticatorDataBuffer.subarray(33, 37).toString("hex")
- });
+ logger.debug("Counter extraction", {
+ credentialId: credential.id,
+ storedCounter: passkey.counter,
+ newCounter,
+ counterBytes: authenticatorDataBuffer.subarray(33, 37).toString("hex"),
+ });
- // Some authenticators (especially software-based ones) always return 0
- // In that case, we skip counter verification but log it
- if (newCounter === 0 && passkey.counter === 0) {
- logger.info("Authenticator uses zero counter - skipping counter verification", {
- credentialId: credential.id
- });
- } else if (newCounter <= passkey.counter) {
- logger.warn("Counter verification failed - possible replay attack", {
- credentialId: credential.id,
- storedCounter: passkey.counter,
- newCounter
- });
- return { verified: false };
- }
+ // Some authenticators (especially software-based ones) always return 0
+ // In that case, we skip counter verification but log it
+ if (newCounter === 0 && passkey.counter === 0) {
+ logger.info("Authenticator uses zero counter - skipping counter verification", {
+ credentialId: credential.id,
+ });
+ } else if (newCounter <= passkey.counter) {
+ logger.warn("Counter verification failed - possible replay attack", {
+ credentialId: credential.id,
+ storedCounter: passkey.counter,
+ newCounter,
+ });
+ return { verified: false };
+ }
- // Create the data to be signed
- const clientDataHash = createHash("sha256")
- .update(Buffer.from(credential.response.clientDataJSON, "base64"))
- .digest();
+ // Create the data to be signed
+ const clientDataHash = createHash("sha256")
+ .update(Buffer.from(credential.response.clientDataJSON, "base64"))
+ .digest();
- const signedData = Buffer.concat([authenticatorDataBuffer, clientDataHash]);
+ const signedData = Buffer.concat([authenticatorDataBuffer, clientDataHash]);
- // Verify the signature
- const publicKeyBuffer = Buffer.from(passkey.publicKey, "base64");
- const signatureBuffer = Buffer.from(credential.response.signature, "base64");
+ // Verify the signature
+ const publicKeyBuffer = Buffer.from(passkey.publicKey, "base64");
+ const signatureBuffer = Buffer.from(credential.response.signature, "base64");
- const isSignatureValid = await this.verifySignature(
- publicKeyBuffer,
- signedData,
- signatureBuffer
- );
+ const isSignatureValid = await this.verifySignature(
+ publicKeyBuffer,
+ signedData,
+ signatureBuffer,
+ );
- if (!isSignatureValid) {
- logger.warn("Signature verification failed", { credentialId: credential.id });
- return { verified: false };
- }
+ if (!isSignatureValid) {
+ logger.warn("Signature verification failed", { credentialId: credential.id });
+ return { verified: false };
+ }
- // Update the counter in the database to prevent replay attacks
- // Only update if the authenticator provides a non-zero counter
- if (newCounter > 0) {
- await db
- .update(userPasskey)
- .set({ counter: newCounter })
- .where(eq(userPasskey.id, passkey.id));
+ // Update the counter in the database to prevent replay attacks
+ // Only update if the authenticator provides a non-zero counter
+ if (newCounter > 0) {
+ await db
+ .update(userPasskey)
+ .set({ counter: newCounter })
+ .where(eq(userPasskey.id, passkey.id));
- logger.debug("Counter updated in database", {
- credentialId: credential.id,
- newCounter
- });
- } else {
- logger.debug("Counter update skipped (zero counter authenticator)", {
- credentialId: credential.id
- });
- }
+ logger.debug("Counter updated in database", {
+ credentialId: credential.id,
+ newCounter,
+ });
+ } else {
+ logger.debug("Counter update skipped (zero counter authenticator)", {
+ credentialId: credential.id,
+ });
+ }
- logger.debug("WebAuthn authentication successful", {
- credentialId: credential.id,
- userId: passkey.userId,
- newCounter,
- counterUpdated: true
- });
+ logger.debug("WebAuthn authentication successful", {
+ credentialId: credential.id,
+ userId: passkey.userId,
+ newCounter,
+ counterUpdated: true,
+ });
- return {
- verified: true,
- userId: passkey.userId,
- newCounter,
- passkeyId: passkey.id
- };
- } catch (error) {
- logger.error("WebAuthn verification error", {
- error: String(error),
- credentialId: credential.id
- });
- return { verified: false };
- }
- }
+ return {
+ verified: true,
+ userId: passkey.userId,
+ newCounter,
+ passkeyId: passkey.id,
+ };
+ } catch (error) {
+ logger.error("WebAuthn verification error", {
+ error: String(error),
+ credentialId: credential.id,
+ });
+ return { verified: false };
+ }
+ }
- /**
- * Verify a signature using the stored public key
- * This is a simplified implementation - in production, you should use a proper WebAuthn library
- * like @simplewebauthn/server for complete verification
- */
- private static async verifySignature(
- publicKey: Buffer,
- signedData: Buffer,
- signature: Buffer
- ): Promise {
- try {
- const crypto = await import("node:crypto");
+ /**
+ * Verify a signature using the stored public key
+ * This is a simplified implementation - in production, you should use a proper WebAuthn library
+ * like @simplewebauthn/server for complete verification
+ */
+ private static async verifySignature(
+ publicKey: Buffer,
+ signedData: Buffer,
+ signature: Buffer,
+ ): Promise {
+ try {
+ const crypto = await import("node:crypto");
- // This is a simplified implementation
- // In production, you should use a proper WebAuthn library that handles:
- // - Different key formats (COSE, etc.)
- // - Different signature algorithms
- // - Proper ASN.1 parsing
- // - Certificate chain validation
+ // This is a simplified implementation
+ // In production, you should use a proper WebAuthn library that handles:
+ // - Different key formats (COSE, etc.)
+ // - Different signature algorithms
+ // - Proper ASN.1 parsing
+ // - Certificate chain validation
- // For now, we'll assume ES256 (ECDSA P-256 with SHA-256)
- // and that the public key is in the correct format
+ // For now, we'll assume ES256 (ECDSA P-256 with SHA-256)
+ // and that the public key is in the correct format
- const verify = crypto.createVerify("SHA256");
- verify.update(signedData);
- verify.end();
+ const verify = crypto.createVerify("SHA256");
+ verify.update(signedData);
+ verify.end();
- // This is a placeholder - proper implementation would:
- // 1. Parse the COSE key format
- // 2. Convert to the correct format for Node.js crypto
- // 3. Handle different algorithms properly
+ // This is a placeholder - proper implementation would:
+ // 1. Parse the COSE key format
+ // 2. Convert to the correct format for Node.js crypto
+ // 3. Handle different algorithms properly
- logger.debug("Signature verification (simplified implementation)", {
- publicKeyLength: publicKey.length,
- signatureLength: signature.length,
- signedDataLength: signedData.length
- });
+ logger.debug("Signature verification (simplified implementation)", {
+ publicKeyLength: publicKey.length,
+ signatureLength: signature.length,
+ signedDataLength: signedData.length,
+ });
- // For development, we'll return true if all data is present
- // TODO: Replace with proper signature verification
- return publicKey.length > 0 && signature.length > 0 && signedData.length > 0;
- } catch (error) {
- logger.error("Signature verification error", { error: String(error) });
- return false;
- }
- }
+ // For development, we'll return true if all data is present
+ // TODO: Replace with proper signature verification
+ return publicKey.length > 0 && signature.length > 0 && signedData.length > 0;
+ } catch (error) {
+ logger.error("Signature verification error", { error: String(error) });
+ return false;
+ }
+ }
- /**
- * Generate a random challenge for WebAuthn authentication
- * This should be stored in the session and used for verification
- */
- static generateChallenge(): string {
- return randomBytes(32).toString("base64url");
- }
+ /**
+ * Generate a random challenge for WebAuthn authentication
+ * This should be stored in the session and used for verification
+ */
+ static generateChallenge(): string {
+ return randomBytes(32).toString("base64url");
+ }
- /**
- * Update the counter for a passkey after successful authentication
- */
- static async updatePasskeyCounter(passkeyId: string, newCounter: number): Promise {
- await db
- .update(userPasskey)
- .set({
- counter: newCounter,
- lastUsedAt: new Date(),
- updatedAt: new Date()
- })
- .where(eq(userPasskey.id, passkeyId));
+ /**
+ * Update the counter for a passkey after successful authentication
+ */
+ static async updatePasskeyCounter(passkeyId: string, newCounter: number): Promise {
+ await db
+ .update(userPasskey)
+ .set({
+ counter: newCounter,
+ lastUsedAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(eq(userPasskey.id, passkeyId));
- logger.debug("Passkey counter updated", { passkeyId, newCounter });
- }
+ logger.debug("Passkey counter updated", { passkeyId, newCounter });
+ }
- /**
- * Get all passkeys for a user
- */
- static async getUserPasskeys(userId: string) {
- return await db.select().from(userPasskey).where(eq(userPasskey.userId, userId));
- }
+ /**
+ * Get all passkeys for a user
+ */
+ static async getUserPasskeys(userId: string) {
+ return await db.select().from(userPasskey).where(eq(userPasskey.userId, userId));
+ }
- /**
- * Get allowed origins for WebAuthn verification
- * Uses SERVER_DOMAIN in production, localhost variants in development
- */
- private static getAllowedOrigins(): string[] {
- const origins: string[] = [];
+ /**
+ * Get allowed origins for WebAuthn verification
+ * Uses SERVER_DOMAIN in production, localhost variants in development
+ */
+ private static getAllowedOrigins(): string[] {
+ const origins: string[] = [];
- // Check if we're in development (NODE_ENV or presence of dev indicators)
- const isDevelopment =
- process.env.NODE_ENV === "development" ||
- process.env.NODE_ENV === "dev" ||
- !process.env.SERVER_DOMAIN;
+ // Check if we're in development (NODE_ENV or presence of dev indicators)
+ const isDevelopment =
+ process.env.NODE_ENV === "development" ||
+ process.env.NODE_ENV === "dev" ||
+ !process.env.SERVER_DOMAIN;
- if (isDevelopment) {
- // Development origins
- origins.push(
- "http://localhost:5173",
- "http://127.0.0.1:5173",
- "http://localhost:4173",
- "http://127.0.0.1:4173"
- );
- } else {
- // Production: Use SERVER_DOMAIN
- const serverDomain = process.env.SERVER_DOMAIN;
- if (serverDomain) {
- // Add main domain with HTTPS
- origins.push(`https://${serverDomain}`);
+ if (isDevelopment) {
+ // Development origins
+ origins.push(
+ "http://localhost:5173",
+ "http://127.0.0.1:5173",
+ "http://localhost:4173",
+ "http://127.0.0.1:4173",
+ );
+ } else {
+ // Production: Use SERVER_DOMAIN
+ const serverDomain = process.env.SERVER_DOMAIN;
+ if (serverDomain) {
+ // Add main domain with HTTPS
+ origins.push(`https://${serverDomain}`);
- // Add www variant if it doesn't already start with www
- if (!serverDomain.startsWith("www.")) {
- origins.push(`https://www.${serverDomain}`);
- }
+ // Add www variant if it doesn't already start with www
+ if (!serverDomain.startsWith("www.")) {
+ origins.push(`https://www.${serverDomain}`);
+ }
- // TODO: Webauthn does not support wildcard domains. We'll need another solution for that.
- }
- }
+ // TODO: Webauthn does not support wildcard domains. We'll need another solution for that.
+ }
+ }
- // Allow manual override via WEBAUTHN_ALLOWED_ORIGINS
- const envOrigins = process.env.WEBAUTHN_ALLOWED_ORIGINS;
- if (envOrigins) {
- origins.push(...envOrigins.split(",").map((o) => o.trim()));
- }
+ // Allow manual override via WEBAUTHN_ALLOWED_ORIGINS
+ const envOrigins = process.env.WEBAUTHN_ALLOWED_ORIGINS;
+ if (envOrigins) {
+ origins.push(...envOrigins.split(",").map((o) => o.trim()));
+ }
- // Ensure we have at least one allowed origin
- if (origins.length === 0) {
- logger.error("No WebAuthn allowed origins configured - this is a security risk!", {
- NODE_ENV: process.env.NODE_ENV,
- SERVER_DOMAIN: process.env.SERVER_DOMAIN,
- WEBAUTHN_ALLOWED_ORIGINS: process.env.WEBAUTHN_ALLOWED_ORIGINS
- });
- // Fallback to localhost in development only
- if (isDevelopment) {
- origins.push("http://localhost:5173");
- }
- }
+ // Ensure we have at least one allowed origin
+ if (origins.length === 0) {
+ logger.error("No WebAuthn allowed origins configured - this is a security risk!", {
+ NODE_ENV: process.env.NODE_ENV,
+ SERVER_DOMAIN: process.env.SERVER_DOMAIN,
+ WEBAUTHN_ALLOWED_ORIGINS: process.env.WEBAUTHN_ALLOWED_ORIGINS,
+ });
+ // Fallback to localhost in development only
+ if (isDevelopment) {
+ origins.push("http://localhost:5173");
+ }
+ }
- logger.debug("WebAuthn allowed origins configured", {
- origins,
- isDevelopment,
- serverDomain: process.env.SERVER_DOMAIN
- });
+ logger.debug("WebAuthn allowed origins configured", {
+ origins,
+ isDevelopment,
+ serverDomain: process.env.SERVER_DOMAIN,
+ });
- return origins;
- }
+ return origins;
+ }
- /**
- * Parse authenticator data structure for debugging
- */
- private static parseAuthenticatorData(authenticatorDataBuffer: Buffer) {
- if (authenticatorDataBuffer.length < 37) {
- return { error: "Buffer too short" };
- }
+ /**
+ * Parse authenticator data structure for debugging
+ */
+ private static parseAuthenticatorData(authenticatorDataBuffer: Buffer) {
+ if (authenticatorDataBuffer.length < 37) {
+ return { error: "Buffer too short" };
+ }
- const rpIdHash = authenticatorDataBuffer.subarray(0, 32);
- const flags = authenticatorDataBuffer.readUInt8(32);
- const counter = authenticatorDataBuffer.readUInt32BE(33);
+ const rpIdHash = authenticatorDataBuffer.subarray(0, 32);
+ const flags = authenticatorDataBuffer.readUInt8(32);
+ const counter = authenticatorDataBuffer.readUInt32BE(33);
- // Parse flags
- const userPresent = !!(flags & 0x01);
- const userVerified = !!(flags & 0x04);
- const attestedCredentialDataIncluded = !!(flags & 0x40);
- const extensionDataIncluded = !!(flags & 0x80);
+ // Parse flags
+ const userPresent = !!(flags & 0x01);
+ const userVerified = !!(flags & 0x04);
+ const attestedCredentialDataIncluded = !!(flags & 0x40);
+ const extensionDataIncluded = !!(flags & 0x80);
- return {
- rpIdHash: rpIdHash.toString("hex"),
- flags: {
- raw: flags,
- userPresent,
- userVerified,
- attestedCredentialDataIncluded,
- extensionDataIncluded
- },
- counter,
- totalLength: authenticatorDataBuffer.length
- };
- }
+ return {
+ rpIdHash: rpIdHash.toString("hex"),
+ flags: {
+ raw: flags,
+ userPresent,
+ userVerified,
+ attestedCredentialDataIncluded,
+ extensionDataIncluded,
+ },
+ counter,
+ totalLength: authenticatorDataBuffer.length,
+ };
+ }
- /**
- * Extract counter from WebAuthn credential response
- * Used during registration to get the initial counter value
- */
- static extractCounterFromCredential(credential: {
- response: {
- authenticatorData: string;
- };
- }): number {
- try {
- // Parse authenticator data
- const authenticatorDataBuffer = Buffer.from(credential.response.authenticatorData, "base64");
+ /**
+ * Extract counter from WebAuthn credential response
+ * Used during registration to get the initial counter value
+ */
+ static extractCounterFromCredential(credential: {
+ response: {
+ authenticatorData: string;
+ };
+ }): number {
+ try {
+ // Parse authenticator data
+ const authenticatorDataBuffer = Buffer.from(credential.response.authenticatorData, "base64");
- // Extract counter from authenticator data (bytes 33-36)
- const counter = authenticatorDataBuffer.readUInt32BE(33);
+ // Extract counter from authenticator data (bytes 33-36)
+ const counter = authenticatorDataBuffer.readUInt32BE(33);
- logger.debug("Counter extracted from credential", { counter });
- return counter;
- } catch (error) {
- logger.error("Failed to extract counter from credential", { error: String(error) });
- return 0; // Fallback to 0 if extraction fails
- }
- }
+ logger.debug("Counter extracted from credential", { counter });
+ return counter;
+ } catch (error) {
+ logger.error("Failed to extract counter from credential", { error: String(error) });
+ return 0; // Fallback to 0 if extraction fails
+ }
+ }
}
diff --git a/src/lib/server/db/base.ts b/src/lib/server/db/base.ts
index 739579f..4e1e551 100644
--- a/src/lib/server/db/base.ts
+++ b/src/lib/server/db/base.ts
@@ -5,7 +5,7 @@ import { customType } from "drizzle-orm/pg-core";
* Used for storing encrypted data, images, and other binary content
*/
export const bytea = customType<{ data: Buffer; driverData: Buffer }>({
- dataType() {
- return "bytea";
- }
+ dataType() {
+ return "bytea";
+ },
});
diff --git a/src/lib/server/db/central-schema.ts b/src/lib/server/db/central-schema.ts
index 8d3b050..8c531c7 100644
--- a/src/lib/server/db/central-schema.ts
+++ b/src/lib/server/db/central-schema.ts
@@ -1,14 +1,14 @@
import type { InferInsertModel, InferSelectModel } from "drizzle-orm";
import {
- pgTable,
- uuid,
- text,
- pgEnum,
- uniqueIndex,
- boolean,
- timestamp,
- integer,
- index
+ pgTable,
+ uuid,
+ text,
+ pgEnum,
+ uniqueIndex,
+ boolean,
+ timestamp,
+ integer,
+ index,
} from "drizzle-orm/pg-core";
import { bytea } from "./base";
@@ -23,10 +23,10 @@ export const configTypeEnum = pgEnum("config_type", ["BOOLEAN", "NUMBER", "STRIN
export const userRoleEnum = pgEnum("user_role", ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"]);
export const tenantSetupState = pgEnum("setup_state", [
- "NEW", // newly created
- "SETTINGS_CREATED", // settings were reviewed
- "AGENTS_SET_UP", // agents set up was triggered or skipped
- "FIRST_CHANNEL_CREATED" // the first channel was set up
+ "NEW", // newly created
+ "SETTINGS_CREATED", // settings were reviewed
+ "AGENTS_SET_UP", // agents set up was triggered or skipped
+ "FIRST_CHANNEL_CREATED", // the first channel was set up
]);
/**
@@ -36,28 +36,28 @@ export const tenantSetupState = pgEnum("setup_state", [
* @table tenant
*/
export const tenant = pgTable(
- "tenant",
- {
- /** Primary key - unique identifier */
- id: uuid("id").primaryKey().defaultRandom(),
- /** Short name used as subdomain (e.g., 'acme' for acme.example.com) */
- shortName: text("short_name").notNull().unique(),
- /** Full organization name displayed to users */
- longName: text("long_name").notNull(),
- /** Optional description of the organization */
- description: text("description"),
- /** Organization logo as binary data (PNG, JPEG, GIF, or WEBP) */
- logo: bytea("logo"),
- /** Database connection string for this tenant's isolated database */
- databaseUrl: text("database_url").notNull(),
- /** STate of tenant setup */
- setupState: tenantSetupState("setup_state").notNull().default("NEW"),
- createdAt: timestamp("created_at").defaultNow(),
- updatedAt: timestamp("updated_at").defaultNow()
- },
- (table) => ({
- tenantDbUrlUnique: uniqueIndex("tenant_database_url_idx").on(table.databaseUrl)
- })
+ "tenant",
+ {
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Short name used as subdomain (e.g., 'acme' for acme.example.com) */
+ shortName: text("short_name").notNull().unique(),
+ /** Full organization name displayed to users */
+ longName: text("long_name").notNull(),
+ /** Optional description of the organization */
+ description: text("description"),
+ /** Organization logo as binary data (PNG, JPEG, GIF, or WEBP) */
+ logo: bytea("logo"),
+ /** Database connection string for this tenant's isolated database */
+ databaseUrl: text("database_url").notNull(),
+ /** STate of tenant setup */
+ setupState: tenantSetupState("setup_state").notNull().default("NEW"),
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+ },
+ (table) => ({
+ tenantDbUrlUnique: uniqueIndex("tenant_database_url_idx").on(table.databaseUrl),
+ }),
);
/**
@@ -66,72 +66,72 @@ export const tenant = pgTable(
* @table tenant_config
*/
export const tenantConfig = pgTable(
- "tenant_config",
- {
- /** Primary key - unique identifier */
- id: uuid("id").primaryKey().defaultRandom(),
- /** Foreign key to tenant */
- tenantId: uuid("tenant_id")
- .notNull()
- .references(() => tenant.id),
- /** Configuration entry name/key */
- name: text("name").notNull(),
- /** Data type of the configuration value */
- type: configTypeEnum("type").notNull(),
- /** Configuration value stored as text (parsed based on type) */
- value: text("value").notNull(),
- createdAt: timestamp("created_at").defaultNow(),
- updatedAt: timestamp("updated_at").defaultNow()
- },
- (table) => ({
- tenantConfigUnique: uniqueIndex("tenant_config_tenant_name_idx").on(table.tenantId, table.name)
- })
+ "tenant_config",
+ {
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Foreign key to tenant */
+ tenantId: uuid("tenant_id")
+ .notNull()
+ .references(() => tenant.id),
+ /** Configuration entry name/key */
+ name: text("name").notNull(),
+ /** Data type of the configuration value */
+ type: configTypeEnum("type").notNull(),
+ /** Configuration value stored as text (parsed based on type) */
+ value: text("value").notNull(),
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+ },
+ (table) => ({
+ tenantConfigUnique: uniqueIndex("tenant_config_tenant_name_idx").on(table.tenantId, table.name),
+ }),
);
export const user = pgTable(
- "user",
- {
- id: uuid("id").primaryKey().defaultRandom(),
- email: text("email").notNull().unique(),
- name: text("name").notNull(),
- role: userRoleEnum("role").notNull().default("GLOBAL_ADMIN"),
- tenantId: uuid("tenant_id").references(() => tenant.id),
- createdAt: timestamp("created_at").defaultNow(),
- updatedAt: timestamp("updated_at").defaultNow(),
- lastLoginAt: timestamp("last_login_at"),
- isActive: boolean("is_active").default(true),
- confirmed: boolean("confirmed").default(false),
- token: text("token"),
- tokenValidUntil: timestamp("token_valid_until"),
- /** Hashed passphrase for password authentication (optional, alternative to WebAuthn) */
- passphraseHash: text("passphrase_hash"),
- /** Recovery passphrase for WebAuthn-only users (stored in plain text, shown only once) */
- recoveryPassphrase: text("recovery_passphrase"),
- /** User's preferred language for emails and interface */
- language: text("language").notNull().default("de")
- },
- (table) => ({
- emailUnique: uniqueIndex("user_email_idx").on(table.email)
- })
+ "user",
+ {
+ id: uuid("id").primaryKey().defaultRandom(),
+ email: text("email").notNull().unique(),
+ name: text("name").notNull(),
+ role: userRoleEnum("role").notNull().default("GLOBAL_ADMIN"),
+ tenantId: uuid("tenant_id").references(() => tenant.id),
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+ lastLoginAt: timestamp("last_login_at"),
+ isActive: boolean("is_active").default(true),
+ confirmed: boolean("confirmed").default(false),
+ token: text("token"),
+ tokenValidUntil: timestamp("token_valid_until"),
+ /** Hashed passphrase for password authentication (optional, alternative to WebAuthn) */
+ passphraseHash: text("passphrase_hash"),
+ /** Recovery passphrase for WebAuthn-only users (stored in plain text, shown only once) */
+ recoveryPassphrase: text("recovery_passphrase"),
+ /** User's preferred language for emails and interface */
+ language: text("language").notNull().default("de"),
+ },
+ (table) => ({
+ emailUnique: uniqueIndex("user_email_idx").on(table.email),
+ }),
);
export const userPasskey = pgTable(
- "user_passkey",
- {
- id: text("id").primaryKey(), // Credential ID as provided by WebAuthn
- userId: uuid("user_id")
- .notNull()
- .references(() => user.id, { onDelete: "cascade" }),
- publicKey: text("public_key").notNull(), // Base64 encoded
- counter: integer("counter").notNull().default(0),
- deviceName: text("device_name"), // "MacBook Pro", "YubiKey 5", etc.
- createdAt: timestamp("created_at").defaultNow(),
- updatedAt: timestamp("updated_at").defaultNow(),
- lastUsedAt: timestamp("last_used_at")
- },
- (table) => ({
- userPasskeyIdx: index("user_passkey_user_idx").on(table.userId)
- })
+ "user_passkey",
+ {
+ id: text("id").primaryKey(), // Credential ID as provided by WebAuthn
+ userId: uuid("user_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+ publicKey: text("public_key").notNull(), // Base64 encoded
+ counter: integer("counter").notNull().default(0),
+ deviceName: text("device_name"), // "MacBook Pro", "YubiKey 5", etc.
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+ lastUsedAt: timestamp("last_used_at"),
+ },
+ (table) => ({
+ userPasskeyIdx: index("user_passkey_user_idx").on(table.userId),
+ }),
);
/**
@@ -141,26 +141,26 @@ export const userPasskey = pgTable(
* @table user_session
*/
export const userSession = pgTable(
- "user_session",
- {
- id: uuid("id").primaryKey().defaultRandom(),
- userId: uuid("user_id")
- .notNull()
- .references(() => user.id, { onDelete: "cascade" }),
- sessionToken: text("session_token").notNull().unique(),
- accessToken: text("access_token").notNull(),
- refreshToken: text("refresh_token").notNull(),
- ipAddress: text("ip_address"),
- userAgent: text("user_agent"),
- createdAt: timestamp("created_at").defaultNow(),
- updatedAt: timestamp("updated_at").defaultNow(),
- expiresAt: timestamp("expires_at").notNull(),
- lastUsedAt: timestamp("last_used_at").defaultNow()
- },
- (table) => ({
- userSessionIdx: index("user_session_user_idx").on(table.userId),
- sessionTokenIdx: uniqueIndex("user_session_token_idx").on(table.sessionToken)
- })
+ "user_session",
+ {
+ id: uuid("id").primaryKey().defaultRandom(),
+ userId: uuid("user_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+ sessionToken: text("session_token").notNull().unique(),
+ accessToken: text("access_token").notNull(),
+ refreshToken: text("refresh_token").notNull(),
+ ipAddress: text("ip_address"),
+ userAgent: text("user_agent"),
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+ expiresAt: timestamp("expires_at").notNull(),
+ lastUsedAt: timestamp("last_used_at").defaultNow(),
+ },
+ (table) => ({
+ userSessionIdx: index("user_session_user_idx").on(table.userId),
+ sessionTokenIdx: uniqueIndex("user_session_token_idx").on(table.sessionToken),
+ }),
);
/**
@@ -170,44 +170,44 @@ export const userSession = pgTable(
* @table user_invite
*/
export const userInvite = pgTable(
- "user_invite",
- {
- /** Primary key - unique identifier */
- id: uuid("id").primaryKey().defaultRandom(),
- /** Secure invite code sent to user (UUID v4) */
- inviteCode: uuid("invite_code").notNull().unique().defaultRandom(),
- /** Email address of invited user */
- email: text("email").notNull(),
- /** Name of invited user */
- name: text("name").notNull(),
- /** Role to assign to user when they register */
- role: userRoleEnum("role").notNull(),
- /** Tenant the user is being invited to */
- tenantId: uuid("tenant_id")
- .notNull()
- .references(() => tenant.id, { onDelete: "cascade" }),
- /** User who sent the invitation */
- invitedBy: uuid("invited_by")
- .notNull()
- .references(() => user.id, { onDelete: "cascade" }),
- /** Language preference for the invitation */
- language: text("language").notNull().default("de"),
- /** Whether the invitation has been used */
- used: boolean("used").notNull().default(false),
- /** When the invitation was used (if applicable) */
- usedAt: timestamp("used_at"),
- /** User ID that was created from this invitation (if used) */
- createdUserId: uuid("created_user_id").references(() => user.id),
- createdAt: timestamp("created_at").defaultNow(),
- updatedAt: timestamp("updated_at").defaultNow(),
- /** Invitation expires after 7 days */
- expiresAt: timestamp("expires_at").notNull()
- },
- (table) => ({
- inviteCodeIdx: uniqueIndex("user_invite_code_idx").on(table.inviteCode),
- inviteEmailIdx: index("user_invite_email_idx").on(table.email),
- inviteTenantIdx: index("user_invite_tenant_idx").on(table.tenantId)
- })
+ "user_invite",
+ {
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Secure invite code sent to user (UUID v4) */
+ inviteCode: uuid("invite_code").notNull().unique().defaultRandom(),
+ /** Email address of invited user */
+ email: text("email").notNull(),
+ /** Name of invited user */
+ name: text("name").notNull(),
+ /** Role to assign to user when they register */
+ role: userRoleEnum("role").notNull(),
+ /** Tenant the user is being invited to */
+ tenantId: uuid("tenant_id")
+ .notNull()
+ .references(() => tenant.id, { onDelete: "cascade" }),
+ /** User who sent the invitation */
+ invitedBy: uuid("invited_by")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+ /** Language preference for the invitation */
+ language: text("language").notNull().default("de"),
+ /** Whether the invitation has been used */
+ used: boolean("used").notNull().default(false),
+ /** When the invitation was used (if applicable) */
+ usedAt: timestamp("used_at"),
+ /** User ID that was created from this invitation (if used) */
+ createdUserId: uuid("created_user_id").references(() => user.id),
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+ /** Invitation expires after 7 days */
+ expiresAt: timestamp("expires_at").notNull(),
+ },
+ (table) => ({
+ inviteCodeIdx: uniqueIndex("user_invite_code_idx").on(table.inviteCode),
+ inviteEmailIdx: index("user_invite_email_idx").on(table.email),
+ inviteTenantIdx: index("user_invite_tenant_idx").on(table.tenantId),
+ }),
);
/**
diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts
index bfc931e..a05d5e1 100644
--- a/src/lib/server/db/index.ts
+++ b/src/lib/server/db/index.ts
@@ -21,39 +21,39 @@ const tenantDbCache = new Map>()
* @returns Promise - The tenant's database connection
*/
export async function getTenantDb(
- tenantId: string
+ tenantId: string,
): Promise> {
- // Check cache first
- if (tenantDbCache.has(tenantId)) {
- return tenantDbCache.get(tenantId)!;
- }
+ // Check cache first
+ if (tenantDbCache.has(tenantId)) {
+ return tenantDbCache.get(tenantId)!;
+ }
- // Get tenant configuration from central database
- const tenant = await centralDb
- .select()
- .from(centralSchema.tenant)
- .where(eq(centralSchema.tenant.id, tenantId))
- .limit(1);
+ // Get tenant configuration from central database
+ const tenant = await centralDb
+ .select()
+ .from(centralSchema.tenant)
+ .where(eq(centralSchema.tenant.id, tenantId))
+ .limit(1);
- if (tenant.length === 0) {
- throw new Error(`Tenant with ID ${tenantId} not found`);
- }
+ if (tenant.length === 0) {
+ throw new Error(`Tenant with ID ${tenantId} not found`);
+ }
- // Create tenant-specific database connection
- const tenantClient = postgres(tenant[0].databaseUrl);
- const tenantDb = drizzle(tenantClient, { schema: tenantSchema });
+ // Create tenant-specific database connection
+ const tenantClient = postgres(tenant[0].databaseUrl);
+ const tenantDb = drizzle(tenantClient, { schema: tenantSchema });
- // Cache the connection
- tenantDbCache.set(tenantId, tenantDb);
+ // Cache the connection
+ tenantDbCache.set(tenantId, tenantDb);
- return tenantDb;
+ return tenantDb;
}
/**
* Clear cached database connections (useful for testing or tenant updates)
*/
export function clearTenantDbCache(): void {
- tenantDbCache.clear();
+ tenantDbCache.clear();
}
// Legacy export for backward compatibility (points to central DB)
diff --git a/src/lib/server/db/tenant-config.ts b/src/lib/server/db/tenant-config.ts
index 5d73b7b..21cde50 100644
--- a/src/lib/server/db/tenant-config.ts
+++ b/src/lib/server/db/tenant-config.ts
@@ -3,73 +3,73 @@ import * as centralSchema from "./central-schema";
import { eq, sql } from "drizzle-orm";
export class TenantConfig {
- private constructor(public readonly tenantId: string) {}
- #configuration: Record = {};
+ private constructor(public readonly tenantId: string) {}
+ #configuration: Record = {};
- public get configuration(): Record {
- return this.#configuration;
- }
+ public get configuration(): Record {
+ return this.#configuration;
+ }
- public static async create(tenantId: string) {
- const tenant = new TenantConfig(tenantId);
- const configEntries = await tenant.#getTenantConfig();
- tenant.#configuration = configEntries.reduce(
- (acc: Record, entry) => {
- switch (entry.type) {
- case "BOOLEAN":
- acc[entry.name] = entry.value == "true";
- break;
- case "NUMBER":
- acc[entry.name] = parseFloat(entry.value);
- break;
- case "STRING":
- acc[entry.name] = entry.value;
- }
- return acc;
- },
- {} as Record
- );
- return tenant;
- }
+ public static async create(tenantId: string) {
+ const tenant = new TenantConfig(tenantId);
+ const configEntries = await tenant.#getTenantConfig();
+ tenant.#configuration = configEntries.reduce(
+ (acc: Record, entry) => {
+ switch (entry.type) {
+ case "BOOLEAN":
+ acc[entry.name] = entry.value == "true";
+ break;
+ case "NUMBER":
+ acc[entry.name] = parseFloat(entry.value);
+ break;
+ case "STRING":
+ acc[entry.name] = entry.value;
+ }
+ return acc;
+ },
+ {} as Record,
+ );
+ return tenant;
+ }
- public async setConfig(key: string, value: boolean | number | string): Promise {
- await centralDb
- .insert(centralSchema.tenantConfig)
- .values({
- tenantId: this.tenantId,
- type: this.#getType(value),
- name: key,
- value: value.toString()
- })
- .onConflictDoUpdate({
- target: [centralSchema.tenantConfig.tenantId, centralSchema.tenantConfig.name],
- set: {
- type: sql`excluded.type`,
- value: sql`excluded.value`
- }
- });
- this.#configuration[key] = value;
- }
+ public async setConfig(key: string, value: boolean | number | string): Promise {
+ await centralDb
+ .insert(centralSchema.tenantConfig)
+ .values({
+ tenantId: this.tenantId,
+ type: this.#getType(value),
+ name: key,
+ value: value.toString(),
+ })
+ .onConflictDoUpdate({
+ target: [centralSchema.tenantConfig.tenantId, centralSchema.tenantConfig.name],
+ set: {
+ type: sql`excluded.type`,
+ value: sql`excluded.value`,
+ },
+ });
+ this.#configuration[key] = value;
+ }
- #getType(value: boolean | number | string): "BOOLEAN" | "NUMBER" | "STRING" {
- if (typeof value == "boolean") {
- return "BOOLEAN";
- }
- if (typeof value == "number") {
- return "NUMBER";
- }
- return "STRING";
- }
+ #getType(value: boolean | number | string): "BOOLEAN" | "NUMBER" | "STRING" {
+ if (typeof value == "boolean") {
+ return "BOOLEAN";
+ }
+ if (typeof value == "number") {
+ return "NUMBER";
+ }
+ return "STRING";
+ }
- /**
- * Get tenant configuration entries by tenant ID
- * @param tenantId - The tenant's UUID
- * @returns Promise - Array of config entries
- */
- async #getTenantConfig(): Promise {
- return await centralDb
- .select()
- .from(centralSchema.tenantConfig)
- .where(eq(centralSchema.tenantConfig.tenantId, this.tenantId));
- }
+ /**
+ * Get tenant configuration entries by tenant ID
+ * @param tenantId - The tenant's UUID
+ * @returns Promise - Array of config entries
+ */
+ async #getTenantConfig(): Promise {
+ return await centralDb
+ .select()
+ .from(centralSchema.tenantConfig)
+ .where(eq(centralSchema.tenantConfig.tenantId, this.tenantId));
+ }
}
diff --git a/src/lib/server/db/tenant-schema.ts b/src/lib/server/db/tenant-schema.ts
index a45ae21..748b751 100644
--- a/src/lib/server/db/tenant-schema.ts
+++ b/src/lib/server/db/tenant-schema.ts
@@ -1,14 +1,14 @@
import type { InferSelectModel } from "drizzle-orm";
import {
- pgTable,
- boolean,
- uuid,
- text,
- date,
- pgEnum,
- time,
- integer,
- json
+ pgTable,
+ boolean,
+ uuid,
+ text,
+ date,
+ pgEnum,
+ time,
+ integer,
+ json,
} from "drizzle-orm/pg-core";
import { bytea } from "./base";
@@ -21,11 +21,11 @@ export const channelTypeEnum = pgEnum("channel_type", ["ROOM", "MACHINE", "PERSO
/** Appointment status enumeration - tracks the lifecycle of appointments */
export const appointmentStatusEnum = pgEnum("appointment_status", [
- "NEW",
- "CONFIRMED",
- "HELD",
- "REJECTED",
- "NO_SHOW"
+ "NEW",
+ "CONFIRMED",
+ "HELD",
+ "REJECTED",
+ "NO_SHOW",
]);
/**
@@ -35,14 +35,14 @@ export const appointmentStatusEnum = pgEnum("appointment_status", [
* @table agent
*/
export const agent = pgTable("agent", {
- /** Primary key - unique identifier */
- id: uuid("id").primaryKey().defaultRandom(),
- /** Agent's display name */
- name: text("name").notNull(),
- /** Optional description of the agent's role or specialties */
- description: text("description"),
- /** Optional logo/profile image for the agent (PNG, JPEG, GIF, or WEBP) */
- logo: bytea("logo")
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Agent's display name */
+ name: text("name").notNull(),
+ /** Optional description of the agent's role or specialties */
+ description: text("description"),
+ /** Optional logo/profile image for the agent (PNG, JPEG, GIF, or WEBP) */
+ logo: bytea("logo"),
});
/**
@@ -53,22 +53,22 @@ export const agent = pgTable("agent", {
* @table channel
*/
export const channel = pgTable("channel", {
- /** Primary key - unique identifier */
- id: uuid("id").primaryKey().defaultRandom(),
- /** Channel display names in multiple languages (array of strings in same order as languages) */
- names: json("names").$type().notNull(),
- /** Optional color for UI display (hex code) */
- color: text("color"),
- /** Whether the channel is paused and does not offer nor accept new appointments */
- pause: boolean("paused").notNull().default(false),
- /** Optional descriptions in multiple languages (array of strings in same order as languages) */
- descriptions: json("descriptions").$type(),
- /** Active languages for this channel (array of language codes) */
- languages: json("languages").$type().notNull(),
- /** Whether channel is publicly bookable or requires internal access */
- isPublic: boolean("is_public"),
- /** Whether appointments must be explicitly confirmed by staff */
- requiresConfirmation: boolean("requires_confirmation")
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Channel display names in multiple languages (array of strings in same order as languages) */
+ names: json("names").$type().notNull(),
+ /** Optional color for UI display (hex code) */
+ color: text("color"),
+ /** Whether the channel is paused and does not offer nor accept new appointments */
+ pause: boolean("paused").notNull().default(false),
+ /** Optional descriptions in multiple languages (array of strings in same order as languages) */
+ descriptions: json("descriptions").$type(),
+ /** Active languages for this channel (array of language codes) */
+ languages: json("languages").$type().notNull(),
+ /** Whether channel is publicly bookable or requires internal access */
+ isPublic: boolean("is_public"),
+ /** Whether appointments must be explicitly confirmed by staff */
+ requiresConfirmation: boolean("requires_confirmation"),
});
/**
@@ -79,16 +79,16 @@ export const channel = pgTable("channel", {
* @table slotTemplate
*/
export const slotTemplate = pgTable("slotTemplate", {
- /** Primary key - unique identifier */
- id: uuid("id").primaryKey().defaultRandom(),
- /** Bitmask for weekdays (1=Monday, 2=Tuesday, 4=Wednesday, etc.) */
- weekdays: integer("weekdays"),
- /** Start time for the slot template */
- from: time("from").notNull(),
- /** End time for the slot template */
- to: time("to").notNull(),
- /** Duration of individual appointment slots in minutes */
- duration: integer("duration").notNull()
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Bitmask for weekdays (1=Monday, 2=Tuesday, 4=Wednesday, etc.) */
+ weekdays: integer("weekdays"),
+ /** Start time for the slot template */
+ from: time("from").notNull(),
+ /** End time for the slot template */
+ to: time("to").notNull(),
+ /** Duration of individual appointment slots in minutes */
+ duration: integer("duration").notNull(),
});
/**
@@ -98,14 +98,14 @@ export const slotTemplate = pgTable("slotTemplate", {
* @table channelAgent
*/
export const channelAgent = pgTable("channel_agent", {
- /** Foreign key to channel */
- channelId: uuid("channel_id")
- .notNull()
- .references(() => channel.id),
- /** Foreign key to agent */
- agentId: uuid("agent_id")
- .notNull()
- .references(() => agent.id)
+ /** Foreign key to channel */
+ channelId: uuid("channel_id")
+ .notNull()
+ .references(() => channel.id),
+ /** Foreign key to agent */
+ agentId: uuid("agent_id")
+ .notNull()
+ .references(() => agent.id),
});
/**
@@ -115,14 +115,14 @@ export const channelAgent = pgTable("channel_agent", {
* @table channelSlotTemplate
*/
export const channelSlotTemplate = pgTable("channel_slot_template", {
- /** Foreign key to channel */
- channelId: uuid("channel_id")
- .notNull()
- .references(() => channel.id),
- /** Foreign key to slot template */
- slotTemplateId: uuid("slot_template_id")
- .notNull()
- .references(() => slotTemplate.id)
+ /** Foreign key to channel */
+ channelId: uuid("channel_id")
+ .notNull()
+ .references(() => channel.id),
+ /** Foreign key to slot template */
+ slotTemplateId: uuid("slot_template_id")
+ .notNull()
+ .references(() => slotTemplate.id),
});
/**
@@ -132,18 +132,18 @@ export const channelSlotTemplate = pgTable("channel_slot_template", {
* @table client
*/
export const client = pgTable("client", {
- /** Primary key - unique identifier */
- id: uuid("id").primaryKey().defaultRandom(),
- /** Hash of client email for identification without storing plaintext */
- hashKey: text("hash_key").notNull().unique(),
- /** Client's public key for end-to-end encryption */
- publicKey: text("public_key").notNull(),
- /** Server-side share of client's private key for recovery */
- privateKeyShare: text("private_key_share").notNull(),
- /** Client email address (optional for privacy) */
- email: text("email"),
- /** Preferred language for communications (de/en) */
- language: text("language")
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Hash of client email for identification without storing plaintext */
+ hashKey: text("hash_key").notNull().unique(),
+ /** Client's public key for end-to-end encryption */
+ publicKey: text("public_key").notNull(),
+ /** Server-side share of client's private key for recovery */
+ privateKeyShare: text("private_key_share").notNull(),
+ /** Client email address (optional for privacy) */
+ email: text("email"),
+ /** Preferred language for communications (de/en) */
+ language: text("language"),
});
/**
@@ -153,20 +153,20 @@ export const client = pgTable("client", {
* @table staff
*/
export const staff = pgTable("staff", {
- /** Primary key - unique identifier */
- id: uuid("id").primaryKey().defaultRandom(),
- /** Hash of staff login name for identification */
- hashKey: text("hash_key").notNull().unique(),
- /** Staff member's public key for end-to-end encryption */
- publicKey: text("public_key").notNull(),
- /** Staff member's display name */
- name: text("name"),
- /** Job title or position within the organization */
- position: text("position"),
- /** Staff email address (required for notifications) */
- email: text("email").notNull(),
- /** Preferred language for communications (de/en) */
- language: text("language")
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Hash of staff login name for identification */
+ hashKey: text("hash_key").notNull().unique(),
+ /** Staff member's public key for end-to-end encryption */
+ publicKey: text("public_key").notNull(),
+ /** Staff member's display name */
+ name: text("name"),
+ /** Job title or position within the organization */
+ position: text("position"),
+ /** Staff email address (required for notifications) */
+ email: text("email").notNull(),
+ /** Preferred language for communications (de/en) */
+ language: text("language"),
});
/**
@@ -176,26 +176,26 @@ export const staff = pgTable("staff", {
* @table appointment
*/
export const appointment = pgTable("appointment", {
- /** Primary key - unique identifier */
- id: uuid("id").primaryKey().defaultRandom(),
- /** Foreign key to client who booked the appointment */
- clientId: uuid("client_id")
- .notNull()
- .references(() => client.id),
- /** Foreign key to channel/resource being booked */
- channelId: uuid("channel_id")
- .notNull()
- .references(() => channel.id),
- /** Date and time of the appointment */
- appointmentDate: date("appointment_date").notNull(),
- /** When appointment data expires and can be auto-deleted */
- expiryDate: date("expiry_date").notNull(),
- /** Appointment title/subject */
- title: text("title").notNull(),
- /** Optional detailed description of the appointment */
- description: text("description"),
- /** Current status of the appointment */
- status: appointmentStatusEnum("status").notNull().default("NEW")
+ /** Primary key - unique identifier */
+ id: uuid("id").primaryKey().defaultRandom(),
+ /** Foreign key to client who booked the appointment */
+ clientId: uuid("client_id")
+ .notNull()
+ .references(() => client.id),
+ /** Foreign key to channel/resource being booked */
+ channelId: uuid("channel_id")
+ .notNull()
+ .references(() => channel.id),
+ /** Date and time of the appointment */
+ appointmentDate: date("appointment_date").notNull(),
+ /** When appointment data expires and can be auto-deleted */
+ expiryDate: date("expiry_date").notNull(),
+ /** Appointment title/subject */
+ title: text("title").notNull(),
+ /** Optional detailed description of the appointment */
+ description: text("description"),
+ /** Current status of the appointment */
+ status: appointmentStatusEnum("status").notNull().default("NEW"),
});
/**
diff --git a/src/lib/server/db/tenant-service.ts b/src/lib/server/db/tenant-service.ts
index c67a534..df7ad20 100644
--- a/src/lib/server/db/tenant-service.ts
+++ b/src/lib/server/db/tenant-service.ts
@@ -7,62 +7,62 @@ import * as tenantSchema from "./tenant-schema";
* Provides a clean interface for working with tenant data
*/
export class TenantService {
- #db: Awaited> | null = null;
- #config?: TenantConfig;
+ #db: Awaited> | null = null;
+ #config?: TenantConfig;
- constructor(public readonly tenantId: string) {
- this.tenantId = tenantId;
- }
+ constructor(public readonly tenantId: string) {
+ this.tenantId = tenantId;
+ }
- /**
- * Get the tenant's database connection (cached)
- */
- async getDb() {
- if (!this.#db) {
- this.#db = await getTenantDb(this.tenantId);
- }
- return this.#db;
- }
+ /**
+ * Get the tenant's database connection (cached)
+ */
+ async getDb() {
+ if (!this.#db) {
+ this.#db = await getTenantDb(this.tenantId);
+ }
+ return this.#db;
+ }
- /**
- * Get all clients for this tenant
- */
- async getClients() {
- const db = await this.getDb();
- return await db.select().from(tenantSchema.client);
- }
+ /**
+ * Get all clients for this tenant
+ */
+ async getClients() {
+ const db = await this.getDb();
+ return await db.select().from(tenantSchema.client);
+ }
- /**
- * Get all staff for this tenant
- */
- async getStaff() {
- const db = await this.getDb();
- return await db.select().from(tenantSchema.staff);
- }
+ /**
+ * Get all staff for this tenant
+ */
+ async getStaff() {
+ const db = await this.getDb();
+ return await db.select().from(tenantSchema.staff);
+ }
- /**
- * Get all channels for this tenant
- */
- async getChannels() {
- const db = await this.getDb();
- return await db.select().from(tenantSchema.channel);
- }
+ /**
+ * Get all channels for this tenant
+ */
+ async getChannels() {
+ const db = await this.getDb();
+ return await db.select().from(tenantSchema.channel);
+ }
- /**
- * Get all appointments for this tenant
- */
- async getAppointments() {
- const db = await this.getDb();
- return await db.select().from(tenantSchema.appointment);
- }
+ /**
+ * Get all appointments for this tenant
+ */
+ async getAppointments() {
+ const db = await this.getDb();
+ return await db.select().from(tenantSchema.appointment);
+ }
- /**
- * Get tenant configuration
- */
- async getConfig() {
- if (!this.#config) {
- this.#config = await TenantConfig.create(this.tenantId);
- }
- return this.#config.configuration;
- }
+ /**
+ * Get tenant configuration
+ */
+ async getConfig() {
+ if (!this.#config) {
+ this.#config = await TenantConfig.create(this.tenantId);
+ }
+ return this.#config.configuration;
+ }
}
diff --git a/src/lib/server/email/__tests__/email-system.test.ts b/src/lib/server/email/__tests__/email-system.test.ts
index a54b4d3..e9f1a14 100644
--- a/src/lib/server/email/__tests__/email-system.test.ts
+++ b/src/lib/server/email/__tests__/email-system.test.ts
@@ -2,34 +2,34 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
// Mock environment variables
vi.mock("$env/dynamic/private", () => ({
- env: {
- SMTP_HOST: "smtp.test.com",
- SMTP_PORT: "587",
- SMTP_SECURE: "false",
- SMTP_USER: "test@example.com",
- SMTP_PASS: "test-password",
- SMTP_FROM_NAME: "Test App",
- SMTP_FROM_EMAIL: "noreply@test.com"
- }
+ env: {
+ SMTP_HOST: "smtp.test.com",
+ SMTP_PORT: "587",
+ SMTP_SECURE: "false",
+ SMTP_USER: "test@example.com",
+ SMTP_PASS: "test-password",
+ SMTP_FROM_NAME: "Test App",
+ SMTP_FROM_EMAIL: "noreply@test.com",
+ },
}));
// Mock nodemailer
const mockSendMail = vi.fn();
const mockVerify = vi.fn();
const mockTransporter = {
- sendMail: mockSendMail,
- verify: mockVerify
+ sendMail: mockSendMail,
+ verify: mockVerify,
};
vi.mock("nodemailer", () => ({
- default: {
- createTransport: vi.fn(() => mockTransporter)
- }
+ default: {
+ createTransport: vi.fn(() => mockTransporter),
+ },
}));
// Mock fs/promises
vi.mock("fs/promises", () => ({
- readFile: vi.fn()
+ readFile: vi.fn(),
}));
// Import modules after mocking
@@ -43,476 +43,476 @@ const mockReadFile = vi.mocked(readFile);
const mockCreateTransporter = vi.mocked(nodemailer.createTransport);
describe("Email System", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- mockSendMail.mockResolvedValue({ messageId: "test-message-id" });
- mockVerify.mockResolvedValue(true);
- });
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockSendMail.mockResolvedValue({ messageId: "test-message-id" });
+ mockVerify.mockResolvedValue(true);
+ });
- describe("Mailer", () => {
- it("should send email successfully", async () => {
- const recipient = { email: "test@example.com", name: "Test User" };
- const subject = "Test Subject";
- const htmlContent = "Test HTML ";
- const textContent = "Test Text";
+ describe("Mailer", () => {
+ it("should send email successfully", async () => {
+ const recipient = { email: "test@example.com", name: "Test User" };
+ const subject = "Test Subject";
+ const htmlContent = "Test HTML ";
+ const textContent = "Test Text";
- await sendEmail(recipient, subject, htmlContent, textContent);
+ await sendEmail(recipient, subject, htmlContent, textContent);
- expect(mockCreateTransporter).toHaveBeenCalledWith({
- host: "smtp.test.com",
- port: 587,
- secure: false,
- auth: {
- user: "test@example.com",
- pass: "test-password"
- }
- });
+ expect(mockCreateTransporter).toHaveBeenCalledWith({
+ host: "smtp.test.com",
+ port: 587,
+ secure: false,
+ auth: {
+ user: "test@example.com",
+ pass: "test-password",
+ },
+ });
- expect(mockSendMail).toHaveBeenCalledWith({
- from: {
- name: "Test App",
- address: "noreply@test.com"
- },
- to: {
- name: "Test User",
- address: "test@example.com"
- },
- subject: "Test Subject",
- html: "Test HTML ",
- text: "Test Text"
- });
- });
+ expect(mockSendMail).toHaveBeenCalledWith({
+ from: {
+ name: "Test App",
+ address: "noreply@test.com",
+ },
+ to: {
+ name: "Test User",
+ address: "test@example.com",
+ },
+ subject: "Test Subject",
+ html: "Test HTML ",
+ text: "Test Text",
+ });
+ });
- it("should test email connection", async () => {
- const result = await testEmailConnection();
- expect(result).toBe(true);
- expect(mockVerify).toHaveBeenCalled();
- });
- });
+ it("should test email connection", async () => {
+ const result = await testEmailConnection();
+ expect(result).toBe(true);
+ expect(mockVerify).toHaveBeenCalled();
+ });
+ });
- describe("Template Engine", () => {
- let templateEngine: EmailTemplateEngine;
+ describe("Template Engine", () => {
+ let templateEngine: EmailTemplateEngine;
- beforeEach(() => {
- templateEngine = new EmailTemplateEngine("test/templates");
- });
+ beforeEach(() => {
+ templateEngine = new EmailTemplateEngine("test/templates");
+ });
- it("should render template with variables", async () => {
- const mockHtml = "Hello {{recipient.name}} ";
- const mockText = "Hello {{recipient.name}}";
+ it("should render template with variables", async () => {
+ const mockHtml = "Hello {{recipient.name}} ";
+ const mockText = "Hello {{recipient.name}}";
- mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
+ mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
- const mockTenant = {
- id: "tenant-1",
- shortName: "test",
- longName: "Test Organization",
- description: null,
- logo: null,
- databaseUrl: "postgresql://test",
- setupState: "FIRST_CHANNEL_CREATED" as const,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ const mockTenant = {
+ id: "tenant-1",
+ shortName: "test",
+ longName: "Test Organization",
+ description: null,
+ logo: null,
+ databaseUrl: "postgresql://test",
+ setupState: "FIRST_CHANNEL_CREATED" as const,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = await templateEngine.renderTemplate("user-created", {
- recipient: { email: "test@example.com", name: "Test User" },
- subject: "Test Subject",
- language: "de",
- tenant: mockTenant
- });
+ const result = await templateEngine.renderTemplate("user-created", {
+ recipient: { email: "test@example.com", name: "Test User" },
+ subject: "Test Subject",
+ language: "de",
+ tenant: mockTenant,
+ });
- expect(result.html).toBe("Hello Test User ");
- expect(result.text).toBe("Hello Test User");
- expect(result.subject).toBe("Test Subject");
- });
+ expect(result.html).toBe("Hello Test User ");
+ expect(result.text).toBe("Hello Test User");
+ expect(result.subject).toBe("Test Subject");
+ });
- it("should handle conditional blocks", async () => {
- const mockHtml = "{{#if showButton}}Click {{/if}}";
- const mockText = "{{#if showButton}}Button available{{/if}}";
+ it("should handle conditional blocks", async () => {
+ const mockHtml = "{{#if showButton}}Click {{/if}}";
+ const mockText = "{{#if showButton}}Button available{{/if}}";
- mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
+ mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
- const mockTenant = {
- id: "tenant-1",
- shortName: "test",
- longName: "Test Organization",
- description: null,
- logo: null,
- databaseUrl: "postgresql://test",
- setupState: "FIRST_CHANNEL_CREATED" as const,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ const mockTenant = {
+ id: "tenant-1",
+ shortName: "test",
+ longName: "Test Organization",
+ description: null,
+ logo: null,
+ databaseUrl: "postgresql://test",
+ setupState: "FIRST_CHANNEL_CREATED" as const,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = await templateEngine.renderTemplate("user-created", {
- recipient: { email: "test@example.com" },
- subject: "Test",
- language: "de",
- tenant: mockTenant,
- showButton: true
- });
+ const result = await templateEngine.renderTemplate("user-created", {
+ recipient: { email: "test@example.com" },
+ subject: "Test",
+ language: "de",
+ tenant: mockTenant,
+ showButton: true,
+ });
- expect(result.html).toBe("Click ");
- expect(result.text).toBe("Button available");
- });
+ expect(result.html).toBe("Click ");
+ expect(result.text).toBe("Button available");
+ });
- it("should handle missing conditionals", async () => {
- const mockHtml = "{{#if showButton}}Click {{/if}}";
- const mockText = "{{#if showButton}}Button available{{/if}}";
+ it("should handle missing conditionals", async () => {
+ const mockHtml = "{{#if showButton}}Click {{/if}}";
+ const mockText = "{{#if showButton}}Button available{{/if}}";
- mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
+ mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
- const mockTenant = {
- id: "tenant-1",
- shortName: "test",
- longName: "Test Organization",
- description: null,
- logo: null,
- databaseUrl: "postgresql://test",
- setupState: "FIRST_CHANNEL_CREATED" as const,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ const mockTenant = {
+ id: "tenant-1",
+ shortName: "test",
+ longName: "Test Organization",
+ description: null,
+ logo: null,
+ databaseUrl: "postgresql://test",
+ setupState: "FIRST_CHANNEL_CREATED" as const,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = await templateEngine.renderTemplate("user-created", {
- recipient: { email: "test@example.com" },
- subject: "Test",
- language: "de",
- tenant: mockTenant,
- showButton: false
- });
+ const result = await templateEngine.renderTemplate("user-created", {
+ recipient: { email: "test@example.com" },
+ subject: "Test",
+ language: "de",
+ tenant: mockTenant,
+ showButton: false,
+ });
- expect(result.html).toBe("");
- expect(result.text).toBe("");
- });
+ expect(result.html).toBe("");
+ expect(result.text).toBe("");
+ });
- it("should handle English language", async () => {
- const mockHtml = "Welcome {{recipient.name}} ";
- const mockText = "Welcome {{recipient.name}}";
+ it("should handle English language", async () => {
+ const mockHtml = "Welcome {{recipient.name}} ";
+ const mockText = "Welcome {{recipient.name}}";
- mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
+ mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
- const mockTenant = {
- id: "tenant-1",
- shortName: "test",
- longName: "Test Organization",
- description: null,
- logo: null,
- databaseUrl: "postgresql://test",
- setupState: "FIRST_CHANNEL_CREATED" as const,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ const mockTenant = {
+ id: "tenant-1",
+ shortName: "test",
+ longName: "Test Organization",
+ description: null,
+ logo: null,
+ databaseUrl: "postgresql://test",
+ setupState: "FIRST_CHANNEL_CREATED" as const,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = await templateEngine.renderTemplate("user-created", {
- recipient: { email: "test@example.com", name: "John Doe" },
- subject: "Welcome",
- language: "en",
- tenant: mockTenant
- });
+ const result = await templateEngine.renderTemplate("user-created", {
+ recipient: { email: "test@example.com", name: "John Doe" },
+ subject: "Welcome",
+ language: "en",
+ tenant: mockTenant,
+ });
- expect(mockReadFile).toHaveBeenCalledWith(
- expect.stringContaining("user-created.en.html"),
- "utf-8"
- );
- expect(result.html).toBe("Welcome John Doe ");
- });
- });
+ expect(mockReadFile).toHaveBeenCalledWith(
+ expect.stringContaining("user-created.en.html"),
+ "utf-8",
+ );
+ expect(result.html).toBe("Welcome John Doe ");
+ });
+ });
- describe("Email Service", () => {
- it("should send user created email in German", async () => {
- const mockHtml = "Willkommen {{recipient.name}} ";
- const mockText = "Willkommen {{recipient.name}}";
+ describe("Email Service", () => {
+ it("should send user created email in German", async () => {
+ const mockHtml = "Willkommen {{recipient.name}} ";
+ const mockText = "Willkommen {{recipient.name}}";
- mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
+ mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
- // Create a mock staff user with German language
- const staffUser = {
- id: "test-id",
- hashKey: "test-hash",
- publicKey: "test-key",
- name: "Max Mustermann",
- position: "Arzt",
- email: "test@example.com",
- language: "de"
- };
- const mockTenant = {
- id: "tenant-1",
- shortName: "test",
- longName: "Test Organization",
- description: null,
- logo: null,
- databaseUrl: "postgresql://test",
- setupState: "FIRST_CHANNEL_CREATED" as const,
- createdAt: new Date(),
- updatedAt: new Date()
- };
- const loginUrl = "https://app.example.com/login";
+ // Create a mock staff user with German language
+ const staffUser = {
+ id: "test-id",
+ hashKey: "test-hash",
+ publicKey: "test-key",
+ name: "Max Mustermann",
+ position: "Arzt",
+ email: "test@example.com",
+ language: "de",
+ };
+ const mockTenant = {
+ id: "tenant-1",
+ shortName: "test",
+ longName: "Test Organization",
+ description: null,
+ logo: null,
+ databaseUrl: "postgresql://test",
+ setupState: "FIRST_CHANNEL_CREATED" as const,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+ const loginUrl = "https://app.example.com/login";
- await sendUserCreatedEmail(staffUser, mockTenant, loginUrl);
+ await sendUserCreatedEmail(staffUser, mockTenant, loginUrl);
- expect(mockReadFile).toHaveBeenCalledWith(
- expect.stringContaining("user-created.de.html"),
- "utf-8"
- );
+ expect(mockReadFile).toHaveBeenCalledWith(
+ expect.stringContaining("user-created.de.html"),
+ "utf-8",
+ );
- expect(mockSendMail).toHaveBeenCalledWith({
- from: {
- name: "Test App",
- address: "noreply@test.com"
- },
- to: {
- name: "Max Mustermann",
- address: "test@example.com"
- },
- subject: "Willkommen bei Open Reception",
- html: "Willkommen Max Mustermann ",
- text: "Willkommen Max Mustermann"
- });
- });
+ expect(mockSendMail).toHaveBeenCalledWith({
+ from: {
+ name: "Test App",
+ address: "noreply@test.com",
+ },
+ to: {
+ name: "Max Mustermann",
+ address: "test@example.com",
+ },
+ subject: "Willkommen bei Open Reception",
+ html: "Willkommen Max Mustermann ",
+ text: "Willkommen Max Mustermann",
+ });
+ });
- it("should send user created email in English", async () => {
- const mockHtml = "Welcome {{recipient.name}} ";
- const mockText = "Welcome {{recipient.name}}";
+ it("should send user created email in English", async () => {
+ const mockHtml = "Welcome {{recipient.name}} ";
+ const mockText = "Welcome {{recipient.name}}";
- mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
+ mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
- // Create a mock staff user with English language
- const staffUser = {
- id: "test-id",
- hashKey: "test-hash",
- publicKey: "test-key",
- name: "John Doe",
- position: "Doctor",
- email: "test@example.com",
- language: "en"
- };
- const mockTenant = {
- id: "tenant-1",
- shortName: "test",
- longName: "Test Organization",
- description: null,
- logo: null,
- databaseUrl: "postgresql://test",
- setupState: "FIRST_CHANNEL_CREATED" as const,
- createdAt: new Date(),
- updatedAt: new Date()
- };
- const loginUrl = "https://app.example.com/login";
+ // Create a mock staff user with English language
+ const staffUser = {
+ id: "test-id",
+ hashKey: "test-hash",
+ publicKey: "test-key",
+ name: "John Doe",
+ position: "Doctor",
+ email: "test@example.com",
+ language: "en",
+ };
+ const mockTenant = {
+ id: "tenant-1",
+ shortName: "test",
+ longName: "Test Organization",
+ description: null,
+ logo: null,
+ databaseUrl: "postgresql://test",
+ setupState: "FIRST_CHANNEL_CREATED" as const,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+ const loginUrl = "https://app.example.com/login";
- await sendUserCreatedEmail(staffUser, mockTenant, loginUrl);
+ await sendUserCreatedEmail(staffUser, mockTenant, loginUrl);
- expect(mockReadFile).toHaveBeenCalledWith(
- expect.stringContaining("user-created.en.html"),
- "utf-8"
- );
+ expect(mockReadFile).toHaveBeenCalledWith(
+ expect.stringContaining("user-created.en.html"),
+ "utf-8",
+ );
- expect(mockSendMail).toHaveBeenCalledWith({
- from: {
- name: "Test App",
- address: "noreply@test.com"
- },
- to: {
- name: "John Doe",
- address: "test@example.com"
- },
- subject: "Welcome to Open Reception",
- html: "Welcome John Doe ",
- text: "Welcome John Doe"
- });
- });
+ expect(mockSendMail).toHaveBeenCalledWith({
+ from: {
+ name: "Test App",
+ address: "noreply@test.com",
+ },
+ to: {
+ name: "John Doe",
+ address: "test@example.com",
+ },
+ subject: "Welcome to Open Reception",
+ html: "Welcome John Doe ",
+ text: "Welcome John Doe",
+ });
+ });
- it("should handle template rendering errors", async () => {
- mockReadFile.mockRejectedValue(new Error("Template not found"));
+ it("should handle template rendering errors", async () => {
+ mockReadFile.mockRejectedValue(new Error("Template not found"));
- const recipient = { email: "test@example.com" };
- const mockTenant = {
- id: "tenant-1",
- shortName: "test",
- longName: "Test Organization",
- description: null,
- logo: null,
- databaseUrl: "postgresql://test",
- setupState: "FIRST_CHANNEL_CREATED" as const,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ const recipient = { email: "test@example.com" };
+ const mockTenant = {
+ id: "tenant-1",
+ shortName: "test",
+ longName: "Test Organization",
+ description: null,
+ logo: null,
+ databaseUrl: "postgresql://test",
+ setupState: "FIRST_CHANNEL_CREATED" as const,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- await expect(
- sendTemplatedEmail("user-created", recipient, "Test", "de", mockTenant, {})
- ).rejects.toThrow("Template not found");
- });
+ await expect(
+ sendTemplatedEmail("user-created", recipient, "Test", "de", mockTenant, {}),
+ ).rejects.toThrow("Template not found");
+ });
- it("should handle SMTP errors", async () => {
- const mockHtml = "Test ";
- const mockText = "Test";
+ it("should handle SMTP errors", async () => {
+ const mockHtml = "Test ";
+ const mockText = "Test";
- mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
+ mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
- mockSendMail.mockRejectedValueOnce(new Error("SMTP Error"));
+ mockSendMail.mockRejectedValueOnce(new Error("SMTP Error"));
- const clientUser = {
- id: "test-id",
- hashKey: "test-hash",
- publicKey: "test-key",
- privateKeyShare: "test-share",
- email: "test@example.com",
- language: "de"
- };
- const mockTenant = {
- id: "tenant-1",
- shortName: "test",
- longName: "Test Organization",
- description: null,
- logo: null,
- databaseUrl: "postgresql://test",
- setupState: "FIRST_CHANNEL_CREATED" as const,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ const clientUser = {
+ id: "test-id",
+ hashKey: "test-hash",
+ publicKey: "test-key",
+ privateKeyShare: "test-share",
+ email: "test@example.com",
+ language: "de",
+ };
+ const mockTenant = {
+ id: "tenant-1",
+ shortName: "test",
+ longName: "Test Organization",
+ description: null,
+ logo: null,
+ databaseUrl: "postgresql://test",
+ setupState: "FIRST_CHANNEL_CREATED" as const,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- await expect(
- sendUserCreatedEmail(clientUser, mockTenant, "https://example.com/login")
- ).rejects.toThrow("Failed to send email to test@example.com");
- });
+ await expect(
+ sendUserCreatedEmail(clientUser, mockTenant, "https://example.com/login"),
+ ).rejects.toThrow("Failed to send email to test@example.com");
+ });
- it("should send confirmation email in German", async () => {
- const mockHtml =
- "Bestätigungscode: {{confirmationCode}} Gültig für {{expirationMinutes}} Minuten
";
- const mockText =
- "Bestätigungscode: {{confirmationCode}} - Gültig für {{expirationMinutes}} Minuten";
+ it("should send confirmation email in German", async () => {
+ const mockHtml =
+ "Bestätigungscode: {{confirmationCode}} Gültig für {{expirationMinutes}} Minuten
";
+ const mockText =
+ "Bestätigungscode: {{confirmationCode}} - Gültig für {{expirationMinutes}} Minuten";
- mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
+ mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
- const staffUser = {
- id: "test-id",
- hashKey: "test-hash",
- publicKey: "test-key",
- name: "Max Mustermann",
- position: "Arzt",
- email: "test@example.com",
- language: "de"
- };
- const mockTenant = {
- id: "tenant-1",
- shortName: "test",
- longName: "Test Organization",
- description: null,
- logo: null,
- databaseUrl: "postgresql://test",
- setupState: "FIRST_CHANNEL_CREATED" as const,
- createdAt: new Date(),
- updatedAt: new Date()
- };
- const confirmationCode = "ABC123";
- const expirationMinutes = 15;
+ const staffUser = {
+ id: "test-id",
+ hashKey: "test-hash",
+ publicKey: "test-key",
+ name: "Max Mustermann",
+ position: "Arzt",
+ email: "test@example.com",
+ language: "de",
+ };
+ const mockTenant = {
+ id: "tenant-1",
+ shortName: "test",
+ longName: "Test Organization",
+ description: null,
+ logo: null,
+ databaseUrl: "postgresql://test",
+ setupState: "FIRST_CHANNEL_CREATED" as const,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+ const confirmationCode = "ABC123";
+ const expirationMinutes = 15;
- await sendConfirmationEmail(staffUser, mockTenant, confirmationCode, expirationMinutes);
+ await sendConfirmationEmail(staffUser, mockTenant, confirmationCode, expirationMinutes);
- expect(mockReadFile).toHaveBeenCalledWith(
- expect.stringContaining("confirmation.de.html"),
- "utf-8"
- );
+ expect(mockReadFile).toHaveBeenCalledWith(
+ expect.stringContaining("confirmation.de.html"),
+ "utf-8",
+ );
- expect(mockSendMail).toHaveBeenCalledWith({
- from: {
- name: "Test App",
- address: "noreply@test.com"
- },
- to: {
- name: "Max Mustermann",
- address: "test@example.com"
- },
- subject: "Registrierung bestätigen",
- html: "Bestätigungscode: ABC123 Gültig für 15 Minuten
",
- text: "Bestätigungscode: ABC123 - Gültig für 15 Minuten"
- });
- });
+ expect(mockSendMail).toHaveBeenCalledWith({
+ from: {
+ name: "Test App",
+ address: "noreply@test.com",
+ },
+ to: {
+ name: "Max Mustermann",
+ address: "test@example.com",
+ },
+ subject: "Registrierung bestätigen",
+ html: "Bestätigungscode: ABC123 Gültig für 15 Minuten
",
+ text: "Bestätigungscode: ABC123 - Gültig für 15 Minuten",
+ });
+ });
- it("should send confirmation email in English", async () => {
- const mockHtml =
- "Confirmation code: {{confirmationCode}} Valid for {{expirationMinutes}} minutes
";
- const mockText =
- "Confirmation code: {{confirmationCode}} - Valid for {{expirationMinutes}} minutes";
+ it("should send confirmation email in English", async () => {
+ const mockHtml =
+ "Confirmation code: {{confirmationCode}} Valid for {{expirationMinutes}} minutes
";
+ const mockText =
+ "Confirmation code: {{confirmationCode}} - Valid for {{expirationMinutes}} minutes";
- mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
+ mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
- const staffUser = {
- id: "test-id",
- hashKey: "test-hash",
- publicKey: "test-key",
- name: "John Doe",
- position: "Doctor",
- email: "test@example.com",
- language: "en"
- };
- const mockTenant = {
- id: "tenant-1",
- shortName: "test",
- longName: "Test Organization",
- description: null,
- logo: null,
- databaseUrl: "postgresql://test",
- setupState: "FIRST_CHANNEL_CREATED" as const,
- createdAt: new Date(),
- updatedAt: new Date()
- };
- const confirmationCode = "XYZ789";
- const expirationMinutes = 10;
+ const staffUser = {
+ id: "test-id",
+ hashKey: "test-hash",
+ publicKey: "test-key",
+ name: "John Doe",
+ position: "Doctor",
+ email: "test@example.com",
+ language: "en",
+ };
+ const mockTenant = {
+ id: "tenant-1",
+ shortName: "test",
+ longName: "Test Organization",
+ description: null,
+ logo: null,
+ databaseUrl: "postgresql://test",
+ setupState: "FIRST_CHANNEL_CREATED" as const,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+ const confirmationCode = "XYZ789";
+ const expirationMinutes = 10;
- await sendConfirmationEmail(staffUser, mockTenant, confirmationCode, expirationMinutes);
+ await sendConfirmationEmail(staffUser, mockTenant, confirmationCode, expirationMinutes);
- expect(mockReadFile).toHaveBeenCalledWith(
- expect.stringContaining("confirmation.en.html"),
- "utf-8"
- );
+ expect(mockReadFile).toHaveBeenCalledWith(
+ expect.stringContaining("confirmation.en.html"),
+ "utf-8",
+ );
- expect(mockSendMail).toHaveBeenCalledWith({
- from: {
- name: "Test App",
- address: "noreply@test.com"
- },
- to: {
- name: "John Doe",
- address: "test@example.com"
- },
- subject: "Confirm Your Registration",
- html: "Confirmation code: XYZ789 Valid for 10 minutes
",
- text: "Confirmation code: XYZ789 - Valid for 10 minutes"
- });
- });
+ expect(mockSendMail).toHaveBeenCalledWith({
+ from: {
+ name: "Test App",
+ address: "noreply@test.com",
+ },
+ to: {
+ name: "John Doe",
+ address: "test@example.com",
+ },
+ subject: "Confirm Your Registration",
+ html: "Confirmation code: XYZ789 Valid for 10 minutes
",
+ text: "Confirmation code: XYZ789 - Valid for 10 minutes",
+ });
+ });
- it("should handle confirmation template rendering", async () => {
- const mockHtml = "Code: {{confirmationCode}}";
- const mockText = "Code: {{confirmationCode}}";
+ it("should handle confirmation template rendering", async () => {
+ const mockHtml = "Code: {{confirmationCode}}";
+ const mockText = "Code: {{confirmationCode}}";
- mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
+ mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
- const templateEngine = new EmailTemplateEngine("test/templates");
- const mockTenant = {
- id: "tenant-1",
- shortName: "test",
- longName: "Test Organization",
- description: null,
- logo: null,
- databaseUrl: "postgresql://test",
- setupState: "FIRST_CHANNEL_CREATED" as const,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ const templateEngine = new EmailTemplateEngine("test/templates");
+ const mockTenant = {
+ id: "tenant-1",
+ shortName: "test",
+ longName: "Test Organization",
+ description: null,
+ logo: null,
+ databaseUrl: "postgresql://test",
+ setupState: "FIRST_CHANNEL_CREATED" as const,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = await templateEngine.renderTemplate("confirmation", {
- recipient: { email: "test@example.com", name: "Test User" },
- subject: "Test Confirmation",
- language: "de",
- tenant: mockTenant,
- confirmationCode: "TEST123",
- expirationMinutes: 15
- });
+ const result = await templateEngine.renderTemplate("confirmation", {
+ recipient: { email: "test@example.com", name: "Test User" },
+ subject: "Test Confirmation",
+ language: "de",
+ tenant: mockTenant,
+ confirmationCode: "TEST123",
+ expirationMinutes: 15,
+ });
- expect(result.html).toBe("Code: TEST123");
- expect(result.text).toBe("Code: TEST123");
- });
- });
+ expect(result.html).toBe("Code: TEST123");
+ expect(result.text).toBe("Code: TEST123");
+ });
+ });
});
diff --git a/src/lib/server/email/__tests__/generate-base-url.test.ts b/src/lib/server/email/__tests__/generate-base-url.test.ts
index d76f5ad..03dd3c0 100644
--- a/src/lib/server/email/__tests__/generate-base-url.test.ts
+++ b/src/lib/server/email/__tests__/generate-base-url.test.ts
@@ -6,322 +6,322 @@ import type { SelectTenant } from "$lib/server/db/central-schema";
const mockEnv = vi.hoisted(() => ({ NODE_ENV: "development" }));
vi.mock("$env/dynamic/private", () => ({
- env: mockEnv
+ env: mockEnv,
}));
describe("generateBaseUrl", () => {
- beforeEach(() => {
- // Reset NODE_ENV to development for each test
- mockEnv.NODE_ENV = "development";
- });
+ beforeEach(() => {
+ // Reset NODE_ENV to development for each test
+ mockEnv.NODE_ENV = "development";
+ });
- afterEach(() => {
- vi.clearAllMocks();
- });
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
- describe("Development/Local Environment", () => {
- it("should return localhost URL regardless of tenant", () => {
- const requestUrl = new URL("http://localhost:5173");
- const tenant: SelectTenant = {
- id: "tenant-1",
- shortName: "acme",
- longName: "ACME Corp",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date(),
- description: "",
- databaseUrl: "",
- setupState: "NEW"
- };
+ describe("Development/Local Environment", () => {
+ it("should return localhost URL regardless of tenant", () => {
+ const requestUrl = new URL("http://localhost:5173");
+ const tenant: SelectTenant = {
+ id: "tenant-1",
+ shortName: "acme",
+ longName: "ACME Corp",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("http://localhost:5173");
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("http://localhost:5173");
+ });
- it("should return localhost URL for null tenant", () => {
- const requestUrl = new URL("http://localhost:3000");
+ it("should return localhost URL for null tenant", () => {
+ const requestUrl = new URL("http://localhost:3000");
- const result = generateBaseUrl(requestUrl, null);
- expect(result).toBe("http://localhost:3000");
- });
+ const result = generateBaseUrl(requestUrl, null);
+ expect(result).toBe("http://localhost:3000");
+ });
- it("should preserve port for localhost", () => {
- const requestUrl = new URL("http://localhost:8080");
- const tenant: SelectTenant = {
- id: "tenant-1",
- shortName: "test",
- longName: "Test Corp",
- description: "",
- databaseUrl: "",
- setupState: "NEW",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ it("should preserve port for localhost", () => {
+ const requestUrl = new URL("http://localhost:8080");
+ const tenant: SelectTenant = {
+ id: "tenant-1",
+ shortName: "test",
+ longName: "Test Corp",
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("http://localhost:8080");
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("http://localhost:8080");
+ });
- it("should handle 127.x.x.x addresses", () => {
- const requestUrl = new URL("http://127.0.0.1:3000");
- const tenant: SelectTenant = {
- id: "tenant-1",
- shortName: "acme",
- longName: "ACME Corp",
- description: "",
- databaseUrl: "",
- setupState: "NEW",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ it("should handle 127.x.x.x addresses", () => {
+ const requestUrl = new URL("http://127.0.0.1:3000");
+ const tenant: SelectTenant = {
+ id: "tenant-1",
+ shortName: "acme",
+ longName: "ACME Corp",
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("http://127.0.0.1:3000");
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("http://127.0.0.1:3000");
+ });
- it("should handle 192.168.x.x addresses", () => {
- const requestUrl = new URL("http://192.168.1.100:8080");
- const tenant: SelectTenant = {
- id: "tenant-1",
- shortName: "acme",
- longName: "ACME Corp",
- description: "",
- databaseUrl: "",
- setupState: "NEW",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ it("should handle 192.168.x.x addresses", () => {
+ const requestUrl = new URL("http://192.168.1.100:8080");
+ const tenant: SelectTenant = {
+ id: "tenant-1",
+ shortName: "acme",
+ longName: "ACME Corp",
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("http://192.168.1.100:8080");
- });
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("http://192.168.1.100:8080");
+ });
+ });
- describe("Production Environment", () => {
- beforeEach(() => {
- mockEnv.NODE_ENV = "production";
- });
+ describe("Production Environment", () => {
+ beforeEach(() => {
+ mockEnv.NODE_ENV = "production";
+ });
- it("should return main domain for null tenant", () => {
- const requestUrl = new URL("https://example.com");
+ it("should return main domain for null tenant", () => {
+ const requestUrl = new URL("https://example.com");
- const result = generateBaseUrl(requestUrl, null);
- expect(result).toBe("https://example.com");
- });
+ const result = generateBaseUrl(requestUrl, null);
+ expect(result).toBe("https://example.com");
+ });
- it("should return main domain with port for null tenant", () => {
- const requestUrl = new URL("https://example.com:8443");
+ it("should return main domain with port for null tenant", () => {
+ const requestUrl = new URL("https://example.com:8443");
- const result = generateBaseUrl(requestUrl, null);
- expect(result).toBe("https://example.com:8443");
- });
+ const result = generateBaseUrl(requestUrl, null);
+ expect(result).toBe("https://example.com:8443");
+ });
- it("should create subdomain URL for tenant on main domain", () => {
- const requestUrl = new URL("https://example.com");
- const tenant: SelectTenant = {
- id: "tenant-1",
- shortName: "acme",
- longName: "ACME Corp",
- description: "",
- databaseUrl: "",
- setupState: "NEW",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ it("should create subdomain URL for tenant on main domain", () => {
+ const requestUrl = new URL("https://example.com");
+ const tenant: SelectTenant = {
+ id: "tenant-1",
+ shortName: "acme",
+ longName: "ACME Corp",
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("https://acme.example.com");
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("https://acme.example.com");
+ });
- it("should create subdomain URL with port", () => {
- const requestUrl = new URL("https://example.com:8443");
- const tenant: SelectTenant = {
- id: "tenant-1",
- shortName: "acme",
- longName: "ACME Corp",
- description: "",
- databaseUrl: "",
- setupState: "NEW",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ it("should create subdomain URL with port", () => {
+ const requestUrl = new URL("https://example.com:8443");
+ const tenant: SelectTenant = {
+ id: "tenant-1",
+ shortName: "acme",
+ longName: "ACME Corp",
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("https://acme.example.com:8443");
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("https://acme.example.com:8443");
+ });
- it("should replace existing subdomain with tenant shortName", () => {
- const requestUrl = new URL("https://old-tenant.example.com");
- const tenant: SelectTenant = {
- id: "tenant-2",
- shortName: "new-tenant",
- longName: "New Tenant Corp",
- description: "",
- databaseUrl: "",
- setupState: "NEW",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ it("should replace existing subdomain with tenant shortName", () => {
+ const requestUrl = new URL("https://old-tenant.example.com");
+ const tenant: SelectTenant = {
+ id: "tenant-2",
+ shortName: "new-tenant",
+ longName: "New Tenant Corp",
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("https://new-tenant.example.com");
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("https://new-tenant.example.com");
+ });
- it("should replace existing subdomain with port", () => {
- const requestUrl = new URL("https://old-tenant.example.com:8443");
- const tenant: SelectTenant = {
- id: "tenant-2",
- shortName: "new-tenant",
- longName: "New Tenant Corp",
- description: "",
- databaseUrl: "",
- setupState: "NEW",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ it("should replace existing subdomain with port", () => {
+ const requestUrl = new URL("https://old-tenant.example.com:8443");
+ const tenant: SelectTenant = {
+ id: "tenant-2",
+ shortName: "new-tenant",
+ longName: "New Tenant Corp",
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("https://new-tenant.example.com:8443");
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("https://new-tenant.example.com:8443");
+ });
- it("should handle complex subdomains (keep last two parts)", () => {
- const requestUrl = new URL("https://admin.api.example.com");
- const tenant: SelectTenant = {
- id: "tenant-1",
- shortName: "tenant",
- longName: "Tenant Corp",
- description: "",
- databaseUrl: "",
- setupState: "NEW",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ it("should handle complex subdomains (keep last two parts)", () => {
+ const requestUrl = new URL("https://admin.api.example.com");
+ const tenant: SelectTenant = {
+ id: "tenant-1",
+ shortName: "tenant",
+ longName: "Tenant Corp",
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("https://tenant.example.com");
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("https://tenant.example.com");
+ });
- it("should handle http protocol", () => {
- const requestUrl = new URL("http://example.com");
- const tenant: SelectTenant = {
- id: "tenant-1",
- shortName: "acme",
- longName: "ACME Corp",
- description: "",
- databaseUrl: "",
- setupState: "NEW",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ it("should handle http protocol", () => {
+ const requestUrl = new URL("http://example.com");
+ const tenant: SelectTenant = {
+ id: "tenant-1",
+ shortName: "acme",
+ longName: "ACME Corp",
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("http://acme.example.com");
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("http://acme.example.com");
+ });
- it("should return main domain when tenant has no shortName", () => {
- const requestUrl = new URL("https://example.com");
- const tenant: SelectTenant = {
- id: "tenant-1",
- shortName: "", // Empty shortName
- longName: "ACME Corp",
- description: "",
- databaseUrl: "",
- setupState: "NEW",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ it("should return main domain when tenant has no shortName", () => {
+ const requestUrl = new URL("https://example.com");
+ const tenant: SelectTenant = {
+ id: "tenant-1",
+ shortName: "", // Empty shortName
+ longName: "ACME Corp",
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("https://example.com");
- });
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("https://example.com");
+ });
+ });
- describe("Edge Cases", () => {
- it("should handle single domain names in production", () => {
- mockEnv.NODE_ENV = "production";
- const requestUrl = new URL("https://app");
- const tenant: SelectTenant = {
- id: "tenant-1",
- shortName: "acme",
- longName: "ACME Corp",
- description: "",
- databaseUrl: "",
- setupState: "NEW",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ describe("Edge Cases", () => {
+ it("should handle single domain names in production", () => {
+ mockEnv.NODE_ENV = "production";
+ const requestUrl = new URL("https://app");
+ const tenant: SelectTenant = {
+ id: "tenant-1",
+ shortName: "acme",
+ longName: "ACME Corp",
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("https://acme.app");
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("https://acme.app");
+ });
- it("should handle localhost in production (still treated as development)", () => {
- mockEnv.NODE_ENV = "production";
- const requestUrl = new URL("https://localhost:8443");
- const tenant: SelectTenant = {
- id: "tenant-1",
- shortName: "acme",
- longName: "ACME Corp",
- description: "",
- databaseUrl: "",
- setupState: "NEW",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ it("should handle localhost in production (still treated as development)", () => {
+ mockEnv.NODE_ENV = "production";
+ const requestUrl = new URL("https://localhost:8443");
+ const tenant: SelectTenant = {
+ id: "tenant-1",
+ shortName: "acme",
+ longName: "ACME Corp",
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("https://localhost:8443");
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("https://localhost:8443");
+ });
- it("should handle IP addresses as development", () => {
- mockEnv.NODE_ENV = "production";
- const requestUrl = new URL("https://192.168.1.100:8443");
- const tenant: SelectTenant = {
- id: "tenant-1",
- shortName: "acme",
- longName: "ACME Corp",
- description: "",
- databaseUrl: "",
- setupState: "NEW",
- logo: null,
- createdAt: new Date(),
- updatedAt: new Date()
- };
+ it("should handle IP addresses as development", () => {
+ mockEnv.NODE_ENV = "production";
+ const requestUrl = new URL("https://192.168.1.100:8443");
+ const tenant: SelectTenant = {
+ id: "tenant-1",
+ shortName: "acme",
+ longName: "ACME Corp",
+ description: "",
+ databaseUrl: "",
+ setupState: "NEW",
+ logo: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
- const result = generateBaseUrl(requestUrl, tenant);
- expect(result).toBe("https://192.168.1.100:8443");
- });
+ const result = generateBaseUrl(requestUrl, tenant);
+ expect(result).toBe("https://192.168.1.100:8443");
+ });
- it("should handle default HTTP port (80)", () => {
- mockEnv.NODE_ENV = "production";
- const requestUrl = new URL("http://example.com:80");
+ it("should handle default HTTP port (80)", () => {
+ mockEnv.NODE_ENV = "production";
+ const requestUrl = new URL("http://example.com:80");
- // URL constructor should handle port 80 correctly
- const result = generateBaseUrl(requestUrl, null);
- // Default HTTP port shouldn't be included in URL
- expect(result).toBe("http://example.com");
- });
+ // URL constructor should handle port 80 correctly
+ const result = generateBaseUrl(requestUrl, null);
+ // Default HTTP port shouldn't be included in URL
+ expect(result).toBe("http://example.com");
+ });
- it("should handle default HTTPS port (443)", () => {
- mockEnv.NODE_ENV = "production";
- const requestUrl = new URL("https://example.com:443");
+ it("should handle default HTTPS port (443)", () => {
+ mockEnv.NODE_ENV = "production";
+ const requestUrl = new URL("https://example.com:443");
- // URL constructor should handle port 443 correctly
- const result = generateBaseUrl(requestUrl, null);
- // Default HTTPS port shouldn't be included in URL
- expect(result).toBe("https://example.com");
- });
- });
+ // URL constructor should handle port 443 correctly
+ const result = generateBaseUrl(requestUrl, null);
+ // Default HTTPS port shouldn't be included in URL
+ expect(result).toBe("https://example.com");
+ });
+ });
});
diff --git a/src/lib/server/email/__tests__/tenant-admin-invite.test.ts b/src/lib/server/email/__tests__/tenant-admin-invite.test.ts
index bf40d38..0c9d601 100644
--- a/src/lib/server/email/__tests__/tenant-admin-invite.test.ts
+++ b/src/lib/server/email/__tests__/tenant-admin-invite.test.ts
@@ -2,47 +2,47 @@ import { describe, it, expect } from "vitest";
import { sendTenantAdminInviteEmail } from "../email-service";
describe("sendTenantAdminInviteEmail", () => {
- const mockTenant = {
- id: "test-tenant-id",
- shortName: "testcorp",
- longName: "Test Corporation GmbH",
- description: "A test corporation",
- databaseUrl: "postgresql://test",
- setupState: "NEW" as const,
- createdAt: new Date(),
- updatedAt: new Date(),
- logo: null
- };
+ const mockTenant = {
+ id: "test-tenant-id",
+ shortName: "testcorp",
+ longName: "Test Corporation GmbH",
+ description: "A test corporation",
+ databaseUrl: "postgresql://test",
+ setupState: "NEW" as const,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ logo: null,
+ };
- it("should accept correct parameters and not throw for German language", () => {
- const adminEmail = "admin@testcorp.com";
- const adminName = "Test Admin";
- const registrationUrl = "http://localhost:5173/register?tenant=test";
+ it("should accept correct parameters and not throw for German language", () => {
+ const adminEmail = "admin@testcorp.com";
+ const adminName = "Test Admin";
+ const registrationUrl = "http://localhost:5173/register?tenant=test";
- // This test just ensures the function accepts the right parameters
- // and doesn't throw on setup (actual email sending will fail without SMTP config)
- expect(() => {
- sendTenantAdminInviteEmail(adminEmail, adminName, mockTenant, registrationUrl, "de");
- }).not.toThrow();
- });
+ // This test just ensures the function accepts the right parameters
+ // and doesn't throw on setup (actual email sending will fail without SMTP config)
+ expect(() => {
+ sendTenantAdminInviteEmail(adminEmail, adminName, mockTenant, registrationUrl, "de");
+ }).not.toThrow();
+ });
- it("should accept correct parameters and not throw for English language", () => {
- const adminEmail = "admin@testcorp.com";
- const adminName = "Test Admin";
- const registrationUrl = "http://localhost:5173/register?tenant=test";
+ it("should accept correct parameters and not throw for English language", () => {
+ const adminEmail = "admin@testcorp.com";
+ const adminName = "Test Admin";
+ const registrationUrl = "http://localhost:5173/register?tenant=test";
- expect(() => {
- sendTenantAdminInviteEmail(adminEmail, adminName, mockTenant, registrationUrl, "en");
- }).not.toThrow();
- });
+ expect(() => {
+ sendTenantAdminInviteEmail(adminEmail, adminName, mockTenant, registrationUrl, "en");
+ }).not.toThrow();
+ });
- it("should accept correct parameters and not throw with default language", () => {
- const adminEmail = "admin@testcorp.com";
- const adminName = "Test Admin";
- const registrationUrl = "http://localhost:5173/register?tenant=test";
+ it("should accept correct parameters and not throw with default language", () => {
+ const adminEmail = "admin@testcorp.com";
+ const adminName = "Test Admin";
+ const registrationUrl = "http://localhost:5173/register?tenant=test";
- expect(() => {
- sendTenantAdminInviteEmail(adminEmail, adminName, mockTenant, registrationUrl);
- }).not.toThrow();
- });
+ expect(() => {
+ sendTenantAdminInviteEmail(adminEmail, adminName, mockTenant, registrationUrl);
+ }).not.toThrow();
+ });
});
diff --git a/src/lib/server/email/email-service.ts b/src/lib/server/email/email-service.ts
index 643492f..593772b 100644
--- a/src/lib/server/email/email-service.ts
+++ b/src/lib/server/email/email-service.ts
@@ -1,9 +1,9 @@
import { sendEmail, type EmailRecipient, createEmailRecipient } from "./mailer";
import {
- templateEngine,
- type EmailTemplateType,
- type TemplateData,
- type Language
+ templateEngine,
+ type EmailTemplateType,
+ type TemplateData,
+ type Language,
} from "./template-engine";
import type { SelectClient, SelectStaff, SelectAppointment } from "$lib/server/db/tenant-schema";
import type { SelectTenant } from "$lib/server/db/central-schema";
@@ -20,28 +20,28 @@ import type { SelectTenant } from "$lib/server/db/central-schema";
* @returns {Promise}
*/
export async function sendTemplatedEmail(
- templateType: EmailTemplateType,
- recipient: EmailRecipient,
- subject: string,
- language: Language = "en",
- tenant: SelectTenant,
- templateData: Record = {}
+ templateType: EmailTemplateType,
+ recipient: EmailRecipient,
+ subject: string,
+ language: Language = "en",
+ tenant: SelectTenant,
+ templateData: Record = {},
): Promise {
- const data: TemplateData = {
- recipient,
- subject,
- language,
- tenant,
- ...templateData
- };
+ const data: TemplateData = {
+ recipient,
+ subject,
+ language,
+ tenant,
+ ...templateData,
+ };
- try {
- const rendered = await templateEngine.renderTemplate(templateType, data);
- await sendEmail(recipient, rendered.subject, rendered.html, rendered.text);
- } catch (error) {
- console.error(`Failed to send templated email (${templateType}):`, error);
- throw error;
- }
+ try {
+ const rendered = await templateEngine.renderTemplate(templateType, data);
+ await sendEmail(recipient, rendered.subject, rendered.html, rendered.text);
+ } catch (error) {
+ console.error(`Failed to send templated email (${templateType}):`, error);
+ throw error;
+ }
}
/**
@@ -53,17 +53,17 @@ export async function sendTemplatedEmail(
* @returns {Promise}
*/
export async function sendUserCreatedEmail(
- user: SelectClient | SelectStaff,
- tenant: SelectTenant,
- loginUrl: string
+ user: SelectClient | SelectStaff,
+ tenant: SelectTenant,
+ loginUrl: string,
): Promise {
- const recipient = createEmailRecipient(user);
- const language = (recipient.language as Language) || "en";
- const subject = language === "en" ? "Welcome to Open Reception" : "Willkommen bei Open Reception";
+ const recipient = createEmailRecipient(user);
+ const language = (recipient.language as Language) || "en";
+ const subject = language === "en" ? "Welcome to Open Reception" : "Willkommen bei Open Reception";
- await sendTemplatedEmail("user-created", recipient, subject, language, tenant, {
- loginUrl
- });
+ await sendTemplatedEmail("user-created", recipient, subject, language, tenant, {
+ loginUrl,
+ });
}
/**
@@ -74,14 +74,14 @@ export async function sendUserCreatedEmail(
* @returns {Promise}
*/
export async function sendPinResetEmail(
- user: SelectClient | SelectStaff,
- tenant: SelectTenant
+ user: SelectClient | SelectStaff,
+ tenant: SelectTenant,
): Promise {
- const recipient = createEmailRecipient(user);
- const language = (recipient.language as Language) || "en";
- const subject = language === "en" ? "PIN Reset Information" : "PIN zurückgesetzt";
+ const recipient = createEmailRecipient(user);
+ const language = (recipient.language as Language) || "en";
+ const subject = language === "en" ? "PIN Reset Information" : "PIN zurückgesetzt";
- await sendTemplatedEmail("pin-reset", recipient, subject, language, tenant, {});
+ await sendTemplatedEmail("pin-reset", recipient, subject, language, tenant, {});
}
/**
@@ -92,14 +92,14 @@ export async function sendPinResetEmail(
* @returns {Promise}
*/
export async function sendKeyResetEmail(
- user: SelectClient | SelectStaff,
- tenant: SelectTenant
+ user: SelectClient | SelectStaff,
+ tenant: SelectTenant,
): Promise {
- const recipient = createEmailRecipient(user);
- const language = (recipient.language as Language) || "en";
- const subject = language === "en" ? "Key Reset Information" : "Schlüssel zurückgesetzt";
+ const recipient = createEmailRecipient(user);
+ const language = (recipient.language as Language) || "en";
+ const subject = language === "en" ? "Key Reset Information" : "Schlüssel zurückgesetzt";
- await sendTemplatedEmail("key-reset", recipient, subject, language, tenant, {});
+ await sendTemplatedEmail("key-reset", recipient, subject, language, tenant, {});
}
/**
@@ -112,23 +112,23 @@ export async function sendKeyResetEmail(
* @returns {Promise}
*/
export async function sendAppointmentReminderEmail(
- user: SelectClient | SelectStaff,
- tenant: SelectTenant,
- appointment: SelectAppointment,
- cancelUrl?: string
+ user: SelectClient | SelectStaff,
+ tenant: SelectTenant,
+ appointment: SelectAppointment,
+ cancelUrl?: string,
): Promise {
- const recipient = createEmailRecipient(user);
- const language = (recipient.language as Language) || "en";
- const subject = language === "en" ? "Appointment Reminder" : "Terminerinnerung";
+ const recipient = createEmailRecipient(user);
+ const language = (recipient.language as Language) || "en";
+ const subject = language === "en" ? "Appointment Reminder" : "Terminerinnerung";
- await sendTemplatedEmail("appointment-reminder", recipient, subject, language, tenant, {
- appointment,
- appointmentDate: appointment.appointmentDate,
- appointmentTime: appointment.appointmentDate, // You might want to add a separate time field
- title: appointment.title,
- description: appointment.description,
- cancelUrl
- });
+ await sendTemplatedEmail("appointment-reminder", recipient, subject, language, tenant, {
+ appointment,
+ appointmentDate: appointment.appointmentDate,
+ appointmentTime: appointment.appointmentDate, // You might want to add a separate time field
+ title: appointment.title,
+ description: appointment.description,
+ cancelUrl,
+ });
}
/**
@@ -141,22 +141,22 @@ export async function sendAppointmentReminderEmail(
* @returns {Promise}
*/
export async function sendAppointmentCreatedEmail(
- user: SelectClient | SelectStaff,
- tenant: SelectTenant,
- appointment: SelectAppointment,
- cancelUrl?: string
+ user: SelectClient | SelectStaff,
+ tenant: SelectTenant,
+ appointment: SelectAppointment,
+ cancelUrl?: string,
): Promise {
- const recipient = createEmailRecipient(user);
- const language = (recipient.language as Language) || "en";
- const subject = language === "en" ? "Appointment Confirmed" : "Termin bestätigt";
+ const recipient = createEmailRecipient(user);
+ const language = (recipient.language as Language) || "en";
+ const subject = language === "en" ? "Appointment Confirmed" : "Termin bestätigt";
- await sendTemplatedEmail("appointment-created", recipient, subject, language, tenant, {
- appointment,
- appointmentDate: appointment.appointmentDate,
- title: appointment.title,
- description: appointment.description,
- cancelUrl
- });
+ await sendTemplatedEmail("appointment-created", recipient, subject, language, tenant, {
+ appointment,
+ appointmentDate: appointment.appointmentDate,
+ title: appointment.title,
+ description: appointment.description,
+ cancelUrl,
+ });
}
/**
@@ -169,22 +169,22 @@ export async function sendAppointmentCreatedEmail(
* @returns {Promise}
*/
export async function sendAppointmentUpdatedEmail(
- user: SelectClient | SelectStaff,
- tenant: SelectTenant,
- appointment: SelectAppointment,
- cancelUrl?: string
+ user: SelectClient | SelectStaff,
+ tenant: SelectTenant,
+ appointment: SelectAppointment,
+ cancelUrl?: string,
): Promise {
- const recipient = createEmailRecipient(user);
- const language = (recipient.language as Language) || "en";
- const subject = language === "en" ? "Appointment Updated" : "Termin aktualisiert";
+ const recipient = createEmailRecipient(user);
+ const language = (recipient.language as Language) || "en";
+ const subject = language === "en" ? "Appointment Updated" : "Termin aktualisiert";
- await sendTemplatedEmail("appointment-updated", recipient, subject, language, tenant, {
- appointment,
- appointmentDate: appointment.appointmentDate,
- title: appointment.title,
- description: appointment.description,
- cancelUrl
- });
+ await sendTemplatedEmail("appointment-updated", recipient, subject, language, tenant, {
+ appointment,
+ appointmentDate: appointment.appointmentDate,
+ title: appointment.title,
+ description: appointment.description,
+ cancelUrl,
+ });
}
/**
@@ -194,31 +194,31 @@ export async function sendAppointmentUpdatedEmail(
* @returns {string} The appropriate base URL
*/
export function generateBaseUrl(requestUrl: URL, tenant: SelectTenant | null): string {
- const protocol = requestUrl.protocol;
- const port = requestUrl.port ? `:${requestUrl.port}` : "";
- const hostname = requestUrl.hostname;
+ const protocol = requestUrl.protocol;
+ const port = requestUrl.port ? `:${requestUrl.port}` : "";
+ const hostname = requestUrl.hostname;
- // In development, always use the original hostname regardless of tenant
- if (hostname === "localhost" || hostname.startsWith("127.") || hostname.startsWith("192.168.")) {
- return `${protocol}//${hostname}${port}`;
- }
+ // In development, always use the original hostname regardless of tenant
+ if (hostname === "localhost" || hostname.startsWith("127.") || hostname.startsWith("192.168.")) {
+ return `${protocol}//${hostname}${port}`;
+ }
- // In production, handle tenant subdomains
- if (tenant?.shortName) {
- const parts = hostname.split(".");
+ // In production, handle tenant subdomains
+ if (tenant?.shortName) {
+ const parts = hostname.split(".");
- if (parts.length > 2) {
- // Complex subdomain - use only the last two parts (domain.tld) and add tenant
- const domain = parts.slice(-2).join(".");
- return `${protocol}//${tenant.shortName}.${domain}${port}`;
- } else {
- // Main domain, prepend tenant subdomain
- return `${protocol}//${tenant.shortName}.${hostname}${port}`;
- }
- }
+ if (parts.length > 2) {
+ // Complex subdomain - use only the last two parts (domain.tld) and add tenant
+ const domain = parts.slice(-2).join(".");
+ return `${protocol}//${tenant.shortName}.${domain}${port}`;
+ } else {
+ // Main domain, prepend tenant subdomain
+ return `${protocol}//${tenant.shortName}.${hostname}${port}`;
+ }
+ }
- // For global admin or no tenant, use main domain
- return `${protocol}//${hostname}${port}`;
+ // For global admin or no tenant, use main domain
+ return `${protocol}//${hostname}${port}`;
}
/**
@@ -232,29 +232,29 @@ export function generateBaseUrl(requestUrl: URL, tenant: SelectTenant | null): s
* @returns {Promise}
*/
export async function sendConfirmationEmail(
- user: { id: string; email: string | null; name: string | null; language?: string | null },
- tenant: SelectTenant,
- confirmationCode: string,
- expirationMinutes: number = 15,
- requestUrl?: URL
+ user: { id: string; email: string | null; name: string | null; language?: string | null },
+ tenant: SelectTenant,
+ confirmationCode: string,
+ expirationMinutes: number = 15,
+ requestUrl?: URL,
): Promise {
- const recipient = createEmailRecipient(user);
- const language = (recipient.language as Language) || "en";
- const subject = language === "en" ? "Confirm Your Registration" : "Registrierung bestätigen";
+ const recipient = createEmailRecipient(user);
+ const language = (recipient.language as Language) || "en";
+ const subject = language === "en" ? "Confirm Your Registration" : "Registrierung bestätigen";
- // Generate appropriate base URL if request URL is provided
- const baseUrl = requestUrl ? generateBaseUrl(requestUrl, tenant) : "http://localhost:5173";
+ // Generate appropriate base URL if request URL is provided
+ const baseUrl = requestUrl ? generateBaseUrl(requestUrl, tenant) : "http://localhost:5173";
- // Create enhanced tenant object with baseUrl
- const tenantWithBaseUrl = {
- ...tenant,
- baseUrl
- };
+ // Create enhanced tenant object with baseUrl
+ const tenantWithBaseUrl = {
+ ...tenant,
+ baseUrl,
+ };
- await sendTemplatedEmail("confirmation", recipient, subject, language, tenantWithBaseUrl, {
- confirmationCode,
- expirationMinutes
- });
+ await sendTemplatedEmail("confirmation", recipient, subject, language, tenantWithBaseUrl, {
+ confirmationCode,
+ expirationMinutes,
+ });
}
/**
@@ -268,24 +268,24 @@ export async function sendConfirmationEmail(
* @returns {Promise}
*/
export async function sendTenantAdminInviteEmail(
- adminEmail: string,
- adminName: string,
- tenant: SelectTenant,
- registrationUrl: string,
- language: Language = "en"
+ adminEmail: string,
+ adminName: string,
+ tenant: SelectTenant,
+ registrationUrl: string,
+ language: Language = "en",
): Promise {
- const recipient: EmailRecipient = {
- email: adminEmail,
- name: adminName,
- language
- };
+ const recipient: EmailRecipient = {
+ email: adminEmail,
+ name: adminName,
+ language,
+ };
- const subject =
- language === "en" ? "Invitation as Tenant Administrator" : "Einladung als Tenant-Administrator";
+ const subject =
+ language === "en" ? "Invitation as Tenant Administrator" : "Einladung als Tenant-Administrator";
- await sendTemplatedEmail("tenant-admin-invite", recipient, subject, language, tenant, {
- registrationUrl
- });
+ await sendTemplatedEmail("tenant-admin-invite", recipient, subject, language, tenant, {
+ registrationUrl,
+ });
}
/**
@@ -300,25 +300,25 @@ export async function sendTenantAdminInviteEmail(
* @returns {Promise}
*/
export async function sendUserInviteEmail(
- userEmail: string,
- userName: string,
- tenant: SelectTenant,
- role: "TENANT_ADMIN" | "STAFF",
- registrationUrl: string,
- language: Language = "en"
+ userEmail: string,
+ userName: string,
+ tenant: SelectTenant,
+ role: "TENANT_ADMIN" | "STAFF",
+ registrationUrl: string,
+ language: Language = "en",
): Promise {
- const recipient: EmailRecipient = {
- email: userEmail,
- name: userName,
- language
- };
+ const recipient: EmailRecipient = {
+ email: userEmail,
+ name: userName,
+ language,
+ };
- const subject =
- language === "en"
- ? `Invitation to ${tenant.longName || tenant.shortName}`
- : `Einladung zu ${tenant.longName || tenant.shortName}`;
+ const subject =
+ language === "en"
+ ? `Invitation to ${tenant.longName || tenant.shortName}`
+ : `Einladung zu ${tenant.longName || tenant.shortName}`;
- await sendTemplatedEmail("user-invite", recipient, subject, language, tenant, {
- registrationUrl
- });
+ await sendTemplatedEmail("user-invite", recipient, subject, language, tenant, {
+ registrationUrl,
+ });
}
diff --git a/src/lib/server/email/mailer.ts b/src/lib/server/email/mailer.ts
index 0099554..db9fc8e 100644
--- a/src/lib/server/email/mailer.ts
+++ b/src/lib/server/email/mailer.ts
@@ -11,9 +11,9 @@ import type Mail from "nodemailer/lib/mailer";
* @property {string} [language] - Optional language preference (de/en)
*/
export interface EmailRecipient {
- email: string;
- name?: string;
- language?: string;
+ email: string;
+ name?: string;
+ language?: string;
}
/**
@@ -23,35 +23,35 @@ export interface EmailRecipient {
* @throws {Error} When user has no email address
*/
export function createEmailRecipient(
- user:
- | SelectClient
- | SelectStaff
- | { id: string; email: string | null; name: string | null; language?: string | null }
+ user:
+ | SelectClient
+ | SelectStaff
+ | { id: string; email: string | null; name: string | null; language?: string | null },
): EmailRecipient {
- // Handle SelectUser type (from central schema)
- if ("email" in user && !("publicKey" in user)) {
- return {
- email: user.email || "",
- name: user.name || undefined,
- language: user.language || "de" // Use user's language preference
- };
- }
- // Handle SelectStaff type (has name property)
- else if ("name" in user) {
- return {
- email: user.email,
- name: user.name || undefined,
- language: user.language || "de"
- };
- }
- // Handle SelectClient type (no name property)
- else {
- return {
- email: user.email || "", // Client email is optional
- name: undefined, // Clients don't have names for privacy
- language: user.language || "de"
- };
- }
+ // Handle SelectUser type (from central schema)
+ if ("email" in user && !("publicKey" in user)) {
+ return {
+ email: user.email || "",
+ name: user.name || undefined,
+ language: user.language || "de", // Use user's language preference
+ };
+ }
+ // Handle SelectStaff type (has name property)
+ else if ("name" in user) {
+ return {
+ email: user.email,
+ name: user.name || undefined,
+ language: user.language || "de",
+ };
+ }
+ // Handle SelectClient type (no name property)
+ else {
+ return {
+ email: user.email || "", // Client email is optional
+ name: undefined, // Clients don't have names for privacy
+ language: user.language || "de",
+ };
+ }
}
/**
@@ -61,19 +61,19 @@ export function createEmailRecipient(
* @private
*/
function createTransporter() {
- if (!env.SMTP_HOST || !env.SMTP_PORT || !env.SMTP_USER || !env.SMTP_PASS) {
- throw new Error("SMTP configuration is incomplete. Please check your environment variables.");
- }
- const config = {
- host: env.SMTP_HOST,
- port: parseInt(env.SMTP_PORT),
- secure: env.SMTP_SECURE === "true",
- auth: {
- user: env.SMTP_USER,
- pass: env.SMTP_PASS
- }
- };
- return nodemailer.createTransport(config);
+ if (!env.SMTP_HOST || !env.SMTP_PORT || !env.SMTP_USER || !env.SMTP_PASS) {
+ throw new Error("SMTP configuration is incomplete. Please check your environment variables.");
+ }
+ const config = {
+ host: env.SMTP_HOST,
+ port: parseInt(env.SMTP_PORT),
+ secure: env.SMTP_SECURE === "true",
+ auth: {
+ user: env.SMTP_USER,
+ pass: env.SMTP_PASS,
+ },
+ };
+ return nodemailer.createTransport(config);
}
/**
@@ -86,46 +86,46 @@ function createTransporter() {
* @returns {Promise}
*/
export async function sendEmail(
- recipient: EmailRecipient,
- subject: string,
- htmlContent: string,
- textContent: string
+ recipient: EmailRecipient,
+ subject: string,
+ htmlContent: string,
+ textContent: string,
): Promise {
- const transporter = createTransporter();
+ const transporter = createTransporter();
- if (!recipient.email) {
- // Recipient has not stored an email address, so no mail will be send
- // TODO: Log this event
- return;
- }
+ if (!recipient.email) {
+ // Recipient has not stored an email address, so no mail will be send
+ // TODO: Log this event
+ return;
+ }
- const from_address = env.SMTP_FROM_EMAIL || env.SMTP_USER;
+ const from_address = env.SMTP_FROM_EMAIL || env.SMTP_USER;
- if (!from_address) {
- throw new Error("From address is required");
- }
+ if (!from_address) {
+ throw new Error("From address is required");
+ }
- const mailOptions: Mail.Options = {
- from: {
- name: env.SMTP_FROM_NAME || "Open Reception",
- address: from_address
- },
- to: {
- name: recipient.name || "",
- address: recipient.email
- },
- subject,
- html: htmlContent,
- text: textContent
- };
+ const mailOptions: Mail.Options = {
+ from: {
+ name: env.SMTP_FROM_NAME || "Open Reception",
+ address: from_address,
+ },
+ to: {
+ name: recipient.name || "",
+ address: recipient.email,
+ },
+ subject,
+ html: htmlContent,
+ text: textContent,
+ };
- try {
- await transporter.sendMail(mailOptions);
- console.log(`Email sent successfully to ${recipient.email}`);
- } catch (error) {
- console.error("Failed to send email:", error);
- throw new Error(`Failed to send email to ${recipient.email}`);
- }
+ try {
+ await transporter.sendMail(mailOptions);
+ console.log(`Email sent successfully to ${recipient.email}`);
+ } catch (error) {
+ console.error("Failed to send email:", error);
+ throw new Error(`Failed to send email to ${recipient.email}`);
+ }
}
/**
@@ -133,12 +133,12 @@ export async function sendEmail(
* @returns {Promise} True if connection successful, false otherwise
*/
export async function testEmailConnection(): Promise {
- try {
- const transporter = createTransporter();
- await transporter.verify();
- return true;
- } catch (error) {
- console.error("SMTP connection test failed:", error);
- return false;
- }
+ try {
+ const transporter = createTransporter();
+ await transporter.verify();
+ return true;
+ } catch (error) {
+ console.error("SMTP connection test failed:", error);
+ return false;
+ }
}
diff --git a/src/lib/server/email/template-engine.ts b/src/lib/server/email/template-engine.ts
index 8d3c40c..a9843d2 100644
--- a/src/lib/server/email/template-engine.ts
+++ b/src/lib/server/email/template-engine.ts
@@ -11,15 +11,15 @@ export type Language = "de" | "en";
* @typedef {'user-created' | 'pin-reset' | 'key-reset' | 'appointment-reminder' | 'appointment-created' | 'appointment-updated' | 'confirmation'} EmailTemplateType
*/
export type EmailTemplateType =
- | "user-created"
- | "pin-reset" // Info that PIN was reset (no code)
- | "key-reset"
- | "appointment-reminder"
- | "appointment-created"
- | "appointment-updated"
- | "confirmation" // Registration confirmation with one-time code
- | "tenant-admin-invite" // Invitation for tenant administrator
- | "user-invite"; // Invitation for new users to existing tenant
+ | "user-created"
+ | "pin-reset" // Info that PIN was reset (no code)
+ | "key-reset"
+ | "appointment-reminder"
+ | "appointment-created"
+ | "appointment-updated"
+ | "confirmation" // Registration confirmation with one-time code
+ | "tenant-admin-invite" // Invitation for tenant administrator
+ | "user-invite"; // Invitation for new users to existing tenant
/**
* Template data interface containing all variables available in templates
@@ -31,11 +31,11 @@ export type EmailTemplateType =
* @property {unknown} [key] - Additional template variables
*/
export interface TemplateData {
- recipient: EmailRecipient;
- subject: string;
- language: Language;
- tenant: SelectTenant;
- [key: string]: unknown;
+ recipient: EmailRecipient;
+ subject: string;
+ language: Language;
+ tenant: SelectTenant;
+ [key: string]: unknown;
}
/**
@@ -46,9 +46,9 @@ export interface TemplateData {
* @property {string} subject - Rendered subject line
*/
export interface RenderedTemplate {
- html: string;
- text: string;
- subject: string;
+ html: string;
+ text: string;
+ subject: string;
}
/**
@@ -56,128 +56,128 @@ export interface RenderedTemplate {
* Supports variable substitution, conditionals, loops, and multilingual templates
*/
export class EmailTemplateEngine {
- private templatePath: string;
+ private templatePath: string;
- /**
- * Create a new template engine instance
- * @param {string} templatePath - Path to template directory
- */
- constructor(templatePath: string = "src/lib/server/email/templates") {
- this.templatePath = templatePath;
- }
+ /**
+ * Create a new template engine instance
+ * @param {string} templatePath - Path to template directory
+ */
+ constructor(templatePath: string = "src/lib/server/email/templates") {
+ this.templatePath = templatePath;
+ }
- /**
- * Render a template with the provided data
- * @param {EmailTemplateType} templateType - Type of template to render
- * @param {TemplateData} data - Template data and variables
- * @returns {Promise} Rendered template with HTML, text, and subject
- * @throws {Error} When template files are not found
- */
- async renderTemplate(
- templateType: EmailTemplateType,
- data: TemplateData
- ): Promise {
- const language = data.language || "de";
- const [htmlContent, textContent] = await Promise.all([
- this.loadTemplate(templateType, "html", language),
- this.loadTemplate(templateType, "txt", language)
- ]);
+ /**
+ * Render a template with the provided data
+ * @param {EmailTemplateType} templateType - Type of template to render
+ * @param {TemplateData} data - Template data and variables
+ * @returns {Promise} Rendered template with HTML, text, and subject
+ * @throws {Error} When template files are not found
+ */
+ async renderTemplate(
+ templateType: EmailTemplateType,
+ data: TemplateData,
+ ): Promise {
+ const language = data.language || "de";
+ const [htmlContent, textContent] = await Promise.all([
+ this.loadTemplate(templateType, "html", language),
+ this.loadTemplate(templateType, "txt", language),
+ ]);
- const renderedHtml = this.replaceVariables(htmlContent, data);
- const renderedText = this.replaceVariables(textContent, data);
- const renderedSubject = this.replaceVariables(data.subject, data);
+ const renderedHtml = this.replaceVariables(htmlContent, data);
+ const renderedText = this.replaceVariables(textContent, data);
+ const renderedSubject = this.replaceVariables(data.subject, data);
- return {
- html: renderedHtml,
- text: renderedText,
- subject: renderedSubject
- };
- }
+ return {
+ html: renderedHtml,
+ text: renderedText,
+ subject: renderedSubject,
+ };
+ }
- /**
- * Load template file with language fallback support
- * @param {EmailTemplateType} templateType - Template type
- * @param {'html' | 'txt'} fileType - File type to load
- * @param {Language} language - Language preference (defaults to 'de')
- * @returns {Promise} Template file content
- * @throws {Error} When template file is not found
- * @private
- */
- private async loadTemplate(
- templateType: EmailTemplateType,
- fileType: "html" | "txt",
- language: Language = "de"
- ): Promise {
- const fileName = `${templateType}.${language}.${fileType}`;
- const filePath = join(process.cwd(), this.templatePath, fileName);
+ /**
+ * Load template file with language fallback support
+ * @param {EmailTemplateType} templateType - Template type
+ * @param {'html' | 'txt'} fileType - File type to load
+ * @param {Language} language - Language preference (defaults to 'de')
+ * @returns {Promise} Template file content
+ * @throws {Error} When template file is not found
+ * @private
+ */
+ private async loadTemplate(
+ templateType: EmailTemplateType,
+ fileType: "html" | "txt",
+ language: Language = "de",
+ ): Promise {
+ const fileName = `${templateType}.${language}.${fileType}`;
+ const filePath = join(process.cwd(), this.templatePath, fileName);
- try {
- return await readFile(filePath, "utf-8");
- } catch (error) {
- // Fallback to German if language-specific template doesn't exist
- if (language !== "de") {
- console.warn(`Template ${fileName} not found, falling back to German`);
- return this.loadTemplate(templateType, fileType, "de");
- }
- throw new Error(`Template file not found: ${fileName}. Error: ${error}`);
- }
- }
+ try {
+ return await readFile(filePath, "utf-8");
+ } catch (error) {
+ // Fallback to German if language-specific template doesn't exist
+ if (language !== "de") {
+ console.warn(`Template ${fileName} not found, falling back to German`);
+ return this.loadTemplate(templateType, fileType, "de");
+ }
+ throw new Error(`Template file not found: ${fileName}. Error: ${error}`);
+ }
+ }
- /**
- * Replace variables in template content using Handlebars-like syntax
- * Supports: {{variable}}, {{#if condition}}...{{/if}}, {{#each items}}...{{/each}}
- * @param {string} content - Template content with variables
- * @param {TemplateData} data - Data for variable substitution
- * @returns {string} Content with variables replaced
- * @private
- */
- private replaceVariables(content: string, data: TemplateData): string {
- let result = content;
+ /**
+ * Replace variables in template content using Handlebars-like syntax
+ * Supports: {{variable}}, {{#if condition}}...{{/if}}, {{#each items}}...{{/each}}
+ * @param {string} content - Template content with variables
+ * @param {TemplateData} data - Data for variable substitution
+ * @returns {string} Content with variables replaced
+ * @private
+ */
+ private replaceVariables(content: string, data: TemplateData): string {
+ let result = content;
- // Replace simple variables like {{variable}}
- result = result.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (match, path) => {
- const value = this.getNestedValue(data, path);
- return value !== undefined ? String(value) : match;
- });
+ // Replace simple variables like {{variable}}
+ result = result.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (match, path) => {
+ const value = this.getNestedValue(data, path);
+ return value !== undefined ? String(value) : match;
+ });
- // Replace conditional blocks like {{#if condition}}...{{/if}}
- result = result.replace(
- /\{\{#if\s+(\w+(?:\.\w+)*)\}\}([\s\S]*?)\{\{\/if\}\}/g,
- (match, path, block) => {
- const value = this.getNestedValue(data, path);
- return value ? block : "";
- }
- );
+ // Replace conditional blocks like {{#if condition}}...{{/if}}
+ result = result.replace(
+ /\{\{#if\s+(\w+(?:\.\w+)*)\}\}([\s\S]*?)\{\{\/if\}\}/g,
+ (match, path, block) => {
+ const value = this.getNestedValue(data, path);
+ return value ? block : "";
+ },
+ );
- // Replace loops like {{#each items}}...{{/each}}
- result = result.replace(
- /\{\{#each\s+(\w+(?:\.\w+)*)\}\}([\s\S]*?)\{\{\/each\}\}/g,
- (match, path, block) => {
- const items = this.getNestedValue(data, path);
- if (Array.isArray(items)) {
- return items.map((item) => this.replaceVariables(block, { ...data, item })).join("");
- }
- return "";
- }
- );
+ // Replace loops like {{#each items}}...{{/each}}
+ result = result.replace(
+ /\{\{#each\s+(\w+(?:\.\w+)*)\}\}([\s\S]*?)\{\{\/each\}\}/g,
+ (match, path, block) => {
+ const items = this.getNestedValue(data, path);
+ if (Array.isArray(items)) {
+ return items.map((item) => this.replaceVariables(block, { ...data, item })).join("");
+ }
+ return "";
+ },
+ );
- return result;
- }
+ return result;
+ }
- /**
- * Get nested value from object using dot notation (e.g., 'tenant.primaryColor')
- * @param {TemplateData} obj - Object to search in
- * @param {string} path - Dot-separated path to value
- * @returns {unknown} Value at path or undefined if not found
- * @private
- */
- private getNestedValue(obj: TemplateData, path: string): unknown {
- return path.split(".").reduce((current: unknown, key: string) => {
- return current && typeof current === "object" && key in current
- ? (current as Record)[key]
- : undefined;
- }, obj);
- }
+ /**
+ * Get nested value from object using dot notation (e.g., 'tenant.primaryColor')
+ * @param {TemplateData} obj - Object to search in
+ * @param {string} path - Dot-separated path to value
+ * @returns {unknown} Value at path or undefined if not found
+ * @private
+ */
+ private getNestedValue(obj: TemplateData, path: string): unknown {
+ return path.split(".").reduce((current: unknown, key: string) => {
+ return current && typeof current === "object" && key in current
+ ? (current as Record)[key]
+ : undefined;
+ }, obj);
+ }
}
/** Global template engine instance */
diff --git a/src/lib/server/email/templates/appointment-reminder.de.html b/src/lib/server/email/templates/appointment-reminder.de.html
index 8605990..44b6a65 100644
--- a/src/lib/server/email/templates/appointment-reminder.de.html
+++ b/src/lib/server/email/templates/appointment-reminder.de.html
@@ -1,121 +1,121 @@
-
-
-
- Terminerinnerung
-
-
-
-
-
+
+
+
+
Terminerinnerung
+
+
+
+
+
-
Terminerinnerung
+
Terminerinnerung
-
Hallo {{recipient.name}},
+
Hallo {{recipient.name}},
-
Dies ist eine Erinnerung an Ihren bevorstehenden Termin:
+
Dies ist eine Erinnerung an Ihren bevorstehenden Termin:
-
-
Termindetails
-
Datum: {{appointmentDate}}
-
Uhrzeit: {{appointmentTime}}
- {{#if location}}
-
Ort: {{location}}
- {{/if}} {{#if description}}
-
Beschreibung: {{description}}
- {{/if}}
-
+
+
Termindetails
+
Datum: {{appointmentDate}}
+
Uhrzeit: {{appointmentTime}}
+ {{#if location}}
+
Ort: {{location}}
+ {{/if}} {{#if description}}
+
Beschreibung: {{description}}
+ {{/if}}
+
-
+
-
-
-
+
+
+
diff --git a/src/lib/server/email/templates/appointment-reminder.en.html b/src/lib/server/email/templates/appointment-reminder.en.html
index 012a9cb..a75af40 100644
--- a/src/lib/server/email/templates/appointment-reminder.en.html
+++ b/src/lib/server/email/templates/appointment-reminder.en.html
@@ -1,121 +1,121 @@
-
-
-
- Appointment Reminder
-
-
-
-
-
+
+
+
+
Appointment Reminder
+
+
+
+
+
-
Appointment Reminder
+
Appointment Reminder
-
Hello {{recipient.name}},
+
Hello {{recipient.name}},
-
This is a reminder about your upcoming appointment:
+
This is a reminder about your upcoming appointment:
-
-
Appointment Details
-
Date: {{appointmentDate}}
-
Time: {{appointmentTime}}
- {{#if location}}
-
Location: {{location}}
- {{/if}} {{#if description}}
-
Description: {{description}}
- {{/if}}
-
+
+
Appointment Details
+
Date: {{appointmentDate}}
+
Time: {{appointmentTime}}
+ {{#if location}}
+
Location: {{location}}
+ {{/if}} {{#if description}}
+
Description: {{description}}
+ {{/if}}
+
-
+
-
-
-
+
+
+
diff --git a/src/lib/server/email/templates/confirmation.de.html b/src/lib/server/email/templates/confirmation.de.html
index 7a15d11..28a444e 100644
--- a/src/lib/server/email/templates/confirmation.de.html
+++ b/src/lib/server/email/templates/confirmation.de.html
@@ -1,174 +1,174 @@
-
-
-
- Registrierung bestätigen
-
-
+ .warning {
+ background-color: #fff3cd;
+ border: 1px solid #ffeaa7;
+ border-radius: 4px;
+ padding: 15px;
+ margin: 20px 0;
+ color: #856404;
+ }
+
+
-
-
-
+
+
+
-
-
Hallo,
+
+
Hallo,
-
- vielen Dank für Ihre Registrierung bei {{tenant.longName}} . Um Ihre
- Registrierung abzuschließen, verwenden Sie bitte den folgenden Bestätigungscode:
-
+
+ vielen Dank für Ihre Registrierung bei {{tenant.longName}} . Um Ihre
+ Registrierung abzuschließen, verwenden Sie bitte den folgenden Bestätigungscode:
+
-
-
Ihr Bestätigungscode:
-
{{confirmationCode}}
-
- Geben Sie diesen Code in das Bestätigungsfeld ein
-
-
Oder hier klicken.
-
+
+
Ihr Bestätigungscode:
+
{{confirmationCode}}
+
+ Geben Sie diesen Code in das Bestätigungsfeld ein
+
+
Oder hier klicken.
+
-
- Wichtiger Hinweis: Dieser Code ist nur für
- {{expirationMinutes}} Minuten gültig und kann nur einmal verwendet
- werden.
-
+
+ Wichtiger Hinweis: Dieser Code ist nur für
+ {{expirationMinutes}} Minuten gültig und kann nur einmal verwendet
+ werden.
+
-
- Falls Sie sich nicht bei {{tenant.longName}} registriert haben, können Sie diese E-Mail
- ignorieren.
-
+
+ Falls Sie sich nicht bei {{tenant.longName}} registriert haben, können Sie diese E-Mail
+ ignorieren.
+
-
Bei Fragen wenden Sie sich gerne an unser Support-Team.
+
Bei Fragen wenden Sie sich gerne an unser Support-Team.
-
- Mit freundlichen Grüßen
- Das {{tenant.longName}} Team
-
-
+
+ Mit freundlichen Grüßen
+ Das {{tenant.longName}} Team
+
+
-
-
-
+
+
+