-- UTF-8 Encoding für deutsche Sonderzeichen sicherstellen SET client_encoding = 'UTF8'; -- Zeitzone auf Europe/Berlin setzen SET timezone = 'Europe/Berlin'; CREATE TABLE IF NOT EXISTS customers ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, is_fake BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, CONSTRAINT unique_email UNIQUE (email) ); CREATE TABLE IF NOT EXISTS licenses ( id SERIAL PRIMARY KEY, license_key TEXT UNIQUE NOT NULL, customer_id INTEGER REFERENCES customers(id), license_type TEXT NOT NULL, valid_from DATE NOT NULL, valid_until DATE NOT NULL, is_active BOOLEAN DEFAULT TRUE, is_fake BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS sessions ( id SERIAL PRIMARY KEY, license_id INTEGER REFERENCES licenses(id), license_key VARCHAR(60), -- Denormalized for performance session_id TEXT UNIQUE NOT NULL, username VARCHAR(50), computer_name VARCHAR(100), hardware_id VARCHAR(100), ip_address TEXT, user_agent TEXT, app_version VARCHAR(20), login_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Alias for started_at last_activity TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Alias for last_heartbeat logout_time TIMESTAMP WITH TIME ZONE, -- Alias for ended_at started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_heartbeat TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, ended_at TIMESTAMP WITH TIME ZONE, is_active BOOLEAN DEFAULT TRUE, active BOOLEAN DEFAULT TRUE -- Alias for is_active ); -- Audit-Log-Tabelle für Änderungsprotokolle CREATE TABLE IF NOT EXISTS audit_log ( id SERIAL PRIMARY KEY, timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, username TEXT NOT NULL, action TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id INTEGER, old_values JSONB, new_values JSONB, ip_address TEXT, user_agent TEXT, additional_info TEXT ); -- Index für bessere Performance bei Abfragen CREATE INDEX idx_audit_log_timestamp ON audit_log(timestamp DESC); CREATE INDEX idx_audit_log_username ON audit_log(username); CREATE INDEX idx_audit_log_entity ON audit_log(entity_type, entity_id); -- Backup-Historie-Tabelle CREATE TABLE IF NOT EXISTS backup_history ( id SERIAL PRIMARY KEY, filename TEXT NOT NULL, filepath TEXT NOT NULL, filesize BIGINT, backup_type TEXT NOT NULL, -- 'manual' oder 'scheduled' status TEXT NOT NULL, -- 'success', 'failed', 'in_progress' error_message TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, created_by TEXT NOT NULL, tables_count INTEGER, records_count INTEGER, duration_seconds NUMERIC, is_encrypted BOOLEAN DEFAULT TRUE ); -- Index für bessere Performance CREATE INDEX idx_backup_history_created_at ON backup_history(created_at DESC); CREATE INDEX idx_backup_history_status ON backup_history(status); -- Login-Attempts-Tabelle für Rate-Limiting CREATE TABLE IF NOT EXISTS login_attempts ( ip_address VARCHAR(45) PRIMARY KEY, attempt_count INTEGER DEFAULT 0, first_attempt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_attempt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, blocked_until TIMESTAMP WITH TIME ZONE NULL, last_username_tried TEXT, last_error_message TEXT ); -- Index für schnelle Abfragen CREATE INDEX idx_login_attempts_blocked_until ON login_attempts(blocked_until); CREATE INDEX idx_login_attempts_last_attempt ON login_attempts(last_attempt DESC); -- Migration: Füge created_at zu licenses hinzu, falls noch nicht vorhanden DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'licenses' AND column_name = 'created_at') THEN ALTER TABLE licenses ADD COLUMN created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP; -- Setze created_at für bestehende Einträge auf das valid_from Datum UPDATE licenses SET created_at = valid_from WHERE created_at IS NULL; END IF; END $$; -- ===================== RESOURCE POOL SYSTEM ===================== -- Haupttabelle für den Resource Pool CREATE TABLE IF NOT EXISTS resource_pools ( id SERIAL PRIMARY KEY, resource_type VARCHAR(20) NOT NULL CHECK (resource_type IN ('domain', 'ipv4', 'phone')), resource_value VARCHAR(255) NOT NULL, status VARCHAR(20) DEFAULT 'available' CHECK (status IN ('available', 'allocated', 'quarantine')), allocated_to_license INTEGER REFERENCES licenses(id) ON DELETE SET NULL, status_changed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, status_changed_by VARCHAR(50), quarantine_reason VARCHAR(100) CHECK (quarantine_reason IN ('abuse', 'defect', 'maintenance', 'blacklisted', 'expired', 'review', NULL)), quarantine_until TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, notes TEXT, is_fake BOOLEAN DEFAULT FALSE, UNIQUE(resource_type, resource_value) ); -- Resource History für vollständige Nachverfolgbarkeit CREATE TABLE IF NOT EXISTS resource_history ( id SERIAL PRIMARY KEY, resource_id INTEGER REFERENCES resource_pools(id) ON DELETE CASCADE, license_id INTEGER REFERENCES licenses(id) ON DELETE SET NULL, action VARCHAR(50) NOT NULL CHECK (action IN ('allocated', 'deallocated', 'quarantined', 'released', 'created', 'deleted')), action_by VARCHAR(50) NOT NULL, action_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, details JSONB, ip_address TEXT ); -- Resource Metrics für Performance-Tracking und ROI CREATE TABLE IF NOT EXISTS resource_metrics ( id SERIAL PRIMARY KEY, resource_id INTEGER REFERENCES resource_pools(id) ON DELETE CASCADE, metric_date DATE NOT NULL, usage_count INTEGER DEFAULT 0, performance_score DECIMAL(5,2) DEFAULT 0.00, cost DECIMAL(10,2) DEFAULT 0.00, revenue DECIMAL(10,2) DEFAULT 0.00, issues_count INTEGER DEFAULT 0, availability_percent DECIMAL(5,2) DEFAULT 100.00, UNIQUE(resource_id, metric_date) ); -- Zuordnungstabelle zwischen Lizenzen und Ressourcen CREATE TABLE IF NOT EXISTS license_resources ( id SERIAL PRIMARY KEY, license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE, resource_id INTEGER REFERENCES resource_pools(id) ON DELETE CASCADE, assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, assigned_by VARCHAR(50), is_active BOOLEAN DEFAULT TRUE, UNIQUE(license_id, resource_id) ); -- Erweiterung der licenses Tabelle um Resource-Counts DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'licenses' AND column_name = 'domain_count') THEN ALTER TABLE licenses ADD COLUMN domain_count INTEGER DEFAULT 1 CHECK (domain_count >= 0 AND domain_count <= 10), ADD COLUMN ipv4_count INTEGER DEFAULT 1 CHECK (ipv4_count >= 0 AND ipv4_count <= 10), ADD COLUMN phone_count INTEGER DEFAULT 1 CHECK (phone_count >= 0 AND phone_count <= 10); END IF; END $$; -- Erweiterung der licenses Tabelle um device_limit DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'licenses' AND column_name = 'device_limit') THEN ALTER TABLE licenses ADD COLUMN device_limit INTEGER DEFAULT 3 CHECK (device_limit >= 1 AND device_limit <= 10); END IF; END $$; -- Tabelle für Geräte-Registrierungen CREATE TABLE IF NOT EXISTS device_registrations ( id SERIAL PRIMARY KEY, license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE, hardware_id TEXT NOT NULL, device_name TEXT, operating_system TEXT, first_seen TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_seen TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, is_active BOOLEAN DEFAULT TRUE, deactivated_at TIMESTAMP WITH TIME ZONE, deactivated_by TEXT, ip_address TEXT, user_agent TEXT, UNIQUE(license_id, hardware_id) ); -- Indizes für device_registrations CREATE INDEX IF NOT EXISTS idx_device_license ON device_registrations(license_id); CREATE INDEX IF NOT EXISTS idx_device_hardware ON device_registrations(hardware_id); CREATE INDEX IF NOT EXISTS idx_device_active ON device_registrations(license_id, is_active) WHERE is_active = TRUE; -- Indizes für Performance CREATE INDEX IF NOT EXISTS idx_resource_status ON resource_pools(status); CREATE INDEX IF NOT EXISTS idx_resource_type_status ON resource_pools(resource_type, status); CREATE INDEX IF NOT EXISTS idx_resource_allocated ON resource_pools(allocated_to_license) WHERE allocated_to_license IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_resource_quarantine ON resource_pools(quarantine_until) WHERE status = 'quarantine'; CREATE INDEX IF NOT EXISTS idx_resource_history_date ON resource_history(action_at DESC); CREATE INDEX IF NOT EXISTS idx_resource_history_resource ON resource_history(resource_id); CREATE INDEX IF NOT EXISTS idx_resource_metrics_date ON resource_metrics(metric_date DESC); CREATE INDEX IF NOT EXISTS idx_license_resources_active ON license_resources(license_id) WHERE is_active = TRUE; -- Users table for authentication with password and 2FA support CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, email VARCHAR(100), totp_secret VARCHAR(32), totp_enabled BOOLEAN DEFAULT FALSE, backup_codes TEXT, -- JSON array of hashed backup codes created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_password_change TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, password_reset_token VARCHAR(64), password_reset_expires TIMESTAMP WITH TIME ZONE, failed_2fa_attempts INTEGER DEFAULT 0, last_failed_2fa TIMESTAMP WITH TIME ZONE ); -- Index for faster login lookups CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(password_reset_token) WHERE password_reset_token IS NOT NULL; -- Migration: Add is_fake column to licenses if it doesn't exist DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'licenses' AND column_name = 'is_fake') THEN ALTER TABLE licenses ADD COLUMN is_fake BOOLEAN DEFAULT FALSE; -- Mark all existing licenses as fake data UPDATE licenses SET is_fake = TRUE; -- Add index for better performance when filtering fake data CREATE INDEX idx_licenses_is_fake ON licenses(is_fake); END IF; END $$; -- Migration: Add is_fake column to customers if it doesn't exist DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'customers' AND column_name = 'is_fake') THEN ALTER TABLE customers ADD COLUMN is_fake BOOLEAN DEFAULT FALSE; -- Mark all existing customers as fake data UPDATE customers SET is_fake = TRUE; -- Add index for better performance CREATE INDEX idx_customers_is_fake ON customers(is_fake); END IF; END $$; -- Migration: Add is_fake column to resource_pools if it doesn't exist DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'resource_pools' AND column_name = 'is_fake') THEN ALTER TABLE resource_pools ADD COLUMN is_fake BOOLEAN DEFAULT FALSE; -- Mark all existing resources as fake data UPDATE resource_pools SET is_fake = TRUE; -- Add index for better performance CREATE INDEX idx_resource_pools_is_fake ON resource_pools(is_fake); END IF; END $$; -- Migration: Add missing columns to sessions table DO $$ BEGIN -- Add license_key column IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'sessions' AND column_name = 'license_key') THEN ALTER TABLE sessions ADD COLUMN license_key VARCHAR(60); END IF; -- Add username column IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'sessions' AND column_name = 'username') THEN ALTER TABLE sessions ADD COLUMN username VARCHAR(50); END IF; -- Add computer_name column IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'sessions' AND column_name = 'computer_name') THEN ALTER TABLE sessions ADD COLUMN computer_name VARCHAR(100); END IF; -- Add hardware_id column IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'sessions' AND column_name = 'hardware_id') THEN ALTER TABLE sessions ADD COLUMN hardware_id VARCHAR(100); END IF; -- Add app_version column IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'sessions' AND column_name = 'app_version') THEN ALTER TABLE sessions ADD COLUMN app_version VARCHAR(20); END IF; -- Add login_time as alias for started_at IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'sessions' AND column_name = 'login_time') THEN ALTER TABLE sessions ADD COLUMN login_time TIMESTAMP WITH TIME ZONE; UPDATE sessions SET login_time = started_at; END IF; -- Add last_activity as alias for last_heartbeat IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'sessions' AND column_name = 'last_activity') THEN ALTER TABLE sessions ADD COLUMN last_activity TIMESTAMP WITH TIME ZONE; UPDATE sessions SET last_activity = last_heartbeat; END IF; -- Add logout_time as alias for ended_at IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'sessions' AND column_name = 'logout_time') THEN ALTER TABLE sessions ADD COLUMN logout_time TIMESTAMP WITH TIME ZONE; UPDATE sessions SET logout_time = ended_at; END IF; -- Add active as alias for is_active IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'sessions' AND column_name = 'active') THEN ALTER TABLE sessions ADD COLUMN active BOOLEAN DEFAULT TRUE; UPDATE sessions SET active = is_active; END IF; END $$; -- ===================== LICENSE SERVER TABLES ===================== -- Following best practices: snake_case for DB fields, clear naming conventions -- Enable UUID extension CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- License tokens for offline validation CREATE TABLE IF NOT EXISTS license_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE, token VARCHAR(512) NOT NULL UNIQUE, hardware_id VARCHAR(255) NOT NULL, valid_until TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_validated TIMESTAMP, validation_count INTEGER DEFAULT 0 ); CREATE INDEX idx_token ON license_tokens(token); CREATE INDEX idx_hardware ON license_tokens(hardware_id); CREATE INDEX idx_valid_until ON license_tokens(valid_until); -- Heartbeat tracking with partitioning support CREATE TABLE IF NOT EXISTS license_heartbeats ( id BIGSERIAL, license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE, hardware_id VARCHAR(255) NOT NULL, ip_address INET, user_agent VARCHAR(500), app_version VARCHAR(50), timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, session_data JSONB, PRIMARY KEY (id, timestamp) ) PARTITION BY RANGE (timestamp); -- Create partitions for the current and next month CREATE TABLE IF NOT EXISTS license_heartbeats_2025_01 PARTITION OF license_heartbeats FOR VALUES FROM ('2025-01-01') TO ('2025-02-01'); CREATE TABLE IF NOT EXISTS license_heartbeats_2025_02 PARTITION OF license_heartbeats FOR VALUES FROM ('2025-02-01') TO ('2025-03-01'); -- Add June 2025 partition for current month CREATE TABLE IF NOT EXISTS license_heartbeats_2025_06 PARTITION OF license_heartbeats FOR VALUES FROM ('2025-06-01') TO ('2025-07-01'); CREATE INDEX idx_heartbeat_license_time ON license_heartbeats(license_id, timestamp DESC); CREATE INDEX idx_heartbeat_hardware_time ON license_heartbeats(hardware_id, timestamp DESC); -- Activation events tracking CREATE TABLE IF NOT EXISTS activation_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE, event_type VARCHAR(50) NOT NULL CHECK (event_type IN ('activation', 'deactivation', 'reactivation', 'transfer')), hardware_id VARCHAR(255), previous_hardware_id VARCHAR(255), ip_address INET, user_agent VARCHAR(500), success BOOLEAN DEFAULT true, error_message TEXT, metadata JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_license_events ON activation_events(license_id, created_at DESC); CREATE INDEX idx_event_type ON activation_events(event_type, created_at DESC); -- API rate limiting CREATE TABLE IF NOT EXISTS api_rate_limits ( id SERIAL PRIMARY KEY, api_key VARCHAR(255) NOT NULL UNIQUE, requests_per_minute INTEGER DEFAULT 60, requests_per_hour INTEGER DEFAULT 1000, requests_per_day INTEGER DEFAULT 10000, burst_size INTEGER DEFAULT 100, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Anomaly detection CREATE TABLE IF NOT EXISTS anomaly_detections ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), license_id INTEGER REFERENCES licenses(id), anomaly_type VARCHAR(100) NOT NULL CHECK (anomaly_type IN ('multiple_ips', 'rapid_hardware_change', 'suspicious_pattern', 'concurrent_use', 'geo_anomaly')), severity VARCHAR(20) NOT NULL CHECK (severity IN ('low', 'medium', 'high', 'critical')), details JSONB NOT NULL, detected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, resolved BOOLEAN DEFAULT false, resolved_at TIMESTAMP, resolved_by VARCHAR(255), action_taken TEXT ); CREATE INDEX idx_unresolved ON anomaly_detections(resolved, severity, detected_at DESC); CREATE INDEX idx_license_anomalies ON anomaly_detections(license_id, detected_at DESC); -- API clients for authentication CREATE TABLE IF NOT EXISTS api_clients ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), client_name VARCHAR(255) NOT NULL, api_key VARCHAR(255) NOT NULL UNIQUE, secret_key VARCHAR(255) NOT NULL, is_active BOOLEAN DEFAULT true, allowed_endpoints TEXT[], created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Feature flags for gradual rollout CREATE TABLE IF NOT EXISTS feature_flags ( id SERIAL PRIMARY KEY, feature_name VARCHAR(100) NOT NULL UNIQUE, is_enabled BOOLEAN DEFAULT false, rollout_percentage INTEGER DEFAULT 0 CHECK (rollout_percentage >= 0 AND rollout_percentage <= 100), whitelist_license_ids INTEGER[], blacklist_license_ids INTEGER[], created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Insert default feature flags INSERT INTO feature_flags (feature_name, is_enabled, rollout_percentage) VALUES ('anomaly_detection', true, 100), ('offline_tokens', true, 100), ('advanced_analytics', false, 0), ('geo_restriction', false, 0) ON CONFLICT (feature_name) DO NOTHING; -- Session management for concurrent use tracking CREATE TABLE IF NOT EXISTS active_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE, hardware_id VARCHAR(255) NOT NULL, session_token VARCHAR(512) NOT NULL UNIQUE, ip_address INET, started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL ); CREATE INDEX idx_session_license ON active_sessions(license_id); CREATE INDEX idx_session_expires ON active_sessions(expires_at); -- Update trigger for updated_at columns CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ language 'plpgsql'; CREATE TRIGGER update_api_rate_limits_updated_at BEFORE UPDATE ON api_rate_limits FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_api_clients_updated_at BEFORE UPDATE ON api_clients FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_feature_flags_updated_at BEFORE UPDATE ON feature_flags FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Function to automatically create monthly partitions for heartbeats CREATE OR REPLACE FUNCTION create_monthly_partition() RETURNS void AS $$ DECLARE start_date date; end_date date; partition_name text; BEGIN start_date := date_trunc('month', CURRENT_DATE + interval '1 month'); end_date := start_date + interval '1 month'; partition_name := 'license_heartbeats_' || to_char(start_date, 'YYYY_MM'); EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF license_heartbeats FOR VALUES FROM (%L) TO (%L)', partition_name, start_date, end_date); END; $$ LANGUAGE plpgsql; -- Migration: Add max_devices column to licenses if it doesn't exist DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'licenses' AND column_name = 'max_devices') THEN ALTER TABLE licenses ADD COLUMN max_devices INTEGER DEFAULT 3 CHECK (max_devices >= 1); END IF; END $$; -- Migration: Add expires_at column to licenses if it doesn't exist DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'licenses' AND column_name = 'expires_at') THEN ALTER TABLE licenses ADD COLUMN expires_at TIMESTAMP; -- Set expires_at based on valid_until for existing licenses UPDATE licenses SET expires_at = valid_until::timestamp WHERE expires_at IS NULL; END IF; END $$; -- Migration: Add features column to licenses if it doesn't exist DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'licenses' AND column_name = 'features') THEN ALTER TABLE licenses ADD COLUMN features TEXT[] DEFAULT '{}'; END IF; END $$; -- Migration: Add updated_at column to licenses if it doesn't exist DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'licenses' AND column_name = 'updated_at') THEN ALTER TABLE licenses ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP; CREATE TRIGGER update_licenses_updated_at BEFORE UPDATE ON licenses FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); END IF; END $$; -- Migration: Add device_type column to device_registrations table DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'device_registrations' AND column_name = 'device_type') THEN ALTER TABLE device_registrations ADD COLUMN device_type VARCHAR(50) DEFAULT 'unknown'; -- Update existing records to have a device_type based on operating system UPDATE device_registrations SET device_type = CASE WHEN operating_system ILIKE '%windows%' THEN 'desktop' WHEN operating_system ILIKE '%mac%' THEN 'desktop' WHEN operating_system ILIKE '%linux%' THEN 'desktop' WHEN operating_system ILIKE '%android%' THEN 'mobile' WHEN operating_system ILIKE '%ios%' THEN 'mobile' ELSE 'unknown' END WHERE device_type IS NULL OR device_type = 'unknown'; END IF; END $$; -- Client configuration table for Account Forger CREATE TABLE IF NOT EXISTS client_configs ( id SERIAL PRIMARY KEY, client_name VARCHAR(100) NOT NULL DEFAULT 'Account Forger', api_key VARCHAR(255) NOT NULL, heartbeat_interval INTEGER DEFAULT 30, -- seconds session_timeout INTEGER DEFAULT 60, -- seconds (2x heartbeat) current_version VARCHAR(20) NOT NULL, minimum_version VARCHAR(20) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- License sessions for single-session enforcement CREATE TABLE IF NOT EXISTS license_sessions ( id SERIAL PRIMARY KEY, license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE, hardware_id VARCHAR(255) NOT NULL, ip_address INET, client_version VARCHAR(20), session_token VARCHAR(255) UNIQUE NOT NULL, started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_heartbeat TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(license_id) -- Only one active session per license ); -- Session history for debugging CREATE TABLE IF NOT EXISTS session_history ( id SERIAL PRIMARY KEY, license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE, hardware_id VARCHAR(255) NOT NULL, ip_address INET, client_version VARCHAR(20), started_at TIMESTAMP, ended_at TIMESTAMP, end_reason VARCHAR(50) -- 'normal', 'timeout', 'forced', 'replaced' ); -- Create indexes for performance CREATE INDEX IF NOT EXISTS idx_license_sessions_license_id ON license_sessions(license_id); CREATE INDEX IF NOT EXISTS idx_license_sessions_last_heartbeat ON license_sessions(last_heartbeat); CREATE INDEX IF NOT EXISTS idx_session_history_license_id ON session_history(license_id); CREATE INDEX IF NOT EXISTS idx_session_history_ended_at ON session_history(ended_at); -- Insert default client configuration if not exists INSERT INTO client_configs (client_name, api_key, current_version, minimum_version) VALUES ('Account Forger', 'AF-' || gen_random_uuid()::text, '1.0.0', '1.0.0') ON CONFLICT DO NOTHING; -- ===================== SYSTEM API KEY TABLE ===================== -- Single API key for system-wide authentication CREATE TABLE IF NOT EXISTS system_api_key ( id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1), -- Ensures single row api_key VARCHAR(255) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, regenerated_at TIMESTAMP WITH TIME ZONE, last_used_at TIMESTAMP WITH TIME ZONE, usage_count INTEGER DEFAULT 0, created_by VARCHAR(50), regenerated_by VARCHAR(50) ); -- Function to generate API key with AF-YYYY- prefix CREATE OR REPLACE FUNCTION generate_api_key() RETURNS VARCHAR AS $$ DECLARE year_part VARCHAR(4); random_part VARCHAR(32); BEGIN year_part := to_char(CURRENT_DATE, 'YYYY'); random_part := upper(substring(md5(random()::text || clock_timestamp()::text) from 1 for 32)); RETURN 'AF-' || year_part || '-' || random_part; END; $$ LANGUAGE plpgsql; -- Initialize with a default API key if none exists INSERT INTO system_api_key (api_key, created_by) SELECT generate_api_key(), 'system' WHERE NOT EXISTS (SELECT 1 FROM system_api_key); -- Audit trigger for API key changes CREATE OR REPLACE FUNCTION audit_api_key_changes() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'UPDATE' AND OLD.api_key != NEW.api_key THEN INSERT INTO audit_log ( timestamp, username, action, entity_type, entity_id, old_values, new_values, additional_info ) VALUES ( CURRENT_TIMESTAMP, COALESCE(NEW.regenerated_by, 'system'), 'api_key_regenerated', 'system_api_key', NEW.id, jsonb_build_object('api_key', LEFT(OLD.api_key, 8) || '...'), jsonb_build_object('api_key', LEFT(NEW.api_key, 8) || '...'), 'API Key regenerated' ); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER audit_system_api_key_changes AFTER UPDATE ON system_api_key FOR EACH ROW EXECUTE FUNCTION audit_api_key_changes();