template-saas-database-v2/ddl/schemas/users/tables/01-users.sql
rckrdmrd 3ce06fbce4 Initial commit - Database de template-saas migrado desde monorepo
Migración desde workspace-v2/projects/template-saas/apps/database
Este repositorio es parte del estándar multi-repo v2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:07:11 -06:00

115 lines
3.9 KiB
PL/PgSQL

-- ============================================
-- TEMPLATE-SAAS: Users Table
-- Schema: users
-- Version: 1.0.0
-- ============================================
CREATE TABLE users.users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
-- Authentication
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255), -- NULL for OAuth-only users
email_verified BOOLEAN DEFAULT FALSE,
email_verified_at TIMESTAMPTZ,
-- Profile
first_name VARCHAR(100),
last_name VARCHAR(100),
display_name VARCHAR(200),
avatar_url VARCHAR(500),
phone VARCHAR(50),
phone_verified BOOLEAN DEFAULT FALSE,
-- Status
status users.user_status DEFAULT 'pending' NOT NULL,
is_owner BOOLEAN DEFAULT FALSE, -- Tenant owner
-- Security
mfa_enabled BOOLEAN DEFAULT FALSE,
mfa_secret VARCHAR(255),
mfa_backup_codes TEXT[], -- Hashed backup codes for MFA recovery
mfa_enabled_at TIMESTAMPTZ, -- When MFA was enabled
password_changed_at TIMESTAMPTZ,
failed_login_attempts INT DEFAULT 0,
locked_until TIMESTAMPTZ,
last_login_ip VARCHAR(45), -- IPv4 or IPv6 address
-- Preferences (JSONB)
preferences JSONB DEFAULT '{}'::jsonb,
-- Example:
-- {
-- "theme": "dark",
-- "language": "es",
-- "notifications": { "email": true, "push": true }
-- }
-- Metadata
metadata JSONB DEFAULT '{}'::jsonb,
-- Activity
last_login_at TIMESTAMPTZ,
last_activity_at TIMESTAMPTZ,
-- Audit
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
created_by UUID,
updated_by UUID,
deleted_at TIMESTAMPTZ,
-- Constraints
CONSTRAINT unique_email_per_tenant UNIQUE (tenant_id, email),
CONSTRAINT valid_email CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
);
-- Indexes
CREATE INDEX idx_users_tenant ON users.users(tenant_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_email ON users.users(tenant_id, email) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_status ON users.users(tenant_id, status) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_owner ON users.users(tenant_id) WHERE is_owner = TRUE AND deleted_at IS NULL;
-- RLS
ALTER TABLE users.users ENABLE ROW LEVEL SECURITY;
CREATE POLICY users_tenant_isolation_select ON users.users
FOR SELECT
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
CREATE POLICY users_tenant_isolation_insert ON users.users
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
CREATE POLICY users_tenant_isolation_update ON users.users
FOR UPDATE
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID)
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
CREATE POLICY users_tenant_isolation_delete ON users.users
FOR DELETE
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- Trigger for updated_at
CREATE OR REPLACE FUNCTION users.update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_users_updated_at
BEFORE UPDATE ON users.users
FOR EACH ROW
EXECUTE FUNCTION users.update_updated_at();
-- Comments
COMMENT ON TABLE users.users IS 'User accounts within tenants';
COMMENT ON COLUMN users.users.is_owner IS 'Tenant owner/admin flag';
COMMENT ON COLUMN users.users.password_hash IS 'bcrypt hashed password, NULL for OAuth users';
COMMENT ON COLUMN users.users.mfa_secret IS 'TOTP secret for 2FA (encrypted)';
COMMENT ON COLUMN users.users.mfa_backup_codes IS 'Array of hashed backup codes for MFA recovery';
COMMENT ON COLUMN users.users.mfa_enabled_at IS 'Timestamp when MFA was enabled';
COMMENT ON COLUMN users.users.last_login_ip IS 'IP address of last successful login';