From 5fa97105c9e1127be2f353c744043f83a0f21eba Mon Sep 17 00:00:00 2001 From: ernestohc21 Date: Thu, 19 Feb 2026 12:08:56 -0600 Subject: [PATCH] feat: improved sidebar UI with larger icons and user info in footer - Increased all sidebar navigation icons from text-sm to text-xl for better visibility - Moved user information and logout button from header to sidebar footer - Added user avatar with name and email in expanded sidebar state - Collapsed sidebar shows avatar icon and logout icon button stacked vertically - Simplified header by removing user badge and logout button - Added version info (v1.0.0) in sidebar footer - Improved responsive design for both collapsed and expanded states --- .env.example | 30 + INSTALACION_USUARIOS.md | 322 +++ database/schema.sql | 66 + docker-compose.postgres.yml | 52 + docs/USUARIOS.md | 220 ++ package-lock.json | 1019 +++++++- package.json | 11 +- postcss.config.cjs | 6 + scripts/generate-password-hash.js | 18 + scripts/init-database.js | 108 + scripts/init-users.sh | 47 + src/app.css | 198 +- src/app.html | 44 +- src/lib/assets/icono.png | Bin 0 -> 16944 bytes src/lib/assets/logo.png | Bin 0 -> 17761 bytes src/lib/components/Navbar.svelte | 64 +- src/lib/components/Sidebar.svelte | 140 +- src/lib/server/auth.ts | 87 + src/lib/server/db.ts | 24 +- src/lib/server/users.ts | 312 +++ src/routes/+page.server.ts | 927 ++++--- src/routes/+page.svelte | 2143 ++++++++++++++--- src/routes/api/dashboard.json/+server.ts | 60 + src/routes/api/health/+server.ts | 23 + src/routes/backup/+server.ts | 30 + src/routes/login/+page.server.ts | 61 + src/routes/login/+page.svelte | 175 ++ src/routes/logout/+server.ts | 16 + src/routes/test/+page.server.ts | 27 + src/routes/usuarios/+page.server.ts | 201 ++ src/routes/usuarios/+page.svelte | 614 +++++ .../usuarios/api/permissions/+server.ts | 19 + tailwind.config.cjs | 8 + 33 files changed, 6160 insertions(+), 912 deletions(-) create mode 100644 .env.example create mode 100644 INSTALACION_USUARIOS.md create mode 100644 database/schema.sql create mode 100644 docker-compose.postgres.yml create mode 100644 docs/USUARIOS.md create mode 100644 postcss.config.cjs create mode 100644 scripts/generate-password-hash.js create mode 100644 scripts/init-database.js create mode 100644 scripts/init-users.sh create mode 100644 src/lib/assets/icono.png create mode 100644 src/lib/assets/logo.png create mode 100644 src/lib/server/auth.ts create mode 100644 src/lib/server/users.ts create mode 100644 src/routes/api/dashboard.json/+server.ts create mode 100644 src/routes/api/health/+server.ts create mode 100644 src/routes/backup/+server.ts create mode 100644 src/routes/login/+page.server.ts create mode 100644 src/routes/login/+page.svelte create mode 100644 src/routes/logout/+server.ts create mode 100644 src/routes/test/+page.server.ts create mode 100644 src/routes/usuarios/+page.server.ts create mode 100644 src/routes/usuarios/+page.svelte create mode 100644 src/routes/usuarios/api/permissions/+server.ts create mode 100644 tailwind.config.cjs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0019580 --- /dev/null +++ b/.env.example @@ -0,0 +1,30 @@ +# SQL Server - Servidor Primary (.202) +DB_PRIMARY_HOST=104.197.7.202 +DB_PRIMARY_USER=sa +DB_PRIMARY_PASS=Clave.2025 +DB_PRIMARY_DB=master + +# SQL Server - Servidor Secondary (.152) +DB_SECONDARY_HOST=104.192.7.152 +DB_SECONDARY_USER=sa +DB_SECONDARY_PASS=Clave.2025 +DB_SECONDARY_DB=master + +# SQL Server - Azure/CONTROLDESK +DB_AZURE_HOST=104.192.7.152 +DB_AZURE_USER=sa +DB_AZURE_PASS=Clave.2025 +DB_AZURE_DB=CONTROLDESK + +# PostgreSQL - Base de datos de usuarios (Docker local) +DB_POSTGRES_HOST=localhost +DB_POSTGRES_PORT=5432 +DB_POSTGRES_USER=postgres +DB_POSTGRES_PASS=Control. +DB_POSTGRES_DB=CONTROLDESK + +# JWT Secret (cambiar en producción) +JWT_SECRET=change-this-secret-in-production-please-use-a-long-random-string + +# Ruta de backups +BACKUP_PATH=D:/BackupSFTP/ diff --git a/INSTALACION_USUARIOS.md b/INSTALACION_USUARIOS.md new file mode 100644 index 0000000..386960e --- /dev/null +++ b/INSTALACION_USUARIOS.md @@ -0,0 +1,322 @@ +# 🚀 Sistema Completo de Gestión de Usuarios y Permisos + +## ✅ ¿Qué se ha implementado? + +### 1. **Sistema de Autenticación** +- ✅ Login con usuario y contraseña +- ✅ Tokens JWT seguros +- ✅ Cookies HTTP-only +- ✅ Sesiones persistentes (7 días) +- ✅ Logout seguro +- ✅ Protección de rutas + +### 2. **Gestión de Usuarios (Solo Admins)** +- ✅ Crear nuevos usuarios +- ✅ Editar usuarios existentes +- ✅ Activar/desactivar usuarios +- ✅ Eliminar usuarios +- ✅ Asignar rol de administrador + +### 3. **Sistema de Permisos** +- ✅ Permisos granulares por base de datos +- ✅ Asignación/revocación de permisos +- ✅ Filtrado automático de bases de datos según permisos +- ✅ Administradores con acceso total + +### 4. **Base de Datos PostgreSQL** +- ✅ Tabla de usuarios +- ✅ Tabla de permisos +- ✅ Tabla de sesiones +- ✅ Índices para rendimiento +- ✅ Constraints de integridad + +### 5. **Seguridad** +- ✅ Contraseñas hasheadas con bcrypt +- ✅ Validación de contraseñas fuertes +- ✅ Tokens JWT con expiración +- ✅ Protección contra SQL injection +- ✅ Sanitización de entradas + +## 📦 Archivos Creados + +### Backend (Server) +``` +src/lib/server/ +├── auth.ts # Utilidades de autenticación JWT y bcrypt +├── users.ts # CRUD de usuarios y permisos +└── db.ts # Conexión a PostgreSQL (actualizado) + +src/routes/ +├── login/ +│ ├── +page.svelte # Interfaz de login +│ └── +page.server.ts # Lógica de autenticación +├── logout/ +│ └── +server.ts # Cerrar sesión +├── usuarios/ +│ ├── +page.svelte # Gestión de usuarios (UI) +│ ├── +page.server.ts # CRUD y permisos +│ └── api/permissions/ +│ └── +server.ts # API de permisos +└── +page.server.ts # Panel principal (actualizado con filtros) +``` + +### Base de Datos +``` +database/ +└── schema.sql # Script de creación de tablas +``` + +### Documentación +``` +docs/ +└── USUARIOS.md # Guía completa del sistema + +scripts/ +├── generate-password-hash.js # Generar hash de contraseñas +└── init-users.sh # Script de inicialización +``` + +### Configuración +``` +.env.example # Variables de entorno de ejemplo +``` + +## 🛠️ Instalación y Configuración + +### Paso 1: Instalar Dependencias + +```bash +npm install +``` + +Las dependencias ya están en package.json: +- `bcrypt` - Hash de contraseñas +- `jsonwebtoken` - Tokens JWT +- `pg` - Cliente PostgreSQL +- `@types/pg`, `@types/bcrypt`, `@types/jsonwebtoken` - Tipos TypeScript + +### Paso 2: Configurar PostgreSQL + +#### Opción A: Desde la línea de comandos +```bash +psql -h 10.0.20.152 -U postgres -d CONTROLDESK -f database/schema.sql +``` + +#### Opción B: Desde pgAdmin o DBeaver +1. Abre el archivo `database/schema.sql` +2. Copia todo el contenido +3. Pégalo en la consola SQL +4. Ejecuta el script + +### Paso 3: Generar Contraseña del Admin + +```bash +node scripts/generate-password-hash.js +``` + +Este script generará un hash para la contraseña `Admin123!`. + +**IMPORTANTE:** Copia el hash generado y actualiza el archivo `database/schema.sql` en la línea del INSERT del usuario admin, reemplazando el hash temporal. + +Luego vuelve a ejecutar el INSERT en PostgreSQL: + +```sql +INSERT INTO usuarios (username, email, password_hash, nombre_completo, es_admin, activo) +VALUES ( + 'admin', + 'admin@aduanasoft.com', + 'EL_HASH_QUE_GENERASTE', -- <-- Pega aquí el hash + 'Administrador del Sistema', + true, + true +) +ON CONFLICT (username) DO UPDATE SET + password_hash = EXCLUDED.password_hash; +``` + +### Paso 4: Configurar Variables de Entorno + +```bash +cp .env.example .env +``` + +Edita `.env` y actualiza los valores: + +```env +# PostgreSQL +DB_POSTGRES_HOST=10.0.20.152 +DB_POSTGRES_PORT=5432 +DB_POSTGRES_USER=postgres +DB_POSTGRES_PASS=Control. +DB_POSTGRES_DB=CONTROLDESK + +# JWT Secret (¡CÁMBIALO!) +JWT_SECRET=un-secreto-muy-largo-y-aleatorio-para-produccion +``` + +**⚠️ IMPORTANTE:** En producción, genera un JWT_SECRET aleatorio y seguro: +```bash +node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" +``` + +### Paso 5: Iniciar el Servidor + +```bash +npm run dev +``` + +### Paso 6: Primer Login + +1. Ve a `http://localhost:5173/login` +2. Usuario: `admin` +3. Contraseña: `Admin123!` (o la que configuraste) + +## 📖 Uso del Sistema + +### Para Administradores + +#### 1. Crear un Nuevo Usuario +1. Ve a "Gestión de Usuarios" en el sidebar +2. Haz clic en "Nuevo Usuario" +3. Completa el formulario: + - **Usuario**: Nombre de usuario único + - **Email**: Correo electrónico + - **Nombre Completo**: Nombre real + - **Contraseña**: Mínimo 8 caracteres con mayúsculas, minúsculas, números y símbolos + - **Es Administrador**: Marca si quieres que sea admin +4. Haz clic en "Crear Usuario" + +#### 2. Asignar Permisos de Bases de Datos +1. En la lista de usuarios, haz clic en el ícono 🔒 (candado) +2. Verás todas las bases de datos disponibles +3. Haz clic en "Sin Acceso" para otorgar acceso +4. Haz clic en "Con Acceso" para revocar acceso +5. Los cambios se guardan automáticamente + +#### 3. Editar un Usuario +1. Haz clic en el ícono ✏️ (editar) +2. Modifica los campos que necesites +3. Para cambiar la contraseña, escribe una nueva (si dejas vacío, no se cambia) +4. Haz clic en "Guardar Cambios" + +#### 4. Desactivar un Usuario +1. Haz clic en el ícono de toggle (activar/desactivar) +2. Confirma la acción +3. El usuario no podrá iniciar sesión, pero sus datos se conservan + +#### 5. Eliminar un Usuario +1. Haz clic en el ícono 🗑️ (eliminar) +2. Confirma la acción +3. **¡CUIDADO!** Esta acción es permanente + +### Para Usuarios Normales + +Los usuarios normales solo verán: +- Las bases de datos que les han sido asignadas +- No tendrán acceso a "Gestión de Usuarios" +- El resto del panel funciona igual + +## 🔒 Cómo Funciona la Seguridad + +### Flujo de Autenticación + +``` +1. Usuario ingresa credenciales + ↓ +2. Servidor verifica con bcrypt + ↓ +3. Se genera token JWT + ↓ +4. Token se guarda en cookie HTTP-only + ↓ +5. Cada petición verifica el token + ↓ +6. Se aplican filtros de permisos +``` + +### Filtrado de Bases de Datos + +```typescript +// En +page.server.ts (líneas 376-379) +if (!currentUser.es_admin) { + // Filtra solo las bases de datos que el usuario puede ver + databaseRows = await filterDatabasesByUserPermissions(currentUser.id, databaseRows); + alertsData = await filterDatabasesByUserPermissions(currentUser.id, alertsData); +} +``` + +## 📊 Estructura de la Base de Datos + +### Tabla `usuarios` +Almacena información de los usuarios del sistema. + +| Campo | Tipo | Descripción | +|-------|------|-------------| +| id | SERIAL | ID único | +| username | VARCHAR(50) | Nombre de usuario único | +| email | VARCHAR(100) | Email único | +| password_hash | VARCHAR(255) | Contraseña hasheada | +| nombre_completo | VARCHAR(100) | Nombre completo | +| activo | BOOLEAN | Si el usuario está activo | +| es_admin | BOOLEAN | Si es administrador | +| fecha_creacion | TIMESTAMP | Fecha de creación | +| ultimo_acceso | TIMESTAMP | Último login | + +### Tabla `usuario_base_datos` +Relaciona usuarios con bases de datos y sus permisos. + +| Campo | Tipo | Descripción | +|-------|------|-------------| +| id | SERIAL | ID único | +| usuario_id | INTEGER | FK a usuarios | +| base_datos_nombre | VARCHAR(255) | Nombre de la BD | +| puede_ver | BOOLEAN | Permiso de visualización | +| puede_descargar_backup | BOOLEAN | Permiso de descarga (futuro) | +| puede_restaurar | BOOLEAN | Permiso de restauración (futuro) | +| fecha_asignacion | TIMESTAMP | Fecha de asignación | + +## 🐛 Solución de Problemas + +### "No puedo iniciar sesión" +1. Verifica que el hash de contraseña sea correcto en la BD +2. Revisa los logs del servidor (terminal donde corre `npm run dev`) +3. Asegúrate de que PostgreSQL esté corriendo +4. Verifica las credenciales en `.env` + +### "No veo ninguna base de datos" +1. Si eres admin, deberías ver todas +2. Si eres usuario normal, verifica que tengas permisos asignados: + ```sql + SELECT * FROM usuario_base_datos WHERE usuario_id = TU_ID; + ``` + +### "Error de conexión a PostgreSQL" +1. Verifica que PostgreSQL esté corriendo: + ```bash + psql -h 10.0.20.152 -U postgres -c "SELECT version();" + ``` +2. Revisa el firewall del servidor +3. Verifica las credenciales en `.env` + +### "Invalid token" o "Session expired" +1. Cierra sesión y vuelve a iniciar +2. Limpia las cookies del navegador +3. Verifica que `JWT_SECRET` sea el mismo en `.env` + +## 🎯 Próximos Pasos Recomendados + +1. **Cambiar el JWT_SECRET** en `.env` a un valor aleatorio y seguro +2. **Actualizar la contraseña del admin** después del primer login +3. **Crear usuarios** para tu equipo +4. **Asignar permisos** según las responsabilidades de cada usuario +5. **Probar el sistema** con diferentes usuarios +6. **Configurar backups** de la base de datos PostgreSQL +7. **Documentar** los usuarios y sus permisos para tu organización + +## 📞 Soporte + +Para preguntas o problemas, contacta al equipo de desarrollo. + +--- + +**Desarrollado para Aduanasoft** - Panel de Gestión de Bases de Datos SCAII diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000..cd414e7 --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,66 @@ +-- Schema para sistema de usuarios y permisos +-- Ejecutar en PostgreSQL (base de datos CONTROLDESK) + +-- Tabla de usuarios +CREATE TABLE IF NOT EXISTS usuarios ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + nombre_completo VARCHAR(100), + activo BOOLEAN DEFAULT true, + es_admin BOOLEAN DEFAULT false, + fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + ultimo_acceso TIMESTAMP, + CONSTRAINT chk_username_length CHECK (char_length(username) >= 3) +); + +-- Tabla de permisos de usuario a bases de datos +CREATE TABLE IF NOT EXISTS usuario_base_datos ( + id SERIAL PRIMARY KEY, + usuario_id INTEGER NOT NULL REFERENCES usuarios(id) ON DELETE CASCADE, + base_datos_nombre VARCHAR(255) NOT NULL, -- Nombre de la base de datos (visible_name) + puede_ver BOOLEAN DEFAULT true, + puede_descargar_backup BOOLEAN DEFAULT false, + puede_restaurar BOOLEAN DEFAULT false, + fecha_asignacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(usuario_id, base_datos_nombre) +); + +-- Tabla de sesiones +CREATE TABLE IF NOT EXISTS sesiones ( + id SERIAL PRIMARY KEY, + usuario_id INTEGER NOT NULL REFERENCES usuarios(id) ON DELETE CASCADE, + token VARCHAR(255) UNIQUE NOT NULL, + ip_address VARCHAR(50), + user_agent TEXT, + fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + fecha_expiracion TIMESTAMP NOT NULL, + activa BOOLEAN DEFAULT true +); + +-- Índices para mejor rendimiento +CREATE INDEX IF NOT EXISTS idx_usuario_base_datos_usuario ON usuario_base_datos(usuario_id); +CREATE INDEX IF NOT EXISTS idx_usuario_base_datos_nombre ON usuario_base_datos(base_datos_nombre); +CREATE INDEX IF NOT EXISTS idx_usuarios_activo ON usuarios(activo); +CREATE INDEX IF NOT EXISTS idx_sesiones_token ON sesiones(token); +CREATE INDEX IF NOT EXISTS idx_sesiones_usuario ON sesiones(usuario_id); + +-- Insertar usuario admin por defecto +-- Password: Admin123! +-- Nota: Este hash es temporal, debes cambiarlo después del primer inicio de sesión +INSERT INTO usuarios (username, email, password_hash, nombre_completo, es_admin, activo) +VALUES ( + 'admin', + 'admin@aduanasoft.com', + '$2b$10$rKvvkYpNwF5J5K5J5J5J5uX5X5X5X5X5X5X5X5X5X5X5X5X5X5X5X', + 'Administrador del Sistema', + true, + true +) +ON CONFLICT (username) DO NOTHING; + +-- Comentarios en tablas +COMMENT ON TABLE usuarios IS 'Usuarios del sistema de gestión de bases de datos'; +COMMENT ON TABLE usuario_base_datos IS 'Permisos de acceso de usuarios a bases de datos específicas'; +COMMENT ON TABLE sesiones IS 'Sesiones activas de usuarios'; diff --git a/docker-compose.postgres.yml b/docker-compose.postgres.yml new file mode 100644 index 0000000..82a2fe3 --- /dev/null +++ b/docker-compose.postgres.yml @@ -0,0 +1,52 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: aduanasoft-postgres + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: Control. + POSTGRES_DB: CONTROLDESK + PGDATA: /var/lib/postgresql/data/pgdata + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./database/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - aduanasoft-network + + # Tu aplicación Node.js (opcional, comentado por ahora) + # app: + # build: . + # container_name: aduanasoft-app + # restart: unless-stopped + # ports: + # - "3000:3000" + # environment: + # - NODE_ENV=production + # - DB_POSTGRES_HOST=postgres + # - DB_POSTGRES_PORT=5432 + # - DB_POSTGRES_USER=postgres + # - DB_POSTGRES_PASS=Control. + # - DB_POSTGRES_DB=CONTROLDESK + # depends_on: + # postgres: + # condition: service_healthy + # networks: + # - aduanasoft-network + +volumes: + postgres_data: + driver: local + +networks: + aduanasoft-network: + driver: bridge diff --git a/docs/USUARIOS.md b/docs/USUARIOS.md new file mode 100644 index 0000000..c8ff09f --- /dev/null +++ b/docs/USUARIOS.md @@ -0,0 +1,220 @@ +# Sistema de Gestión de Usuarios y Permisos + +Este sistema permite administrar usuarios y controlar qué bases de datos puede ver cada usuario. + +## Características + +- ✅ Autenticación con JWT +- ✅ Roles de usuario (Admin y Usuario normal) +- ✅ Permisos granulares por base de datos +- ✅ Gestión completa de usuarios (CRUD) +- ✅ Sesiones seguras con cookies HTTP-only +- ✅ Validación de contraseñas fuertes + +## Instalación + +### 1. Instalar dependencias + +```bash +npm install bcrypt jsonwebtoken pg @types/pg @types/bcrypt @types/jsonwebtoken +``` + +### 2. Configurar PostgreSQL + +Ejecuta el script SQL en tu base de datos PostgreSQL: + +```bash +psql -h 10.0.20.152 -U postgres -d CONTROLDESK -f database/schema.sql +``` + +O manualmente desde pgAdmin o DBeaver. + +### 3. Generar hash de contraseña para admin + +```bash +node scripts/generate-password-hash.js +``` + +Copia el hash generado y actualiza el script `database/schema.sql` con la contraseña real. + +### 4. Configurar variables de entorno + +Copia `.env.example` a `.env` y actualiza los valores: + +```bash +cp .env.example .env +``` + +Asegúrate de cambiar el `JWT_SECRET` por un valor seguro y aleatorio. + +## Uso + +### Primer Login + +1. Ve a `http://localhost:5173/login` +2. Usuario: `admin` +3. Contraseña: `Admin123!` (o la que hayas configurado) + +### Gestión de Usuarios (Solo Admins) + +1. Inicia sesión como admin +2. Ve a "Gestión de Usuarios" en el sidebar +3. Podrás: + - Crear nuevos usuarios + - Editar usuarios existentes + - Activar/desactivar usuarios + - Eliminar usuarios + - Asignar permisos de bases de datos + +### Asignar Permisos + +1. En la página de "Gestión de Usuarios", haz clic en el ícono de candado 🔒 del usuario +2. Verás la lista de todas las bases de datos disponibles +3. Haz clic en "Sin Acceso" para otorgar acceso a una base de datos +4. Haz clic en "Con Acceso" para revocar el acceso + +**Nota:** Los usuarios con rol de Administrador tienen acceso a todas las bases de datos automáticamente. + +### Permisos + +Cada usuario puede tener los siguientes permisos por base de datos: + +- **puede_ver**: Permite ver la base de datos en el panel +- **puede_descargar_backup**: Permite descargar archivos de backup (futuro) +- **puede_restaurar**: Permite ejecutar restauraciones (futuro) + +Actualmente, el sistema filtra las bases de datos que el usuario puede ver en: +- Panel principal (dashboard) +- Alertas críticas + +## Roles + +### Administrador +- Acceso completo a todas las bases de datos +- Puede gestionar usuarios y permisos +- Puede ver la sección "Gestión de Usuarios" + +### Usuario Normal +- Solo ve las bases de datos que le han sido asignadas +- No puede gestionar usuarios +- No tiene acceso a la sección de administración + +## Seguridad + +### Contraseñas + +Las contraseñas deben cumplir con los siguientes requisitos: +- Mínimo 8 caracteres +- Al menos una mayúscula +- Al menos una minúscula +- Al menos un número +- Al menos un carácter especial (!@#$%^&*(),.?":{}|<>) + +### Sesiones + +- Las sesiones usan JWT (JSON Web Tokens) +- Los tokens se almacenan en cookies HTTP-only (no accesibles desde JavaScript) +- Los tokens expiran después de 7 días +- En producción, las cookies usan el flag `secure` (solo HTTPS) + +## Estructura de Base de Datos + +### Tabla: `usuarios` +```sql +id SERIAL PRIMARY KEY +username VARCHAR(50) UNIQUE NOT NULL +email VARCHAR(100) UNIQUE NOT NULL +password_hash VARCHAR(255) NOT NULL +nombre_completo VARCHAR(100) +activo BOOLEAN DEFAULT true +es_admin BOOLEAN DEFAULT false +fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP +ultimo_acceso TIMESTAMP +``` + +### Tabla: `usuario_base_datos` +```sql +id SERIAL PRIMARY KEY +usuario_id INTEGER NOT NULL (FK a usuarios) +base_datos_nombre VARCHAR(255) NOT NULL +puede_ver BOOLEAN DEFAULT true +puede_descargar_backup BOOLEAN DEFAULT false +puede_restaurar BOOLEAN DEFAULT false +fecha_asignacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP +``` + +### Tabla: `sesiones` +```sql +id SERIAL PRIMARY KEY +usuario_id INTEGER NOT NULL (FK a usuarios) +token VARCHAR(255) UNIQUE NOT NULL +ip_address VARCHAR(50) +user_agent TEXT +fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP +fecha_expiracion TIMESTAMP NOT NULL +activa BOOLEAN DEFAULT true +``` + +## API + +### Rutas de Autenticación + +- `GET /login` - Página de inicio de sesión +- `POST /login?/login` - Procesar inicio de sesión +- `GET /logout` - Cerrar sesión + +### Rutas de Usuarios (Solo Admins) + +- `GET /usuarios` - Página de gestión de usuarios +- `POST /usuarios?/createUser` - Crear usuario +- `POST /usuarios?/updateUser` - Actualizar usuario +- `POST /usuarios?/deleteUser` - Eliminar usuario +- `POST /usuarios?/managePermissions` - Asignar/revocar permisos +- `GET /usuarios/api/permissions?userId=X` - Obtener permisos de un usuario + +## Troubleshooting + +### Error: "Could not connect to PostgreSQL" + +Verifica que: +1. PostgreSQL esté corriendo en el servidor +2. Las credenciales en `.env` sean correctas +3. El firewall permita conexiones al puerto 5432 +4. La base de datos `CONTROLDESK` exista + +### Error: "Invalid token" + +1. Cierra sesión y vuelve a iniciar sesión +2. Verifica que `JWT_SECRET` sea consistente +3. Limpia las cookies del navegador + +### Error: "User not found" + +1. Verifica que hayas ejecutado el script SQL +2. Revisa que el usuario admin se haya creado correctamente: + ```sql + SELECT * FROM usuarios WHERE username = 'admin'; + ``` + +### No puedo ver algunas bases de datos + +Si eres usuario normal (no admin): +1. Contacta al administrador para que te asigne permisos +2. Verifica en la base de datos: + ```sql + SELECT * FROM usuario_base_datos WHERE usuario_id = TU_ID; + ``` + +## Futuras Mejoras + +- [ ] Recuperación de contraseña por email +- [ ] Autenticación de dos factores (2FA) +- [ ] Registro de auditoría de acciones +- [ ] Permisos más granulares (lectura, escritura, eliminación) +- [ ] Grupos de usuarios +- [ ] API REST para integraciones +- [ ] Notificaciones por email de cambios de permisos + +## Soporte + +Para reportar problemas o sugerencias, contacta al equipo de desarrollo. diff --git a/package-lock.json b/package-lock.json index c0d5155..4e1b4b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,24 +8,46 @@ "name": "transmitiras-dashboard", "version": "0.0.1", "dependencies": { + "bcrypt": "^6.0.0", "bootstrap": "^5.3.3", "dotenv": "^16.4.5", - "mssql": "^10.0.2" + "jsonwebtoken": "^9.0.3", + "mssql": "^10.0.2", + "pg": "^8.18.0" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.3.1", "@sveltejs/adapter-node": "^5.5.2", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0", + "@types/bcrypt": "^6.0.0", + "@types/jsonwebtoken": "^9.0.10", "@types/mssql": "^9.1.5", "@types/node": "^22.0.0", + "@types/pg": "^8.16.0", + "autoprefixer": "^10.4.24", + "postcss": "^8.5.6", "svelte": "^5.0.0-next.1", "svelte-check": "^3.6.0", + "tailwindcss": "^3.4.19", "tslib": "^2.4.1", "typescript": "^5.0.0", "vite": "^5.0.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@azure-rest/core-client": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", @@ -737,6 +759,44 @@ "integrity": "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==", "license": "BSD-3-Clause" }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -1390,6 +1450,16 @@ "integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==", "license": "MIT" }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -1404,6 +1474,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mssql": { "version": "9.1.9", "resolved": "https://registry.npmjs.org/@types/mssql/-/mssql-9.1.9.tgz", @@ -1425,6 +1513,18 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/pug": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", @@ -1496,6 +1596,13 @@ "node": ">= 14" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1510,6 +1617,13 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -1566,6 +1680,43 @@ "node": ">= 0.4" } }, + "node_modules/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1618,6 +1769,30 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1686,6 +1861,40 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1789,6 +1998,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1857,6 +2097,19 @@ "node": ">= 0.6" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2029,6 +2282,20 @@ "dev": true, "license": "MIT" }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -2064,6 +2331,13 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, "node_modules/es-abstract": { "version": "1.24.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", @@ -2262,6 +2536,16 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/esm-env": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", @@ -2304,6 +2588,33 @@ "node": ">=0.8.x" } }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2332,6 +2643,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3167,6 +3492,16 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/js-md4": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", @@ -3232,6 +3567,26 @@ "node": ">=6" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -3300,6 +3655,30 @@ "node": ">= 0.4" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -3551,6 +3930,18 @@ "node": ">=16" } }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -3582,6 +3973,33 @@ "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3592,6 +4010,26 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -3696,6 +4134,95 @@ "dev": true, "license": "MIT" }, + "node_modules/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.11.0", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", + "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3716,6 +4243,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -3754,6 +4301,176 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-5.1.0.tgz", + "integrity": "sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1", + "yaml": "^2.4.2" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -3763,6 +4480,37 @@ "node": ">= 0.6.0" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -3855,6 +4603,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -3933,6 +4692,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -4215,6 +4998,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -4322,6 +5114,39 @@ "node": ">=8" } }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -4446,6 +5271,57 @@ } } }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/tarn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", @@ -4477,6 +5353,77 @@ "node": ">=18.17" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4500,6 +5447,13 @@ "node": ">=6" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -4618,6 +5572,44 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -4815,6 +5807,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/zimmerframe": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", diff --git a/package.json b/package.json index 440f310..d7d4ce2 100644 --- a/package.json +++ b/package.json @@ -13,18 +13,27 @@ "@sveltejs/adapter-node": "^5.5.2", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0", + "@types/bcrypt": "^6.0.0", + "@types/jsonwebtoken": "^9.0.10", "@types/mssql": "^9.1.5", "@types/node": "^22.0.0", + "@types/pg": "^8.16.0", + "autoprefixer": "^10.4.24", + "postcss": "^8.5.6", "svelte": "^5.0.0-next.1", "svelte-check": "^3.6.0", + "tailwindcss": "^3.4.19", "tslib": "^2.4.1", "typescript": "^5.0.0", "vite": "^5.0.0" }, "dependencies": { + "bcrypt": "^6.0.0", "bootstrap": "^5.3.3", "dotenv": "^16.4.5", - "mssql": "^10.0.2" + "jsonwebtoken": "^9.0.3", + "mssql": "^10.0.2", + "pg": "^8.18.0" }, "type": "module" } diff --git a/postcss.config.cjs b/postcss.config.cjs new file mode 100644 index 0000000..5cbc2c7 --- /dev/null +++ b/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/scripts/generate-password-hash.js b/scripts/generate-password-hash.js new file mode 100644 index 0000000..233e232 --- /dev/null +++ b/scripts/generate-password-hash.js @@ -0,0 +1,18 @@ +// Script para generar hash de contraseña para el usuario admin +import bcrypt from 'bcrypt'; + +const password = 'Admin123!'; // Cambiar esta contraseña +const saltRounds = 10; + +bcrypt.hash(password, saltRounds, (err, hash) => { + if (err) { + console.error('Error generando hash:', err); + process.exit(1); + } + + console.log('\n=== Hash de Contraseña Generado ==='); + console.log('Contraseña:', password); + console.log('Hash:', hash); + console.log('\nUsa este hash en el archivo database/schema.sql'); + console.log('para actualizar la contraseña del usuario admin.\n'); +}); diff --git a/scripts/init-database.js b/scripts/init-database.js new file mode 100644 index 0000000..7d5bfd2 --- /dev/null +++ b/scripts/init-database.js @@ -0,0 +1,108 @@ +// Script para inicializar la base de datos PostgreSQL +import { Pool } from 'pg'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Configuración de PostgreSQL (Docker local) +const pool = new Pool({ + host: 'localhost', + port: 5432, + database: 'CONTROLDESK', + user: 'postgres', + password: 'Control.', + max: 1 +}); + +async function initDatabase() { + console.log('================================='); + console.log('Inicializando Base de Datos'); + console.log('=================================\n'); + + try { + // Leer el archivo SQL + const schemaPath = path.join(__dirname, '..', 'database', 'schema.sql'); + console.log('1. Leyendo schema.sql...'); + const sql = fs.readFileSync(schemaPath, 'utf8'); + + // Conectar a PostgreSQL + console.log('2. Conectando a PostgreSQL...'); + const client = await pool.connect(); + console.log(' ✓ Conectado exitosamente\n'); + + // Ejecutar el script SQL + console.log('3. Ejecutando script SQL...'); + await client.query(sql); + console.log(' ✓ Tablas creadas exitosamente\n'); + + // Verificar las tablas creadas + console.log('4. Verificando tablas creadas...'); + const result = await client.query(` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name IN ('usuarios', 'usuario_base_datos', 'sesiones') + ORDER BY table_name; + `); + + if (result.rows.length === 3) { + console.log(' ✓ Tablas verificadas:'); + result.rows.forEach(row => { + console.log(` - ${row.table_name}`); + }); + } else { + console.log(' ⚠ No todas las tablas fueron creadas'); + } + + // Verificar usuario admin + console.log('\n5. Verificando usuario admin...'); + const userResult = await client.query('SELECT username, email, es_admin FROM usuarios WHERE username = $1', ['admin']); + + if (userResult.rows.length > 0) { + console.log(' ✓ Usuario admin creado:'); + console.log(` - Username: ${userResult.rows[0].username}`); + console.log(` - Email: ${userResult.rows[0].email}`); + console.log(` - Es Admin: ${userResult.rows[0].es_admin}`); + } else { + console.log(' ⚠ Usuario admin no encontrado'); + } + + client.release(); + + console.log('\n================================='); + console.log('✓ Inicialización Completa'); + console.log('=================================\n'); + console.log('IMPORTANTE: Ahora debes actualizar la contraseña del admin:'); + console.log('1. Ejecuta: node scripts/generate-password-hash.js'); + console.log('2. Copia el hash generado'); + console.log('3. Ejecuta este SQL en PostgreSQL:'); + console.log(' UPDATE usuarios SET password_hash = \'TU_HASH_AQUI\' WHERE username = \'admin\';'); + console.log('\n4. Luego inicia el servidor: npm run dev'); + console.log('5. Ve a: http://localhost:5173/login'); + console.log(' Usuario: admin'); + console.log(' Contraseña: Admin123!\n'); + + } catch (error) { + console.error('\n❌ Error durante la inicialización:'); + console.error(error.message); + + if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') { + console.error('\n⚠ No se pudo conectar a PostgreSQL.'); + console.error('Verifica que:'); + console.error('1. PostgreSQL esté corriendo en Docker:'); + console.error(' docker-compose -f docker-compose.postgres.yml up -d'); + console.error('2. Verifica el estado: docker ps'); + console.error('3. Ve los logs: docker-compose -f docker-compose.postgres.yml logs postgres'); + } + + process.exit(1); + } finally { + await pool.end(); + } +} + +// Ejecutar +initDatabase(); diff --git a/scripts/init-users.sh b/scripts/init-users.sh new file mode 100644 index 0000000..062ce4c --- /dev/null +++ b/scripts/init-users.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Script de inicialización del sistema de usuarios +echo "===================================" +echo "Inicialización del Sistema de Usuarios" +echo "===================================" +echo "" + +# 1. Verificar si PostgreSQL está disponible +echo "1. Verificando conexión a PostgreSQL..." +if command -v psql &> /dev/null; then + echo " ✓ PostgreSQL CLI encontrado" +else + echo " ✗ PostgreSQL CLI no encontrado" + echo " Por favor, ejecuta el script SQL manualmente" + exit 1 +fi + +# 2. Generar hash de contraseña +echo "" +echo "2. Generando hash de contraseña para admin..." +node scripts/generate-password-hash.js + +# 3. Instrucciones +echo "" +echo "===================================" +echo "Pasos siguientes:" +echo "===================================" +echo "" +echo "1. Copia el hash generado arriba" +echo "" +echo "2. Edita el archivo database/schema.sql y reemplaza el hash temporal" +echo "" +echo "3. Ejecuta el script SQL en PostgreSQL:" +echo " psql -h 10.0.20.152 -U postgres -d CONTROLDESK -f database/schema.sql" +echo "" +echo "4. O ejecuta el SQL manualmente desde pgAdmin/DBeaver" +echo "" +echo "5. Copia .env.example a .env y configura las variables" +echo "" +echo "6. Inicia el servidor: npm run dev" +echo "" +echo "7. Ve a http://localhost:5173/login" +echo " Usuario: admin" +echo " Contraseña: Admin123! (o la que configuraste)" +echo "" +echo "===================================" diff --git a/src/app.css b/src/app.css index 2dac828..582da90 100644 --- a/src/app.css +++ b/src/app.css @@ -1,196 +1,8 @@ -:root { - /* Google Material Colors */ - --md-primary: #1a73e8; - --md-primary-hover: #1557b0; - --md-secondary: #5f6368; - --md-success: #1e8e3e; - --md-danger: #d93025; - --md-warning: #f9ab00; - - --md-bg: #f8f9fa; /* Light gray background */ - --md-surface: #ffffff; - - --md-text-primary: #202124; - --md-text-secondary: #5f6368; - - /* Elevation Shadows */ - --shadow-1: 0 1px 2px 0 rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15); - --shadow-2: 0 1px 3px 0 rgba(60,64,67,0.3), 0 4px 8px 3px rgba(60,64,67,0.15); - - --border-radius: 8px; - --transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} +@tailwind base; +@tailwind components; +@tailwind utilities; body { - font-family: 'Inter', Roboto, sans-serif; - background-color: var(--md-bg); - color: var(--md-text-primary); - min-height: 100vh; -} - -h1, h2, h3, h4, h5, h6 { - color: var(--md-text-primary); - font-weight: 500; -} - -/* Navbar */ -.md-navbar { - background: var(--md-surface); - height: 64px; - box-shadow: var(--shadow-1); - z-index: 1040; -} - -.brand-text { - font-size: 1.25rem; - font-weight: 500; - color: var(--md-text-secondary); -} - -.brand-logo { - color: var(--md-primary); -} - -/* Sidebar */ -.md-sidebar { - background: var(--md-surface); - width: 260px; - height: 100vh; - position: fixed; - top: 0; - left: 0; - z-index: 1030; - padding-top: 64px; /* Matches navbar height */ - border-right: none; /* Shadow handles separation */ - box-shadow: 1px 0 0 rgba(0,0,0,0.12); /* Subtle divider */ - transition: transform 0.3s ease; -} - -.md-nav-link { - display: flex; - align-items: center; - padding: 0 24px; - height: 48px; - color: var(--md-text-secondary); - text-decoration: none; - font-weight: 500; - font-size: 0.875rem; - border-radius: 0 24px 24px 0; /* Material pill shape on one side or full pill */ - margin: 4px 12px 4px 0; - border: none; - background: transparent; - width: 100%; - text-align: left; - cursor: pointer; - transition: background 0.2s; -} - -.md-nav-link:hover { - background-color: #f1f3f4; - color: var(--md-text-primary); -} - -.md-nav-link.active { - background-color: #e8f0fe; - color: var(--md-primary); -} - -.md-nav-icon { - margin-right: 16px; - font-size: 24px; -} - -/* Main Content */ -.md-content-wrapper { - margin-left: 260px; - padding: 24px; - padding-top: 88px; /* 64px navbar + 24px spacing */ - min-height: 100vh; - transition: margin-left 0.3s ease; -} - -/* Cards */ -.md-card { - background: var(--md-surface); - border-radius: var(--border-radius); - padding: 24px; - box-shadow: var(--shadow-1); - margin-bottom: 24px; - border: none; -} - -.md-card-title { - font-size: 1.125rem; - color: var(--md-text-primary); - margin-bottom: 24px; -} - -/* Metrics */ -.md-metric-card { - background: var(--md-surface); - border-radius: var(--border-radius); - padding: 24px; - box-shadow: var(--shadow-1); - display: flex; - flex-direction: column; - height: 100%; -} - -.md-metric-value { - font-size: 2.25rem; - font-weight: 400; - color: var(--md-text-primary); - margin: 8px 0; -} - -.md-metric-label { - color: var(--md-text-secondary); - font-size: 0.875rem; -} - -.md-metric-icon { - color: var(--md-primary); - font-size: 32px; -} - -/* Tables */ -table.dataTable { - width: 100% !important; - border-collapse: collapse !important; -} - -table.dataTable thead th { - border-bottom: 1px solid rgba(0,0,0,0.12) !important; - color: var(--md-text-secondary); - font-weight: 500; - font-size: 0.75rem; - text-transform: uppercase; - padding: 16px !important; -} - -table.dataTable tbody td { - padding: 16px !important; - border-bottom: 1px solid rgba(0,0,0,0.06); - color: var(--md-text-primary); - font-size: 0.875rem; -} - -/* Responsive */ -@media (max-width: 992px) { - .md-sidebar { - transform: translateX(-100%); - } - .md-sidebar.show { - transform: translateX(0); - box-shadow: var(--shadow-2); - } - .md-content-wrapper { - margin-left: 0; - } + @apply bg-slate-100 text-slate-900 min-h-screen antialiased; + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } diff --git a/src/app.html b/src/app.html index a7d8143..24e5b48 100644 --- a/src/app.html +++ b/src/app.html @@ -2,42 +2,16 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + %sveltekit.head% diff --git a/src/lib/assets/icono.png b/src/lib/assets/icono.png new file mode 100644 index 0000000000000000000000000000000000000000..7b4a555aeb1ac47b667e27045009aa828851f0dd GIT binary patch literal 16944 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 literal 0 HcmV?d00001 diff --git a/src/lib/assets/logo.png b/src/lib/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..853d11762749cd64a7ff0365738cbc3ad3352cd4 GIT binary patch literal 17761 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 -