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.
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.
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
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.