feat: add tenant and user management with migrations, handlers, and tests
- Implemented tenant and user management features including: - Tenant creation and management with associated migrations. - User creation and management with associated migrations. - Tenant membership management with associated migrations. - Platform user roles management with associated migrations. - Quota management with associated migrations. - Article and template management with associated migrations. - Added HTTP handlers for templates and workspaces. - Created tests for protected and public routes. - Introduced a script to check tenant scope in SQL queries. - Documented task plan for backend completion and frontend foundation.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS tenants;
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE tenants (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
frozen_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_tenant_name_active ON tenants(name) WHERE deleted_at IS NULL;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
phone VARCHAR(20),
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
avatar_url TEXT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_users_email_active ON users(email) WHERE deleted_at IS NULL;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS tenant_memberships;
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE tenant_memberships (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
tenant_role VARCHAR(20) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_tenant_user_active ON tenant_memberships(tenant_id, user_id) WHERE deleted_at IS NULL;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS platform_user_roles;
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE platform_user_roles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
platform_role VARCHAR(20) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_platform_user_role_active ON platform_user_roles(user_id, platform_role) WHERE deleted_at IS NULL;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS plans;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE plans (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
plan_code VARCHAR(50) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
quota_policy_json JSONB NOT NULL DEFAULT '{}',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_plan_code_active ON plans(plan_code) WHERE deleted_at IS NULL;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS tenant_plan_subscriptions;
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE tenant_plan_subscriptions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
plan_id BIGINT NOT NULL REFERENCES plans(id),
|
||||
start_at TIMESTAMPTZ NOT NULL,
|
||||
end_at TIMESTAMPTZ NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_tenant_plan_active ON tenant_plan_subscriptions(tenant_id) WHERE status = 'active' AND deleted_at IS NULL;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS tenant_quota_ledgers;
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE tenant_quota_ledgers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
quota_type VARCHAR(50) NOT NULL,
|
||||
delta INT NOT NULL,
|
||||
balance_after INT NOT NULL,
|
||||
reason VARCHAR(255),
|
||||
reference_type VARCHAR(50),
|
||||
reference_id BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_tenant_quota_tenant_type_created ON tenant_quota_ledgers(tenant_id, quota_type, created_at DESC);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS quota_reservations;
|
||||
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE quota_reservations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
quota_type VARCHAR(50) NOT NULL,
|
||||
resource_type VARCHAR(50) NOT NULL,
|
||||
resource_id BIGINT NOT NULL,
|
||||
reserved_amount INT NOT NULL,
|
||||
consumed_amount INT NOT NULL DEFAULT 0,
|
||||
refunded_amount INT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
expire_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_quota_reservation_tenant_status ON quota_reservations(tenant_id, status);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS article_templates;
|
||||
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE article_templates (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
scope VARCHAR(20) NOT NULL,
|
||||
tenant_id BIGINT REFERENCES tenants(id),
|
||||
origin_type VARCHAR(20) NOT NULL DEFAULT 'platform',
|
||||
share_code_id BIGINT,
|
||||
template_key VARCHAR(100) NOT NULL,
|
||||
template_name VARCHAR(200) NOT NULL,
|
||||
schema_json JSONB NOT NULL DEFAULT '{}',
|
||||
prompt_template TEXT,
|
||||
prompt_visibility VARCHAR(20) NOT NULL DEFAULT 'visible',
|
||||
protected_prompt_asset_key TEXT,
|
||||
card_config_json JSONB NOT NULL DEFAULT '{}',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
version_no INT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_template_scope_key_version_active ON article_templates(scope, tenant_id, template_key, version_no) WHERE deleted_at IS NULL;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS articles;
|
||||
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE articles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
source_type VARCHAR(30) NOT NULL,
|
||||
template_id BIGINT REFERENCES article_templates(id),
|
||||
prompt_rule_id BIGINT,
|
||||
current_version_id BIGINT,
|
||||
generate_status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
publish_status VARCHAR(20) NOT NULL DEFAULT 'unpublished',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX idx_article_tenant_created ON articles(tenant_id, created_at DESC);
|
||||
CREATE INDEX idx_article_tenant_status ON articles(tenant_id, generate_status, publish_status, created_at DESC);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS article_versions;
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE article_versions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
article_id BIGINT NOT NULL REFERENCES articles(id),
|
||||
version_no INT NOT NULL,
|
||||
title VARCHAR(500),
|
||||
html_content TEXT,
|
||||
markdown_content TEXT,
|
||||
word_count INT NOT NULL DEFAULT 0,
|
||||
source_label VARCHAR(100),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uk_article_version_no UNIQUE (article_id, version_no)
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS generation_tasks;
|
||||
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE generation_tasks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
article_id BIGINT REFERENCES articles(id),
|
||||
task_batch_id VARCHAR(100),
|
||||
quota_reservation_id BIGINT REFERENCES quota_reservations(id),
|
||||
task_type VARCHAR(50) NOT NULL,
|
||||
request_hash VARCHAR(64),
|
||||
input_params_json JSONB,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'queued',
|
||||
error_message TEXT,
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_generation_task_status_created ON generation_tasks(status, created_at);
|
||||
CREATE INDEX idx_generation_task_tenant ON generation_tasks(tenant_id, created_at DESC);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS brands;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE brands (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_brand_tenant_name_active ON brands(tenant_id, name) WHERE deleted_at IS NULL;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS brand_keywords;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE brand_keywords (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
brand_id BIGINT NOT NULL REFERENCES brands(id),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_brand_keyword_name_active ON brand_keywords(brand_id, name) WHERE deleted_at IS NULL;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS brand_questions;
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE brand_questions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
brand_id BIGINT NOT NULL REFERENCES brands(id),
|
||||
keyword_id BIGINT NOT NULL REFERENCES brand_keywords(id),
|
||||
current_version_id BIGINT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX idx_brand_questions_keyword ON brand_questions(keyword_id);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS brand_question_versions;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE brand_question_versions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
question_id BIGINT NOT NULL REFERENCES brand_questions(id),
|
||||
question_text TEXT NOT NULL,
|
||||
question_hash VARCHAR(64) NOT NULL,
|
||||
version_no INT NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uk_question_version_no UNIQUE (question_id, version_no)
|
||||
);
|
||||
CREATE INDEX idx_question_hash ON brand_question_versions(question_hash);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS competitors;
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE competitors (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
brand_id BIGINT NOT NULL REFERENCES brands(id),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
website TEXT,
|
||||
description TEXT,
|
||||
product_lines_json JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX idx_competitor_brand ON competitors(brand_id);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS task_records;
|
||||
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE task_records (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
task_type VARCHAR(50) NOT NULL,
|
||||
resource_type VARCHAR(50) NOT NULL,
|
||||
resource_id BIGINT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
retry_count INT NOT NULL DEFAULT 0,
|
||||
task_batch_id VARCHAR(100),
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_task_record_status_type ON task_records(status, task_type);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS audit_logs;
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE audit_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
operator_id BIGINT NOT NULL REFERENCES users(id),
|
||||
tenant_id BIGINT,
|
||||
module VARCHAR(50) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
before_json JSONB,
|
||||
after_json JSONB,
|
||||
result VARCHAR(20),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_audit_module_created ON audit_logs(module, created_at DESC);
|
||||
Reference in New Issue
Block a user