✨ feat(auth): magic-link request + consume HTTP handlers (ADR-0028 Phase A.4)
Adds the two passwordless-auth endpoints behind /api/v1/auth/:
POST /magic-link/request — body {email}; always 200 (no enumeration leak)
GET /magic-link/consume — ?token=...; signs in (signup-on-first-link)
Sign-up flow: first consume for an unknown email creates the user with a
random unguessable bcrypt-hashed password — keeps the schema NOT NULL
constraint while permanently locking the password endpoints out.
Failure modes (missing/expired/already-consumed) collapse to a single
401 to prevent attackers distinguishing them. DB persist failures on
request silently degrade to the generic 200 to avoid leaking internal
state.
Config:
auth.magic_link.ttl (default 15m, env DLC_AUTH_MAGIC_LINK_TTL)
auth.magic_link.base_url (default http://localhost:8080)
Tests: 11 unit tests against fakes (repo, user service, sender) cover
happy path (new + existing user), normalization, bad JSON, persist
failure, missing/unknown/expired/consumed token, URL builder.
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"dance-lessons-coach/pkg/cache"
|
||||
"dance-lessons-coach/pkg/config"
|
||||
"dance-lessons-coach/pkg/email"
|
||||
"dance-lessons-coach/pkg/greet"
|
||||
"dance-lessons-coach/pkg/middleware"
|
||||
"dance-lessons-coach/pkg/telemetry"
|
||||
@@ -252,6 +253,29 @@ func (s *Server) registerApiV1Routes(r chi.Router) {
|
||||
handler := userapi.NewAuthHandler(s.userService, s.userService, s.validator)
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
handler.RegisterRoutes(r)
|
||||
// Magic-link routes (ADR-0028 Phase A). Mounted only when the
|
||||
// userRepo also implements MagicLinkRepository (PostgresRepository does).
|
||||
if mlRepo, ok := s.userRepo.(user.MagicLinkRepository); ok {
|
||||
emailCfg := s.config.GetEmailConfig()
|
||||
sender := email.NewSMTPSender(email.SMTPConfig{
|
||||
Host: emailCfg.SMTPHost,
|
||||
Port: emailCfg.SMTPPort,
|
||||
Username: emailCfg.SMTPUsername,
|
||||
Password: emailCfg.SMTPPassword,
|
||||
UseTLS: emailCfg.SMTPUseTLS,
|
||||
Timeout: emailCfg.Timeout,
|
||||
})
|
||||
mlHandler := userapi.NewMagicLinkHandler(
|
||||
mlRepo,
|
||||
s.userService,
|
||||
s.userRepo,
|
||||
sender,
|
||||
s.config.GetMagicLinkConfig(),
|
||||
emailCfg.From,
|
||||
s.validator,
|
||||
)
|
||||
mlHandler.RegisterRoutes(r)
|
||||
}
|
||||
})
|
||||
|
||||
// Register admin routes
|
||||
|
||||
Reference in New Issue
Block a user