directlx-claude-config/plans/enchanted-meandering-aho.md

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