90ac0b81d1
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>
228 lines
9.6 KiB
SQL
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;
|