package externaladmin import ( "hyapp-admin-server/internal/middleware" "hyapp-admin-server/internal/modules/appconfig" "hyapp-admin-server/internal/modules/appuser" "hyapp-admin-server/internal/modules/hostorg" "hyapp-admin-server/internal/modules/prettyid" "hyapp-admin-server/internal/modules/resource" "hyapp-admin-server/internal/modules/roomadmin" "hyapp-admin-server/internal/modules/upload" "hyapp-admin-server/internal/modules/vipconfig" "github.com/gin-gonic/gin" ) type BusinessHandlers struct { AppUser *appuser.Handler HostOrg *hostorg.Handler RoomAdmin *roomadmin.Handler Resource *resource.Handler PrettyID *prettyid.Handler AppConfig *appconfig.Handler VIPConfig *vipconfig.Handler Upload *upload.Handler } func RegisterAdminRoutes(protected *gin.RouterGroup, h *Handler) { if protected == nil || h == nil { return } protected.GET("/admin/external-admin-users", middleware.RequirePermission("external-admin-user:view"), h.ListAccounts) protected.GET("/admin/external-admin-users/target", middleware.RequirePermission("external-admin-user:create"), h.ResolveTarget) protected.POST("/admin/external-admin-users", middleware.RequirePermission("external-admin-user:create"), h.CreateAccount) protected.PATCH("/admin/external-admin-users/:id/status", middleware.RequirePermission("external-admin-user:status"), h.SetStatus) protected.POST("/admin/external-admin-users/:id/password", middleware.RequirePermission("external-admin-user:reset-password"), h.ResetPassword) } func RegisterExternalRoutes(api *gin.RouterGroup, h *Handler, handlers BusinessHandlers) { if api == nil || h == nil { return } external := api.Group("/external") auth := external.Group("/auth") // Credentials, session state and CSRF secrets must never be cached by browsers, // service workers or intermediary proxies. This middleware runs before session // auth so 4xx/5xx responses carry the same protection as successful responses. auth.Use(noStore()) auth.GET("/apps", h.ListApps) auth.POST("/login", h.Login) protectedAuth := auth.Group("") // Audit wraps CSRF so forged writes are recorded as failed attempts instead of disappearing // before the audit middleware gets control. protectedAuth.Use(h.AuthRequired(), h.Audit(), h.RequireCSRF()) protectedAuth.GET("/me", h.Me) protectedAuth.POST("/logout", h.Logout) protectedAuth.POST("/change-password", h.ChangePassword) // Initial/reset credentials are deliberately limited to me/logout/change-password. // Only after changing the temporary password can the account reach business handlers. business := external.Group("") business.Use(h.AuthRequired(), h.Audit(), h.RequireCSRF(), h.RequirePasswordChanged()) registerBusinessWhitelist(business, h, handlers) } func noStore() gin.HandlerFunc { return func(c *gin.Context) { c.Header("Cache-Control", "no-store") c.Header("Pragma", "no-cache") c.Next() } } func registerBusinessWhitelist(group *gin.RouterGroup, h *Handler, handlers BusinessHandlers) { // "My team" is not a generic manager-list alias. Its handler derives the root // manager from the external session and never accepts a client-selected user ID. group.GET("/admin/my-team/agencies", middleware.RequirePermission("bd:view"), h.ListMyTeamAgencies) if handlers.AppUser != nil { group.GET("/app/users", middleware.RequirePermission("app-user:view"), handlers.AppUser.ListUsers) group.GET("/app/users/bans", middleware.RequirePermission("app-user:view"), handlers.AppUser.ListBannedUsers) group.GET("/app/users/:id", middleware.RequirePermission("app-user:view"), handlers.AppUser.GetUser) group.PATCH("/app/users/:id", middleware.RequirePermission("app-user:update"), handlers.AppUser.UpdateUser) group.PUT("/app/users/:id/levels", middleware.RequirePermission("app-user:level"), handlers.AppUser.AdjustTemporaryLevels) group.POST("/app/users/:id/ban", middleware.RequirePermission("app-user:status"), handlers.AppUser.BanUser) group.POST("/app/users/:id/unban", middleware.RequirePermission("app-user:status"), handlers.AppUser.UnbanUser) } if handlers.HostOrg != nil { group.GET("/admin/hosts", middleware.RequirePermission("host:view"), handlers.HostOrg.ListHosts) group.GET("/admin/agencies", middleware.RequirePermission("agency:view"), handlers.HostOrg.ListAgencies) group.GET("/admin/bds", middleware.RequirePermission("bd:view"), handlers.HostOrg.ListBDs) group.GET("/admin/bd-leaders", middleware.RequirePermission("bd:view"), handlers.HostOrg.ListBDLeaders) group.GET("/admin/managers", middleware.RequirePermission("bd:view"), handlers.HostOrg.ListManagers) } if handlers.RoomAdmin != nil { group.GET("/admin/rooms", middleware.RequirePermission("room:view"), handlers.RoomAdmin.ListRooms) group.PATCH("/admin/rooms/:room_id", middleware.RequirePermission("room:update"), handlers.RoomAdmin.UpdateRoom) } if handlers.Resource != nil { group.GET("/admin/resources", middleware.RequirePermission("resource:view"), handlers.Resource.ListResources) group.GET("/admin/resource-grants", middleware.RequirePermission("resource-grant:view"), handlers.Resource.ListResourceGrants) group.GET("/admin/resource-grants/target", middleware.RequirePermission("resource-grant:create"), handlers.Resource.LookupResourceGrantTarget) group.POST("/admin/resource-grants/resource", middleware.RequirePermission("resource-grant:create"), handlers.Resource.GrantResource) group.POST("/admin/resource-grants/group", middleware.RequirePermission("resource-grant:create"), handlers.Resource.GrantResourceGroup) } if handlers.PrettyID != nil { group.GET("/admin/users/pretty-ids", middleware.RequirePermission("pretty-id:view"), handlers.PrettyID.ListIDs) group.POST("/admin/users/pretty-ids/grant", middleware.RequirePermission("pretty-id:grant"), handlers.PrettyID.Grant) } if handlers.AppConfig != nil { group.GET("/admin/app-config/banners", middleware.RequirePermission("app-config:view"), handlers.AppConfig.ListBanners) group.POST("/admin/app-config/banners", middleware.RequirePermission("app-config:update"), handlers.AppConfig.CreateBanner) } if handlers.VIPConfig != nil { // Grant mode and level IDs are owner-service facts. Read them with the same narrowly scoped // grant permission so the portal can select direct-VIP vs trial-card without config write access. group.GET("/admin/activity/vip-program", middleware.RequirePermission("vip-config:grant"), handlers.VIPConfig.GetProgram) group.GET("/admin/activity/vip-levels", middleware.RequirePermission("vip-config:grant"), handlers.VIPConfig.ListLevels) group.POST("/admin/activity/vip-trial-card-grants", middleware.RequirePermission("vip-config:grant"), handlers.VIPConfig.GrantVIPTrialCard) group.POST("/admin/activity/vip-grants", middleware.RequirePermission("vip-config:grant"), handlers.VIPConfig.GrantVIP) } if handlers.Upload != nil { // Banner/avatar forms need a real upload endpoint; no other file-management route is exposed. group.POST("/admin/files/image/upload", middleware.RequirePermission("upload:create"), handlers.Upload.UploadImage) } }