Commit Graph

18 Commits

Author SHA1 Message Date
darroyo 0ef2991bcd fix(change-password): return updated user via record outcome w/ flush
CI / Build Native (push) Successful in 7m49s
User.getEntityManager().refresh() requiere una tx activa, y el
caller (AuthResource.changePassword) no es @Transactional -> ese
approach tiraba TransactionRequiredException.

En lugar de refrescar en el caller, cambio AuthService.changePassword
para devolver la User ya actualizada en un record
ChangePasswordOutcome, despues de un flush() explicito para que los
cambios lleguen a DB antes de cerrar la tx. Asi el response del
endpoint refleja el estado real de la fila.
2026-08-15 13:30:26 -04:00
darroyo 7b76bde15b fix(change-password): force entity refresh in response to avoid stale mustChangePassword
CI / Build Native (push) Successful in 7m2s
El cambio-password persistia bien a la DB (login con la nueva pass
daba 200 OK), pero la respuesta del endpoint seguia reportando
mustChangePassword: true. Era un problema de Panache/Hibernate cache:
la user entity quedaba cacheada en el persistence context con el
estado pre-cambio, y User.findById() siguiente devolvia esa misma
instancia cacheada.

Fix: despues de findById, forzar refresh desde DB con
User.getEntityManager().refresh(refreshed). Asi el response
refleja el estado actual real de la fila.
2026-08-15 12:02:57 -04:00
darroyo 615b78a847 feat(db): migrate to Flyway for schema persistence
CI / Build Native (push) Successful in 8m10s
'create-drop' borra las tablas en cada shutdown, perdiendo todos
los usuarios (y el cambio de password). Aunque el bind mount
persiste el archivo H2 en /root/docker/manual/lineage2/data/, las
tablas se recreaban vacias en cada restart del container.

Solucion: Flyway para manejar el schema, Hibernate solo valida.

- pom.xml: + quarkus-flyway
- src/main/resources/db/migration/V1__init.sql: schema de users,
  user_state, production_runs + index idx_runs_user_created
- application.properties: database.generation=create-drop -> validate
  + quarkus.flyway.migrate-at-start=true + baseline-on-migrate=true

BootstrapAdmin queda igual: chequea User.count() y crea admin si 0.
Flyway corre antes de Hibernate, asi que las tablas existen cuando
StartupEvent dispara.
2026-08-15 11:40:16 -04:00
darroyo 2ceb7ca7ea fix(frontend): reorder await api.me() before setUser in changePassword
CI / Build Native (push) Successful in 6m22s
En el fix anterior, await api.me() estaba DESPUES de setUser(u). Eso
significaba que React podria re-renderizar AuthenticatedApp entre el
setUser y el await, disparando el usePersistedState.load() que llama
api.getState() mientras la cookie todavia no estaba aplicada al
cookie store del browser. El await api.me() quedaba corriendo mientras
el 401 ya habia sido reportado.

Invertir el orden: await api.me() PRIMERO (confirma que la cookie
quedo realmente aplicada), despues setUser(u). Asi React re-renderiza
con la cookie ya activa para los fetches internos.
2026-08-15 11:24:36 -04:00
darroyo b161710952 fix(frontend): post-change-password cookie timing + defensive 401 fallback
CI / Build Native (push) Successful in 7m45s
Dos cambios para destrabar el flujo post-change-password:

1. AuthContext.changePassword: despues de setUser(u), hacer un round-trip
   a api.me() para confirmar que la nueva cookie ya es utilizable. Sin
   este paso, React monta AuthenticatedApp tan rapido que el primer
   fetch desde usePersistedState.load() corre antes de que el browser
   haya propagado el Set-Cookie (race condition reportado en Chrome).

2. usePersistedState.load: en 401, caer en makeDefaultAppState() en lugar
   de setStateInternal(null). Antes dejaba al usuario varado en un
   <LoadingScreen>Cargando tu estado...</LoadingScreen> para siempre. Ahora
   ve la app vacia con SaveIndicator, y al primer cambio real el
   auto-save sube el estado al server.
2026-08-15 03:27:31 -04:00
darroyo ec5ac6735c chore: revert debug logging after the auth issue was diagnosed
CI / Build Native (push) Successful in 6m24s
Found the actual issue was a stale Docker image in the LXC (the CI
runner was stuck for ~30h starting at task 323 on Aug 14). The newly
pulled image with all prior fixes (no @Authenticated, records w/
@RegisterForReflection, create-drop for tables) has been verified
end-to-end: login returns 200, /me returns 200 with the user,
change-password returns 200 after password validation.

