template-saas/apps/database/ddl/schemas/users/tables/01-users.sql
rckrdmrd 26f0e52ca7 feat: Initial commit - template-saas
Template base para proyectos SaaS multi-tenant.

Estructura inicial:
- apps/backend (NestJS API)
- apps/frontend (React/Vite)
- apps/database (PostgreSQL DDL)
- docs/ (Documentación)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 04:41:24 -06:00

109 lines
3.4 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),
password_changed_at TIMESTAMPTZ,
failed_login_attempts INT DEFAULT 0,
locked_until TIMESTAMPTZ,
-- 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)';