66 lines
3.8 KiB
Markdown
66 lines
3.8 KiB
Markdown
# Plan: Admin API Endpoint — Reset User Password
|
|
|
|
## Context
|
|
hiveops-auth has no password reset functionality. The login page has a dead "Forgot Password" link. The admin role and `/auth/api/admin/**` route prefix already exist. Adding an admin password reset endpoint gives operators a way to reset user passwords without direct DB access. The `PASSWORD_CHANGED` audit event type and `logPasswordChanged()` method are already defined but never called.
|
|
|
|
## Files to Create / Modify
|
|
|
|
| Action | File |
|
|
|--------|------|
|
|
| **CREATE** | `src/main/java/com/hiveops/auth/dto/request/AdminPasswordResetRequest.java` |
|
|
| **MODIFY** | `src/main/java/com/hiveops/auth/service/AuthService.java` |
|
|
| **CREATE** | `src/main/java/com/hiveops/auth/controller/UserAdminController.java` |
|
|
|
|
## Implementation Steps
|
|
|
|
### 1. DTO — `AdminPasswordResetRequest.java`
|
|
- Single field: `newPassword` (String)
|
|
- Jakarta validation: `@NotBlank`, `@Size(min=12)` — mirrors the existing `RegisterRequest` pattern
|
|
- Package: `com.hiveops.auth.dto.request`
|
|
|
|
### 2. AuthService — `resetUserPassword(String email, String newPassword)`
|
|
- Lookup user with `userRepository.findByEmail(email)` → throw `UsernameNotFoundException` (Spring Security) if absent (already handled by `GlobalExceptionHandler` returning 404-ish; check and use an appropriate existing exception or plain `RuntimeException`)
|
|
- Validate password strength via `PasswordValidator.validatePassword(newPassword)` — throw `IllegalArgumentException` with the error message if invalid
|
|
- Encode: `passwordEncoder.encode(newPassword)` → `user.setPasswordHash(...)`
|
|
- Side effects for security:
|
|
- `user.resetFailedLoginAttempts()` — clears lock + counter
|
|
- Revoke all existing refresh tokens via `refreshTokenRepository.revokeAllByUser(user)` (already exists — sets `revoked=true`, preserving audit history)
|
|
- `userRepository.save(user)`
|
|
- `authAuditService.logPasswordChanged(user)` — already implemented, just unused
|
|
|
|
### 3. Controller — `UserAdminController.java`
|
|
- Pattern mirrors `RateLimitAdminController` exactly:
|
|
- `@RestController`, `@RequestMapping("/auth/api/admin/users")`, `@RequiredArgsConstructor`, `@Slf4j`
|
|
- `@Tag(name = "User Admin", description = "User management (Admin only)")`
|
|
- All endpoints: `@PreAuthorize("hasRole('ADMIN')")`
|
|
- Single endpoint for now:
|
|
```
|
|
POST /auth/api/admin/users/{email}/reset-password
|
|
Body: { "newPassword": "..." }
|
|
Response 200: { "message": "Password reset successfully", "email": "..." }
|
|
Response 404: { "message": "User not found", "email": "..." }
|
|
Response 400: { "message": "<validation error>", "email": "..." }
|
|
```
|
|
- Log at INFO level: `"Admin password reset for user: {}"` (email only, never the password)
|
|
|
|
## Key Reused Components
|
|
- `PasswordEncoder` bean (BCrypt strength 12) — injected into `AuthService`
|
|
- `PasswordValidator.validatePassword()` — `util/PasswordValidator.java`
|
|
- `authAuditService.logPasswordChanged(user)` — `service/AuthAuditService.java`
|
|
- `userRepository.findByEmail()` — `repository/UserRepository.java`
|
|
- `refreshTokenRepository.revokeAllByUser(user)` — `repository/RefreshTokenRepository.java`
|
|
- `user.resetFailedLoginAttempts()` — `entity/User.java`
|
|
- `@PreAuthorize("hasRole('ADMIN')")` pattern — `RateLimitAdminController.java`
|
|
|
|
## Verification
|
|
1. Build: `./mvnw compile` — must be clean
|
|
2. Start service (dev profile, H2 in-memory)
|
|
3. Register a test user, obtain an admin JWT
|
|
4. `POST /auth/api/admin/users/{email}/reset-password` with valid new password → 200
|
|
5. `POST /auth/api/login` with old password → 401
|
|
6. `POST /auth/api/login` with new password → 200
|
|
7. Attempt same endpoint without ADMIN role → 403
|
|
8. Attempt with unknown email → 404
|
|
9. Attempt with weak password (< 12 chars) → 400
|
|
10. Check audit log table for `PASSWORD_CHANGED` event
|