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.
This commit is contained in:
2026-08-14 20:07:12 -04:00
parent 1d6fc08a25
commit a171943f98
10 changed files with 410 additions and 4 deletions
@@ -0,0 +1,24 @@
package com.l2.shots.auth;
import java.time.Instant;
import java.util.UUID;
public class AdminUserSummary {
public UUID id;
public String username;
public Instant createdAt;
public Instant lastLoginAt;
public boolean isAdmin;
public boolean mustChangePassword;
public AdminUserSummary() {}
public AdminUserSummary(User u) {
this.id = u.id;
this.username = u.username;
this.createdAt = u.createdAt;
this.lastLoginAt = u.lastLoginAt;
this.isAdmin = u.isAdmin;
this.mustChangePassword = u.mustChangePassword;
}
}
@@ -144,6 +144,13 @@ public class AuthResource {
.orElse(Response.status(401).build());
}
@GET
@Path("/admin/users")
@RolesAllowed("admin")
public Response adminListUsers() {
return Response.ok(authService.listUsersForAdmin()).build();
}
@GET
@Path("/check")
@Authenticated
@@ -64,14 +64,23 @@ public class AuthService {
return user;
}
@Transactional
public Optional<User> authenticate(String username, String password) {
if (username == null || password == null) return Optional.empty();
User user = User.findByUsernameCaseInsensitive(username.trim());
if (user == null) return Optional.empty();
if (!BcryptUtil.matches(password, user.passwordHash)) return Optional.empty();
user.lastLoginAt = Instant.now();
user.persist();
return Optional.of(user);
}
public java.util.List<AdminUserSummary> listUsersForAdmin() {
return User.<User>listAll().stream()
.map(AdminUserSummary::new)
.collect(java.util.stream.Collectors.toList());
}
public String buildToken(UUID userId) {
User user = User.findById(userId);
if (user == null) throw new IllegalStateException("user not found: " + userId);
@@ -31,6 +31,9 @@ public class User extends PanacheEntityBase {
@Column(name = "is_admin", nullable = false)
public boolean isAdmin = false;
@Column(name = "last_login_at")
public Instant lastLoginAt;
public static User findByUsername(String username) {
return find("username", username.toLowerCase()).firstResult();
}