package server import ( "context" "errors" "net/http" "galaxy/backend/internal/admin" "galaxy/backend/internal/server/handlers" "galaxy/backend/internal/server/httperr" "galaxy/backend/internal/telemetry" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // AdminAdminAccountsHandlers groups the admin-account CRUD handlers // under `/api/v1/admin/admin-accounts/*`. The current implementation ships real // implementations backed by `*admin.Service`; tests that supply a nil // service fall back to the Stage-3 placeholder body so the contract // test continues to validate the OpenAPI envelope without booting a // database. type AdminAdminAccountsHandlers struct { svc *admin.Service logger *zap.Logger } // NewAdminAdminAccountsHandlers constructs the handler set. svc may be // nil — in that case every handler returns 501 not_implemented, // matching the pre-Stage-5.3 placeholder. logger may also be nil; // zap.NewNop is used in that case. func NewAdminAdminAccountsHandlers(svc *admin.Service, logger *zap.Logger) *AdminAdminAccountsHandlers { if logger == nil { logger = zap.NewNop() } return &AdminAdminAccountsHandlers{svc: svc, logger: logger.Named("http.admin.admin-accounts")} } // List handles GET /api/v1/admin/admin-accounts. func (h *AdminAdminAccountsHandlers) List() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminAdminAccountsList") } return func(c *gin.Context) { ctx := c.Request.Context() admins, err := h.svc.List(ctx) if err != nil { respondAdminAccountError(c, h.logger, "admin admin-accounts list", ctx, err) return } c.JSON(http.StatusOK, adminAccountListToWire(admins)) } } // Create handles POST /api/v1/admin/admin-accounts. func (h *AdminAdminAccountsHandlers) Create() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminAdminAccountsCreate") } return func(c *gin.Context) { var req adminAccountCreateRequestWire if err := c.ShouldBindJSON(&req); err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "request body must be valid JSON") return } ctx := c.Request.Context() created, err := h.svc.Create(ctx, admin.CreateInput{ Username: req.Username, Password: req.Password, }) if err != nil { respondAdminAccountError(c, h.logger, "admin admin-accounts create", ctx, err) return } c.JSON(http.StatusCreated, adminAccountToWire(created)) } } // Get handles GET /api/v1/admin/admin-accounts/{username}. func (h *AdminAdminAccountsHandlers) Get() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminAdminAccountsGet") } return func(c *gin.Context) { ctx := c.Request.Context() got, err := h.svc.Get(ctx, c.Param("username")) if err != nil { respondAdminAccountError(c, h.logger, "admin admin-accounts get", ctx, err) return } c.JSON(http.StatusOK, adminAccountToWire(got)) } } // Disable handles POST /api/v1/admin/admin-accounts/{username}/disable. func (h *AdminAdminAccountsHandlers) Disable() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminAdminAccountsDisable") } return func(c *gin.Context) { ctx := c.Request.Context() updated, err := h.svc.Disable(ctx, c.Param("username")) if err != nil { respondAdminAccountError(c, h.logger, "admin admin-accounts disable", ctx, err) return } c.JSON(http.StatusOK, adminAccountToWire(updated)) } } // Enable handles POST /api/v1/admin/admin-accounts/{username}/enable. func (h *AdminAdminAccountsHandlers) Enable() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminAdminAccountsEnable") } return func(c *gin.Context) { ctx := c.Request.Context() updated, err := h.svc.Enable(ctx, c.Param("username")) if err != nil { respondAdminAccountError(c, h.logger, "admin admin-accounts enable", ctx, err) return } c.JSON(http.StatusOK, adminAccountToWire(updated)) } } // ResetPassword handles POST /api/v1/admin/admin-accounts/{username}/reset-password. func (h *AdminAdminAccountsHandlers) ResetPassword() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminAdminAccountsResetPassword") } return func(c *gin.Context) { var req adminAccountResetPasswordRequestWire if err := c.ShouldBindJSON(&req); err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "request body must be valid JSON") return } ctx := c.Request.Context() updated, err := h.svc.ResetPassword(ctx, c.Param("username"), req.Password) if err != nil { respondAdminAccountError(c, h.logger, "admin admin-accounts reset-password", ctx, err) return } c.JSON(http.StatusOK, adminAccountToWire(updated)) } } // respondAdminAccountError maps admin-package sentinels to the standard // JSON envelope. Unknown errors fall through to 500 with a structured // log so operators can correlate. func respondAdminAccountError(c *gin.Context, logger *zap.Logger, op string, ctx context.Context, err error) { switch { case errors.Is(err, admin.ErrNotFound): httperr.Abort(c, http.StatusNotFound, httperr.CodeNotFound, "admin account not found") case errors.Is(err, admin.ErrUsernameTaken): httperr.Abort(c, http.StatusConflict, httperr.CodeConflict, "username already in use") case errors.Is(err, admin.ErrInvalidInput): httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, err.Error()) default: logger.Error(op+" failed", append(telemetry.TraceFieldsFromContext(ctx), zap.Error(err))..., ) httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "internal error") } } // adminAccountToWire renders an admin.Admin into the OpenAPI // `AdminAccount` schema declared at openapi.yaml:2596. func adminAccountToWire(a admin.Admin) adminAccountWire { out := adminAccountWire{ Username: a.Username, CreatedAt: a.CreatedAt.UTC().Format(timestampLayout), } if a.LastUsedAt != nil { t := a.LastUsedAt.UTC().Format(timestampLayout) out.LastUsedAt = &t } if a.DisabledAt != nil { t := a.DisabledAt.UTC().Format(timestampLayout) out.DisabledAt = &t } return out } // adminAccountListToWire renders the admin slice into the OpenAPI // `AdminAccountList` schema. func adminAccountListToWire(admins []admin.Admin) adminAccountListWire { out := adminAccountListWire{ Items: make([]adminAccountWire, 0, len(admins)), } for _, a := range admins { out.Items = append(out.Items, adminAccountToWire(a)) } return out } // adminAccountWire mirrors `AdminAccount`. type adminAccountWire struct { Username string `json:"username"` CreatedAt string `json:"created_at"` LastUsedAt *string `json:"last_used_at,omitempty"` DisabledAt *string `json:"disabled_at,omitempty"` } // adminAccountListWire mirrors `AdminAccountList`. type adminAccountListWire struct { Items []adminAccountWire `json:"items"` } // adminAccountCreateRequestWire mirrors `AdminAccountCreateRequest`. type adminAccountCreateRequestWire struct { Username string `json:"username"` Password string `json:"password"` } // adminAccountResetPasswordRequestWire mirrors // `AdminAccountResetPasswordRequest`. type adminAccountResetPasswordRequestWire struct { Password string `json:"password"` }