138 lines
8.5 KiB
Go

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/permission-catalog", middleware.RequirePermission("external-admin-user:permissions"), h.ListPermissionCatalog)
protected.GET("/admin/external-admin-users/:id/permissions", middleware.RequirePermission("external-admin-user:permissions"), h.GetAccountPermissions)
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"), middleware.RequirePermission("external-admin-user:permissions"), h.CreateAccount)
protected.PUT("/admin/external-admin-users/:id/permissions", middleware.RequirePermission("external-admin-user:permissions"), h.UpdateAccountPermissions)
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("team:view"), h.ListMyTeamAgencies)
if handlers.AppUser != nil {
group.GET("/app/users", middleware.RequireAnyPermission("user:list", "user:update", "user:ban", "user-level:grant"), handlers.AppUser.ListUsers)
group.GET("/app/users/bans", middleware.RequireAnyPermission("user-ban:list", "user:unban"), handlers.AppUser.ListBannedUsers)
// Detail is a support read for each user-targeted action, not a second product
// capability. Any independently assigned user action can resolve its exact target.
group.GET("/app/users/:id", middleware.RequireAnyPermission("user:list", "user:update", "user:ban", "user:unban", "user-level:grant"), handlers.AppUser.GetUser)
group.PATCH("/app/users/:id", middleware.RequirePermission("user:update"), handlers.AppUser.UpdateUser)
group.PUT("/app/users/:id/levels", middleware.RequirePermission("user-level:grant"), handlers.AppUser.AdjustTemporaryLevels)
group.POST("/app/users/:id/ban", middleware.RequirePermission("user:ban"), handlers.AppUser.BanUser)
group.POST("/app/users/:id/unban", middleware.RequirePermission("user:unban"), handlers.AppUser.UnbanUser)
}
if handlers.HostOrg != nil {
group.GET("/admin/hosts", middleware.RequirePermission("host:list"), handlers.HostOrg.ListHosts)
group.GET("/admin/agencies", middleware.RequirePermission("agency:list"), handlers.HostOrg.ListAgencies)
group.GET("/admin/bds", middleware.RequirePermission("bd:list"), handlers.HostOrg.ListBDs)
// The shared host-org API names are historical: external BD Manager uses
// /managers, while the Super Admin projection uses /bd-leaders.
group.GET("/admin/bd-leaders", middleware.RequirePermission("super-admin:list"), handlers.HostOrg.ListBDLeaders)
group.GET("/admin/managers", middleware.RequirePermission("bd-manager:list"), handlers.HostOrg.ListManagers)
}
if handlers.RoomAdmin != nil {
group.GET("/admin/rooms", middleware.RequireAnyPermission("room:list", "room:update"), handlers.RoomAdmin.ListRooms)
group.PATCH("/admin/rooms/:room_id", middleware.RequirePermission("room:update"), handlers.RoomAdmin.UpdateRoom)
}
if handlers.Resource != nil {
// Resource owner data cannot yet classify generic entitlements into the three
// product labels below. The assignment validator therefore keeps them atomic,
// and the route repeats all three checks so corrupt DB JSON still fails closed.
group.GET("/admin/resources", middleware.RequirePermission("privilege:grant"), middleware.RequirePermission("user-title:grant"), middleware.RequirePermission("room-background:grant"), handlers.Resource.ListExternalGrantResources)
group.GET("/admin/resource-grants", middleware.RequirePermission("privilege:list"), handlers.Resource.ListResourceGrants)
group.GET("/admin/resource-grants/target", middleware.RequireAnyPermission("privilege:grant", "pretty-id:grant", "user-title:grant", "room-background:grant"), handlers.Resource.LookupResourceGrantTarget)
group.POST("/admin/resource-grants/resource", middleware.RequirePermission("privilege:grant"), middleware.RequirePermission("user-title:grant"), middleware.RequirePermission("room-background:grant"), handlers.Resource.GrantExternalResource)
// External UI never uses group grants. Without an owner-level semantic category,
// a mixed group cannot be authorized safely, so it is intentionally not exposed.
}
if handlers.PrettyID != nil {
group.GET("/admin/users/pretty-ids", middleware.RequirePermission("pretty-id:grant"), 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("banner:create"), handlers.AppConfig.ListBanners)
group.POST("/admin/app-config/banners", middleware.RequirePermission("banner:create"), 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("user-level:grant"), handlers.VIPConfig.GetProgram)
group.GET("/admin/activity/vip-levels", middleware.RequirePermission("user-level:grant"), handlers.VIPConfig.ListLevels)
group.POST("/admin/activity/vip-trial-card-grants", middleware.RequirePermission("user-level:grant"), handlers.VIPConfig.GrantExternalVIPTrialCard)
group.POST("/admin/activity/vip-grants", middleware.RequirePermission("user-level:grant"), handlers.VIPConfig.GrantExternalVIP)
}
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("banner:create"), handlers.Upload.UploadImage)
}
}