package server import ( "net/http" "galaxy/backend/internal/diplomail" "galaxy/backend/internal/server/clientip" "galaxy/backend/internal/server/handlers" "galaxy/backend/internal/server/httperr" "galaxy/backend/internal/server/middleware/basicauth" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // AdminDiplomailHandlers groups the diplomatic-mail handlers exposed // under `/api/v1/admin/games/{game_id}/mail` (per-game admin send / // broadcast). The handler is intentionally separate from // `AdminMailHandlers`, which owns the unrelated email outbox surface // under `/api/v1/admin/mail/*`. type AdminDiplomailHandlers struct { svc *diplomail.Service logger *zap.Logger } // NewAdminDiplomailHandlers constructs the handler set. svc may be // nil — in that case every handler returns 501 not_implemented. func NewAdminDiplomailHandlers(svc *diplomail.Service, logger *zap.Logger) *AdminDiplomailHandlers { if logger == nil { logger = zap.NewNop() } return &AdminDiplomailHandlers{svc: svc, logger: logger.Named("http.admin.diplomail")} } // Send handles POST /api/v1/admin/games/{game_id}/mail. The body // shape mirrors the owner route: `target="user"` requires // `recipient_user_id`; `target="all"` accepts an optional // `recipients` scope. The authenticated admin username is captured // from the basicauth context and persisted as `sender_username`. func (h *AdminDiplomailHandlers) Send() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminDiplomailSend") } return func(c *gin.Context) { username, ok := basicauth.UsernameFromContext(c.Request.Context()) if !ok || username == "" { httperr.Abort(c, http.StatusUnauthorized, httperr.CodeUnauthorized, "admin authentication is required") return } gameID, ok := parseGameIDParam(c) if !ok { return } var req userMailSendAdminRequestWire 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() switch req.Target { case "", "user": recipientID, parseErr := uuid.Parse(req.RecipientUserID) if parseErr != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID") return } msg, rcpt, sendErr := h.svc.SendAdminPersonal(ctx, diplomail.SendAdminPersonalInput{ GameID: gameID, CallerKind: diplomail.CallerKindAdmin, CallerUsername: username, RecipientUserID: recipientID, Subject: req.Subject, Body: req.Body, SenderIP: clientip.ExtractSourceIP(c), }) if sendErr != nil { respondDiplomailError(c, h.logger, "admin mail send personal", ctx, sendErr) return } c.JSON(http.StatusCreated, mailMessageDetailToWire(diplomail.InboxEntry{Message: msg, Recipient: rcpt}, true)) case "all": msg, recipients, sendErr := h.svc.SendAdminBroadcast(ctx, diplomail.SendAdminBroadcastInput{ GameID: gameID, CallerKind: diplomail.CallerKindAdmin, CallerUsername: username, RecipientScope: req.Recipients, Subject: req.Subject, Body: req.Body, SenderIP: clientip.ExtractSourceIP(c), }) if sendErr != nil { respondDiplomailError(c, h.logger, "admin mail send broadcast", ctx, sendErr) return } c.JSON(http.StatusCreated, mailBroadcastReceiptToWire(msg, recipients)) default: httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "target must be 'user' or 'all'") } } }