-- Enable required extensions CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Update function for updated_at timestamps CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Update function for last_accessed CREATE OR REPLACE FUNCTION update_last_accessed() RETURNS TRIGGER AS $$ BEGIN NEW.last_accessed = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; -- ========================================== -- USERS TABLE -- ========================================== CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, role VARCHAR(50) DEFAULT 'admin', is_active BOOLEAN DEFAULT TRUE, mfa_enabled BOOLEAN DEFAULT FALSE, mfa_secret VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP, failed_login_attempts INTEGER DEFAULT 0, locked_until TIMESTAMP ); CREATE TRIGGER users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -- ========================================== -- SESSIONS TABLE -- ========================================== CREATE TABLE IF NOT EXISTS sessions ( id VARCHAR(255) PRIMARY KEY, user_id INTEGER, data JSONB DEFAULT '{}', expires_at TIMESTAMP NOT NULL, last_accessed TIMESTAMP DEFAULT CURRENT_TIMESTAMP, user_agent TEXT, ip_address INET, is_remember_me BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TRIGGER sessions_accessed BEFORE UPDATE ON sessions FOR EACH ROW EXECUTE FUNCTION update_last_accessed(); CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id); CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at); -- ========================================== -- CSRF TOKENS TABLE -- ========================================== CREATE TABLE IF NOT EXISTS csrf_tokens ( id VARCHAR(255) PRIMARY KEY, user_id INTEGER, session_id VARCHAR(255) NOT NULL, token_hash VARCHAR(255) UNIQUE NOT NULL, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_csrf_tokens_session ON csrf_tokens(session_id); CREATE INDEX IF NOT EXISTS idx_csrf_tokens_expires ON csrf_tokens(expires_at); -- ========================================== -- MFA BACKUP CODES TABLE -- ========================================== CREATE TABLE IF NOT EXISTS mfa_backup_codes ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, code_hash VARCHAR(255) NOT NULL, is_used BOOLEAN DEFAULT FALSE, used_at TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_mfa_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_mfa_backup_codes_user ON mfa_backup_codes(user_id); CREATE INDEX IF NOT EXISTS idx_mfa_backup_codes_unused ON mfa_backup_codes(user_id, is_used) WHERE is_used = FALSE; -- ========================================== -- SESSION AUDIT LOG TABLE -- ========================================== CREATE TABLE IF NOT EXISTS session_audit_log ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, session_id VARCHAR(255), event VARCHAR(50) CHECK (event IN ('login', 'logout', 'expired', 'created', 'destroyed', 'mfa_enabled', 'mfa_disabled')) NOT NULL, ip_address INET, user_agent TEXT, metadata JSONB DEFAULT '{}', timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_audit_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_audit_user ON session_audit_log(user_id); CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON session_audit_log(timestamp DESC); -- ========================================== -- CONFIG TABLE -- ========================================== CREATE TABLE IF NOT EXISTS config ( id SERIAL PRIMARY KEY, key VARCHAR(255) UNIQUE NOT NULL, value JSONB NOT NULL, description TEXT, category VARCHAR(100) DEFAULT 'general', is_encrypted BOOLEAN DEFAULT FALSE, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_by INTEGER ); CREATE TRIGGER config_updated_at BEFORE UPDATE ON config FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE INDEX IF NOT EXISTS idx_config_category ON config(category); CREATE INDEX IF NOT EXISTS idx_config_key ON config(key); -- ========================================== -- CUSTOMERS TABLE -- ========================================== CREATE TABLE IF NOT EXISTS customers ( id SERIAL PRIMARY KEY, sap_customer_id VARCHAR(255), plesk_customer_id VARCHAR(255), name VARCHAR(255) NOT NULL, email VARCHAR(255), status VARCHAR(50) CHECK (status IN ('active', 'inactive', 'pending', 'deleted')) DEFAULT 'active', sync_status VARCHAR(50) CHECK (sync_status IN ('in_sync', 'pending_sync', 'sync_error', 'manual_override')) DEFAULT 'pending_sync', metadata JSONB DEFAULT '{}', last_sync TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TRIGGER customers_updated_at BEFORE UPDATE ON customers FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE UNIQUE INDEX IF NOT EXISTS idx_customer_mapping ON customers(sap_customer_id, plesk_customer_id) WHERE sap_customer_id IS NOT NULL AND plesk_customer_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_customers_status ON customers(status); CREATE INDEX IF NOT EXISTS idx_customers_sync_status ON customers(sync_status); -- ========================================== -- SUBSCRIPTIONS TABLE -- ========================================== CREATE TABLE IF NOT EXISTS subscriptions ( id SERIAL PRIMARY KEY, customer_id INTEGER NOT NULL, sap_subscription_id VARCHAR(255), plesk_subscription_id VARCHAR(255), sap_item_code VARCHAR(255), name VARCHAR(255) NOT NULL, description TEXT, start_date DATE, end_date DATE, billing_cycle VARCHAR(50) CHECK (billing_cycle IN ('monthly', 'annually', 'quarterly', 'custom')), status VARCHAR(50) CHECK (status IN ('active', 'suspended', 'terminated', 'pending')) DEFAULT 'active', sync_status VARCHAR(50) CHECK (sync_status IN ('in_sync', 'pending_sync', 'sync_error', 'manual_override')) DEFAULT 'pending_sync', pricing_data JSONB DEFAULT '{}', features JSONB DEFAULT '{}', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_subscription_customer FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE ); CREATE TRIGGER subscriptions_updated_at BEFORE UPDATE ON subscriptions FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE INDEX IF NOT EXISTS idx_subscriptions_customer ON subscriptions(customer_id); CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status); -- ========================================== -- USAGE METRICS TABLE -- ========================================== CREATE TYPE metric_type AS ENUM ('cpu', 'ram', 'disk', 'bandwidth', 'database', 'requests', 'emails'); CREATE TABLE IF NOT EXISTS usage_metrics ( id SERIAL PRIMARY KEY, subscription_id INTEGER NOT NULL, metric_type metric_type NOT NULL, metric_value NUMERIC(15,4) NOT NULL, unit VARCHAR(50), recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, metadata JSONB DEFAULT '{}', CONSTRAINT fk_usage_subscription FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_usage_subscription ON usage_metrics(subscription_id); CREATE INDEX IF NOT EXISTS idx_usage_time ON usage_metrics(recorded_at DESC); -- ========================================== -- SYNC JOBS TABLE -- ========================================== CREATE TYPE sync_job_status AS ENUM ('pending', 'running', 'completed', 'failed', 'cancelled', 'paused'); CREATE TABLE IF NOT EXISTS sync_jobs ( id SERIAL PRIMARY KEY, job_type VARCHAR(50) CHECK (job_type IN ('full_sync', 'incremental_sync', 'partial_sync', 'manual_sync')) NOT NULL, sync_direction VARCHAR(50) CHECK (sync_direction IN ('sap_to_plesk', 'plesk_to_sap', 'bidirectional')) NOT NULL, status sync_job_status DEFAULT 'pending', started_at TIMESTAMP, completed_at TIMESTAMP, records_processed INTEGER DEFAULT 0, records_failed INTEGER DEFAULT 0, records_skipped INTEGER DEFAULT 0, error_message TEXT, config_snapshot JSONB DEFAULT '{}', progress_percentage NUMERIC(5,2) DEFAULT 0, estimated_completion TIMESTAMP, created_by INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TRIGGER sync_jobs_updated_at BEFORE UPDATE ON sync_jobs FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE INDEX IF NOT EXISTS idx_sync_status ON sync_jobs(status); CREATE INDEX IF NOT EXISTS idx_sync_created_at ON sync_jobs(created_at DESC); -- ========================================== -- NOTIFICATIONS TABLE -- ========================================== CREATE TABLE IF NOT EXISTS notifications ( id SERIAL PRIMARY KEY, type VARCHAR(50) CHECK (type IN ('info', 'success', 'warning', 'error', 'sync', 'security', 'system')) NOT NULL, title VARCHAR(255) NOT NULL, message TEXT NOT NULL, sent_via JSONB NOT NULL DEFAULT '[]', recipient_type VARCHAR(50) DEFAULT 'admin', recipient VARCHAR(255), is_read BOOLEAN DEFAULT FALSE, is_actionable BOOLEAN DEFAULT FALSE, action_url TEXT, data JSONB DEFAULT '{}', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, read_at TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications(is_read) WHERE is_read = FALSE; CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at DESC); -- ========================================== -- WEBHOOKS TABLE -- ========================================== CREATE TABLE IF NOT EXISTS webhooks ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, url VARCHAR(500) NOT NULL, secret_key VARCHAR(255), events JSONB NOT NULL DEFAULT '[]', is_active BOOLEAN DEFAULT TRUE, retry_policy JSONB DEFAULT '{"max_retries":3,"retry_delay":60}', last_triggered TIMESTAMP, last_status VARCHAR(50), last_error TEXT, trigger_count INTEGER DEFAULT 0, success_count INTEGER DEFAULT 0, failure_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by INTEGER ); CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(is_active) WHERE is_active = TRUE; -- ========================================== -- BACKUPS TABLE -- ========================================== CREATE TABLE IF NOT EXISTS backups ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, type VARCHAR(50) CHECK (type IN ('full', 'config', 'data', 'database')) NOT NULL, backup_path VARCHAR(500), size_bytes BIGINT, checksum VARCHAR(255), is_restorable BOOLEAN DEFAULT TRUE, metadata JSONB DEFAULT '{}', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by INTEGER ); -- ========================================== -- SYNC LOGS TABLE -- ========================================== CREATE TABLE IF NOT EXISTS sync_logs ( id SERIAL PRIMARY KEY, sync_job_id INTEGER NOT NULL, entity_type VARCHAR(50) NOT NULL, entity_id VARCHAR(255) NOT NULL, action VARCHAR(50) NOT NULL, status VARCHAR(50) NOT NULL, error_message TEXT, metadata JSONB DEFAULT '{}', timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, resolution_status VARCHAR(50) DEFAULT 'pending', resolution_action VARCHAR(50), resolved_by INTEGER REFERENCES users(id) ON DELETE SET NULL, resolved_at TIMESTAMP, conflict_details JSONB, CONSTRAINT fk_sync_log_job FOREIGN KEY (sync_job_id) REFERENCES sync_jobs(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_sync_logs_job ON sync_logs(sync_job_id); CREATE INDEX IF NOT EXISTS idx_sync_logs_timestamp ON sync_logs(timestamp DESC); CREATE INDEX IF NOT EXISTS idx_sync_logs_status ON sync_logs(status); CREATE INDEX IF NOT EXISTS idx_sync_logs_resolution ON sync_logs(resolution_status) WHERE resolution_status = 'pending'; -- ========================================== -- PLESK SERVERS TABLE -- ========================================== CREATE TABLE IF NOT EXISTS plesk_servers ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, host VARCHAR(255) NOT NULL, port INTEGER DEFAULT 8443, api_key TEXT, username VARCHAR(255), password_hash TEXT, use_https BOOLEAN DEFAULT TRUE, verify_ssl BOOLEAN DEFAULT TRUE, two_factor_enabled BOOLEAN DEFAULT FALSE, two_factor_method VARCHAR(50), connection_status VARCHAR(50) DEFAULT 'unknown', last_connected TIMESTAMP, is_active BOOLEAN DEFAULT TRUE, metadata JSONB DEFAULT '{}', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TRIGGER plesk_servers_updated_at BEFORE UPDATE ON plesk_servers FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE INDEX IF NOT EXISTS idx_plesk_active ON plesk_servers(is_active) WHERE is_active = TRUE; CREATE INDEX IF NOT EXISTS idx_plesk_host ON plesk_servers(host); -- ========================================== -- SAP SERVERS TABLE -- ========================================== CREATE TABLE IF NOT EXISTS sap_servers ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, host VARCHAR(255) NOT NULL, port INTEGER DEFAULT 50000, company_db VARCHAR(255) NOT NULL, username VARCHAR(255), password_hash TEXT, use_ssl BOOLEAN DEFAULT TRUE, timeout_seconds INTEGER DEFAULT 30, connection_status VARCHAR(50) DEFAULT 'unknown', last_connected TIMESTAMP, is_active BOOLEAN DEFAULT TRUE, metadata JSONB DEFAULT '{}', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TRIGGER sap_servers_updated_at BEFORE UPDATE ON sap_servers FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE INDEX IF NOT EXISTS idx_sap_active ON sap_servers(is_active) WHERE is_active = TRUE; CREATE INDEX IF NOT EXISTS idx_sap_company ON sap_servers(company_db); -- ========================================== -- SCHEDULED SYNC TABLE -- ========================================== CREATE TABLE IF NOT EXISTS scheduled_syncs ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, schedule_type VARCHAR(50) CHECK (schedule_type IN ('daily', 'weekly', 'monthly', 'custom')) NOT NULL, schedule_config JSONB NOT NULL, job_type VARCHAR(50) NOT NULL, sync_direction VARCHAR(50) NOT NULL, plesk_server_id INTEGER, sap_server_id INTEGER, is_active BOOLEAN DEFAULT TRUE, last_run TIMESTAMP, next_run TIMESTAMP, created_by INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_scheduled_plesk FOREIGN KEY (plesk_server_id) REFERENCES plesk_servers(id) ON DELETE SET NULL, CONSTRAINT fk_scheduled_sap FOREIGN KEY (sap_server_id) REFERENCES sap_servers(id) ON DELETE SET NULL ); CREATE TRIGGER scheduled_syncs_updated_at BEFORE UPDATE ON scheduled_syncs FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE INDEX IF NOT EXISTS idx_scheduled_active ON scheduled_syncs(is_active) WHERE is_active = TRUE; CREATE INDEX IF NOT EXISTS idx_scheduled_next_run ON scheduled_syncs(next_run); -- ========================================== -- PRICING CONFIG TABLE -- ========================================== CREATE TABLE IF NOT EXISTS pricing_config ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, metric_type metric_type NOT NULL, unit VARCHAR(50) NOT NULL, rate_per_unit NUMERIC(10,4) NOT NULL, currency VARCHAR(3) DEFAULT 'EUR', is_active BOOLEAN DEFAULT TRUE, valid_from DATE DEFAULT CURRENT_DATE, valid_to DATE, notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(metric_type, unit, valid_from) ); CREATE INDEX IF NOT EXISTS idx_pricing_active ON pricing_config(is_active) WHERE is_active = TRUE; CREATE INDEX IF NOT EXISTS idx_pricing_metric ON pricing_config(metric_type); -- ========================================== -- BILLING RECORDS TABLE -- ========================================== CREATE TABLE IF NOT EXISTS billing_records ( id SERIAL PRIMARY KEY, subscription_id INTEGER NOT NULL, customer_id INTEGER NOT NULL, period_start DATE NOT NULL, period_end DATE NOT NULL, usage_data JSONB NOT NULL, calculated_amount NUMERIC(12,2) NOT NULL, currency VARCHAR(3) DEFAULT 'EUR', sap_invoice_id VARCHAR(255), sap_invoice_number VARCHAR(255), invoice_status VARCHAR(50) CHECK (invoice_status IN ('draft', 'pending', 'sent', 'synced', 'failed')) DEFAULT 'draft', invoice_pdf_path VARCHAR(500), notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, sent_to_sap_at TIMESTAMP, CONSTRAINT fk_billing_subscription FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE, CONSTRAINT fk_billing_customer FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE ); CREATE TRIGGER billing_records_updated_at BEFORE UPDATE ON billing_records FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE INDEX IF NOT EXISTS idx_billing_period ON billing_records(period_start, period_end); CREATE INDEX IF NOT EXISTS idx_billing_status ON billing_records(invoice_status); CREATE INDEX IF NOT EXISTS idx_billing_customer ON billing_records(customer_id); -- ========================================== -- ALERT THRESHOLDS TABLE -- ========================================== CREATE TABLE IF NOT EXISTS alert_thresholds ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, subscription_id INTEGER, metric_type metric_type NOT NULL, threshold_value NUMERIC(15,4) NOT NULL, comparison_operator VARCHAR(10) CHECK (comparison_operator IN ('>', '>=', '<', '<=', '=')) NOT NULL, action VARCHAR(50) CHECK (action IN ('notify', 'notify_and_suspend', 'notify_and_limit')) NOT NULL, notification_channels JSONB DEFAULT '["email"]', is_active BOOLEAN DEFAULT TRUE, last_triggered TIMESTAMP, created_by INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TRIGGER alert_thresholds_updated_at BEFORE UPDATE ON alert_thresholds FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE INDEX IF NOT EXISTS idx_alerts_active ON alert_thresholds(is_active) WHERE is_active = TRUE; CREATE INDEX IF NOT EXISTS idx_alerts_subscription ON alert_thresholds(subscription_id); CREATE INDEX IF NOT EXISTS idx_alerts_metric ON alert_thresholds(metric_type); -- ========================================== -- ALERT HISTORY TABLE -- ========================================== CREATE TABLE IF NOT EXISTS alert_history ( id SERIAL PRIMARY KEY, threshold_id INTEGER NOT NULL, subscription_id INTEGER, actual_value NUMERIC(15,4) NOT NULL, triggered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, action_taken VARCHAR(50), notification_sent BOOLEAN DEFAULT FALSE, notification_error TEXT, CONSTRAINT fk_alert_history_threshold FOREIGN KEY (threshold_id) REFERENCES alert_thresholds(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_alert_history_triggered ON alert_history(triggered_at DESC); CREATE INDEX IF NOT EXISTS idx_alert_history_threshold ON alert_history(threshold_id); -- Add server_id columns to customers table ALTER TABLE customers ADD COLUMN IF NOT EXISTS plesk_server_id INTEGER REFERENCES plesk_servers(id) ON DELETE SET NULL; ALTER TABLE customers ADD COLUMN IF NOT EXISTS sap_server_id INTEGER REFERENCES sap_servers(id) ON DELETE SET NULL; ALTER TABLE customers ADD COLUMN IF NOT EXISTS last_conflict TIMESTAMP; CREATE INDEX IF NOT EXISTS idx_customers_plesk_server ON customers(plesk_server_id); CREATE INDEX IF NOT EXISTS idx_customers_sap_server ON customers(sap_server_id); -- Add server_id columns to sync_jobs table ALTER TABLE sync_jobs ADD COLUMN IF NOT EXISTS plesk_server_id INTEGER REFERENCES plesk_servers(id) ON DELETE SET NULL; ALTER TABLE sync_jobs ADD COLUMN IF NOT EXISTS sap_server_id INTEGER REFERENCES sap_servers(id) ON DELETE SET NULL; ALTER TABLE sync_jobs ADD COLUMN IF NOT EXISTS scheduled_sync_id INTEGER REFERENCES scheduled_syncs(id) ON DELETE SET NULL; CREATE INDEX IF NOT EXISTS idx_sync_jobs_plesk_server ON sync_jobs(plesk_server_id); CREATE INDEX IF NOT EXISTS idx_sync_jobs_sap_server ON sync_jobs(sap_server_id); CREATE INDEX IF NOT EXISTS idx_sync_logs_job ON sync_logs(sync_job_id); CREATE INDEX IF NOT EXISTS idx_sync_logs_timestamp ON sync_logs(timestamp DESC); CREATE INDEX IF NOT EXISTS idx_sync_logs_status ON sync_logs(status); -- ========================================== -- INITIAL DATA -- ========================================== -- Default configuration INSERT INTO config (key, value, description, category) VALUES ('sync.default_direction', '"sap_to_plesk"', 'Default sync direction', 'sync'), ('sync.conflict_resolution', '"timestamp_based"', 'Default conflict resolution strategy', 'sync'), ('sync.interval_minutes', '60', 'Default sync interval in minutes', 'sync'), ('auth.session_timeout', '1800', 'Session timeout in seconds (30 min)', 'auth'), ('auth.max_login_attempts', '5', 'Max failed login attempts before lockout', 'auth'), ('auth.lockout_duration', '3600', 'Lockout duration in seconds (1 hour)', 'auth'), ('notifications.email_enabled', 'true', 'Enable email notifications', 'notifications'), ('notifications.webhook_enabled', 'true', 'Enable webhook notifications', 'notifications'), ('system.initialized', 'true', 'System initialization flag', 'system') ON CONFLICT (key) DO NOTHING; -- Note: Default admin user will be created dynamically by the backend on first startup -- Username: admin -- Password: Admin123! (default, can be changed via environment variables) -- IMPORTANT: Change password immediately after first login