diff --git a/migrations/0014_green_moonstone.sql b/migrations/0014_green_moonstone.sql new file mode 100644 index 0000000..307ebd9 --- /dev/null +++ b/migrations/0014_green_moonstone.sql @@ -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"; \ No newline at end of file diff --git a/migrations/meta/0014_snapshot.json b/migrations/meta/0014_snapshot.json new file mode 100644 index 0000000..88b4fae --- /dev/null +++ b/migrations/meta/0014_snapshot.json @@ -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": {} + } +} diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index e2fbc68..99b1a35 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -99,6 +99,13 @@ "when": 1780252077820, "tag": "0013_abnormal_vengeance", "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1788247574429, + "tag": "0014_green_moonstone", + "breakpoints": true } ] } diff --git a/project.inlang/messages/de.json b/project.inlang/messages/de.json index 320c2f0..4001c25 100644 --- a/project.inlang/messages/de.json +++ b/project.inlang/messages/de.json @@ -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": { diff --git a/project.inlang/messages/en.json b/project.inlang/messages/en.json index 581fea4..524d147 100644 --- a/project.inlang/messages/en.json +++ b/project.inlang/messages/en.json @@ -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": { diff --git a/src/lib/const/tenants.ts b/src/lib/const/tenants.ts index 4ce870d..5484034 100644 --- a/src/lib/const/tenants.ts +++ b/src/lib/const/tenants.ts @@ -1 +1,5 @@ export const SETUP_STATES_LIST = ["SETTINGS", "AGENTS", "CHANNELS", "STAFF", "READY"]; + +export const TENANT_FEATURE_FLAGS = [ + // List of supported features flags +]; diff --git a/src/lib/server/db/central-schema.ts b/src/lib/server/db/central-schema.ts index 896b037..ff29463 100644 --- a/src/lib/server/db/central-schema.ts +++ b/src/lib/server/db/central-schema.ts @@ -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().notNull(), /** Domain for the tenant */ domain: text("domain").notNull().unique(), createdAt: timestamp("created_at").defaultNow(), diff --git a/src/lib/server/services/tenant-admin-service.ts b/src/lib/server/services/tenant-admin-service.ts index f170f3e..15ecc43 100644 --- a/src/lib/server/services/tenant-admin-service.ts +++ b/src/lib/server/services/tenant-admin-service.ts @@ -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; @@ -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, diff --git a/src/lib/types/tenant.ts b/src/lib/types/tenant.ts index 97a3eb2..a2f95fd 100644 --- a/src/lib/types/tenant.ts +++ b/src/lib/types/tenant.ts @@ -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; }; diff --git a/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/add-tenant-form.svelte b/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/add-tenant-form.svelte index a76da32..056ead5 100644 --- a/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/add-tenant-form.svelte +++ b/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/add-tenant-form.svelte @@ -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> } } = - $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 @@ + {#if page.data.streamed.hasFeatureFlags} + + + {#snippet children({ props })} + {m["tenants.add.features.title"]()} + ($formData.features = v)} + > + + {$formData.features.length > 0 + ? $formData.features + .map((id) => TENANT_FEATURE_FLAGS.find((x) => x === id)) + .join(", ") + : m["tenants.add.features.placeholder"]()} + + + {#each TENANT_FEATURE_FLAGS as feature (feature)} + {feature} + {/each} + + + {/snippet} + + + + {/if} {#snippet children({ props })} diff --git a/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/schema.ts b/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/schema.ts index 883b5c0..b1f0d07 100644 --- a/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/schema.ts +++ b/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/schema.ts @@ -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; diff --git a/src/routes/(pages)/dashboard/tenants/(components)/edit-tenant-form/edit-tenant-form.svelte b/src/routes/(pages)/dashboard/tenants/(components)/edit-tenant-form/edit-tenant-form.svelte index 74e1214..9ad0623 100644 --- a/src/routes/(pages)/dashboard/tenants/(components)/edit-tenant-form/edit-tenant-form.svelte +++ b/src/routes/(pages)/dashboard/tenants/(components)/edit-tenant-form/edit-tenant-form.svelte @@ -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 @@ + {#if page.data.streamed.hasFeatureFlags} + + + {#snippet children({ props })} + {m["tenants.add.features.title"]()} + ($formData.features = v)} + > + + {$formData.features.length > 0 + ? $formData.features + .map((id) => TENANT_FEATURE_FLAGS.find((x) => x === id)) + .join(", ") + : m["tenants.add.features.placeholder"]()} + + + {#each TENANT_FEATURE_FLAGS as feature (feature)} + {feature} + {/each} + + + {/snippet} + + + + {/if}