-- Panel Transmitiras: usuarios, permisos y sesiones (mismo modelo que -- ~/dev/a24c/backend/api/v1/modules/dashboard — tablas en inglés, esquema a24c). -- Ejecutar en PostgreSQL (misma base donde está el catálogo ControlDesk, p. ej. CONTROLDESK). CREATE SCHEMA IF NOT EXISTS a24c; CREATE TABLE IF NOT EXISTS a24c.dashboard_users ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, full_name VARCHAR(100), is_active BOOLEAN NOT NULL DEFAULT true, is_admin BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_access_at TIMESTAMPTZ, CONSTRAINT chk_dashboard_users_username_length CHECK (char_length(username) >= 3) ); CREATE TABLE IF NOT EXISTS a24c.dashboard_user_database_permissions ( id SERIAL PRIMARY KEY, dashboard_user_id INTEGER NOT NULL REFERENCES a24c.dashboard_users (id) ON DELETE CASCADE, database_name VARCHAR(255) NOT NULL, can_view BOOLEAN NOT NULL DEFAULT true, can_download_backup BOOLEAN NOT NULL DEFAULT false, can_restore BOOLEAN NOT NULL DEFAULT false, assigned_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (dashboard_user_id, database_name) ); CREATE TABLE IF NOT EXISTS a24c.dashboard_sessions ( id SERIAL PRIMARY KEY, dashboard_user_id INTEGER NOT NULL REFERENCES a24c.dashboard_users (id) ON DELETE CASCADE, token VARCHAR(255) UNIQUE NOT NULL, ip_address VARCHAR(50), user_agent TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), expires_at TIMESTAMPTZ NOT NULL, is_active BOOLEAN NOT NULL DEFAULT true ); CREATE INDEX IF NOT EXISTS idx_a24c_dashboard_user_db_perm_user ON a24c.dashboard_user_database_permissions (dashboard_user_id); CREATE INDEX IF NOT EXISTS idx_a24c_dashboard_user_db_perm_dbname ON a24c.dashboard_user_database_permissions (database_name); CREATE INDEX IF NOT EXISTS idx_a24c_dashboard_users_active ON a24c.dashboard_users (is_active); CREATE INDEX IF NOT EXISTS idx_a24c_dashboard_sessions_token ON a24c.dashboard_sessions (token); CREATE INDEX IF NOT EXISTS idx_a24c_dashboard_sessions_user ON a24c.dashboard_sessions (dashboard_user_id); COMMENT ON TABLE a24c.dashboard_users IS 'Operadores del panel Transmitiras (JWT/bcrypt). Paridad api/v1/modules/dashboard/users.'; COMMENT ON TABLE a24c.dashboard_user_database_permissions IS 'Permisos por base de datos. Paridad dashboard/user_database_permissions.'; COMMENT ON TABLE a24c.dashboard_sessions IS 'Sesiones persistidas (opcional; el panel usa JWT en cookie). Paridad dashboard/sessions.'; -- Usuario admin por defecto (cambiar password_hash tras primer arranque; ver scripts/generate-password-hash.js) INSERT INTO a24c.dashboard_users (username, email, password_hash, full_name, is_admin, is_active) VALUES ( 'admin', 'admin@aduanasoft.com', '$2b$10$rKvvkYpNwF5J5K5J5J5J5uX5X5X5X5X5X5X5X5X5X5X5X5X5X5X5X', 'Administrador del Sistema', true, true ) ON CONFLICT (username) DO NOTHING; -- ============================================================================ -- Integración CloudRestoreAS: servidores de restauración y bitácora de jobs. -- Equivalente versionado en database/migrations/001_restore_targets.{up,down}.sql -- ============================================================================ -- Servidores SQL Server destino de restauración (Alfa, Omega, Gamma). Son máquinas -- EXTERNAS independientes: el .bak se transfiere por SFTP/SSH al servidor y el SQL Server -- restaura desde su disco local. Cada base (a24c.database_nodes.restore_target_id) se asigna -- a uno de ellos. Las contraseñas (SQL y SSH) se guardan cifradas con AES-256-GCM, nunca en plano. CREATE TABLE IF NOT EXISTS a24c.restore_targets ( id SERIAL PRIMARY KEY, name VARCHAR(120) NOT NULL UNIQUE, -- Alfa | Omega | Gamma server_ip VARCHAR(255), -- SQL Server: IP/hostname; acepta "ip,puerto" sql_username VARCHAR(128), sql_password_encrypted TEXT, -- sobre gcm:iv:tag:ciphertext data_folder VARCHAR(500), -- ruta .mdf/.ldf EN el server remoto (C:\SQLData) ssh_host VARCHAR(255), -- host SSH del servidor (suele ser el mismo equipo) ssh_port INTEGER DEFAULT 22, ssh_username VARCHAR(128), ssh_password_encrypted TEXT, -- credencial SSH cifrada (AES-256-GCM) remote_inbox_path VARCHAR(500), -- ruta en el server donde se sube el .bak y se restaura (C:\RestoreInbox) notes TEXT, -- Características de hardware (opcionales, capturadas a mano). Se usan para distribuir bases -- por capacidad; el disco es la capacidad que manda. NULL = sin capturar. os VARCHAR(50), -- Sistema operativo (Linux/Windows/…) ram_gb INTEGER, -- RAM en GB disk_gb INTEGER, -- Disco en GB location VARCHAR(255), -- Ubicación (ej: Kansas City, United States) size_category VARCHAR(20), -- Etiqueta: Chico | Mediano | Grande created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Tres servidores fijos de partida (editables). Datos de conexión vacíos hasta configurarse. INSERT INTO a24c.restore_targets (name) VALUES ('Alfa'), ('Omega'), ('Gamma') ON CONFLICT (name) DO NOTHING; -- Asignación servidor de restauración ↔ base de datos (la tabla database_nodes la aprovisiona -- el backend a24c; aquí solo se agrega la columna de asignación). ALTER TABLE a24c.database_nodes ADD COLUMN IF NOT EXISTS restore_target_id INTEGER REFERENCES a24c.restore_targets (id) ON DELETE SET NULL; -- Backfill único: la credencial SQL del nodo (database_nodes.sql_password, columna de a24c) se -- copia del restaurador asignado como sobre cifrado gcm: (el panel lo descifra al conectar). -- Idempotente: solo rellena nodos ya asignados sin contraseña; no pisa valores existentes. -- Guardado por si a24c aún no aprovisionó la columna sql_password (evita romper el init). -- Rollback: UPDATE a24c.database_nodes SET sql_password = NULL; DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'a24c' AND table_name = 'database_nodes' AND column_name = 'sql_password' ) THEN UPDATE a24c.database_nodes n SET sql_password = rt.sql_password_encrypted FROM a24c.restore_targets rt WHERE n.restore_target_id = rt.id AND rt.sql_password_encrypted IS NOT NULL AND (n.sql_password IS NULL OR n.sql_password = ''); END IF; END $$; -- Bitácora de restauraciones reportadas por CloudRestoreAS. CREATE TABLE IF NOT EXISTS a24c.restore_job_logs ( id SERIAL PRIMARY KEY, filename VARCHAR(500) NOT NULL, restore_target_id INTEGER REFERENCES a24c.restore_targets (id) ON DELETE SET NULL, db_name VARCHAR(255), status VARCHAR(20) NOT NULL CHECK (status IN ('completed', 'failed', 'forwarded')), duration_ms INTEGER, error_message TEXT, restored_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_a24c_restore_job_logs_target ON a24c.restore_job_logs (restore_target_id); CREATE INDEX IF NOT EXISTS idx_a24c_restore_job_logs_restored_at ON a24c.restore_job_logs (restored_at); CREATE INDEX IF NOT EXISTS idx_a24c_restore_job_logs_status ON a24c.restore_job_logs (status); -- Bitácora por servidor en el panel: últimas N restauraciones de un target. CREATE INDEX IF NOT EXISTS idx_a24c_restore_job_logs_target_date ON a24c.restore_job_logs (restore_target_id, restored_at DESC); COMMENT ON TABLE a24c.restore_targets IS 'Servidores SQL Server destino de restauración (CloudRestoreAS). Contraseña cifrada AES-256-GCM.'; COMMENT ON TABLE a24c.restore_job_logs IS 'Bitácora de restauraciones reportadas por CloudRestoreAS.'; -- Equivalente versionado en database/migrations/002_cloudrestore_status.{up,down}.sql -- Carpeta de entrada reportada por CloudRestoreAS (solo lectura en el panel). CREATE TABLE IF NOT EXISTS a24c.cloudrestore_status ( id SERIAL PRIMARY KEY, instance_key VARCHAR(120) NOT NULL UNIQUE DEFAULT 'default', input_folder VARCHAR(500) NOT NULL, host_name VARCHAR(255), app_version VARCHAR(50), reported_at TIMESTAMPTZ NOT NULL DEFAULT now() ); COMMENT ON TABLE a24c.cloudrestore_status IS 'Carpeta de entrada vigente reportada por CloudRestoreAS. Solo lectura en el panel.';