package server import ( "errors" "net/http" "galaxy/backend/internal/auth" "galaxy/backend/internal/server/handlers" "galaxy/backend/internal/server/httperr" "galaxy/backend/internal/telemetry" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // InternalSessionsHandlers groups the gateway-only session handlers // under `/api/v1/internal/sessions/*`. The current implementation ships real // implementations; nil *auth.Service falls back to the Stage-3 // placeholder so the contract test continues to validate the OpenAPI // envelope without booting a database. type InternalSessionsHandlers struct { svc *auth.Service logger *zap.Logger } // NewInternalSessionsHandlers constructs the handler set. svc may be // nil — in that case every handler returns 501 not_implemented, matching // the pre-Stage-5.1 placeholder. logger may also be nil; zap.NewNop is // used in that case. func NewInternalSessionsHandlers(svc *auth.Service, logger *zap.Logger) *InternalSessionsHandlers { if logger == nil { logger = zap.NewNop() } return &InternalSessionsHandlers{svc: svc, logger: logger.Named("http.internal.sessions")} } // Get handles GET /api/v1/internal/sessions/{device_session_id}. func (h *InternalSessionsHandlers) Get() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("internalSessionsGet") } return func(c *gin.Context) { deviceSessionID, err := uuid.Parse(c.Param("device_session_id")) if err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "device_session_id must be a valid UUID") return } ctx := c.Request.Context() sess, err := h.svc.GetSession(ctx, deviceSessionID) if err != nil { if errors.Is(err, auth.ErrSessionNotFound) { httperr.Abort(c, http.StatusNotFound, httperr.CodeNotFound, "device session not found") return } h.logger.Error("internal sessions get failed", append(telemetry.TraceFieldsFromContext(ctx), zap.Error(err))..., ) httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "service error") return } c.JSON(http.StatusOK, deviceSessionToWire(sess)) } } // Revoke handles POST /api/v1/internal/sessions/{device_session_id}/revoke. func (h *InternalSessionsHandlers) Revoke() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("internalSessionsRevoke") } return func(c *gin.Context) { deviceSessionID, err := uuid.Parse(c.Param("device_session_id")) if err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "device_session_id must be a valid UUID") return } ctx := c.Request.Context() sess, err := h.svc.RevokeSession(ctx, deviceSessionID) if err != nil { if errors.Is(err, auth.ErrSessionNotFound) { httperr.Abort(c, http.StatusNotFound, httperr.CodeNotFound, "device session not found") return } h.logger.Error("internal sessions revoke failed", append(telemetry.TraceFieldsFromContext(ctx), zap.Error(err))..., ) httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "service error") return } c.JSON(http.StatusOK, deviceSessionToWire(sess)) } } // RevokeAllForUser handles POST /api/v1/internal/sessions/users/{user_id}/revoke-all. func (h *InternalSessionsHandlers) RevokeAllForUser() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("internalSessionsRevokeAllForUser") } return func(c *gin.Context) { userID, err := uuid.Parse(c.Param("user_id")) if err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "user_id must be a valid UUID") return } ctx := c.Request.Context() revoked, err := h.svc.RevokeAllForUser(ctx, userID) if err != nil { h.logger.Error("internal sessions revoke-all failed", append(telemetry.TraceFieldsFromContext(ctx), zap.Error(err))..., ) httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "service error") return } c.JSON(http.StatusOK, gin.H{ "user_id": userID.String(), "revoked_count": len(revoked), }) } }