So we can drop the LOG statements and the SQL debug log without
regressing anything.
2026-08-15 02:39:23 -04:00
darroyo 5a83470587 debug(auth): trace de login en AuthService + SQL log en app props
CI / Build Native (push) Failing after 1m19s
Para diagnosticar por qué el login devuelve 401 con la password
correcta del bootstrap admin. Imprime:
- username/pw.length al entrar
- si la busqueda del user en la DB da null
- hash.len y hash.prefix para confirmar que el user esta bien
  almacenado
- bcrypt.matches(true/false)

Tambien activamos quarkus.hibernate-orm.log.sql=true y
quarkus.log.category.'com.l2.shots.auth'.level=DEBUG.
2026-08-15 01:59:08 -04:00
darroyo 995ed39dbc debug: enable SQL logging + auth category logging
CI / Build Native (push) Successful in 6m23s
Para diagnosticar por qué el login devuelve 401 con la password
correcta del bootstrap admin. Vamos a ver qué queries corre
Hibernate y qué pasa en el authenticate.
2026-08-15 01:46:22 -04:00
darroyo 7041fc206b fix(auth): drop @Authenticated/@RolesAllowed on cookie-based endpoints
CI / Build Native (push) Successful in 6m14s
Quarkus security solo lee JWT del header 'Authorization: Bearer ...'
por defecto. Esta app entrega el JWT en una cookie HttpOnly
('auth-token'), entonces @Authenticated/@RolesAllowed rebotaban con
401 antes de que el endpoint pudiera validar manualmente.

Reemplazos:
- @Authenticated en /change-password: sacada. El endpoint ya hace
  jwtCookieAuth.extractToken() y devuelve 401 manual si falla.
- @Authenticated en /check: sacada. Endpoint ahora extrae y valida
  el JWT a mano.
- @Authenticated en /me: never estuvo, OK.
- @RolesAllowed('admin') en /admin/reset-password y /admin/users:
  reemplazada por chequeo manual jwt.getGroups().contains('admin'),
  devolviendo 403 si no es admin.

Sigue funcionando el MustChangePasswordFilter porque sigue siendo
un ContainerRequestFilter con @Priority(AUTHENTICATION+100) y no
requiere auth previa para correr: simplemente aborta con 403 si
mustChangePassword=true salvo allowlist (change-password, logout).

Removida inyeccion no usada de currentJwt (JsonWebToken) que solo
estaba para @RolesAllowed.
2026-08-15 01:26:46 -04:00
darroyo cc09fe8943 fix(dtos): annotate records with @RegisterForReflection for native Jackson
CI / Build Native (push) Successful in 6m18s
Las clases record NO se serializan bien en Quarkus 3.20.1 native-image
aunque Jackson 2.17+ las soporta en JVM. GraalVM strip-ea la metadata
<Erecord> del bytecode mas los accessors auto-generados, y Jackson no
puede detectar que la clase es un record -> cae al BeanSerializer
clasico -> 'no properties discovered to create BeanSerializer'.

@RegisterForReflection le indica a Quarkus: incluí esta clase en la
metadata de reflexion del native-image, asi Jackson la ve completa.

Aplicado a los 16 DTOs (records + nested records):
- auth: AuthMeResponse, AdminUserSummary, Credentials,
        ChangePasswordRequest, AdminResetPasswordRequest, ErrorBody,
        MustChangeBody
- history: RunSummary, RunDetails, RunItem, RunSnapshot, RunIn,
           HistoryStats
- state: AppState + Insumos + FormulaDto (nested)
2026-08-15 01:10:47 -04:00
darroyo 017c58a2b2 refactor(dtos): convert all DTOs to Java records for native-mode Jackson
CI / Build Native (push) Successful in 6m23s
En Quarkus native-image, Jackson no introspecta public fields de clases
con normales (deja de refleccionar field metadata). Salia error:

  No serializer found for class com.l2.shots.auth.AuthMeResponse and
  no properties discovered to create BeanSerializer

Los records tienen auto-accessors (id(), username(), etc) que Jackson
serializa nativamente, sin necesidad de reflection o
@RegisterForReflection.

Convertidos:
- auth: AuthMeResponse, AdminUserSummary, Credentials,
         ChangePasswordRequest, AdminResetPasswordRequest, ErrorBody,
         MustChangeBody
