diff --git a/backend/scripts/reset_passwords.py b/backend/scripts/reset_passwords.py new file mode 100644 index 0000000..156640e --- /dev/null +++ b/backend/scripts/reset_passwords.py @@ -0,0 +1,49 @@ +""" +Script para resetear contraseñas de todos los usuarios a valores conocidos. +Ejecutar con: python -m scripts.reset_passwords (desde /app en el contenedor) +""" +import asyncio +from sqlalchemy import select, update +from app.core.database import AsyncSessionLocal +from app.core.security import security +from app.models.user import User + +# Mapa email -> nueva contraseña +PASSWORD_MAP = { + "admin@aduanasoft.com": "admin123", + "admin@test.com": "admin123", + "manager@aduanasoft.com": "manager123", + "agente@aduanasoft.com": "agente123", + "auditor1@test.com": "auditor123", + "admin-cliente@empresa-demo.com": "clienteadmin123", + "cliente@empresa-demo.com": "cliente123", + "test_user@aduanasoft.com": "test123", +} + +async def reset_all_passwords(): + async with AsyncSessionLocal() as db: + result = await db.execute(select(User)) + users = result.scalars().all() + + updated = 0 + skipped = 0 + for user in users: + if user.email in PASSWORD_MAP: + plain = PASSWORD_MAP[user.email] + user.password_hash = security.hash_password(plain) + user.email_verified = True + user.is_active = True + updated += 1 + print(f" ✅ {user.email} → {plain}") + else: + skipped += 1 + print(f" ⚠️ {user.email} (sin contraseña definida, se omite)") + + await db.commit() + print(f"\nResumen: {updated} actualizados, {skipped} omitidos") + print("\n📋 Credenciales listas:") + for email, pwd in PASSWORD_MAP.items(): + print(f" {email} / {pwd}") + +if __name__ == "__main__": + asyncio.run(reset_all_passwords()) diff --git a/db/schema.sql b/db/schema.sql index 2285005..eb0637a 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -415,18 +415,19 @@ INSERT INTO tenants (name, slug, contact_email) VALUES ('Aduanasoft Demo', 'aduanasoft-demo', 'demo@aduanasoft.com'); -- Usuario admin por defecto (password: admin123) --- Hash generado con Argon2: $argon2id$v=19$m=65536,t=3,p=4$... +-- Hash Argon2id generado con m=65536,t=3,p=4 INSERT INTO users (tenant_id, email, first_name, last_name, password_hash, role, is_active, email_verified) SELECT id, 'admin@aduanasoft.com', 'Admin', 'Sistema', - '$argon2id$v=19$m=65536,t=3,p=4$example_hash_here', + '$argon2id$v=19$m=65536,t=3,p=4$wpjz/t+bM4bQmtM6B6A0pg$ELwnGUL4S1Y6tywp0LS6cre0bvWEoVuJ845spZ9Z9IQ', 'ADMIN', true, true -FROM tenants WHERE slug = 'aduanasoft-demo'; +FROM tenants WHERE slug = 'aduanasoft-demo' +ON CONFLICT (tenant_id, email) DO NOTHING; -- Categorías por defecto INSERT INTO ticket_categories (tenant_id, name, description, sla_response_hours, sla_resolution_hours) diff --git a/docker-compose.yml b/docker-compose.yml index c34a9cd..1b85d3a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -164,6 +164,8 @@ services: - NODE_ENV=${ENVIRONMENT:-development} - PUBLIC_API_URL=http://backend:8000 - PUBLIC_APP_NAME=ServiceManager Cliente + - PORT=3000 + - HMR_CLIENT_PORT=3000 volumes: - ./frontend-client:/app - /app/node_modules @@ -189,7 +191,8 @@ services: - NODE_ENV=${ENVIRONMENT:-development} - PUBLIC_API_URL=http://backend:8000 - PUBLIC_APP_NAME=ServiceManager Admin - - PORT=3000 # El contendor corre en 3000; docker mapea 3001:3000 al host + - PORT=3000 + - HMR_CLIENT_PORT=3001 volumes: - ./frontend-internal:/app - /app/node_modules diff --git a/frontend-client/src/routes/+layout.svelte b/frontend-client/src/routes/+layout.svelte index c83fa45..1c526ac 100644 --- a/frontend-client/src/routes/+layout.svelte +++ b/frontend-client/src/routes/+layout.svelte @@ -4,13 +4,25 @@ import Toast from '$lib/components/Toast.svelte'; import { onMount } from 'svelte'; import { auth } from '$lib/stores/auth.js'; + import { goto } from '$app/navigation'; import { page } from '$app/stores'; + import { browser } from '$app/environment'; import '../app.css'; + let mounted = false; + onMount(() => { auth.init(); + mounted = true; }); + // Guard reactivo global: redirige a /login si no está autenticado en rutas protegidas + $: if (browser && mounted && !$auth.isAuthenticated && + !$page.url.pathname.startsWith('/login') && + !$page.url.pathname.startsWith('/register')) { + goto('/login'); + } + $: showHeader = !$page.url.pathname.startsWith('/login') && !$page.url.pathname.startsWith('/register'); diff --git a/frontend-client/vite.config.js b/frontend-client/vite.config.js index 389110b..1713f1d 100644 --- a/frontend-client/vite.config.js +++ b/frontend-client/vite.config.js @@ -10,6 +10,16 @@ export default defineConfig({ usePolling: true, interval: 500 }, + // HMR: el browser llega al contenedor en el mismo puerto 3000 + hmr: { + host: 'localhost', + clientPort: parseInt(process.env.HMR_CLIENT_PORT || '3000') + }, + // Permitir que Vite sirva archivos del filesystem del contenedor + fs: { + allow: ['/app', '.'], + strict: false + }, proxy: { '/api': { target: process.env.PUBLIC_API_URL || 'http://localhost:8000', diff --git a/frontend-internal/src/routes/+layout.svelte b/frontend-internal/src/routes/+layout.svelte index d555adb..b73fdda 100644 --- a/frontend-internal/src/routes/+layout.svelte +++ b/frontend-internal/src/routes/+layout.svelte @@ -5,14 +5,24 @@ import { toast } from '$lib/stores/toast.js'; import { onMount } from 'svelte'; import { auth } from '$lib/stores/auth.js'; + import { goto } from '$app/navigation'; + import { page } from '$app/stores'; + import { browser } from '$app/environment'; import '../app.css'; let sidebarOpen = false; + let mounted = false; onMount(() => { auth.init(); + mounted = true; }); + // Guard reactivo global: redirige a /login si no está autenticado + $: if (browser && mounted && !$auth.isAuthenticated && $page.url.pathname !== '/login') { + goto('/login'); + } + function toggleSidebar() { sidebarOpen = !sidebarOpen; } diff --git a/frontend-internal/vite.config.js b/frontend-internal/vite.config.js index c599023..f86f799 100644 --- a/frontend-internal/vite.config.js +++ b/frontend-internal/vite.config.js @@ -4,13 +4,23 @@ import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()], server: { - // Puerto: 3001 por defecto en local; Docker lo sobreescribe con PORT=3000 + // Puerto: dentro del contenedor siempre 3000; Docker mapea 3001:3000 al host port: parseInt(process.env.PORT || '3001'), host: '0.0.0.0', watch: { usePolling: true, interval: 500 }, + // HMR: el browser llega al contenedor a través del puerto 3001 del host + hmr: { + host: 'localhost', + clientPort: parseInt(process.env.HMR_CLIENT_PORT || '3001') + }, + // Permitir que Vite sirva archivos del filesystem del contenedor + fs: { + allow: ['/app', '.'], + strict: false + }, proxy: { '/api': { target: process.env.PUBLIC_API_URL || 'http://localhost:8000', @@ -23,7 +33,7 @@ export default defineConfig({ port: parseInt(process.env.PORT || '3001'), host: '0.0.0.0' }, - build: { - target: 'esnext' - } + build: { + target: 'esnext' + } }); diff --git a/scripts/seed_data.py b/scripts/seed_data.py index b1eb32a..9c29889 100644 --- a/scripts/seed_data.py +++ b/scripts/seed_data.py @@ -216,15 +216,18 @@ async def main(): if user_data["email"] in existing_emails: print(f" ⏭ Ya existe: {user_data['email']}") continue - pwd = user_data.pop("password") + # Usar copia para no mutar el dict original (permite re-ejecutar el script) + ud = user_data.copy() + pwd = ud.pop("password") hashed_pwd = security.hash_password(pwd) user = User( tenant_id=tenant_id, password_hash=hashed_pwd, - **user_data, + email_verified=True, # Marcar como verificado para permitir login + **ud, ) session.add(user) - print(f" ✓ {user_data['email']} [{user_data['role'].value}] pwd={pwd}") + print(f" ✓ {ud['email']} [{ud['role'].value}] pwd={pwd}") created_users += 1 await session.commit()