package user import ( "fmt" "strings" "git.golem15.com/golem15/sm-user-plugin/controllers" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/surf" ) func (p *Plugin) Routes(r pact.Router) error { guard, err := p.turnstileGuard() if err != nil { return err } public, err := p.publicPasswordRoutes() if err != nil { return err } r.Group("/_user/api/v1", surf.Use("throttle:user-api"), func(g pact.Router) { if public { g.Post("/login", controllers.Login(p.app), guard...) } g.Post("/logout", controllers.Logout(p.app)) g.Get("/fetch", controllers.Fetch(p.app)) g.Post("/refresh", controllers.Refresh(p.app)) g.Post("/register", controllers.Register(p.app), guard...) g.Get("/oauth-providers", controllers.OAuthProviders(p.app)) if public { g.Post("/forgot-password", controllers.ForgotPassword(p.app)) g.Post("/reset-password", controllers.ResetPassword(p.app)) } g.Post("/activate", controllers.Activate(p.app)) g.Post("/activate-by-code", controllers.ActivateByCode(p.app)) g.Post("/update", controllers.Update(p.app)) g.Post("/change-password", controllers.ChangePassword(p.app)) g.Post("/marketing-consent", controllers.MarketingConsent(p.app)) g.Post("/avatar", controllers.UploadAvatar(p.app)) g.Post("/avatar/remove", controllers.RemoveAvatar(p.app)) g.Post("/tokens", controllers.APITokenStore(p.app), "jwt.auth") g.Get("/tokens", controllers.APITokenIndex(p.app), "jwt.auth") g.Get("/tokens/scopes", controllers.APITokenScopes(p.app), "jwt.auth") g.Delete("/tokens/{id}", controllers.APITokenDestroy(p.app), "jwt.auth") g.Where("id", "[0-9]+") g.Get("/backend-link/{code}", controllers.BackendLinkShow(p.app)) g.Where("code", "[a-f0-9]{64}") g.Post("/backend-link/show", controllers.BackendLinkShowPost(p.app)) g.Post("/backend-link/confirm", controllers.BackendLinkConfirm(p.app), "jwt.auth") g.Get("/token/me", controllers.TokenMe(p.app), "user.api_token", "user.scope:read") }) return nil } // turnstileGuard returns the middleware that golem15.user.turnstile adds to // POST /login and /register. The key is opt-in: empty (the default) adds // nothing, "open" and "closed" select the framework's turnstile:{mode} // middleware (summercms v0.3.0 and later), and any other value is refused so // a typo cannot silently leave the forms unprotected. func (p *Plugin) turnstileGuard() ([]string, error) { if p.app == nil || p.app.Config == nil { return nil, nil } switch mode := strings.TrimSpace(p.app.Config.String("golem15.user.turnstile")); mode { case "": return nil, nil case "open", "closed": return []string{"turnstile:" + mode}, nil default: return nil, fmt.Errorf("golem15.user: golem15.user.turnstile must be empty, open or closed, got %q", mode) } } // publicPasswordRoutes reports golem15.user.password.public_routes, which // decides whether POST /login, /forgot-password and /reset-password are // registered. The default is true. False leaves them unregistered, so they // answer the router's own 404 like any unknown path. The key is read when the // routes are built, so a change needs a restart. It takes a bool or the // string true or false (any case, surrounding spaces trimmed; empty or unset // means true); any other value is refused so a typo cannot silently remove // (or keep) the sign-in routes. func (p *Plugin) publicPasswordRoutes() (bool, error) { if p.app == nil || p.app.Config == nil { return true, nil } const key = "golem15.user.password.public_routes" raw, ok := p.app.Config.Lookup(key) if !ok || raw == nil { return true, nil } switch v := raw.(type) { case bool: return v, nil case string: switch s := strings.TrimSpace(v); { case s == "", strings.EqualFold(s, "true"): return true, nil case strings.EqualFold(s, "false"): return false, nil } return false, fmt.Errorf("golem15.user: %s must be true or false, got %q", key, v) default: return false, fmt.Errorf("golem15.user: %s must be true or false, got %v", key, v) } }