veza/veza-backend-api/internal/handlers/queue_session_handler.go

223 lines
8.4 KiB
Go
Raw Normal View History

package handlers
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/services"
)
// QueueSessionHandler handles collaborative queue session HTTP requests (v0.203 Lot D1)
type QueueSessionHandler struct {
svc *services.QueueSessionService
logger *zap.Logger
}
// NewQueueSessionHandler creates a new QueueSessionHandler
func NewQueueSessionHandler(svc *services.QueueSessionService, logger *zap.Logger) *QueueSessionHandler {
return &QueueSessionHandler{svc: svc, logger: logger}
}
// CreateSession creates a new shared queue session
feat(openapi): annotate queue + password-reset handlers + regen Closes the two annotation gaps that blocked finishing the orval migration in v1.0.8 : - queue_handler.go (5 routes — GetQueue, UpdateQueue, AddQueueItem, RemoveQueueItem, ClearQueue) — under @Tags Queue with @Security BearerAuth, @Param body/path, @Success/@Failure on the standard APIResponse envelope. - queue_session_handler.go (5 routes — CreateSession, GetSession, DeleteSession, AddToSession, RemoveFromSession). GetSession is public (no @Security tag) since the share-token URL is meant for join-via-link from outside the auth wall. - password_reset_handler.go (2 routes — RequestPasswordReset and ResetPassword factory functions). Both are public (no @Security) since they're the entry-points for users who can't log in. The request-side annotation documents the intentional generic 200 response (anti-enumeration: same body whether the email exists or not). After regen : - openapi.yaml gains 7 queue paths (/queue, /queue/items[/{id}], /queue/session[/{token}[/items[/{id}]]]) and 2 password paths (/auth/password/reset, /auth/password/reset-request). +568 LOC. - docs/{docs.go,swagger.json,swagger.yaml} updated identically by swag init. - apps/web/src/services/generated/queue/queue.ts created (10 HTTP funcs + matching React Query hooks). model/ index extended with the queue + password-reset request/response shapes. Validates with `swag init` (Swagger 2.0). go build ./... clean. No runtime behaviour change — annotations are pure metadata read by the spec generator. The orval regen IS the wiring point for the follow-up frontend commit (queue.ts migration + authService finish). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:55:26 +00:00
// @Summary Create collaborative queue session
// @Description Creates a shared queue session and returns its share token + URL. The session creator is recorded as host.
// @Tags Queue
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 201 {object} APIResponse{data=object{session=object,share_token=string,share_url=string}} "Created session"
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 500 {object} APIResponse "Internal Error"
// @Router /queue/session [post]
func (h *QueueSessionHandler) CreateSession(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
session, err := h.svc.CreateSession(c.Request.Context(), userID)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create session", err))
return
}
RespondSuccess(c, http.StatusCreated, gin.H{
2026-03-05 22:03:43 +00:00
"session": session,
"share_token": session.ShareToken,
"share_url": "/queue?session=" + session.ShareToken,
})
}
// GetSession returns a session's queue (auth optional for joining via link)
feat(openapi): annotate queue + password-reset handlers + regen Closes the two annotation gaps that blocked finishing the orval migration in v1.0.8 : - queue_handler.go (5 routes — GetQueue, UpdateQueue, AddQueueItem, RemoveQueueItem, ClearQueue) — under @Tags Queue with @Security BearerAuth, @Param body/path, @Success/@Failure on the standard APIResponse envelope. - queue_session_handler.go (5 routes — CreateSession, GetSession, DeleteSession, AddToSession, RemoveFromSession). GetSession is public (no @Security tag) since the share-token URL is meant for join-via-link from outside the auth wall. - password_reset_handler.go (2 routes — RequestPasswordReset and ResetPassword factory functions). Both are public (no @Security) since they're the entry-points for users who can't log in. The request-side annotation documents the intentional generic 200 response (anti-enumeration: same body whether the email exists or not). After regen : - openapi.yaml gains 7 queue paths (/queue, /queue/items[/{id}], /queue/session[/{token}[/items[/{id}]]]) and 2 password paths (/auth/password/reset, /auth/password/reset-request). +568 LOC. - docs/{docs.go,swagger.json,swagger.yaml} updated identically by swag init. - apps/web/src/services/generated/queue/queue.ts created (10 HTTP funcs + matching React Query hooks). model/ index extended with the queue + password-reset request/response shapes. Validates with `swag init` (Swagger 2.0). go build ./... clean. No runtime behaviour change — annotations are pure metadata read by the spec generator. The orval regen IS the wiring point for the follow-up frontend commit (queue.ts migration + authService finish). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:55:26 +00:00
// @Summary Get queue session by share token
// @Description Returns the session metadata + items for a given share token. No auth required (public — anyone with the link can join).
// @Tags Queue
// @Accept json
// @Produce json
// @Param token path string true "Session share token"
// @Success 200 {object} APIResponse{data=object{session=object,items=[]object}} "Session and queue items"
// @Failure 400 {object} APIResponse "Token required"
// @Failure 404 {object} APIResponse "Session not found"
// @Failure 500 {object} APIResponse "Internal Error"
// @Router /queue/session/{token} [get]
func (h *QueueSessionHandler) GetSession(c *gin.Context) {
token := c.Param("token")
if token == "" {
RespondWithAppError(c, apperrors.NewValidationError("token is required"))
return
}
session, items, err := h.svc.GetSessionByToken(c.Request.Context(), token)
if err != nil {
if errors.Is(err, services.ErrQueueSessionNotFound) {
RespondWithAppError(c, apperrors.NewNotFoundError("session not found"))
return
}
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get session", err))
return
}
// Map items to response with tracks
type itemResp struct {
ID string `json:"id"`
Position int `json:"position"`
AddedAt string `json:"added_at"`
Track interface{} `json:"track,omitempty"`
}
respItems := make([]itemResp, 0, len(items))
for _, it := range items {
r := itemResp{
ID: it.ID.String(),
Position: it.Position,
AddedAt: it.AddedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if it.Track.ID != uuid.Nil {
r.Track = it.Track
}
respItems = append(respItems, r)
}
RespondSuccess(c, http.StatusOK, gin.H{
"session": session,
"items": respItems,
})
}
// DeleteSession deletes a session (creator only)
feat(openapi): annotate queue + password-reset handlers + regen Closes the two annotation gaps that blocked finishing the orval migration in v1.0.8 : - queue_handler.go (5 routes — GetQueue, UpdateQueue, AddQueueItem, RemoveQueueItem, ClearQueue) — under @Tags Queue with @Security BearerAuth, @Param body/path, @Success/@Failure on the standard APIResponse envelope. - queue_session_handler.go (5 routes — CreateSession, GetSession, DeleteSession, AddToSession, RemoveFromSession). GetSession is public (no @Security tag) since the share-token URL is meant for join-via-link from outside the auth wall. - password_reset_handler.go (2 routes — RequestPasswordReset and ResetPassword factory functions). Both are public (no @Security) since they're the entry-points for users who can't log in. The request-side annotation documents the intentional generic 200 response (anti-enumeration: same body whether the email exists or not). After regen : - openapi.yaml gains 7 queue paths (/queue, /queue/items[/{id}], /queue/session[/{token}[/items[/{id}]]]) and 2 password paths (/auth/password/reset, /auth/password/reset-request). +568 LOC. - docs/{docs.go,swagger.json,swagger.yaml} updated identically by swag init. - apps/web/src/services/generated/queue/queue.ts created (10 HTTP funcs + matching React Query hooks). model/ index extended with the queue + password-reset request/response shapes. Validates with `swag init` (Swagger 2.0). go build ./... clean. No runtime behaviour change — annotations are pure metadata read by the spec generator. The orval regen IS the wiring point for the follow-up frontend commit (queue.ts migration + authService finish). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:55:26 +00:00
// @Summary Delete queue session
// @Description Deletes a collaborative queue session. Only the original session creator can delete.
// @Tags Queue
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param token path string true "Session share token"
// @Success 200 {object} APIResponse{data=object{message=string}} "Session deleted"
// @Failure 400 {object} APIResponse "Token required"
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 403 {object} APIResponse "Only the creator can delete this session"
// @Failure 404 {object} APIResponse "Session not found"
// @Router /queue/session/{token} [delete]
func (h *QueueSessionHandler) DeleteSession(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
token := c.Param("token")
if token == "" {
RespondWithAppError(c, apperrors.NewValidationError("token is required"))
return
}
if err := h.svc.DeleteSession(c.Request.Context(), token, userID); err != nil {
if errors.Is(err, services.ErrQueueSessionNotFound) {
RespondWithAppError(c, apperrors.NewNotFoundError("session not found"))
return
}
if errors.Is(err, services.ErrQueueSessionForbidden) {
RespondWithAppError(c, apperrors.NewForbiddenError("only the creator can delete this session"))
return
}
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to delete session", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "session deleted"})
}
// AddToSessionRequest represents the request body
type AddToSessionRequest struct {
TrackID uuid.UUID `json:"track_id" binding:"required"`
}
// AddToSession adds a track to a session's queue
feat(openapi): annotate queue + password-reset handlers + regen Closes the two annotation gaps that blocked finishing the orval migration in v1.0.8 : - queue_handler.go (5 routes — GetQueue, UpdateQueue, AddQueueItem, RemoveQueueItem, ClearQueue) — under @Tags Queue with @Security BearerAuth, @Param body/path, @Success/@Failure on the standard APIResponse envelope. - queue_session_handler.go (5 routes — CreateSession, GetSession, DeleteSession, AddToSession, RemoveFromSession). GetSession is public (no @Security tag) since the share-token URL is meant for join-via-link from outside the auth wall. - password_reset_handler.go (2 routes — RequestPasswordReset and ResetPassword factory functions). Both are public (no @Security) since they're the entry-points for users who can't log in. The request-side annotation documents the intentional generic 200 response (anti-enumeration: same body whether the email exists or not). After regen : - openapi.yaml gains 7 queue paths (/queue, /queue/items[/{id}], /queue/session[/{token}[/items[/{id}]]]) and 2 password paths (/auth/password/reset, /auth/password/reset-request). +568 LOC. - docs/{docs.go,swagger.json,swagger.yaml} updated identically by swag init. - apps/web/src/services/generated/queue/queue.ts created (10 HTTP funcs + matching React Query hooks). model/ index extended with the queue + password-reset request/response shapes. Validates with `swag init` (Swagger 2.0). go build ./... clean. No runtime behaviour change — annotations are pure metadata read by the spec generator. The orval regen IS the wiring point for the follow-up frontend commit (queue.ts migration + authService finish). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:55:26 +00:00
// @Summary Add track to queue session
// @Description Adds a track to a collaborative queue session. Anyone with the share token can add.
// @Tags Queue
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param token path string true "Session share token"
// @Param request body AddToSessionRequest true "Track to enqueue"
// @Success 201 {object} APIResponse{data=object{session=object,items=[]object}} "Updated session and items"
// @Failure 400 {object} APIResponse "Validation error"
// @Failure 404 {object} APIResponse "Session not found"
// @Failure 500 {object} APIResponse "Internal Error"
// @Router /queue/session/{token}/items [post]
func (h *QueueSessionHandler) AddToSession(c *gin.Context) {
token := c.Param("token")
if token == "" {
RespondWithAppError(c, apperrors.NewValidationError("token is required"))
return
}
var req AddToSessionRequest
if err := c.ShouldBindJSON(&req); err != nil {
RespondWithAppError(c, apperrors.NewValidationError("track_id is required"))
return
}
if err := h.svc.AddToSession(c.Request.Context(), token, req.TrackID); err != nil {
if errors.Is(err, services.ErrQueueSessionNotFound) {
RespondWithAppError(c, apperrors.NewNotFoundError("session not found"))
return
}
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to add to session", err))
return
}
// Return updated session
session, items, _ := h.svc.GetSessionByToken(c.Request.Context(), token)
RespondSuccess(c, http.StatusCreated, gin.H{"session": session, "items": items})
}
// RemoveFromSession removes an item from a session's queue
feat(openapi): annotate queue + password-reset handlers + regen Closes the two annotation gaps that blocked finishing the orval migration in v1.0.8 : - queue_handler.go (5 routes — GetQueue, UpdateQueue, AddQueueItem, RemoveQueueItem, ClearQueue) — under @Tags Queue with @Security BearerAuth, @Param body/path, @Success/@Failure on the standard APIResponse envelope. - queue_session_handler.go (5 routes — CreateSession, GetSession, DeleteSession, AddToSession, RemoveFromSession). GetSession is public (no @Security tag) since the share-token URL is meant for join-via-link from outside the auth wall. - password_reset_handler.go (2 routes — RequestPasswordReset and ResetPassword factory functions). Both are public (no @Security) since they're the entry-points for users who can't log in. The request-side annotation documents the intentional generic 200 response (anti-enumeration: same body whether the email exists or not). After regen : - openapi.yaml gains 7 queue paths (/queue, /queue/items[/{id}], /queue/session[/{token}[/items[/{id}]]]) and 2 password paths (/auth/password/reset, /auth/password/reset-request). +568 LOC. - docs/{docs.go,swagger.json,swagger.yaml} updated identically by swag init. - apps/web/src/services/generated/queue/queue.ts created (10 HTTP funcs + matching React Query hooks). model/ index extended with the queue + password-reset request/response shapes. Validates with `swag init` (Swagger 2.0). go build ./... clean. No runtime behaviour change — annotations are pure metadata read by the spec generator. The orval regen IS the wiring point for the follow-up frontend commit (queue.ts migration + authService finish). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:55:26 +00:00
// @Summary Remove item from queue session
// @Description Removes an item from a collaborative queue session by item ID
// @Tags Queue
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param token path string true "Session share token"
// @Param id path string true "Queue item ID (UUID)"
// @Success 200 {object} APIResponse{data=object{message=string}} "Item removed"
// @Failure 400 {object} APIResponse "Validation error"
// @Failure 404 {object} APIResponse "Session or item not found"
// @Failure 500 {object} APIResponse "Internal Error"
// @Router /queue/session/{token}/items/{id} [delete]
func (h *QueueSessionHandler) RemoveFromSession(c *gin.Context) {
token := c.Param("token")
if token == "" {
RespondWithAppError(c, apperrors.NewValidationError("token is required"))
return
}
itemID, err := uuid.Parse(c.Param("id"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid item ID"))
return
}
if err := h.svc.RemoveFromSession(c.Request.Context(), token, itemID); err != nil {
if errors.Is(err, services.ErrQueueSessionNotFound) {
RespondWithAppError(c, apperrors.NewNotFoundError("session or item not found"))
return
}
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to remove from session", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "item removed"})
}