PORTFOLIO/database/schema.sql

231 lines
12 KiB
SQL

-- ==============================================================================
-- BASE DE DATOS: portfolio_db
-- MOTOR: MySQL 8.0+ / MariaDB 10.4+ / InnoDB
-- CHARSET: utf8mb4 / utf8mb4_unicode_ci
-- DESCRIPCIÓN: Esquema relacional minimalista y sin duplicación para
-- Portafolio Interactivo (Zen + Retro + Clean Arch .NET & Angular)
-- NOTA DBEAVER: Ejecutar todo el archivo con Alt + X (Ejecutar Script SQL / ▶▶).
-- No usar Ctrl + Enter sobre múltiples sentencias (error 1064).
-- ==============================================================================
CREATE DATABASE IF NOT EXISTS `portfolio_db`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE `portfolio_db`;
-- ------------------------------------------------------------------------------
-- 1. TABLA: users (Autenticación y RBAC del Administrador)
-- Justificación: Almacena la cuenta del propietario (Bryan) para acceder al panel
-- administrativo. No se crean tablas complejas de 'roles' o 'permisos' porque
-- solo existen 2 roles en el sistema (Admin y Visitor). Un campo 'role' es la
-- solución más limpia y minimalista (Ponytail).
-- ------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `users` (
`id` CHAR(36) NOT NULL,
`username` VARCHAR(50) NOT NULL,
`email` VARCHAR(100) NOT NULL,
`password_hash` VARCHAR(255) NOT NULL,
`role` VARCHAR(20) NOT NULL DEFAULT 'Admin',
`created_at_utc` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at_utc` DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_users_username` (`username`),
UNIQUE KEY `uq_users_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ------------------------------------------------------------------------------
-- 2. TABLA: refresh_tokens (Rotación segura de Tokens JWT)
-- Justificación: Permite mantener la sesión de administración de Bryan de forma
-- segura sin almacenar JWTs de larga duración. Si se detecta un token revocado,
-- se invalida la cadena. Relación 1:N con 'users' con borrado en cascada.
-- ------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `refresh_tokens` (
`id` CHAR(36) NOT NULL,
`user_id` CHAR(36) NOT NULL,
`token_hash` VARCHAR(255) NOT NULL,
`expires_at_utc` DATETIME NOT NULL,
`is_revoked` BOOLEAN NOT NULL DEFAULT FALSE,
`created_at_utc` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`replaced_by_token` VARCHAR(255) NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_refresh_tokens_user` (`user_id`),
KEY `idx_refresh_tokens_validation` (`user_id`, `is_revoked`, `expires_at_utc`),
CONSTRAINT `fk_refresh_tokens_user` FOREIGN KEY (`user_id`)
REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ------------------------------------------------------------------------------
-- 3. TABLA: skills (Catálogo de Habilidades Técnicas)
-- Justificación: Administradas por Bryan vía CRUD. Se renderizan en el frontend
-- y se consultan desde el comando 'skills' de la terminal Linux interactiva.
-- Normaliza los nombres técnicos para que no se dupliquen strings en los proyectos.
-- ------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `skills` (
`id` CHAR(36) NOT NULL,
`name` VARCHAR(50) NOT NULL,
`category` ENUM('Backend', 'Frontend', 'Database', 'DevOps', 'Architecture') NOT NULL,
`icon_slug` VARCHAR(50) NULL DEFAULT NULL,
`proficiency_percentage` TINYINT UNSIGNED NOT NULL DEFAULT 85,
`display_order` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
`is_featured` BOOLEAN NOT NULL DEFAULT TRUE,
`created_at_utc` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_skills_name` (`name`),
KEY `idx_skills_category_order` (`category`, `display_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ------------------------------------------------------------------------------
-- 4. TABLA: projects (Proyectos del Portafolio)
-- Justificación: Catálogo de proyectos destacados y proyectos generales. Permite
-- a Bryan crear/editar proyectos sin tocar código. Contiene slugs amigables para
-- URLs y terminal (`projects show [slug]`), enlaces a repositorios y demos en vivo.
-- ------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `projects` (
`id` CHAR(36) NOT NULL,
`slug` VARCHAR(100) NOT NULL,
`title` VARCHAR(150) NOT NULL,
`short_description` VARCHAR(255) NOT NULL,
`full_description` TEXT NOT NULL,
`repository_url` VARCHAR(255) NOT NULL,
`live_demo_url` VARCHAR(255) NULL DEFAULT NULL,
`thumbnail_url` VARCHAR(255) NULL DEFAULT NULL,
`is_featured` BOOLEAN NOT NULL DEFAULT FALSE,
`display_order` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
`created_at_utc` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at_utc` DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_projects_slug` (`slug`),
KEY `idx_projects_featured_order` (`is_featured`, `display_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ------------------------------------------------------------------------------
-- 5. TABLA: project_skills (Tabla Intermedia N:M Proyectos <-> Habilidades)
-- Justificación: Evita la duplicación de datos (3FN). Un proyecto usa muchas
-- tecnologías y una tecnología está en muchos proyectos. Permite filtrar
-- proyectos por tecnología con indexación real sin recurrir a campos de texto
-- plano o arrays desnormalizados difíciles de consultar.
-- ------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `project_skills` (
`project_id` CHAR(36) NOT NULL,
`skill_id` CHAR(36) NOT NULL,
PRIMARY KEY (`project_id`, `skill_id`),
KEY `idx_project_skills_skill` (`skill_id`),
CONSTRAINT `fk_project_skills_project` FOREIGN KEY (`project_id`)
REFERENCES `projects` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_project_skills_skill` FOREIGN KEY (`skill_id`)
REFERENCES `skills` (`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ------------------------------------------------------------------------------
-- 6. TABLA: interactive_messages (Libro de Visitas / Mensajes Interactivos)
-- Justificación: Permite a reclutadores y visitantes dejar una firma o mensaje
-- desde el comando interactivo de la terminal Linux (`guestbook sign "Mensaje"`)
-- o el probador de API en vivo. Demuestra operaciones de escritura inmediatas
-- y controladas en el backend con almacenamiento persistente.
-- ------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `interactive_messages` (
`id` CHAR(36) NOT NULL,
`sender_name` VARCHAR(60) NOT NULL,
`sender_contact` VARCHAR(100) NULL DEFAULT NULL,
`message` VARCHAR(500) NOT NULL,
`origin` ENUM('Terminal', 'WebForm', 'ApiTester') NOT NULL DEFAULT 'Terminal',
`is_approved` BOOLEAN NOT NULL DEFAULT TRUE,
`created_at_utc` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_messages_approved_date` (`is_approved`, `created_at_utc` DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ------------------------------------------------------------------------------
-- 7. TABLA: contact_inquiries (Consultas Técnicas & Solicitudes de Propuesta)
-- Justificación: Almacena leads comerciales B2B y requerimientos de clientes de
-- forma privada y segura. Separada estrictamente de 'interactive_messages' (que es
-- un libro de visitas público). Soporta filtrado por estado y auditoría por IP.
-- ------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `contact_inquiries` (
`id` CHAR(36) NOT NULL,
`full_name_or_company` VARCHAR(100) NOT NULL,
`email` VARCHAR(100) NOT NULL,
`phone` VARCHAR(30) NULL DEFAULT NULL,
`service_type` VARCHAR(50) NOT NULL DEFAULT 'Otro',
`project_details` TEXT NOT NULL,
`status` ENUM('New', 'Read', 'Replied', 'Archived') NOT NULL DEFAULT 'New',
`ip_address` VARCHAR(45) NULL DEFAULT NULL,
`created_at_utc` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`replied_at_utc` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_inquiries_status_created` (`status`, `created_at_utc` DESC),
KEY `idx_inquiries_created` (`created_at_utc` DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ==============================================================================
-- SEED DATA INICIAL (Datos de prueba para arranque inmediato)
-- ==============================================================================
-- 1. Usuario Administrador Inicial (Bryan)
-- Contraseña temporal por defecto: 'Admin123*!' (hasheada con BCrypt/PBKDF2 para prueba)
INSERT INTO `users` (`id`, `username`, `email`, `password_hash`, `role`)
VALUES (
'11111111-1111-1111-1111-111111111111',
'bryan',
'bryan@portfolio.local',
'$2a$11$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy',
'Admin'
) ON DUPLICATE KEY UPDATE `username` = `username`;
-- 2. Habilidades Iniciales
INSERT INTO `skills` (`id`, `name`, `category`, `icon_slug`, `proficiency_percentage`, `display_order`, `is_featured`)
VALUES
('22222222-0001-0000-0000-000000000001', '.NET 8/9 / C#', 'Backend', 'dotnet', 95, 1, TRUE),
('22222222-0002-0000-0000-000000000002', 'Clean Architecture & CQRS', 'Architecture', 'architecture', 95, 2, TRUE),
('22222222-0003-0000-0000-000000000003', 'Angular & Signals', 'Frontend', 'angular', 90, 3, TRUE),
('22222222-0004-0000-0000-000000000004', 'MySQL / EF Core', 'Database', 'mysql', 90, 4, TRUE),
('22222222-0005-0000-0000-000000000005', 'Docker & CI/CD', 'DevOps', 'docker', 85, 5, TRUE)
ON DUPLICATE KEY UPDATE `name` = `name`;
-- 3. Proyecto Destacado Inicial
INSERT INTO `projects` (`id`, `slug`, `title`, `short_description`, `full_description`, `repository_url`, `live_demo_url`, `is_featured`, `display_order`)
VALUES (
'33333333-0001-0000-0000-000000000001',
'portfolio-interactivo-zen',
'Portafolio Profesional Interactivo',
'Portafolio interactivo con estética Zen Dark, 3D Hero desarmable, terminal Linux y backend .NET Clean Architecture.',
'Sistema completo desarrollado con .NET 8/9, EF Core, MySQL, y Angular con Clean Architecture y Signals reactivos.',
'https://github.com/bryan/portfolio',
'https://portfolio.bryan.dev',
TRUE,
1
) ON DUPLICATE KEY UPDATE `slug` = `slug`;
-- 4. Vinculación Proyecto - Habilidades
INSERT INTO `project_skills` (`project_id`, `skill_id`)
VALUES
('33333333-0001-0000-0000-000000000001', '22222222-0001-0000-0000-000000000001'),
('33333333-0001-0000-0000-000000000001', '22222222-0002-0000-0000-000000000002'),
('33333333-0001-0000-0000-000000000001', '22222222-0003-0000-0000-000000000003'),
('33333333-0001-0000-0000-000000000001', '22222222-0004-0000-0000-000000000004')
ON DUPLICATE KEY UPDATE `project_id` = `project_id`;
-- 5. Mensaje de Bienvenida en el Libro de Visitas
INSERT INTO `interactive_messages` (`id`, `sender_name`, `sender_contact`, `message`, `origin`)
VALUES (
UUID(),
'Kernel Bot',
'system@zen.os',
'Bienvenido a la terminal interactiva. Ejecuta "help" para comenzar.',
'Terminal'
) ON DUPLICATE KEY UPDATE `sender_name` = `sender_name`;
-- 6. Consulta de Contacto Inicial de Prueba
INSERT INTO `contact_inquiries` (`id`, `full_name_or_company`, `email`, `phone`, `service_type`, `project_details`, `status`)
VALUES (
'44444444-0001-0000-0000-000000000001',
'Enterprise Solutions Corp',
'contacto@enterprisesolutions.com',
'+593 99 123 4567',
'Desarrollo Backend',
'Requerimos modernización de arquitectura monolítica a microservicios con .NET 9, Clean Architecture y CQRS.',
'New'
) ON DUPLICATE KEY UPDATE `full_name_or_company` = `full_name_or_company`;