feat(server/m7-user-mgmt): forgot + reset password endpoints (U3)
POST /api/auth/forgot-password and POST /api/auth/reset-password. Forgot-password ALWAYS returns 200 with empty JSON to prevent enumeration of registered emails. Side effect: when email matches a user with email-on-file, generates a 32-byte hex token (24h TTL), inserts into password_resets, and sends the reset email via the mailer. Mailer failures are logged (not surfaced) and the audit log carries metadata.email_match for operator visibility. Reset-password atomically claims the token via UsePasswordReset (:execrows; concurrent calls can't both succeed). On rows=1, hashes the new password and writes via ChangeUserPassword. Returns 204 on success, 400 invalid_token on stale/used/missing tokens, 400 password_too_short for short passwords. Audits ActionPasswordResetByEmail. Wires the mailer.Sender into the handlers struct via Mount; production sender (NewSMTPSender) constructed in server.Router(); tests inject FakeSender via testHandlers default. The reset URL embedded in the email is derived from r.Host (no PublicURL config setting in v1; self-hosted operators see their own hostname). Tests cover happy-path send + token-row insertion, unknown email returns 200 with no send, mailer failure still returns 200, reset happy path verifies bcrypt match + used_at set, already-used token 400, expired token 400, short password 400, bogus token 400. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ import (
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrconfig"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrquarantine"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrrequests"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/mailer"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/playevents"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/playlists"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/subsonic"
|
||||
@@ -115,7 +116,8 @@ func (s *Server) Router() http.Handler {
|
||||
lidarrQuar := lidarrquarantine.NewService(s.Pool, lidarrCfg, lidarrClientFn)
|
||||
tracksSvc := tracks.NewService(s.Pool, s.Logger, lidarrUnmonitorAdapter{fn: lidarrClientFn}, s.DataDir)
|
||||
playlistsSvc := playlists.NewService(s.Pool, s.Logger, s.DataDir)
|
||||
api.Mount(r, s.Pool, s.Logger, writer, s.RecommendationCfg, lidarrCfg, lidarrReqs, lidarrQuar, tracksSvc, playlistsSvc, s.CoverEnricher, s.CoverArtBackfillCap, s.CoverSettings, s.LibraryScanner, s.ScanCfg, s.Scheduler, s.DataDir)
|
||||
smtpSender := mailer.NewSMTPSender(s.Pool, s.Logger.With("component", "mailer"))
|
||||
api.Mount(r, s.Pool, s.Logger, writer, s.RecommendationCfg, lidarrCfg, lidarrReqs, lidarrQuar, tracksSvc, playlistsSvc, s.CoverEnricher, s.CoverArtBackfillCap, s.CoverSettings, s.LibraryScanner, s.ScanCfg, s.Scheduler, s.DataDir, smtpSender)
|
||||
// /api/admin/scan is the only admin route owned by the server package
|
||||
// (it needs the Scanner). Register it as a single inline-middleware
|
||||
// route — using r.Route("/api/admin", ...) here would create a second
|
||||
|
||||
Reference in New Issue
Block a user