mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-10-01 21:24:51 +02:00
Untangle invite from user. Removed redundant data fields.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "user" DROP COLUMN "token";--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "token_valid_until";
|
||||
@@ -0,0 +1,853 @@
|
||||
{
|
||||
"id": "f0ff401a-6299-471e-9812-80c678b9d47e",
|
||||
"prevId": "4ab7df30-6490-4189-93b7-e1b25b0c8e90",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.challenge_throttle": {
|
||||
"name": "challenge_throttle",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"tenant_id": {
|
||||
"name": "tenant_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"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": {
|
||||
"challenge_throttle_tenant_id_tenant_id_fk": {
|
||||
"name": "challenge_throttle_tenant_id_tenant_id_fk",
|
||||
"tableFrom": "challenge_throttle",
|
||||
"tableTo": "tenant",
|
||||
"columnsFrom": ["tenant_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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": 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
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,13 @@
|
||||
"when": 1779177151355,
|
||||
"tag": "0011_nebulous_stephen_strange",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "7",
|
||||
"when": 1780157869249,
|
||||
"tag": "0012_polite_impossible_man",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@ const mockUser: SelectUser = {
|
||||
lastLoginAt: new Date(),
|
||||
isActive: true,
|
||||
confirmationState: "ACCESS_GRANTED" as const,
|
||||
token: null,
|
||||
tokenValidUntil: null,
|
||||
passphraseHash: null,
|
||||
recoveryPassphrase: null,
|
||||
language: "de",
|
||||
|
||||
@@ -122,8 +122,6 @@ export const user = pgTable(
|
||||
lastLoginAt: timestamp("last_login_at"),
|
||||
isActive: boolean("is_active").default(true),
|
||||
confirmationState: confirmationStateEnum("confirmation_state").default("INVITED"),
|
||||
token: text("token"),
|
||||
tokenValidUntil: timestamp("token_valid_until"),
|
||||
/** Hashed passphrase for password authentication (optional, alternative to WebAuthn) */
|
||||
passphraseHash: text("passphrase_hash"),
|
||||
/** Recovery passphrase for WebAuthn-only users (stored in plain text, shown only once) */
|
||||
|
||||
@@ -10,6 +10,7 @@ vi.mock("../../db", () => ({
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
transaction: vi.fn(),
|
||||
limit: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -89,16 +90,24 @@ describe("UserService", () => {
|
||||
name: "Test Admin",
|
||||
email: "test@example.com",
|
||||
language: "de" as const,
|
||||
role: "GLOBAL_ADMIN" as const,
|
||||
};
|
||||
|
||||
const mockCreatedAdminInvite = {
|
||||
id: "invite-123",
|
||||
tenantId: "018f-a1b2-c3d4-a5f6-789abcdef019",
|
||||
inviteCode: "018f-a1b2-c3d4-e5f6-789abcdef012",
|
||||
used: false,
|
||||
expiresAt: new Date("2024-01-01T12:10:00Z"),
|
||||
};
|
||||
|
||||
const mockCreatedAdmin = {
|
||||
id: "018f-a1b2-c3d4-e5f6-789abcdef012",
|
||||
name: "Test Admin",
|
||||
email: "test@example.com",
|
||||
token: "018f-a1b2-c3d4-e5f6-789abcdef012",
|
||||
tokenValidUntil: new Date("2024-01-01T12:10:00Z"),
|
||||
confirmationState: "INVITED" as const,
|
||||
isActive: false,
|
||||
confirmationState: "ACCESS_GRANTED" as const,
|
||||
role: "GLOBAL_ADMIN" as const,
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
const mockInsertBuilder = {
|
||||
@@ -106,17 +115,25 @@ describe("UserService", () => {
|
||||
returning: vi.fn().mockResolvedValue([mockCreatedAdmin]),
|
||||
};
|
||||
|
||||
mockCentralDb.insert.mockReturnValue(mockInsertBuilder);
|
||||
const mockInviteInsertBuilder = {
|
||||
values: vi.fn().mockReturnThis(),
|
||||
returning: vi.fn().mockResolvedValue([mockCreatedAdminInvite]),
|
||||
};
|
||||
|
||||
mockCentralDb.insert
|
||||
.mockReturnValueOnce(mockInviteInsertBuilder)
|
||||
.mockReturnValueOnce(mockInsertBuilder);
|
||||
|
||||
const result = await UserService.createUser(adminData);
|
||||
|
||||
expect(mockCentralDb.insert).toHaveBeenCalled();
|
||||
expect(mockInsertBuilder.values).toHaveBeenCalledWith({
|
||||
...adminData,
|
||||
token: "018f-a1b2-c3d4-e5f6-789abcdef012",
|
||||
tokenValidUntil: expect.any(Date),
|
||||
confirmationState: "INVITED" as const,
|
||||
isActive: false,
|
||||
confirmationState: "ACCESS_GRANTED" as const,
|
||||
isActive: true,
|
||||
role: "GLOBAL_ADMIN" as const,
|
||||
tenantId: undefined,
|
||||
recoveryPassphrase: expect.any(String),
|
||||
});
|
||||
expect(result).toEqual(mockCreatedAdmin);
|
||||
});
|
||||
@@ -178,6 +195,20 @@ describe("UserService", () => {
|
||||
limit: vi.fn().mockResolvedValue([{ id: "user-123", recoveryPassphrase: "recovery-123" }]),
|
||||
};
|
||||
|
||||
const mockUserInviteBuilder = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
limit: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "invite-123",
|
||||
tenantId: "tenant-123",
|
||||
inviteCode: token,
|
||||
used: false,
|
||||
expiresAt: new Date("2024-01-01T12:10:00Z"),
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const mockCountSelectBuilder = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockResolvedValue([{ count: 1 }]),
|
||||
@@ -195,6 +226,7 @@ describe("UserService", () => {
|
||||
|
||||
// First call for user lookup, second call for tenant admin count, third for total count
|
||||
mockCentralDb.select
|
||||
.mockReturnValueOnce(mockUserInviteBuilder)
|
||||
.mockReturnValueOnce(mockSelectBuilder)
|
||||
.mockReturnValueOnce(mockCountSelectBuilder)
|
||||
.mockReturnValueOnce(mockTotalCountSelectBuilder);
|
||||
@@ -202,7 +234,7 @@ describe("UserService", () => {
|
||||
|
||||
const result = await UserService.confirm(token);
|
||||
|
||||
expect(mockCentralDb.select).toHaveBeenCalledTimes(3);
|
||||
expect(mockCentralDb.select).toHaveBeenCalledTimes(4);
|
||||
expect(mockCentralDb.update).toHaveBeenCalled();
|
||||
expect(mockUpdateBuilder.set).toHaveBeenCalledWith({
|
||||
confirmationState: "ACCESS_GRANTED" as const,
|
||||
|
||||
@@ -33,7 +33,7 @@ export class InviteService {
|
||||
tenantId,
|
||||
invitedBy,
|
||||
language,
|
||||
expiresAt: sql`timezone('utc', now()) + interval '30 minutes'`,
|
||||
expiresAt: sql`timezone('utc', now()) + interval '10 minutes'`,
|
||||
used: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js";
|
||||
import { AppointmentService } from "./appointment-service";
|
||||
|
||||
export type InsertUser = InferInsertModel<typeof centralSchema.user>;
|
||||
export type InsertUserInvite = InferInsertModel<typeof centralSchema.userInvite>;
|
||||
export type InsertUserPasskey = InferInsertModel<typeof centralSchema.userPasskey>;
|
||||
export type UserTransaction = PgTransaction<
|
||||
PostgresJsQueryResultHKT,
|
||||
@@ -45,10 +46,8 @@ const userCreationSchema = z.object({
|
||||
name: z.string().min(5),
|
||||
email: z.email(),
|
||||
role: z.enum(["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"]).optional(),
|
||||
tenantId: z.string().uuid().optional(),
|
||||
tenantId: z.uuid().optional(),
|
||||
passphrase: z.string().min(12).optional(),
|
||||
token: z.uuidv7().optional(),
|
||||
tokenValidUntil: z.date().optional(),
|
||||
language: z.enum(["de", "en"]).optional().default("de"),
|
||||
confirmationState: z.enum(["INVITED", "CONFIRMED", "ACCESS_GRANTED"]).optional(),
|
||||
// Note: passphraseHash and recoveryPassphrase are handled internally, not via user input
|
||||
@@ -122,8 +121,15 @@ export class UserService {
|
||||
throw new ValidationError("Passphrase must be at least 12 characters long");
|
||||
}
|
||||
|
||||
userData.token = uuidv7();
|
||||
userData.tokenValidUntil = addMinutes(new Date(), 10);
|
||||
const userInviteForDb: InsertUserInvite = {
|
||||
email: userData.email,
|
||||
name: userData.name,
|
||||
role: userData.role!,
|
||||
tenantId: userData.tenantId!,
|
||||
invitedBy: "system",
|
||||
expiresAt: addMinutes(new Date(), 10),
|
||||
inviteCode: uuidv7(),
|
||||
};
|
||||
|
||||
// Prepare user data for database
|
||||
const userDataForDb: InsertUser = {
|
||||
@@ -131,8 +137,6 @@ export class UserService {
|
||||
email: userData.email,
|
||||
role: userData.role,
|
||||
tenantId: userData.tenantId,
|
||||
token: userData.token,
|
||||
tokenValidUntil: userData.tokenValidUntil,
|
||||
language: userData.language || "de",
|
||||
confirmationState: userData.confirmationState || "INVITED",
|
||||
isActive: false,
|
||||
@@ -162,43 +166,49 @@ export class UserService {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await centralDb.insert(centralSchema.user).values(userDataForDb).returning();
|
||||
const [inviteResult] = await centralDb
|
||||
.insert(centralSchema.userInvite)
|
||||
.values(userInviteForDb)
|
||||
.returning();
|
||||
const [insertedUser] = await centralDb
|
||||
.insert(centralSchema.user)
|
||||
.values(userDataForDb)
|
||||
.returning();
|
||||
|
||||
log.debug("User account created successfully", {
|
||||
userId: result[0].id,
|
||||
email: result[0].email,
|
||||
tokenValidUntil: result[0].tokenValidUntil,
|
||||
hasPassphrase: !!result[0].passphraseHash,
|
||||
hasRecoveryPassphrase: !!result[0].recoveryPassphrase,
|
||||
userId: insertedUser.id,
|
||||
email: insertedUser.email,
|
||||
hasPassphrase: !!insertedUser.passphraseHash,
|
||||
hasRecoveryPassphrase: !!insertedUser.recoveryPassphrase,
|
||||
});
|
||||
|
||||
// Send confirmation email to user (token is used as confirmation code)
|
||||
try {
|
||||
if (result[0].email && result[0].token) {
|
||||
const tenant = await getTenantForUser(result[0]);
|
||||
if (insertedUser?.email && inviteResult?.inviteCode) {
|
||||
const tenant = await getTenantForUser(insertedUser);
|
||||
await sendConfirmationEmail(
|
||||
result[0],
|
||||
insertedUser,
|
||||
tenant,
|
||||
result[0].token,
|
||||
inviteResult.inviteCode,
|
||||
10, // 10 minutes expiration to match tokenValidUntil
|
||||
requestUrl,
|
||||
);
|
||||
log.debug("Confirmation email sent successfully", {
|
||||
userId: result[0].id,
|
||||
email: result[0].email,
|
||||
tenantId: result[0].tenantId,
|
||||
userId: insertedUser.id,
|
||||
email: insertedUser.email,
|
||||
tenantId: insertedUser.tenantId,
|
||||
});
|
||||
}
|
||||
} catch (emailError) {
|
||||
log.warn("Failed to send confirmation email", {
|
||||
userId: result[0].id,
|
||||
email: result[0].email,
|
||||
userId: insertedUser?.id,
|
||||
email: insertedUser?.email,
|
||||
error: String(emailError),
|
||||
});
|
||||
// Don't throw - user creation succeeded, email is just a bonus
|
||||
}
|
||||
|
||||
return result[0];
|
||||
return insertedUser;
|
||||
} catch (error) {
|
||||
log.error("Failed to create user account", { email: userData.email, error: String(error) });
|
||||
throw error;
|
||||
@@ -294,7 +304,32 @@ export class UserService {
|
||||
}
|
||||
| undefined = undefined;
|
||||
|
||||
const userData = await centralDb
|
||||
const [matchingInvite] = await centralDb
|
||||
.select({
|
||||
id: centralSchema.userInvite.id,
|
||||
email: centralSchema.userInvite.email,
|
||||
tenantId: centralSchema.userInvite.tenantId,
|
||||
role: centralSchema.userInvite.role,
|
||||
name: centralSchema.userInvite.name,
|
||||
language: centralSchema.userInvite.language,
|
||||
})
|
||||
.from(centralSchema.userInvite)
|
||||
.where(
|
||||
and(
|
||||
eq(centralSchema.userInvite.inviteCode, linkToken),
|
||||
gt(centralSchema.userInvite.expiresAt, sql`timezone('utc', now())`),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!matchingInvite) {
|
||||
log.warn("User confirmation failed: Invalid or expired invite code", {
|
||||
token: linkToken.substring(0, 8) + "...",
|
||||
});
|
||||
throw new NotFoundError("Invalid or expired invite code");
|
||||
}
|
||||
|
||||
const [userData] = await centralDb
|
||||
.select({
|
||||
id: centralSchema.user.id,
|
||||
recoveryPassphrase: centralSchema.user.recoveryPassphrase,
|
||||
@@ -305,64 +340,37 @@ export class UserService {
|
||||
.from(centralSchema.user)
|
||||
.where(
|
||||
and(
|
||||
eq(centralSchema.user.token, linkToken),
|
||||
gt(centralSchema.user.tokenValidUntil, sql`timezone('utc', now())`),
|
||||
eq(centralSchema.user.email, matchingInvite.email),
|
||||
eq(centralSchema.user.tenantId, matchingInvite.tenantId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (userData.length === 0) {
|
||||
const inviteData = await centralDb
|
||||
.select({
|
||||
id: centralSchema.userInvite.id,
|
||||
tenantId: centralSchema.userInvite.tenantId,
|
||||
role: centralSchema.userInvite.role,
|
||||
email: centralSchema.userInvite.email,
|
||||
name: centralSchema.userInvite.name,
|
||||
language: centralSchema.userInvite.language,
|
||||
})
|
||||
.from(centralSchema.userInvite)
|
||||
.where(
|
||||
and(
|
||||
eq(centralSchema.userInvite.inviteCode, linkToken),
|
||||
gt(centralSchema.userInvite.expiresAt, sql`timezone('utc', now())`),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (!userData) {
|
||||
resultData = { ...matchingInvite, recoveryPassphrase: null };
|
||||
const userDataForDb: InsertUser = {
|
||||
name: resultData.name!,
|
||||
email: resultData.email,
|
||||
role: resultData.role,
|
||||
tenantId: resultData.tenantId,
|
||||
language: resultData.language || "de",
|
||||
recoveryPassphrase: "warum landet der hier",
|
||||
confirmationState: "CONFIRMED",
|
||||
isActive: true,
|
||||
};
|
||||
const retVal = await centralDb.insert(centralSchema.user).values(userDataForDb).returning();
|
||||
resultData.id = retVal[0].id;
|
||||
|
||||
if (inviteData.length === 0) {
|
||||
log.warn("User confirmation failed: Invalid or expired token", {
|
||||
token: linkToken.substring(0, 8) + "...",
|
||||
});
|
||||
throw new NotFoundError("Invalid or timed-out token");
|
||||
} else {
|
||||
resultData = { ...inviteData[0], recoveryPassphrase: null };
|
||||
const userDataForDb: InsertUser = {
|
||||
name: resultData.name!,
|
||||
email: resultData.email,
|
||||
role: resultData.role,
|
||||
tenantId: resultData.tenantId,
|
||||
language: resultData.language || "de",
|
||||
confirmationState: "CONFIRMED",
|
||||
isActive: true,
|
||||
};
|
||||
const retVal = await centralDb
|
||||
.insert(centralSchema.user)
|
||||
.values(userDataForDb)
|
||||
.returning();
|
||||
resultData.id = retVal[0].id;
|
||||
await InviteService.markInviteAsUsed(linkToken, resultData.id);
|
||||
log.debug("Invitation marked as used", {
|
||||
inviteCode: linkToken,
|
||||
userId: resultData.id,
|
||||
});
|
||||
|
||||
await InviteService.markInviteAsUsed(linkToken, resultData.id);
|
||||
log.debug("Invitation marked as used", {
|
||||
inviteCode: linkToken,
|
||||
userId: resultData.id,
|
||||
});
|
||||
|
||||
const adminService = await TenantAdminService.getTenantById(resultData.tenantId!);
|
||||
adminService.validateSetupState();
|
||||
}
|
||||
const adminService = await TenantAdminService.getTenantById(resultData.tenantId!);
|
||||
adminService.validateSetupState();
|
||||
} else {
|
||||
resultData = userData[0];
|
||||
resultData = userData;
|
||||
}
|
||||
|
||||
// Check if this is the first tenant admin for the tenant
|
||||
|
||||
@@ -212,6 +212,7 @@ export const POST: RequestHandler = async ({ request, locals, url }) => {
|
||||
invitedEmail: email,
|
||||
tenantId,
|
||||
role,
|
||||
language,
|
||||
});
|
||||
|
||||
return json({
|
||||
|
||||
Reference in New Issue
Block a user