feat(auth): add admin-only /api/auth/reset-password endpoint

POST /api/auth/reset-password
Body: { email, new_password }
Auth: requires admin role (get_current_admin)
Response: { ok, email, id, reset_by }

- Validates new_password >= 6 chars
- Returns 404 if target email not found
- Logs admin_id + target_user_id for audit trail
- Fixes passlib sha256_crypt cross-version verify bug by allowing
  admin to rehash any user's password on demand
This commit is contained in:
IT狗
2026-07-22 10:56:09 +08:00
parent a7dfd208d2
commit 5596ab2cb5
+39
View File
@@ -266,6 +266,45 @@ async def login(form_data: LoginRequest, db: Session = Depends(get_db)):
async def get_me(current_user: User = Depends(get_current_user)):
return current_user
class ResetPasswordRequest(BaseModel):
email: str
new_password: str
@app.post("/api/auth/reset-password")
async def reset_password(
payload: ResetPasswordRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin),
):
"""Admin-only: reset another user's password.
Body: {"email": "<target email>", "new_password": "<new plaintext>"}
"""
if not payload.email or not payload.new_password:
raise HTTPException(status_code=400, detail="email and new_password are required")
if len(payload.new_password) < 6:
raise HTTPException(status_code=400, detail="new_password must be at least 6 characters")
target = db.query(User).filter(User.email == payload.email).first()
if not target:
raise HTTPException(status_code=404, detail=f"User not found: {payload.email}")
target.password_hash = get_password_hash(payload.new_password)
db.commit()
db.refresh(target)
logger.info(
"password reset by admin",
extra={"context": {"admin_id": current_user.id, "target_user_id": target.id, "email": payload.email}},
)
return {
"ok": True,
"email": target.email,
"id": target.id,
"reset_by": current_user.email,
}
# ============ Excel File Storage ============
def get_excel_path(section: str) -> str:
"""Get path to the stored Excel file for a section"""