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>
115 lines
3.9 KiB
PL/PgSQL
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';
|