package server import ( "context" "errors" "net/http" "strings" "galaxy/backend/internal/admin" "galaxy/backend/internal/adminconsole" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // OperatorAdmin is the subset of the admin-account service the console uses. // *admin.Service satisfies it. type OperatorAdmin interface { List(ctx context.Context) ([]admin.Admin, error) Create(ctx context.Context, in admin.CreateInput) (admin.Admin, error) Disable(ctx context.Context, username string) (admin.Admin, error) Enable(ctx context.Context, username string) (admin.Admin, error) ResetPassword(ctx context.Context, username, password string) (admin.Admin, error) } // OperatorsList renders GET /_gm/operators. func (h *AdminConsoleHandlers) OperatorsList() gin.HandlerFunc { return func(c *gin.Context) { if h.operators == nil { h.renderMessage(c, http.StatusServiceUnavailable, "operators", "Operators", "Operator administration is not available.", "bad", "/_gm/") return } admins, err := h.operators.List(c.Request.Context()) if err != nil { h.logger.Error("admin console: list operators", zap.Error(err)) h.renderMessage(c, http.StatusInternalServerError, "operators", "Operators", "Failed to load operators.", "bad", "/_gm/") return } h.render(c, http.StatusOK, "operators", "operators", "Operators", toOperatorsData(admins)) } } // OperatorCreate handles POST /_gm/operators. func (h *AdminConsoleHandlers) OperatorCreate() gin.HandlerFunc { return func(c *gin.Context) { if h.operators == nil { h.renderMessage(c, http.StatusServiceUnavailable, "operators", "Operators", "Operator administration is not available.", "bad", "/_gm/") return } _, err := h.operators.Create(c.Request.Context(), admin.CreateInput{ Username: strings.TrimSpace(c.PostForm("username")), Password: c.PostForm("password"), }) if err != nil { switch { case errors.Is(err, admin.ErrUsernameTaken): h.renderMessage(c, http.StatusConflict, "operators", "Username taken", "That username is already in use.", "bad", "/_gm/operators") case errors.Is(err, admin.ErrInvalidInput): h.renderMessage(c, http.StatusBadRequest, "operators", "Invalid input", "Username and password are required.", "bad", "/_gm/operators") default: h.logger.Error("admin console: create operator", zap.Error(err)) h.renderMessage(c, http.StatusInternalServerError, "operators", "Create failed", "Failed to create the operator.", "bad", "/_gm/operators") } return } c.Redirect(http.StatusSeeOther, "/_gm/operators") } } // OperatorDisable handles POST /_gm/operators/:username/disable. func (h *AdminConsoleHandlers) OperatorDisable() gin.HandlerFunc { return h.operatorAction("disable", func(ctx context.Context, username string) error { _, err := h.operators.Disable(ctx, username) return err }) } // OperatorEnable handles POST /_gm/operators/:username/enable. func (h *AdminConsoleHandlers) OperatorEnable() gin.HandlerFunc { return h.operatorAction("enable", func(ctx context.Context, username string) error { _, err := h.operators.Enable(ctx, username) return err }) } // OperatorResetPassword handles POST /_gm/operators/:username/reset-password. func (h *AdminConsoleHandlers) OperatorResetPassword() gin.HandlerFunc { return func(c *gin.Context) { if h.operators == nil { h.renderMessage(c, http.StatusServiceUnavailable, "operators", "Operators", "Operator administration is not available.", "bad", "/_gm/") return } username := c.Param("username") password := c.PostForm("password") if strings.TrimSpace(password) == "" { h.renderMessage(c, http.StatusBadRequest, "operators", "Invalid input", "A new password is required.", "bad", "/_gm/operators") return } if _, err := h.operators.ResetPassword(c.Request.Context(), username, password); err != nil { if errors.Is(err, admin.ErrNotFound) { h.renderMessage(c, http.StatusNotFound, "operators", "Operator not found", "No such operator.", "bad", "/_gm/operators") return } if errors.Is(err, admin.ErrInvalidInput) { h.renderMessage(c, http.StatusBadRequest, "operators", "Invalid input", "The password was rejected.", "bad", "/_gm/operators") return } h.logger.Error("admin console: reset operator password", zap.Error(err)) h.renderMessage(c, http.StatusInternalServerError, "operators", "Reset failed", "Failed to reset the password.", "bad", "/_gm/operators") return } c.Redirect(http.StatusSeeOther, "/_gm/operators") } } // operatorAction is the shared shape for operator POST actions that take only // the username and redirect back to the list. func (h *AdminConsoleHandlers) operatorAction(label string, run func(context.Context, string) error) gin.HandlerFunc { return func(c *gin.Context) { if h.operators == nil { h.renderMessage(c, http.StatusServiceUnavailable, "operators", "Operators", "Operator administration is not available.", "bad", "/_gm/") return } if err := run(c.Request.Context(), c.Param("username")); err != nil { if errors.Is(err, admin.ErrNotFound) { h.renderMessage(c, http.StatusNotFound, "operators", "Operator not found", "No such operator.", "bad", "/_gm/operators") return } h.logger.Error("admin console: operator "+label, zap.Error(err)) h.renderMessage(c, http.StatusInternalServerError, "operators", "Action failed", "The "+label+" action failed.", "bad", "/_gm/operators") return } c.Redirect(http.StatusSeeOther, "/_gm/operators") } } // toOperatorsData maps admin accounts into the operators view model. func toOperatorsData(admins []admin.Admin) adminconsole.OperatorsData { data := adminconsole.OperatorsData{Items: make([]adminconsole.OperatorRow, 0, len(admins))} for _, a := range admins { data.Items = append(data.Items, adminconsole.OperatorRow{ Username: a.Username, CreatedAt: fmtConsoleTime(a.CreatedAt), LastUsedAt: fmtConsoleTimePtr(a.LastUsedAt), Disabled: a.DisabledAt != nil, }) } return data }