253 lines
8.2 KiB
PL/PgSQL
253 lines
8.2 KiB
PL/PgSQL
-- ============================================
|
|
-- Migration: V20260120_001
|
|
-- Description: Migrate auth.sessions structure
|
|
-- Changes:
|
|
-- - Rename session_token -> token_hash (VARCHAR(64) -> VARCHAR(255))
|
|
-- - Change is_active (BOOLEAN) -> status (ENUM)
|
|
-- - Add device_name, browser, os, location columns
|
|
-- - Add revoked_at, revoked_reason columns
|
|
-- ============================================
|
|
|
|
-- UP Migration
|
|
BEGIN;
|
|
|
|
-- ============================================
|
|
-- 1. Check if we need to migrate (legacy structure)
|
|
-- ============================================
|
|
|
|
DO $$
|
|
DECLARE
|
|
has_session_token BOOLEAN;
|
|
has_is_active BOOLEAN;
|
|
has_status BOOLEAN;
|
|
BEGIN
|
|
-- Check for legacy column session_token
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'auth'
|
|
AND table_name = 'sessions'
|
|
AND column_name = 'session_token'
|
|
) INTO has_session_token;
|
|
|
|
-- Check for legacy column is_active
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'auth'
|
|
AND table_name = 'sessions'
|
|
AND column_name = 'is_active'
|
|
) INTO has_is_active;
|
|
|
|
-- Check if status column already exists
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'auth'
|
|
AND table_name = 'sessions'
|
|
AND column_name = 'status'
|
|
) INTO has_status;
|
|
|
|
-- Store results in temp table for use in subsequent statements
|
|
CREATE TEMP TABLE _migration_flags (
|
|
flag_name VARCHAR(50) PRIMARY KEY,
|
|
flag_value BOOLEAN
|
|
) ON COMMIT DROP;
|
|
|
|
INSERT INTO _migration_flags VALUES
|
|
('has_session_token', has_session_token),
|
|
('has_is_active', has_is_active),
|
|
('has_status', has_status);
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 2. Ensure session_status ENUM exists
|
|
-- ============================================
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'session_status' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'auth')) THEN
|
|
CREATE TYPE auth.session_status AS ENUM ('active', 'expired', 'revoked');
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 3. Rename session_token -> token_hash (if legacy)
|
|
-- ============================================
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM _migration_flags WHERE flag_name = 'has_session_token' AND flag_value = TRUE) THEN
|
|
-- Drop old index if exists
|
|
DROP INDEX IF EXISTS auth.idx_sessions_token;
|
|
|
|
-- Rename column
|
|
ALTER TABLE auth.sessions RENAME COLUMN session_token TO token_hash;
|
|
|
|
-- Expand varchar size (64 -> 255)
|
|
ALTER TABLE auth.sessions ALTER COLUMN token_hash TYPE VARCHAR(255);
|
|
|
|
-- Recreate index
|
|
CREATE INDEX idx_sessions_token ON auth.sessions(token_hash);
|
|
|
|
RAISE NOTICE 'Renamed session_token to token_hash and expanded to VARCHAR(255)';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 4. Convert is_active (BOOLEAN) -> status (ENUM)
|
|
-- ============================================
|
|
|
|
DO $$
|
|
DECLARE
|
|
has_is_active BOOLEAN;
|
|
has_status BOOLEAN;
|
|
BEGIN
|
|
SELECT flag_value INTO has_is_active FROM _migration_flags WHERE flag_name = 'has_is_active';
|
|
SELECT flag_value INTO has_status FROM _migration_flags WHERE flag_name = 'has_status';
|
|
|
|
IF has_is_active AND NOT has_status THEN
|
|
-- Add status column
|
|
ALTER TABLE auth.sessions ADD COLUMN status auth.session_status;
|
|
|
|
-- Migrate data: is_active = true -> 'active', is_active = false -> 'revoked'
|
|
UPDATE auth.sessions
|
|
SET status = CASE
|
|
WHEN is_active = true THEN 'active'::auth.session_status
|
|
ELSE 'revoked'::auth.session_status
|
|
END;
|
|
|
|
-- Mark expired sessions
|
|
UPDATE auth.sessions
|
|
SET status = 'expired'::auth.session_status
|
|
WHERE expires_at < NOW() AND status = 'active';
|
|
|
|
-- Set NOT NULL and default
|
|
ALTER TABLE auth.sessions ALTER COLUMN status SET NOT NULL;
|
|
ALTER TABLE auth.sessions ALTER COLUMN status SET DEFAULT 'active';
|
|
|
|
-- Drop old column
|
|
ALTER TABLE auth.sessions DROP COLUMN is_active;
|
|
|
|
-- Update indexes to use status instead of is_active
|
|
DROP INDEX IF EXISTS auth.idx_sessions_user;
|
|
DROP INDEX IF EXISTS auth.idx_sessions_tenant;
|
|
DROP INDEX IF EXISTS auth.idx_sessions_expires;
|
|
|
|
CREATE INDEX idx_sessions_user ON auth.sessions(user_id) WHERE status = 'active';
|
|
CREATE INDEX idx_sessions_tenant ON auth.sessions(tenant_id) WHERE status = 'active';
|
|
CREATE INDEX idx_sessions_expires ON auth.sessions(expires_at) WHERE status = 'active';
|
|
|
|
RAISE NOTICE 'Converted is_active to status enum';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 5. Add new columns if they don't exist
|
|
-- ============================================
|
|
|
|
-- device_name
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'auth' AND table_name = 'sessions' AND column_name = 'device_name'
|
|
) THEN
|
|
ALTER TABLE auth.sessions ADD COLUMN device_name VARCHAR(200);
|
|
RAISE NOTICE 'Added device_name column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- browser
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'auth' AND table_name = 'sessions' AND column_name = 'browser'
|
|
) THEN
|
|
ALTER TABLE auth.sessions ADD COLUMN browser VARCHAR(100);
|
|
RAISE NOTICE 'Added browser column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- os
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'auth' AND table_name = 'sessions' AND column_name = 'os'
|
|
) THEN
|
|
ALTER TABLE auth.sessions ADD COLUMN os VARCHAR(100);
|
|
RAISE NOTICE 'Added os column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- location
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'auth' AND table_name = 'sessions' AND column_name = 'location'
|
|
) THEN
|
|
ALTER TABLE auth.sessions ADD COLUMN location VARCHAR(200);
|
|
RAISE NOTICE 'Added location column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- revoked_at
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'auth' AND table_name = 'sessions' AND column_name = 'revoked_at'
|
|
) THEN
|
|
ALTER TABLE auth.sessions ADD COLUMN revoked_at TIMESTAMPTZ;
|
|
RAISE NOTICE 'Added revoked_at column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- revoked_reason
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'auth' AND table_name = 'sessions' AND column_name = 'revoked_reason'
|
|
) THEN
|
|
ALTER TABLE auth.sessions ADD COLUMN revoked_reason VARCHAR(100);
|
|
RAISE NOTICE 'Added revoked_reason column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 6. Update cleanup function to use status
|
|
-- ============================================
|
|
|
|
CREATE OR REPLACE FUNCTION auth.cleanup_expired_sessions()
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
deleted_count INTEGER;
|
|
BEGIN
|
|
WITH deleted AS (
|
|
DELETE FROM auth.sessions
|
|
WHERE expires_at < NOW() - INTERVAL '7 days'
|
|
OR (status = 'revoked' AND revoked_at < NOW() - INTERVAL '30 days')
|
|
RETURNING *
|
|
)
|
|
SELECT COUNT(*) INTO deleted_count FROM deleted;
|
|
|
|
RETURN deleted_count;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- ============================================
|
|
-- 7. Update comments
|
|
-- ============================================
|
|
|
|
COMMENT ON COLUMN auth.sessions.token_hash IS 'SHA256 hash of session token for secure storage';
|
|
COMMENT ON COLUMN auth.sessions.status IS 'Session status: active, expired, revoked';
|
|
COMMENT ON COLUMN auth.sessions.device_name IS 'Human-readable device name';
|
|
COMMENT ON COLUMN auth.sessions.browser IS 'Browser name and version';
|
|
COMMENT ON COLUMN auth.sessions.os IS 'Operating system name and version';
|
|
COMMENT ON COLUMN auth.sessions.location IS 'Geographic location (City, Country)';
|
|
COMMENT ON COLUMN auth.sessions.revoked_at IS 'Timestamp when session was revoked';
|
|
COMMENT ON COLUMN auth.sessions.revoked_reason IS 'Reason for revocation: logout, security, password_change, admin';
|
|
|
|
COMMIT;
|