Move significant config to base tenant config

This commit is contained in:
Karl Ludwig Weise
2025-10-13 15:25:28 +02:00
parent ce0ac25b82
commit db67350f2d
19 changed files with 903 additions and 52 deletions
+2
View File
@@ -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;
+793
View File
@@ -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": {}
}
}
+7
View File
@@ -43,6 +43,13 @@
"when": 1759421310945,
"tag": "0005_confused_rockslide",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1760360156572,
"tag": "0006_legal_dracula",
"breakpoints": true
}
]
}
+1 -1
View File
@@ -565,7 +565,7 @@
"sections": {
"general": "Allgemein",
"address": "Adresse",
"legal": "Rechtliches",
"links": "Links",
"advanced": "Erweitert"
},
"fields": {
+1 -1
View File
@@ -574,7 +574,7 @@
"sections": {
"general": "General",
"address": "Address",
"legal": "Legal",
"links": "Links",
"advanced": "Advanced"
},
"fields": {
+7
View File
@@ -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<string[]>().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(),
},
@@ -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(),
@@ -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(),
};
@@ -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", () => {
@@ -89,7 +89,9 @@ describe("TenantAdminService", () => {
longName: "",
descriptions: { en: "" },
languages: ["en"],
defaultLanguage: "en",
description: "A test clinic",
links: { website: "", imprint: "", privacyStatement: "" },
};
const mockCreatedTenant = {
@@ -27,16 +27,12 @@ export type TenantCreationRequest = z.infer<typeof tenantCreationSchema>;
export interface TenantConfiguration extends Record<string, string | number | boolean> {
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) });
+2
View File
@@ -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(),
@@ -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"]()}
</Select.Trigger>
<Select.Content>
{#each $formData.defaultLanguage as language (language)}
{#each $formData.languages as language (language)}
<Select.Item value={language}>
{availableLocales.find((x) => x.key === language)?.label}
</Select.Item>
@@ -238,12 +239,12 @@
</Form.Field>
</div>
</FormGridItem>
<FormGridItem title={m["settings.form.sections.legal"]()}>
<Form.Field {form} name="legal.website" class="flex-3/5">
<FormGridItem title={m["settings.form.sections.links"]()}>
<Form.Field {form} name="links.website" class="flex-3/5">
<Form.Control>
{#snippet children({ props })}
<Form.Label>{m["settings.form.fields.website.title"]()}</Form.Label>
<Input {...props} bind:value={$formData.legal.website} type="text" />
<Input {...props} bind:value={$formData.links.website} type="text" />
{/snippet}
</Form.Control>
<Form.Description>
@@ -251,11 +252,11 @@
</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="legal.imprint" class="flex-3/5">
<Form.Field {form} name="links.imprint" class="flex-3/5">
<Form.Control>
{#snippet children({ props })}
<Form.Label>{m["settings.form.fields.imprint.title"]()}</Form.Label>
<Input {...props} bind:value={$formData.legal.imprint} type="text" />
<Input {...props} bind:value={$formData.links.imprint} type="text" />
{/snippet}
</Form.Control>
<Form.Description>
@@ -263,11 +264,11 @@
</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="legal.privacyStatement" class="flex-3/5">
<Form.Field {form} name="links.privacyStatement" class="flex-3/5">
<Form.Control>
{#snippet children({ props })}
<Form.Label>{m["settings.form.fields.privacyStatement.title"]()}</Form.Label>
<Input {...props} bind:value={$formData.legal.privacyStatement} type="text" />
<Input {...props} bind:value={$formData.links.privacyStatement} type="text" />
{/snippet}
</Form.Control>
<Form.Description>
@@ -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"],
});
}
});
@@ -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,
}),
});
-3
View File
@@ -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 });
@@ -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",
@@ -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",
-4
View File
@@ -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);