Files
flatrender/backend/db/migrations/02_identity_users.sql
T
soroush.asadi 90ac0b81d1 feat: V2 microservices stack — backend services, gateway, JWT auth
Add full V2 architecture: identity, content, studio (.NET 10) and file,
render, notification, gateway (Go) services with vendored deps, plus DB
migrations, event/API contracts, and an init-db script.

Wire the Next.js frontend to the gateway: server-side JWT auth routes
(login/register/refresh/logout/me), gateway fetch helper, and session/
cookie/jwt helpers under src/lib.

Containerize the stack via docker-compose.v2.yml and per-service
Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and
MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via
next/font/local to avoid Google Fonts (geo-blocked).

Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 23:29:31 +03:30

228 lines
9.6 KiB
SQL

-- =====================================================================
-- IDENTITY SCHEMA — Part 2: Users, Auth, Sessions
-- =====================================================================
SET search_path TO identity, public;
CREATE TYPE register_mode AS ENUM ('Email','Mobile','Google','Telegram','SSO','Reseller');
CREATE TYPE gender_kind AS ENUM ('Male','Female','Other','PreferNotToSay');
-- ---------------------------------------------------------------------
-- users
-- ---------------------------------------------------------------------
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE RESTRICT,
-- Auth
email CITEXT,
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
email_verified_at TIMESTAMPTZ,
phone_number TEXT,
phone_country_code TEXT,
phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
phone_verified_at TIMESTAMPTZ,
password_hash TEXT, -- bcrypt; NULL for OAuth-only
password_set_at TIMESTAMPTZ,
last_password_reset_date TIMESTAMPTZ,
register_mode register_mode NOT NULL DEFAULT 'Email',
external_provider TEXT, -- google_oauth_subject etc.
external_provider_id TEXT,
-- Profile
full_name TEXT,
avatar_url TEXT,
birth_date DATE,
gender gender_kind,
national_code TEXT, -- Iran-specific
country_code TEXT,
company_name TEXT,
website_name TEXT,
slogan TEXT,
about_me TEXT,
method_of_introduction TEXT,
-- Balances (cents/rial; use BIGINT to avoid float)
balance_minor BIGINT NOT NULL DEFAULT 0,
affiliate_balance_minor BIGINT NOT NULL DEFAULT 0,
affiliate_owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
profit_percentage NUMERIC(5,2) NOT NULL DEFAULT 0,
-- Gamification (kept lean)
loyalty_score INT NOT NULL DEFAULT 0,
purple_point INT NOT NULL DEFAULT 0,
-- Render quotas (computed from plan + bonuses)
daily_remain_render_count INT NOT NULL DEFAULT 0,
max_daily_render_count INT NOT NULL DEFAULT 0,
parallel_rendering_ceiling INT NOT NULL DEFAULT 1,
user_daily_free_charge_sec INT NOT NULL DEFAULT 0,
daily_free_charge_reset_date TIMESTAMPTZ,
max_preview_duration_sec INT NOT NULL DEFAULT 30,
force_render_queue BOOLEAN NOT NULL DEFAULT FALSE,
remove_watermark_service BOOLEAN NOT NULL DEFAULT FALSE,
-- Telegram (legacy but kept)
telegram_id TEXT,
telegram_token TEXT,
telegram_token_expire_date TIMESTAMPTZ,
telegram_tell_me BOOLEAN NOT NULL DEFAULT FALSE,
telegram_reset_date TIMESTAMPTZ,
user_telegram_charge INT NOT NULL DEFAULT 0,
-- Comms preferences
email_tell_me BOOLEAN NOT NULL DEFAULT TRUE,
sms_tell_me BOOLEAN NOT NULL DEFAULT FALSE,
push_tell_me BOOLEAN NOT NULL DEFAULT TRUE,
-- Storage
storage_endpoint TEXT,
used_storage_bytes BIGINT NOT NULL DEFAULT 0,
-- Status
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
is_tenant_admin BOOLEAN NOT NULL DEFAULT FALSE, -- admin within a tenant
ban_account BOOLEAN NOT NULL DEFAULT FALSE,
ban_reason TEXT,
unblock_date TIMESTAMPTZ,
-- Activity
last_active_date TIMESTAMPTZ,
last_login_at TIMESTAMPTZ,
last_login_ip INET,
registered_with_mobile_app BOOLEAN NOT NULL DEFAULT FALSE,
register_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Misc
cid TEXT, -- legacy
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
-- Uniqueness scoped to tenant (so two tenants can have user@x.com independently)
CONSTRAINT uq_users_tenant_email UNIQUE (tenant_id, email),
CONSTRAINT uq_users_tenant_phone UNIQUE (tenant_id, phone_number),
CONSTRAINT uq_users_external UNIQUE (external_provider, external_provider_id)
);
CREATE INDEX idx_users_tenant ON users(tenant_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_email ON users(email) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_affiliate ON users(affiliate_owner_id) WHERE affiliate_owner_id IS NOT NULL;
CREATE INDEX idx_users_last_active ON users(last_active_date DESC);
CREATE INDEX idx_users_fullname_trgm ON users USING gin (full_name gin_trgm_ops);
CREATE TRIGGER tg_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- Now fix FK from tenant_api_keys.created_by_user_id
ALTER TABLE tenant_api_keys
ADD CONSTRAINT fk_api_keys_creator
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL;
-- ---------------------------------------------------------------------
-- user_sessions — JWT refresh tokens / device tracking
-- ---------------------------------------------------------------------
CREATE TABLE user_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
refresh_token_hash TEXT NOT NULL,
device_id TEXT,
device_name TEXT,
user_agent TEXT,
ip_address INET,
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX uq_sessions_token ON user_sessions(refresh_token_hash);
CREATE INDEX idx_sessions_user ON user_sessions(user_id) WHERE revoked_at IS NULL;
-- ---------------------------------------------------------------------
-- confirmation_tokens — email/phone verify, password reset, MFA
-- ---------------------------------------------------------------------
CREATE TYPE token_purpose AS ENUM (
'EmailVerification','PhoneVerification',
'PasswordReset','MfaSetup','Login','EmailChange'
);
CREATE TABLE confirmation_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
purpose token_purpose NOT NULL,
identifier CITEXT NOT NULL, -- email or phone being verified
next_identifier CITEXT, -- for email change
token_hash TEXT NOT NULL,
code TEXT, -- 6-digit OTP (hashed in token_hash)
is_consumed BOOLEAN NOT NULL DEFAULT FALSE,
consumed_at TIMESTAMPTZ,
try_count INT NOT NULL DEFAULT 0,
max_tries INT NOT NULL DEFAULT 5,
request_ip INET,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_conf_tokens_user ON confirmation_tokens(user_id, purpose) WHERE is_consumed = FALSE;
CREATE INDEX idx_conf_tokens_lookup ON confirmation_tokens(token_hash) WHERE is_consumed = FALSE;
-- ---------------------------------------------------------------------
-- push_subscriptions — PWA Web Push
-- ---------------------------------------------------------------------
CREATE TABLE push_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
endpoint TEXT NOT NULL,
p256dh_key TEXT NOT NULL,
auth_key TEXT NOT NULL,
user_agent TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
last_used_at TIMESTAMPTZ,
failure_count INT NOT NULL DEFAULT 0,
last_failure_at TIMESTAMPTZ,
last_failure_status INT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, endpoint)
);
CREATE INDEX idx_push_subs_user_active ON push_subscriptions(user_id) WHERE is_active = TRUE;
-- ---------------------------------------------------------------------
-- mfa_factors — TOTP, SMS, recovery codes
-- ---------------------------------------------------------------------
CREATE TYPE mfa_factor_type AS ENUM ('TOTP','SMS','Email','RecoveryCode');
CREATE TABLE mfa_factors (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
factor_type mfa_factor_type NOT NULL,
secret_encrypted TEXT,
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
label TEXT,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_mfa_user ON mfa_factors(user_id) WHERE is_verified = TRUE;