diff --git a/migrations/0006_legal_dracula.sql b/migrations/0006_legal_dracula.sql new file mode 100644 index 0000000..3ea9a33 --- /dev/null +++ b/migrations/0006_legal_dracula.sql @@ -0,0 +1,2 @@ +ALTER TABLE "tenant" ADD COLUMN "defaultLanguage" text DEFAULT 'en' NOT NULL;--> statement-breakpoint +ALTER TABLE "tenant" ADD COLUMN "links" json DEFAULT '{}'::json NOT NULL; \ No newline at end of file diff --git a/migrations/meta/0006_snapshot.json b/migrations/meta/0006_snapshot.json new file mode 100644 index 0000000..e706131 --- /dev/null +++ b/migrations/meta/0006_snapshot.json @@ -0,0 +1,793 @@ +{ + "id": "c7e97210-1d91-4d46-942d-157da03e44e5", + "prevId": "27656a23-f444-413b-8c60-57b59232a615", + "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 + }, + "descriptions": { + "name": "descriptions", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "languages": { + "name": "languages", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "defaultLanguage": { + "name": "defaultLanguage", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "logo": { + "name": "logo", + "type": "varchar(100000)", + "primaryKey": false, + "notNull": false + }, + "database_url": { + "name": "database_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "setup_state": { + "name": "setup_state", + "type": "setup_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NEW'" + }, + "links": { + "name": "links", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "tenant_database_url_idx": { + "name": "tenant_database_url_idx", + "columns": [ + { + "expression": "database_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tenant_short_name_unique": { + "name": "tenant_short_name_unique", + "nullsNotDistinct": false, + "columns": ["short_name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant_config": { + "name": "tenant_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "config_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "tenant_config_tenant_name_idx": { + "name": "tenant_config_tenant_name_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tenant_config_tenant_id_tenant_id_fk": { + "name": "tenant_config_tenant_id_tenant_id_fk", + "tableFrom": "tenant_config", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'STAFF'" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "confirmation_state": { + "name": "confirmation_state", + "type": "confirmation_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'INVITED'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_valid_until": { + "name": "token_valid_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "passphrase_hash": { + "name": "passphrase_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recovery_passphrase": { + "name": "recovery_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'de'" + } + }, + "indexes": { + "user_email_idx": { + "name": "user_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_tenant_id_tenant_id_fk": { + "name": "user_tenant_id_tenant_id_fk", + "tableFrom": "user", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_invite": { + "name": "user_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_code": { + "name": "invite_code", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'de'" + }, + "used": { + "name": "used", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "used_at": { + "name": "used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_user_id": { + "name": "created_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_invite_code_idx": { + "name": "user_invite_code_idx", + "columns": [ + { + "expression": "invite_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_invite_email_idx": { + "name": "user_invite_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_invite_tenant_idx": { + "name": "user_invite_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_invite_tenant_id_tenant_id_fk": { + "name": "user_invite_tenant_id_tenant_id_fk", + "tableFrom": "user_invite", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_invite_invited_by_user_id_fk": { + "name": "user_invite_invited_by_user_id_fk", + "tableFrom": "user_invite", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_invite_created_user_id_user_id_fk": { + "name": "user_invite_created_user_id_user_id_fk", + "tableFrom": "user_invite", + "tableTo": "user", + "columnsFrom": ["created_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_invite_invite_code_unique": { + "name": "user_invite_invite_code_unique", + "nullsNotDistinct": false, + "columns": ["invite_code"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_passkey": { + "name": "user_passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_name": { + "name": "device_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_passkey_user_idx": { + "name": "user_passkey_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_passkey_user_id_user_id_fk": { + "name": "user_passkey_user_id_user_id_fk", + "tableFrom": "user_passkey", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_session": { + "name": "user_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "user_session_user_idx": { + "name": "user_session_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_session_token_idx": { + "name": "user_session_token_idx", + "columns": [ + { + "expression": "session_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_session_user_id_user_id_fk": { + "name": "user_session_user_id_user_id_fk", + "tableFrom": "user_session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_session_session_token_unique": { + "name": "user_session_session_token_unique", + "nullsNotDistinct": false, + "columns": ["session_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.config_type": { + "name": "config_type", + "schema": "public", + "values": ["BOOLEAN", "NUMBER", "STRING"] + }, + "public.confirmation_state": { + "name": "confirmation_state", + "schema": "public", + "values": ["INVITED", "CONFIRMED", "ACCESS_GRANTED"] + }, + "public.setup_state": { + "name": "setup_state", + "schema": "public", + "values": ["NEW", "SETTINGS_CREATED", "AGENTS_SET_UP", "FIRST_CHANNEL_CREATED"] + }, + "public.user_role": { + "name": "user_role", + "schema": "public", + "values": ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 7b9ab1a..6880a1e 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1759421310945, "tag": "0005_confused_rockslide", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1760360156572, + "tag": "0006_legal_dracula", + "breakpoints": true } ] } diff --git a/project.inlang/messages/de.json b/project.inlang/messages/de.json index 709e1ed..f2ccb6e 100644 --- a/project.inlang/messages/de.json +++ b/project.inlang/messages/de.json @@ -565,7 +565,7 @@ "sections": { "general": "Allgemein", "address": "Adresse", - "legal": "Rechtliches", + "links": "Links", "advanced": "Erweitert" }, "fields": { diff --git a/project.inlang/messages/en.json b/project.inlang/messages/en.json index c4e50e8..4adbba4 100644 --- a/project.inlang/messages/en.json +++ b/project.inlang/messages/en.json @@ -574,7 +574,7 @@ "sections": { "general": "General", "address": "Address", - "legal": "Legal", + "links": "Links", "advanced": "Advanced" }, "fields": { diff --git a/src/lib/server/db/central-schema.ts b/src/lib/server/db/central-schema.ts index 22d3877..2718aa0 100644 --- a/src/lib/server/db/central-schema.ts +++ b/src/lib/server/db/central-schema.ts @@ -54,12 +54,19 @@ export const tenant = pgTable( descriptions: json("descriptions").$type<{ [key: string]: string }>().notNull(), /** Active languages for this tenant (array of language codes) */ languages: json("languages").$type().notNull(), + /** Active languages for this tenant (array of language codes) */ + defaultLanguage: text("defaultLanguage").notNull().default("en"), /** Organization logo as binary data (PNG, JPEG, GIF, or WEBP) */ logo: varchar("logo", { length: 100_000 }), /** 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"), + /** Links (object) */ + links: json("links") + .$type<{ imprint?: string; privacyStatement?: string; website?: string }>() + .notNull() + .default({}), createdAt: timestamp("created_at").defaultNow(), updatedAt: timestamp("updated_at").defaultNow(), }, diff --git a/src/lib/server/email/__tests__/email-system.test.ts b/src/lib/server/email/__tests__/email-system.test.ts index 5eefe1b..432c196 100644 --- a/src/lib/server/email/__tests__/email-system.test.ts +++ b/src/lib/server/email/__tests__/email-system.test.ts @@ -109,7 +109,9 @@ describe("Email System", () => { longName: "Test Organization", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, databaseUrl: "postgresql://test", setupState: "FIRST_CHANNEL_CREATED" as const, createdAt: new Date(), @@ -140,7 +142,9 @@ describe("Email System", () => { longName: "Test Organization", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, databaseUrl: "postgresql://test", setupState: "FIRST_CHANNEL_CREATED" as const, createdAt: new Date(), @@ -171,7 +175,9 @@ describe("Email System", () => { longName: "Test Organization", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, databaseUrl: "postgresql://test", setupState: "FIRST_CHANNEL_CREATED" as const, createdAt: new Date(), @@ -202,7 +208,9 @@ describe("Email System", () => { longName: "Test Organization", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, databaseUrl: "postgresql://test", setupState: "FIRST_CHANNEL_CREATED" as const, createdAt: new Date(), @@ -258,7 +266,9 @@ describe("Email System", () => { longName: "Test Organization", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, databaseUrl: "postgresql://test", setupState: "FIRST_CHANNEL_CREATED" as const, createdAt: new Date(), @@ -322,7 +332,9 @@ describe("Email System", () => { longName: "Test Organization", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, databaseUrl: "postgresql://test", setupState: "FIRST_CHANNEL_CREATED" as const, createdAt: new Date(), @@ -362,7 +374,9 @@ describe("Email System", () => { longName: "Test Organization", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, databaseUrl: "postgresql://test", setupState: "FIRST_CHANNEL_CREATED" as const, createdAt: new Date(), @@ -396,7 +410,9 @@ describe("Email System", () => { longName: "Test Organization", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, databaseUrl: "postgresql://test", setupState: "FIRST_CHANNEL_CREATED" as const, createdAt: new Date(), @@ -430,7 +446,9 @@ describe("Email System", () => { longName: "Test Organization", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, databaseUrl: "postgresql://test", setupState: "FIRST_CHANNEL_CREATED" as const, createdAt: new Date(), @@ -483,7 +501,9 @@ describe("Email System", () => { longName: "Test Organization", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, databaseUrl: "postgresql://test", setupState: "FIRST_CHANNEL_CREATED" as const, createdAt: new Date(), @@ -527,7 +547,9 @@ describe("Email System", () => { longName: "Test Organization", descriptions: { en: "Tenant Description" }, languages: ["en"], + defaultLanguage: "en", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, databaseUrl: "postgresql://test", setupState: "FIRST_CHANNEL_CREATED" as const, createdAt: new Date(), 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 27eaba3..3ee2ef8 100644 --- a/src/lib/server/email/__tests__/generate-base-url.test.ts +++ b/src/lib/server/email/__tests__/generate-base-url.test.ts @@ -31,8 +31,10 @@ describe("generateBaseUrl", () => { updatedAt: new Date(), descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", + links: { website: "", imprint: "", privacyStatement: "" }, }; const result = generateBaseUrl(requestUrl, tenant); @@ -54,9 +56,11 @@ describe("generateBaseUrl", () => { longName: "Test Corp", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, createdAt: new Date(), updatedAt: new Date(), }; @@ -73,9 +77,11 @@ describe("generateBaseUrl", () => { longName: "ACME Corp", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, createdAt: new Date(), updatedAt: new Date(), }; @@ -92,9 +98,11 @@ describe("generateBaseUrl", () => { longName: "ACME Corp", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, createdAt: new Date(), updatedAt: new Date(), }; @@ -131,9 +139,11 @@ describe("generateBaseUrl", () => { longName: "ACME Corp", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, createdAt: new Date(), updatedAt: new Date(), }; @@ -150,9 +160,11 @@ describe("generateBaseUrl", () => { longName: "ACME Corp", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, createdAt: new Date(), updatedAt: new Date(), }; @@ -169,9 +181,11 @@ describe("generateBaseUrl", () => { longName: "New Tenant Corp", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, createdAt: new Date(), updatedAt: new Date(), }; @@ -188,9 +202,11 @@ describe("generateBaseUrl", () => { longName: "New Tenant Corp", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, createdAt: new Date(), updatedAt: new Date(), }; @@ -207,9 +223,11 @@ describe("generateBaseUrl", () => { longName: "Tenant Corp", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, createdAt: new Date(), updatedAt: new Date(), }; @@ -226,9 +244,11 @@ describe("generateBaseUrl", () => { longName: "ACME Corp", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, createdAt: new Date(), updatedAt: new Date(), }; @@ -245,9 +265,11 @@ describe("generateBaseUrl", () => { longName: "ACME Corp", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, createdAt: new Date(), updatedAt: new Date(), }; @@ -267,9 +289,11 @@ describe("generateBaseUrl", () => { longName: "ACME Corp", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, createdAt: new Date(), updatedAt: new Date(), }; @@ -287,9 +311,11 @@ describe("generateBaseUrl", () => { longName: "ACME Corp", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, createdAt: new Date(), updatedAt: new Date(), }; @@ -307,9 +333,11 @@ describe("generateBaseUrl", () => { longName: "ACME Corp", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "", setupState: "NEW", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, createdAt: new Date(), updatedAt: new Date(), }; 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 6286c57..fa34f58 100644 --- a/src/lib/server/email/__tests__/tenant-admin-invite.test.ts +++ b/src/lib/server/email/__tests__/tenant-admin-invite.test.ts @@ -8,11 +8,17 @@ describe("sendTenantAdminInviteEmail", () => { longName: "Test Corporation GmbH", descriptions: { en: "A test corporation" }, languages: ["en"], + defaultLanguage: "en", databaseUrl: "postgresql://test", setupState: "NEW" as const, createdAt: new Date(), updatedAt: new Date(), logo: null, + links: { + website: "https://testcorp.com", + imprint: "https://testcorp.com/imprint", + privacyStatement: "https://testcorp.com/privacy", + }, }; it("should accept correct parameters and not throw for German language", () => { diff --git a/src/lib/server/services/__tests__/tenant-admin-service.test.ts b/src/lib/server/services/__tests__/tenant-admin-service.test.ts index 7e50f94..dc85d73 100644 --- a/src/lib/server/services/__tests__/tenant-admin-service.test.ts +++ b/src/lib/server/services/__tests__/tenant-admin-service.test.ts @@ -89,7 +89,9 @@ describe("TenantAdminService", () => { longName: "", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", description: "A test clinic", + links: { website: "", imprint: "", privacyStatement: "" }, }; const mockCreatedTenant = { diff --git a/src/lib/server/services/tenant-admin-service.ts b/src/lib/server/services/tenant-admin-service.ts index f6f2ef1..b5847fd 100644 --- a/src/lib/server/services/tenant-admin-service.ts +++ b/src/lib/server/services/tenant-admin-service.ts @@ -27,16 +27,12 @@ export type TenantCreationRequest = z.infer; export interface TenantConfiguration extends Record { brandColor: string; - defaultLanguage: string; maxChannels: number; maxTeamMembers: number; autoDeleteDays: number; requireEmail: boolean; requirePhone: boolean; nextChannelColor: number; - website: string; - imprint: string; - privacyStatement: string; } export class TenantAdminService { @@ -49,16 +45,12 @@ export class TenantAdminService { static getConfigDefaults(): TenantConfiguration { return { brandColor: "#E11E15", - defaultLanguage: "DE", maxChannels: -1, maxTeamMembers: -1, autoDeleteDays: 30, requireEmail: true, requirePhone: false, nextChannelColor: 0, - website: "", - imprint: "", - privacyStatement: "", }; } @@ -104,6 +96,12 @@ export class TenantAdminService { databaseUrl: "", descriptions: { en: "" }, languages: ["en"], + defaultLanguage: "en", + links: { + website: "", + imprint: "", + privacyStatement: "", + }, }; newTenant.databaseUrl = urlParts.join("/") + "/" + newTenant.shortName; @@ -219,7 +217,11 @@ export class TenantAdminService { throw new NotFoundError(`Tenant with ID ${id} not found`); } - log.debug("Tenant service loaded successfully", { tenantId: id, data: tenant.#tenant }); + log.debug("Tenant service loaded successfully", { + tenantId: id, + // Do not log logo to not spam logs + data: { ...tenant.#tenant, logo: tenant.#tenant.logo ? "removed-from-log-but-set" : null }, + }); return tenant; } catch (error) { log.error("Failed to get tenant by ID", { tenantId: id, error: String(error) }); diff --git a/src/lib/server/services/user-service.ts b/src/lib/server/services/user-service.ts index 6189dbd..abb6358 100644 --- a/src/lib/server/services/user-service.ts +++ b/src/lib/server/services/user-service.ts @@ -45,7 +45,9 @@ function createSystemTenant(): SelectTenant { longName: "Open Reception", descriptions: { en: "Secure appointment booking platform" }, languages: ["en"], + defaultLanguage: "en", logo: null, + links: { website: "", imprint: "", privacyStatement: "" }, databaseUrl: "", setupState: "FIRST_CHANNEL_CREATED", createdAt: new Date(), diff --git a/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form.svelte b/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form.svelte index 2a360db..f5854ff 100644 --- a/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form.svelte +++ b/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form.svelte @@ -27,7 +27,6 @@ { key: 180, label: m["settings.form.fields.autoDeleteDays.options.180"]() }, { key: 365, label: m["settings.form.fields.autoDeleteDays.options.365"]() }, ]; - const tenantLocales = get(tenants).currentTenant?.languages ?? []; // TODO: allow update within form const availableLocales = supportedLocales.map((it) => ({ key: it, label: translatedLocales[it as keyof typeof translatedLocales], @@ -40,12 +39,14 @@ shortName: entity.shortName, longName: entity.longName, logo: entity.logo, - descriptions: tenantLocales.reduce( - (acc, locale) => ({ ...acc, [locale]: "" }), + descriptions: supportedLocales.reduce( + (acc, locale) => { + return { ...acc, [locale]: entity.descriptions[locale] || "" }; + }, {} as { [key: string]: string }, ), address: entity.address, - legal: entity.legal, + links: entity.links, settings: { autoDeleteDays: entity.settings.autoDeleteDays, requirePhone: entity.settings.requirePhone || false, @@ -131,7 +132,7 @@ : m["settings.form.fields.defaultLanguage.placeholder"]()} - {#each $formData.defaultLanguage as language (language)} + {#each $formData.languages as language (language)} {availableLocales.find((x) => x.key === language)?.label} @@ -238,12 +239,12 @@ - - + + {#snippet children({ props })} {m["settings.form.fields.website.title"]()} - + {/snippet} @@ -251,11 +252,11 @@ - + {#snippet children({ props })} {m["settings.form.fields.imprint.title"]()} - + {/snippet} @@ -263,11 +264,11 @@ - + {#snippet children({ props })} {m["settings.form.fields.privacyStatement.title"]()} - + {/snippet} diff --git a/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/schema.ts b/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/schema.ts index c0fbf14..d324e13 100644 --- a/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/schema.ts +++ b/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/schema.ts @@ -17,7 +17,7 @@ export const formSchema = z zip: z.string().min(4, { message: m["form.errors.zip"]() }), city: z.string().min(1, { message: m["form.errors.city"]() }), }), - legal: z.object({ + links: z.object({ website: z.string().url({ message: m["form.errors.url"]() }).optional().default(""), imprint: z.string().url({ message: m["form.errors.url"]() }).optional().default(""), privacyStatement: z.string().url({ message: m["form.errors.url"]() }).optional().default(""), @@ -32,7 +32,7 @@ export const formSchema = z ctx.addIssue({ code: z.ZodIssueCode.custom, message: m["settings.form.fields.defaultLanguage.errors.notInLanguages"](), - path: ["settings", "defaultLanguage"], + path: ["defaultLanguage"], }); } }); diff --git a/src/routes/(pages)/dashboard/settings/+page.server.ts b/src/routes/(pages)/dashboard/settings/+page.server.ts index f5e1d49..7910a95 100644 --- a/src/routes/(pages)/dashboard/settings/+page.server.ts +++ b/src/routes/(pages)/dashboard/settings/+page.server.ts @@ -34,15 +34,15 @@ export const load = async (event) => { return { id: body.tenant.id, languages: body.tenant.languages, - defaultLanguage: "en", // TODO: has to be moved to base + defaultLanguage: body.tenant.defaultLanguage, shortName: body.tenant.shortName, longName: body.tenant.longName, logo: body.tenant.logo || "", descriptions: body.tenant.descriptions, - legal: { - website: "https://example.com", // TODO: has to be moved to base - imprint: "https://example.com/imprint", // TODO: has to be moved to base - privacyStatement: "https://example.com/privacy", // TODO: has to be moved to base + links: { + website: body.tenant.links.website || "", + imprint: body.tenant.links.imprint || "", + privacyStatement: body.tenant.links.privacyStatement || "", }, }; } catch (error) { @@ -122,9 +122,7 @@ export const actions: Actions = { longName: form.data.longName, logo: form.data.logo, descriptions: removeEmptyTranslations(form.data.descriptions), - website: form.data.legal.website, - imprint: form.data.legal.imprint, - privacyStatement: form.data.legal.privacyStatement, + links: form.data.links, }), }); diff --git a/src/routes/api/tenants/[id]/+server.ts b/src/routes/api/tenants/[id]/+server.ts index 33ed714..29c3dc8 100644 --- a/src/routes/api/tenants/[id]/+server.ts +++ b/src/routes/api/tenants/[id]/+server.ts @@ -444,8 +444,6 @@ export const GET: RequestHandler = async ({ params, locals }) => { try { const tenantId = params.id; - console.log("🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈"); - console.log("tenantId", tenantId); if (!tenantId) { throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID); @@ -460,7 +458,6 @@ export const GET: RequestHandler = async ({ params, locals }) => { const tenantService = await TenantAdminService.getTenantById(tenantId); const tenantData = tenantService.tenantData; - console.log("tenantData", tenantData); log.debug("Retrieved tenant data", { tenantService }); diff --git a/src/routes/api/tenants/[id]/config/+server.ts b/src/routes/api/tenants/[id]/config/+server.ts index be2758f..6c99d97 100644 --- a/src/routes/api/tenants/[id]/config/+server.ts +++ b/src/routes/api/tenants/[id]/config/+server.ts @@ -30,11 +30,6 @@ registerOpenAPIRoute("/tenants/{id}/config", "GET", { type: "object", properties: { brandColor: { type: "string", description: "Brand color", example: "#E11E15" }, - defaultLanguage: { - type: "string", - description: "Default language code", - example: "DE", - }, maxChannels: { type: "number", description: "Maximum channels (-1 for unlimited)", @@ -108,7 +103,6 @@ registerOpenAPIRoute("/tenants/{id}/config", "PUT", { type: "object", properties: { brandColor: { type: "string", description: "Brand color" }, - defaultLanguage: { type: "string", description: "Default language code" }, maxChannels: { type: "number", description: "Maximum channels (-1 for unlimited)" }, maxTeamMembers: { type: "number", diff --git a/src/routes/api/tenants/config/defaults/+server.ts b/src/routes/api/tenants/config/defaults/+server.ts index 8643c58..97815de 100644 --- a/src/routes/api/tenants/config/defaults/+server.ts +++ b/src/routes/api/tenants/config/defaults/+server.ts @@ -22,11 +22,6 @@ registerOpenAPIRoute("/tenants/config/defaults", "GET", { description: "Default brand color", example: "#E11E15", }, - defaultLanguage: { - type: "string", - description: "Default language code", - example: "DE", - }, maxChannels: { type: "number", description: "Maximum channels (-1 for unlimited)", @@ -55,7 +50,6 @@ registerOpenAPIRoute("/tenants/config/defaults", "GET", { }, required: [ "brandColor", - "defaultLanguage", "maxChannels", "maxTeamMembers", "autoDeleteDays", diff --git a/src/routes/api/tenants/tenants.test.ts b/src/routes/api/tenants/tenants.test.ts index 9a5cb21..7ec85e1 100644 --- a/src/routes/api/tenants/tenants.test.ts +++ b/src/routes/api/tenants/tenants.test.ts @@ -276,16 +276,12 @@ describe("/api/tenants", () => { const mockDefaults = { brandColor: "#E11E15", - defaultLanguage: "DE", maxChannels: -1, maxTeamMembers: -1, autoDeleteDays: 30, requireEmail: true, requirePhone: false, nextChannelColor: 0, - website: "", - imprint: "", - privacyStatement: "", }; vi.mocked(TenantAdminService.getConfigDefaults).mockReturnValue(mockDefaults);