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