Files
galaxy-game/backend/internal/server/handlers_admin_admin_accounts.go
T
2026-05-06 10:14:55 +03:00

221 lines
7.1 KiB
Go

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"`
}