- history: RunSummary, RunDetails, RunItem, RunSnapshot, RunIn,
           HistoryStats
- state: AppState (con Insumos y FormulaDto anidados)

Callers actualizados para usar accessors en vez de field access:
- AuthService.listUsersForAdmin -> AdminUserSummary.from
- AuthService.saveRun -> RunSummary.from / RunDetails.of
- AuthResource.register/login -> creds.username() / creds.password()
- HistoryResource.saveRun -> input.items() etc

Notas:
- RunDetails NO puede extender RunSummary en records (JLS no permite
  extends entre records). Va como record independiente con todos los
  campos. El JSON que produce matchea la interface RunDetails del
  frontend (que extendia RunSummary).
- HistoryStats ahora se construye de una sola vez al final de
  computeStats en lugar de ir mutando campos.
- MustChangeBody quedo con un constructor no-canonico no-arg para
  mantener el call site original (new MustChangeBody()).
2026-08-15 00:46:19 -04:00
darroyo 89a224ee0c fix(bootstrap): use create-drop so tables exist on fresh H2
CI / Build Native (push) Successful in 6m16s
El schema-gen con database.generation=update NO corre en Quarkus native
(3.20.1) ni forzando quarkus.hibernate-orm.schema-management.run-on-startup=true.
El primer 'Table USERS not found' sigue saliendo en BootstrapAdmin.onStart.

Cambio a 'create-drop':
- En startup: crea las tablas que falten (no dropa las existentes).
- En shutdown limpio: drop -> POR CADA docker stop SE PIERDEN LOS USERS.

Para persistencia real a futuro: integrar Flyway con migrations y pasar
database.generation=validate. Para esta entrega, dejamos create-drop
que al menos arranca la app + bootstrap admin en un H2 fresco, y los
restarts sin SIGTERM (docker kill, caida de energia) preservan datos.

Saca tambien el run-on-startup=true que no estaba surtiendo efecto.
2026-08-15 00:15:24 -04:00
darroyo 07588979e4 fix(bootstrap): force EMF init + enable schema-gen on startup
CI / Build Native (push) Successful in 6m24s
El error 'Table USERS not found' en BootstrapAdmin.User.count() se daba
porque en Quarkus native-image, con database.generation=update, el
schema-gen NO corre en startup del container por defecto (corre solo en
dev mode). Resultado: en un H2 fresco, el primer SELECT COUNT(*) del
bootstrap pierde contra una DB sin tablas.

Dos cambios:
1. application.properties: agregar
   quarkus.hibernate-orm.schema-management.run-on-startup=true
   para forzar el schema-gen en startup en prod/native.
2. BootstrapAdmin:
   - @Transactional + EntityManager (em) inyectados
   - em.createNativeQuery('SELECT 1') al inicio de onStart() para
     asegurar que el EMF esté listo y la session esté abierta antes
     del count(). Belt-and-suspenders.
2026-08-15 00:04:17 -04:00
darroyo 6a7f01289b fix(auth): move SecureRandom out of static field for native-image compat
CI / Build Native (push) Successful in 7m25s
GraalVM native-image rejects static fields of type Random/SecureRandom
(their internal state is captured at build time and can't be replayed
deterministically). Move the instance into generatePassword() as a
local variable - it is only used once at startup anyway.
2026-08-14 22:16:36 -04:00
darroyo a171943f98 feat(auth): admin UI for user management + change-password frontend
CI / Build Native (push) Failing after 3m4s
Backend additions:
- User.lastLoginAt column (updated on each successful login)
- AdminUserSummary DTO (id, username, createdAt, lastLoginAt, isAdmin, mustChangePassword)
- GET /api/auth/admin/users (RolesAllowed("admin")) -> array of summaries
- AuthService.listUsersForAdmin() + AuthService.authenticate() now @Transactional and bumps lastLoginAt

Frontend (Phase 1):
- User type extended with mustChangePassword + isAdmin
- api.changePassword() / api.adminResetPassword() / api.adminListUsers()
- AuthContext exposes changePassword
- ChangePasswordPage.tsx (full-page, current + new + confirm, errors inline)
- App.tsx routes LoginPage -> ChangePasswordPage -> AuthenticatedApp

Frontend (Phase 2):
- TabBar supports optional 'usuarios' tab (shown only if user.isAdmin)
- UsersAdminPage.tsx: lista todos los usuarios con badges de rol y estado,
  botón "Resetear contraseña" con modal inline que llama adminResetPassword
