feat(auth): admin UI for user management + change-password frontend
CI / Build Native (push) Failing after 3m4s
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:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user