feat(go-api): implement password-reset flow (issue #15282) (#15293)

## Summary

Ports the Python password-reset flow to Go, adding 4 unauthenticated
endpoints under `/api/v1/auth/password/`:

- `POST /auth/password/forgot/captcha` — generates and returns a PNG
captcha image; stores the plaintext code in Redis (60 s TTL)
- `POST /auth/password/forgot/otp` — verifies captcha, enforces resend
cooldown (60 s), generates HMAC-SHA256-hashed OTP (300 s TTL), sends
plain-text email via SMTP
- `POST /auth/password/forgot/otp/verify` — verifies OTP with attempt
counting (lock after 5 failures for 30 min), sets a
`otp:verified:{email}` flag (300 s TTL) on success
- `POST /auth/password/reset` — checks verified flag, decrypts +
validates passwords, updates user record, auto-logs in (issues JWT,
returns user profile)

Closes #15282
This commit is contained in:
web-dev0521
2026-06-01 19:38:02 -06:00
committed by GitHub
parent 1748723971
commit 1696d4ead6
9 changed files with 1424 additions and 3 deletions

View File

@@ -26,6 +26,21 @@ import (
// HandleNoRoute handles requests to undefined routes
func HandleNoRoute(c *gin.Context) {
// Python parity: GET /api/v1/auth/login/ (an empty OAuth channel) resolves
// to a Werkzeug MethodNotAllowed in the Python API, which
// server_error_response renders as HTTP 200 / code 100 with the
// exception's repr() as the message. gin instead falls through to
// NoRoute, so emit the same body here to keep the auth error paths
// byte-for-byte aligned.
if c.Request.Method == http.MethodGet && c.Request.URL.Path == "/api/v1/auth/login/" {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeExceptionError,
"data": nil,
"message": "<MethodNotAllowed '405: Method Not Allowed'>",
})
return
}
// Log the request details on server side
common.Logger.Warn("The requested URL was not found",
zap.String("method", c.Request.Method),