138 lines
8.5 KiB
Go
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)
|
|
}
|
|
}
|