mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-09-13 12:47:39 +02:00
Added feature flags for tenants
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "challenge_throttle" DROP CONSTRAINT "challenge_throttle_tenant_id_tenant_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "tenant" ADD COLUMN "features" json NOT NULL DEFAULT '[]';--> statement-breakpoint
|
||||
ALTER TABLE "challenge_throttle" DROP COLUMN "tenant_id";
|
||||
@@ -0,0 +1,843 @@
|
||||
{
|
||||
"id": "4a67faf9-fa46-4310-a127-1c5b8f12e3c0",
|
||||
"prevId": "8da6b050-6e61-46c0-8480-8b0c2ac62e55",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.challenge_throttle": {
|
||||
"name": "challenge_throttle",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"failed_attempts": {
|
||||
"name": "failed_attempts",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"last_attempt_at": {
|
||||
"name": "last_attempt_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"reset_at": {
|
||||
"name": "reset_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"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": "'SETTINGS'"
|
||||
},
|
||||
"links": {
|
||||
"name": "links",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::json"
|
||||
},
|
||||
"features": {
|
||||
"name": "features",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"domain": {
|
||||
"name": "domain",
|
||||
"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"]
|
||||
},
|
||||
"tenant_domain_unique": {
|
||||
"name": "tenant_domain_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["domain"]
|
||||
}
|
||||
},
|
||||
"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'"
|
||||
},
|
||||
"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": false
|
||||
},
|
||||
"invited_by": {
|
||||
"name": "invited_by",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"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
|
||||
},
|
||||
"passkey_id": {
|
||||
"name": "passkey_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"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": ["SETTINGS", "AGENTS", "CHANNELS", "STAFF", "READY"]
|
||||
},
|
||||
"public.user_role": {
|
||||
"name": "user_role",
|
||||
"schema": "public",
|
||||
"values": ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,13 @@
|
||||
"when": 1780252077820,
|
||||
"tag": "0013_abnormal_vengeance",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "7",
|
||||
"when": 1788247574429,
|
||||
"tag": "0014_green_moonstone",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -190,6 +190,10 @@
|
||||
"startEndDash": "Darf nicht mit einem Bindestrich beginnen oder enden"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Feature Flags",
|
||||
"placeholder": "Funktionen auswählen"
|
||||
},
|
||||
"domain": {
|
||||
"description": "Muss einmalig auf diesem Server sein.",
|
||||
"errors": {
|
||||
|
||||
@@ -198,6 +198,10 @@
|
||||
"startEndDash": "Must not start or end with a dash"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Feature Flags",
|
||||
"placeholder": "Select Features"
|
||||
},
|
||||
"domain": {
|
||||
"description": "Must be unique on this server.",
|
||||
"errors": {
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
export const SETUP_STATES_LIST = ["SETTINGS", "AGENTS", "CHANNELS", "STAFF", "READY"];
|
||||
|
||||
export const TENANT_FEATURE_FLAGS = [
|
||||
// List of supported features flags
|
||||
];
|
||||
|
||||
@@ -69,6 +69,8 @@ export const tenant = pgTable(
|
||||
.$type<{ imprint?: string; privacyStatement?: string; website?: string }>()
|
||||
.notNull()
|
||||
.default({}),
|
||||
/** Active features for this tenant (array of feature codes) */
|
||||
features: json("features").$type<string[]>().notNull(),
|
||||
/** Domain for the tenant */
|
||||
domain: text("domain").notNull().unique(),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
|
||||
@@ -22,6 +22,7 @@ type UpdateTenantBody = Partial<
|
||||
| "domain"
|
||||
| "links"
|
||||
| "defaultLanguage"
|
||||
| "features"
|
||||
>
|
||||
>;
|
||||
|
||||
@@ -41,6 +42,7 @@ const tenantCreationSchema = z.object({
|
||||
.max(253)
|
||||
.toLowerCase()
|
||||
.regex(/^([a-z0-9]+(-[a-z0-9]+)*\.)*[a-z0-9]+(-[a-z0-9]+)*$/, "Invalid domain format"),
|
||||
features: z.array(z.string().min(1).max(25)).optional(),
|
||||
});
|
||||
|
||||
export type TenantCreationRequest = z.infer<typeof tenantCreationSchema>;
|
||||
@@ -120,6 +122,7 @@ export class TenantAdminService {
|
||||
privacyStatement: "",
|
||||
},
|
||||
domain: request.domain,
|
||||
features: request.features || [],
|
||||
};
|
||||
newTenant.databaseUrl = urlParts.join("/") + "/" + newTenant.shortName;
|
||||
|
||||
@@ -243,6 +246,7 @@ export class TenantAdminService {
|
||||
logo: updateData.logo,
|
||||
links: updateData.links,
|
||||
domain: updateData.domain,
|
||||
features: updateData.features,
|
||||
};
|
||||
|
||||
const log = logger.setContext("TenantAdminService");
|
||||
@@ -298,6 +302,7 @@ export class TenantAdminService {
|
||||
longName: centralSchema.tenant.longName,
|
||||
descriptions: centralSchema.tenant.descriptions || {},
|
||||
domain: centralSchema.tenant.domain,
|
||||
features: centralSchema.tenant.features,
|
||||
logo: centralSchema.tenant.logo,
|
||||
setupState: centralSchema.tenant.setupState,
|
||||
links: centralSchema.tenant.links,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { SelectTenant } from "$lib/server/db/central-schema";
|
||||
|
||||
export type TTenant = Pick<
|
||||
SelectTenant,
|
||||
"id" | "shortName" | "languages" | "setupState" | "domain"
|
||||
"id" | "shortName" | "languages" | "setupState" | "domain" | "features"
|
||||
> & {
|
||||
logo: string | null;
|
||||
};
|
||||
|
||||
+67
-28
@@ -3,38 +3,48 @@
|
||||
import CheckboxWithLabel from "$lib/components/ui/checkbox-with-label/checkbox-with-label.svelte";
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { type Infer, superForm, type SuperValidated } from "sveltekit-superforms";
|
||||
import { zod4Client as zodClient } from "sveltekit-superforms/adapters";
|
||||
import { formSchema, type FormSchema } from ".";
|
||||
import { ERRORS } from "$lib/errors";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
import { zod4Client as zodClient } from "sveltekit-superforms/adapters";
|
||||
import { formSchema } from ".";
|
||||
import { TENANT_FEATURE_FLAGS } from "$lib/const/tenants";
|
||||
import { page } from "$app/state";
|
||||
|
||||
let { data, done }: { done: () => void; data: { form: SuperValidated<Infer<FormSchema>> } } =
|
||||
$props();
|
||||
let { done }: { done: () => void } = $props();
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm($state.snapshot(data.form), {
|
||||
validators: zodClient(formSchema),
|
||||
onResult: async (event) => {
|
||||
if (event.result.type === "success") {
|
||||
toast.success(m["tenants.add.success"]());
|
||||
done();
|
||||
} else if (event.result.type === "failure") {
|
||||
switch (event.result.data?.error) {
|
||||
case ERRORS.USERS.EMAIL_EXISTS:
|
||||
toast.error(m["tenants.add.errors.emailTaken"]());
|
||||
break;
|
||||
case ERRORS.TENANTS.NAME_EXISTS:
|
||||
toast.error(m["tenants.add.errors.nameTaken"]());
|
||||
break;
|
||||
default:
|
||||
toast.error(m["tenants.add.errors.unknown"]());
|
||||
}
|
||||
}
|
||||
isSubmitting = false;
|
||||
const form = superForm(
|
||||
{
|
||||
shortName: "",
|
||||
domain: "",
|
||||
features: [] as string[],
|
||||
inviteAdmin: false,
|
||||
email: "",
|
||||
},
|
||||
onSubmit: () => (isSubmitting = true),
|
||||
});
|
||||
{
|
||||
validators: zodClient(formSchema),
|
||||
onResult: async (event) => {
|
||||
if (event.result.type === "success") {
|
||||
toast.success(m["tenants.add.success"]());
|
||||
done();
|
||||
} else if (event.result.type === "failure") {
|
||||
switch (event.result.data?.error) {
|
||||
case ERRORS.USERS.EMAIL_EXISTS:
|
||||
toast.error(m["tenants.add.errors.emailTaken"]());
|
||||
break;
|
||||
case ERRORS.TENANTS.NAME_EXISTS:
|
||||
toast.error(m["tenants.add.errors.nameTaken"]());
|
||||
break;
|
||||
default:
|
||||
toast.error(m["tenants.add.errors.unknown"]());
|
||||
}
|
||||
}
|
||||
isSubmitting = false;
|
||||
},
|
||||
onSubmit: () => (isSubmitting = true),
|
||||
},
|
||||
);
|
||||
|
||||
let isSubmitting = $state(false);
|
||||
|
||||
@@ -59,6 +69,35 @@
|
||||
</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
{#if page.data.streamed.hasFeatureFlags}
|
||||
<Form.Field {form} name="features">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["tenants.add.features.title"]()}</Form.Label>
|
||||
<Select.Root
|
||||
type="multiple"
|
||||
bind:value={$formData.features}
|
||||
name={props.name}
|
||||
onValueChange={(v) => ($formData.features = v)}
|
||||
>
|
||||
<Select.Trigger {...props} class="w-full">
|
||||
{$formData.features.length > 0
|
||||
? $formData.features
|
||||
.map((id) => TENANT_FEATURE_FLAGS.find((x) => x === id))
|
||||
.join(", ")
|
||||
: m["tenants.add.features.placeholder"]()}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each TENANT_FEATURE_FLAGS as feature (feature)}
|
||||
<Select.Item value={feature}>{feature}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
{/if}
|
||||
<Form.Field {form} name="domain">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
|
||||
@@ -15,8 +15,9 @@ export const formSchema = z.object({
|
||||
.max(253)
|
||||
.toLowerCase()
|
||||
.regex(/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/, m["tenants.add.domain.errors.urlFormat"]()),
|
||||
features: z.array(z.string()).optional(),
|
||||
inviteAdmin: z.boolean(),
|
||||
email: z.string().email(m["form.errors.email"]()).optional().or(z.literal("")),
|
||||
email: z.email(m["form.errors.email"]()).optional().or(z.literal("")),
|
||||
});
|
||||
|
||||
export type FormSchema = typeof formSchema;
|
||||
|
||||
+33
-1
@@ -7,12 +7,15 @@
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
import { zod4Client as zodClient } from "sveltekit-superforms/adapters";
|
||||
import { formSchema } from ".";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { TENANT_FEATURE_FLAGS } from "$lib/const/tenants";
|
||||
import { page } from "$app/state";
|
||||
|
||||
let { entity, done }: { entity: TTenant; done: () => void } = $props();
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{ id: entity.id, domain: entity.domain },
|
||||
{ id: entity.id, domain: entity.domain, features: entity.features ?? ([] as string[]) },
|
||||
{
|
||||
validators: zodClient(formSchema),
|
||||
onResult: async (event) => {
|
||||
@@ -49,6 +52,35 @@
|
||||
</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
{#if page.data.streamed.hasFeatureFlags}
|
||||
<Form.Field {form} name="features">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["tenants.add.features.title"]()}</Form.Label>
|
||||
<Select.Root
|
||||
type="multiple"
|
||||
bind:value={$formData.features}
|
||||
name={props.name}
|
||||
onValueChange={(v) => ($formData.features = v)}
|
||||
>
|
||||
<Select.Trigger {...props} class="w-full">
|
||||
{$formData.features.length > 0
|
||||
? $formData.features
|
||||
.map((id) => TENANT_FEATURE_FLAGS.find((x) => x === id))
|
||||
.join(", ")
|
||||
: m["tenants.add.features.placeholder"]()}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each TENANT_FEATURE_FLAGS as feature (feature)}
|
||||
<Select.Item value={feature}>{feature}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
{/if}
|
||||
<Form.Field {form} name="id" class="hidden">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
|
||||
@@ -2,13 +2,14 @@ import { m } from "$i18n/messages";
|
||||
import { z } from "zod";
|
||||
|
||||
export const formSchema = z.object({
|
||||
id: z.string(),
|
||||
domain: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(253)
|
||||
.toLowerCase()
|
||||
.regex(/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/, m["tenants.add.domain.errors.urlFormat"]()),
|
||||
id: z.string(),
|
||||
features: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type FormSchema = typeof formSchema;
|
||||
|
||||
@@ -7,8 +7,10 @@ import { formSchema as addFormSchema } from "./(components)/add-tenant-form";
|
||||
import { formSchema as editFormSchema } from "./(components)/edit-tenant-form";
|
||||
import { formSchema as deleteFormSchema } from "./(components)/delete-tenant-form";
|
||||
import type { TTenant } from "$lib/types/tenant";
|
||||
import { env } from "$env/dynamic/private";
|
||||
|
||||
const log = logger.setContext(import.meta.filename);
|
||||
const hasFeatureFlags = env.ALLOW_FEATURE_FLAGS === "true";
|
||||
|
||||
export const load = async (event) => {
|
||||
const list = event
|
||||
@@ -34,9 +36,9 @@ export const load = async (event) => {
|
||||
});
|
||||
|
||||
return {
|
||||
form: await superValidate(zod(addFormSchema)),
|
||||
streamed: {
|
||||
list,
|
||||
hasFeatureFlags,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -63,6 +65,7 @@ export const actions: Actions = {
|
||||
body: JSON.stringify({
|
||||
shortName: form.data.shortName,
|
||||
domain: form.data.domain,
|
||||
...(hasFeatureFlags ? { features: form.data.features } : {}),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -129,7 +132,10 @@ export const actions: Actions = {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({ domain: form.data.domain }),
|
||||
body: JSON.stringify({
|
||||
domain: form.data.domain,
|
||||
...(hasFeatureFlags ? { features: form.data.features } : {}),
|
||||
}),
|
||||
});
|
||||
|
||||
if (resp.status < 400) {
|
||||
|
||||
@@ -59,7 +59,6 @@
|
||||
<PlusIcon /> {m["tenants.add.title"]()}
|
||||
{/snippet}
|
||||
<AddTenantForm
|
||||
{data}
|
||||
done={() => {
|
||||
invalidate(ROUTES.DASHBOARD.TENANTS);
|
||||
tenantsStore.reload();
|
||||
|
||||
@@ -35,6 +35,11 @@ registerOpenAPIRoute("/tenants", "POST", {
|
||||
description: "Domain for the tenant",
|
||||
example: "https://acme-corp.example.com",
|
||||
},
|
||||
features: {
|
||||
type: "string",
|
||||
description: "Feature flags for the tenant",
|
||||
example: "feature1,feature2",
|
||||
},
|
||||
},
|
||||
required: ["shortName", "domain"],
|
||||
},
|
||||
@@ -56,6 +61,7 @@ registerOpenAPIRoute("/tenants", "POST", {
|
||||
id: { type: "string", description: "Generated tenant ID" },
|
||||
shortName: { type: "string", description: "Tenant short name" },
|
||||
domain: { type: "string", format: "uri", description: "Tenant domain" },
|
||||
features: { type: "string", description: "Feature flags for the tenant" },
|
||||
},
|
||||
required: ["id", "shortName", "domain"],
|
||||
},
|
||||
@@ -67,6 +73,8 @@ registerOpenAPIRoute("/tenants", "POST", {
|
||||
tenant: {
|
||||
id: "01234567-89ab-cdef-0123-456789abcdef",
|
||||
shortName: "acme-corp",
|
||||
domain: "https://acme-corp.example.com",
|
||||
features: "feature1,feature2",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -117,6 +125,7 @@ registerOpenAPIRoute("/tenants", "GET", {
|
||||
longName: { type: "string", description: "Tenant long name" },
|
||||
logo: { type: "string", format: "uri", description: "URL of the tenant logo" },
|
||||
domain: { type: "string", format: "uri", description: "Tenant domain" },
|
||||
features: { type: "string", description: "Feature flags for the tenant" },
|
||||
setupState: {
|
||||
type: "string",
|
||||
enum: ["NEW", "SETTINGS_CREATED", "AGENTS_SET_UP", "FIRST_CHANNEL_CREATED"],
|
||||
@@ -135,6 +144,8 @@ registerOpenAPIRoute("/tenants", "GET", {
|
||||
id: "01234567-89ab-cdef-0123-456789abcdef",
|
||||
shortName: "acme-corp",
|
||||
longName: "ACME Corporation",
|
||||
domain: "https://acme-corp.example.com",
|
||||
features: "feature1,feature2",
|
||||
setupState: "FIRST_CHANNEL_CREATED",
|
||||
},
|
||||
],
|
||||
@@ -176,6 +187,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
||||
const tenantService = await TenantAdminService.createTenant({
|
||||
shortName: body.shortName,
|
||||
domain: body.domain,
|
||||
features: body.features,
|
||||
});
|
||||
|
||||
log.debug("Tenant created successfully", {
|
||||
@@ -225,6 +237,7 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
.select({
|
||||
id: tenant.id,
|
||||
domain: tenant.domain,
|
||||
features: tenant.features,
|
||||
shortName: tenant.shortName,
|
||||
languages: tenant.languages,
|
||||
setupState: tenant.setupState,
|
||||
@@ -246,6 +259,7 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
.select({
|
||||
id: tenant.id,
|
||||
domain: tenant.domain,
|
||||
features: tenant.features,
|
||||
shortName: tenant.shortName,
|
||||
languages: tenant.languages,
|
||||
setupState: tenant.setupState,
|
||||
|
||||
Reference in New Issue
Block a user