-- ============================================================================= -- AURGANIZE V6.2 - SESSIONS TABLE (USER-ISOLATED, NO MULTI-TENANCY) -- ============================================================================= -- Migration: 000002_add_sessions -- Description: Creates sessions table for JWT refresh token lifecycle -- Author: Aurganize Team -- Date: 2025-12-11 -- Version: 2.1 (Aligned to Go Model, Tenant-less RLS Edition) -- ============================================================================= -- This migration creates the sessions table exactly matching Go models.Session. -- Multi-tenant isolation is intentionally removed (no tenant_id column). -- RLS still protects session rows, ensuring users cannot see other users’ data. -- ============================================================================= -- ============================================================================= -- SECTION 1: SESSIONS TABLE -- ============================================================================= CREATE TABLE sessions ( -- ====================================================================== -- IDENTITY COLUMNS -- ====================================================================== id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), -- Purpose: Unique identifier for a session -- Example: "a3bb189e-8bf9-4558-93c9-62cd9c8b9e5e" user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Purpose: Maps each session to a specific user -- Cascade delete ensures all user sessions are removed when user is deleted -- ====================================================================== -- AUTHENTICATION TOKENS -- ====================================================================== refresh_token_hash TEXT NOT NULL, -- Purpose: Stores bcrypt/SHA256 hash of refresh token (never plaintext) -- Security: Hash-only approach protects against DB compromise -- ====================================================================== -- DEVICE + CLIENT METADATA -- ====================================================================== user_agent TEXT, -- Purpose: Browser/device fingerprinting for security and activity display ip_address INET, -- Purpose: Track login-origin IP for anomaly detection device_name TEXT, -- Purpose: Optional user-friendly device label (e.g., "John's iPhone") device_type TEXT NOT NULL DEFAULT 'unknown', -- Purpose: Categorize device ("mobile", "desktop", "web", "unknown") -- ====================================================================== -- SESSION LIFECYCLE -- ====================================================================== expires_at TIMESTAMPTZ NOT NULL, -- Purpose: Refresh token expiry timestamp is_revoked BOOLEAN NOT NULL DEFAULT FALSE, -- Purpose: Marks a session as invalidated due to logout/security rules revoked_at TIMESTAMPTZ NULL, -- Purpose: Timestamp of revocation; NULL means active session revoked_reason TEXT, -- Purpose: Optional context ("logout", "password_change", "admin_action") -- ====================================================================== -- AUDIT TIMESTAMPS -- ====================================================================== created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Purpose: Login timestamp (never updated) last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- Purpose: Updated on every refresh token usage ); -- ============================================================================= -- SECTION 2: INDEXES FOR PERFORMANCE & SECURITY -- ============================================================================= -- Fast retrieval of all sessions for a user CREATE INDEX idx_sessions_user_id ON sessions(user_id); -- Lookup active sessions quickly CREATE INDEX idx_sessions_active ON sessions(user_id, is_revoked) WHERE is_revoked = FALSE; -- Cleanup expired sessions CREATE INDEX idx_sessions_expires_at ON sessions(expires_at) WHERE is_revoked = FALSE; -- IP anomaly investigations CREATE INDEX idx_sessions_ip_address ON sessions(ip_address) WHERE is_revoked = FALSE; -- Idle session detection CREATE INDEX idx_sessions_last_used ON sessions(last_used_at) WHERE is_revoked = FALSE; -- ============================================================================= -- SECTION 3: AUTOMATIC TRIGGERS -- ============================================================================= -- Note: updated_at column removed because the Go model does not include it. -- No trigger required. -- ============================================================================= -- SECTION 4: ROW LEVEL SECURITY (RLS) -- ============================================================================= -- Enable row-level isolation ALTER TABLE sessions ENABLE ROW LEVEL SECURITY; -- User-based isolation policy CREATE POLICY sessions_user_isolation ON sessions FOR ALL USING ( user_id = current_setting('app.current_user_id', true)::UUID ) WITH CHECK ( user_id = current_setting('app.current_user_id', true)::UUID ); COMMENT ON POLICY sessions_user_isolation ON sessions IS 'Restricts all session operations to the authenticated user (no tenant-level RLS).'; -- ============================================================================= -- SECTION 5: TABLE CONSTRAINTS & VALIDATION -- ============================================================================= -- Expiry must occur after creation ALTER TABLE sessions ADD CONSTRAINT sessions_valid_expiry CHECK (expires_at > created_at); -- Revoked_at cannot be before created_at ALTER TABLE sessions ADD CONSTRAINT sessions_valid_revocation CHECK (revoked_at IS NULL OR revoked_at >= created_at); -- ============================================================================= -- SECTION 6: COMMENTS FOR DOCUMENTATION -- ============================================================================= COMMENT ON TABLE sessions IS 'User authentication sessions with refresh token hashes. Exact match to Go models.Session (tenant-less).'; COMMENT ON COLUMN sessions.id IS 'Unique session ID (UUID v4).'; COMMENT ON COLUMN sessions.user_id IS 'User who owns this session.'; COMMENT ON COLUMN sessions.refresh_token_hash IS 'Hash of refresh token (never store plaintext).'; COMMENT ON COLUMN sessions.user_agent IS 'Client user-agent string.'; COMMENT ON COLUMN sessions.ip_address IS 'IP address at session creation.'; COMMENT ON COLUMN sessions.device_name IS 'Optional user-friendly device name.'; COMMENT ON COLUMN sessions.device_type IS 'Device category: mobile/desktop/web.'; COMMENT ON COLUMN sessions.expires_at IS 'Refresh token expiration timestamp.'; COMMENT ON COLUMN sessions.is_revoked IS 'TRUE when session has been explicitly revoked.'; COMMENT ON COLUMN sessions.revoked_at IS 'Timestamp of revocation event.'; COMMENT ON COLUMN sessions.revoked_reason IS 'Reason for revocation.'; COMMENT ON COLUMN sessions.created_at IS 'Timestamp when session was created.'; COMMENT ON COLUMN sessions.last_used_at IS 'Timestamp of last refresh token usage.'; -- ============================================================================= -- END OF MIGRATION 000002_add_sessions.up.sql -- =============================================================================