-- ===================================================================== -- 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;