package api import ( "strconv" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/limiter" samlauth "wucher/internal/auth" "wucher/internal/service" "wucher/internal/transport/http/handlers" "wucher/internal/transport/http/middleware" ) func registerAuthRoutes(v1 fiber.Router, authHandler *handlers.AuthHandler, authSvc *service.AuthService, samlHandler *samlauth.Handler) { auth := v1.Group("/auth") auth.Post("/register", authHandler.Register) auth.Post("/invite", middleware.AuthRequired(authSvc), middleware.PermissionRequired(authSvc, "user.create"), authHandler.Invite) auth.Post("/invite/resend-set-password", middleware.AuthRequired(authSvc), middleware.PermissionRequired(authSvc, "user.update"), forgotPasswordIPLimiter(authSvc), authHandler.ResendSetPasswordInvite) auth.Post("/set-password", authHandler.SetPassword) auth.Post("/forgot-password", forgotPasswordIPLimiter(authSvc), authHandler.ForgotPassword) auth.Post("/reset-password", authHandler.ResetPassword) auth.Post("/pin/setup", middleware.AuthRequired(authSvc), authHandler.SetupSecurityPIN) auth.Post("/pin/verify", middleware.AuthRequired(authSvc), authHandler.VerifySecurityPIN) auth.Post("/pin/change", middleware.AuthRequired(authSvc), authHandler.ChangeSecurityPIN) auth.Post("/pin/request-reset", middleware.AuthRequired(authSvc), forgotPasswordIPLimiter(authSvc), authHandler.RequestSecurityPINReset) auth.Post("/pin/forgot", forgotPasswordIPLimiter(authSvc), authHandler.ForgotSecurityPIN) auth.Post("/pin/reset", authHandler.ResetSecurityPIN) auth.Post("/login", loginIPLimiter(authSvc), authHandler.Login) auth.Post("/exchange", authHandler.Exchange) auth.Post("/webauthn/register/begin", middleware.AuthRequired(authSvc), authHandler.WebAuthnRegisterBegin) auth.Post("/webauthn/register/finish", middleware.AuthRequired(authSvc), authHandler.WebAuthnRegisterFinish) auth.Post("/webauthn/login/begin", authHandler.WebAuthnLoginBegin) auth.Post("/webauthn/login/finish", authHandler.WebAuthnLoginFinish) auth.Post("/webauthn/reset-setup", middleware.AuthRequired(authSvc), authHandler.WebAuthnResetSetup) auth.Post("/refresh", authHandler.Refresh) auth.Post("/logout", authHandler.Logout) auth.Get("/sessions", middleware.AuthRequired(authSvc), authHandler.Sessions) auth.Delete("/sessions/:session_id", middleware.AuthRequired(authSvc), authHandler.DeleteSession) auth.Get("/me", middleware.AuthRequired(authSvc), authHandler.Me) auth.Patch("/me/timezone", middleware.AuthRequired(authSvc), authHandler.UpdateTimezone) auth.Get("/verify-email", authHandler.VerifyEmail) if samlHandler != nil { auth.Get("/microsoft/login", samlHandler.HandleLogin) auth.Get("/microsoft/metadata", samlHandler.HandleMetadata) } else { auth.Get("/microsoft/login", authHandler.MicrosoftLogin) } auth.Get("/microsoft/login-check", authHandler.MicrosoftLoginCheck) auth.Get("/microsoft/link", middleware.AuthRequired(authSvc), authHandler.MicrosoftLink) auth.Get("/microsoft/reauth", middleware.AuthRequired(authSvc), authHandler.MicrosoftReauth) if samlHandler != nil { auth.Get("/microsoft/callback", samlHandler.HandleSAMLCallback) auth.Post("/microsoft/callback", samlHandler.HandleSAMLCallback) auth.Get("/microsoft/logout", func(c *fiber.Ctx) error { if c.Query("SAMLRequest") != "" { return samlHandler.HandleSAMLLogoutRequest(c) } if c.Query("SAMLResponse") != "" { return samlHandler.HandleSAMLLogoutResponse(c) } return samlHandler.HandleBrowserLocalLogout(c) }) auth.Post("/microsoft/logout", func(c *fiber.Ctx) error { if c.FormValue("SAMLRequest") != "" { return samlHandler.HandleSAMLLogoutRequest(c) } if c.FormValue("SAMLResponse") != "" { return samlHandler.HandleSAMLLogoutResponse(c) } return samlHandler.HandleBrowserLocalLogout(c) }) } else { auth.Get("/microsoft/callback", authHandler.MicrosoftCallback) auth.Post("/microsoft/logout", middleware.AuthRequired(authSvc), authHandler.MicrosoftLogout) } // OIDC front-channel logout: invoked by Microsoft Entra in a hidden iframe when the // user signs out at the IdP. Must be public (the IdP has no app credentials) and // available regardless of the active Microsoft auth mode. auth.Get("/microsoft/front-channel-logout", authHandler.MicrosoftFrontChannelLogout) auth.Post("/microsoft/front-channel-logout", authHandler.MicrosoftFrontChannelLogout) auth.Post("/microsoft/refresh", middleware.AuthRequired(authSvc), authHandler.MicrosoftRefresh) auth.Post("/sso/link", middleware.AuthRequired(authSvc), authHandler.SSOLink) auth.Delete("/sso/unlink", middleware.AuthRequired(authSvc), authHandler.SSOUnlink) auth.Post("/totp/setup", middleware.AuthRequired(authSvc), authHandler.TOTPSetup) auth.Post("/totp/confirm", middleware.AuthRequired(authSvc), authHandler.TOTPConfirm) auth.Post("/totp/verify", authHandler.TOTPVerify) auth.Post("/email-otp/send", authHandler.SendEmailOTP) auth.Post("/email-otp/verify", authHandler.VerifyEmailOTP) auth.Post("/totp/disable", middleware.AuthRequired(authSvc), authHandler.TOTPDisable) auth.Post("/totp/reset-setup", middleware.AuthRequired(authSvc), authHandler.TOTPResetSetup) } func forgotPasswordIPLimiter(authSvc *service.AuthService) fiber.Handler { window := authSvc.ForgotPasswordIPRateWindow() return limiter.New(limiter.Config{ Max: authSvc.ForgotPasswordIPRateMax(), Expiration: window, KeyGenerator: func(c *fiber.Ctx) string { return c.IP() }, LimitReached: func(c *fiber.Ctx) error { retryAfter := int(window.Seconds()) if retryAfter < 1 { retryAfter = 1 } c.Set(fiber.HeaderRetryAfter, strconv.Itoa(retryAfter)) return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ "errors": []fiber.Map{{ "status": "429", "title": "Too Many Requests", "detail": "rate limit exceeded", }}, }) }, }) } func loginIPLimiter(authSvc *service.AuthService) fiber.Handler { window := authSvc.LoginIPRateWindow() return limiter.New(limiter.Config{ Max: authSvc.LoginIPRateMax(), Expiration: window, KeyGenerator: func(c *fiber.Ctx) string { return c.IP() }, LimitReached: func(c *fiber.Ctx) error { retryAfter := int(window.Seconds()) if retryAfter < 1 { retryAfter = 1 } c.Set(fiber.HeaderRetryAfter, strconv.Itoa(retryAfter)) return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ "errors": []fiber.Map{{ "status": "429", "title": "Too Many Requests", "detail": "rate limit exceeded", }}, }) }, }) }