- Header de AuthenticatedApp muestra un badge 'admin' al lado del username

Both mvn compile and npm tsc + vite build pass clean.
2026-08-14 20:07:12 -04:00
darroyo 1d6fc08a25 feat(auth): bootstrap admin on first boot + forced password change + admin reset
CI / Build Native (push) Has been cancelled
Backend changes (no frontend yet):

Schema (User entity)
- + mustChange_password (boolean NOT NULL, default false)
- + is_admin (boolean NOT NULL, default false)
Hibernate update mode adds both columns automatically.

BootstrapAdmin (new, ApplicationScoped, @Observes StartupEvent)
- runs only when User.count() == 0 and app.bootstrap.admin.enabled=true
- generates a 20-char random password (alphabet without 0/o/O/1/l/I)
- persists the user with isAdmin=true, mustChangePassword=true
- prints a banner to stdout AND to the JBoss logger so docker logs
  picks it up:
    BOOTSTRAP-ADMIN-USERNAME admin
    BOOTSTRAP-ADMIN-PASSWORD <random>
    BOOTSTRAP-ADMIN-CHANGE   This password MUST be changed on first login ...
- idempotent: skips if any user already exists

MustChangePasswordFilter (new, @Provider ContainerRequestFilter)
- runs after JWT auth (Priorities.AUTHENTICATION + 100)
- for authenticated requests with mustChangePassword=true, returns
  403 with {error, mustChangePassword:true} unless the path is
  /api/auth/change-password or /api/auth/logout

Change-password endpoint (POST /api/auth/change-password)
- @Authenticated, body {currentPassword, newPassword}
- verifies currentPassword via bcrypt, validates newPassword>=8 chars,
  updates hash and sets mustChangePassword=false
- returns updated AuthMeResponse and re-issues the auth cookie

Admin reset endpoint (POST /api/auth/admin/reset-password)
- @RolesAllowed("admin")
- body {username, newPassword}
- sets target's passwordHash and mustChangePassword=true (forces change
  on next login)
- security: only users in the JWT 'admin' group can hit it; isAdmin
  is stored on the user record so a stale token can't promote itself

JWT groups now include 'admin' for isAdmin users; previously everyone
was just 'user'.

Config (application.properties)
- app.bootstrap.admin.enabled=true
- app.bootstrap.admin.username=admin
2026-08-14 20:04:44 -04:00
darroyo 7b89066ef3 fix(ci): gitignore was excluding src/frontend/src/data/defaults.ts
CI / Build Native (push) Failing after 3m18s
CI / Build & Push Native Image (push) Has been skipped
The .gitignore had 'data/' which matches any 'data' directory at
any depth. That excluded src/frontend/src/data/ from git, so the
file defaults.ts (with DEFAULT_INSUMOS, DEFAULT_FORMULAS, etc.)
was never committed. CI was failing with TS2307 because the file
was in the local working tree but not in the git checkout.

Tighten the rule to '/data/' so only the root runtime H2 database
folder is ignored, not the source data directory.
2026-08-12 17:57:23 -04:00
darroyo 9c9fb3a2ca feat: shot-crafter-calculator with H2 persistence and production history
Quarkus 3.20.1 monolith serving React 18 + TypeScript + Tailwind SPA.

Features:
- Three-tab calculator (Insumos, Fórmulas, Calculadora) for
  Soulshot, Spiritshot and Blessed Spiritshot crafting in Lineage 2
  Interlude/Clásico with all 15 grades and pre-loaded recipes
- Real-time profitability computation (cristales → ore →
  crafteos → shots → cost → sale → ganancia)
- Multi-user auth with JWT in httpOnly cookie (bcrypt + RSA 2048)
- H2 file-based persistence in ./data/shots.mv.db (file-based, H2)
- Auto-save on state changes (debounced 500ms)
- Production history with stats (total/avg/best/worst/last5avg)
  and per-run detail modal with snapshot of insumos+formulas

Stack:
- Backend: Quarkus REST + Hibernate ORM Panache + smallrye-jwt
- Frontend: React 18 + TypeScript + Vite + Tailwind 3
- Build: Maven runs frontend-maven-plugin (Node 22 + npm ci)
  then copies dist to META-INF/resources for Quarkus to serve

Verified:
- 5 backend endpoints + 5 history endpoints with curl
- 35/35 browser tests via Playwright + Chromium
- All TS strict, all builds green
2026-08-12 16:03:51 -04:00