Compare commits

...

3 commits

Author SHA1 Message Date
senke
9e948d5102 feat(openapi): annotate profile_handler users endpoints (v1.0.8 B-annot)
Some checks failed
Veza CI / Frontend (Web) (push) Failing after 0s
Veza CI / Rust (Stream Server) (push) Failing after 0s
Frontend CI / test (push) Failing after 0s
Security Scan / Secret Scanning (gitleaks) (push) Failing after 0s
Veza CI / Notify on failure (push) Failing after 0s
Veza CI / Backend (Go) (push) Failing after 0s
Fourth batch. Closes the user/profile surface consumed by the
frontend users service. 6 handlers annotated across
internal/handlers/profile_handler.go (now 12/15 annotated).

Handlers annotated:
- SearchUsers            — GET    /users/search
- FollowUser             — POST   /users/{id}/follow
- GetFollowSuggestions   — GET    /users/suggestions
- UnfollowUser           — DELETE /users/{id}/follow
- BlockUser              — POST   /users/{id}/block
- UnblockUser            — DELETE /users/{id}/block

Added a blank `_ "veza-backend-api/internal/models"` import so swaggo
can resolve models.User in doc comments without forcing runtime use
(same pattern as track_hls_handler.go / track_waveform_handler.go).

Spec coverage: /users/* paths now 12 (all frontend-consumed endpoints).
make openapi:  · go build ./...: .

Completes the B-2 backend annotation scope for auth / users / tracks /
playlists — the four services that will migrate to orval in the next
commit. Remaining unannotated handlers (admin, moderation, analytics,
education, cloud, gear, social_group, etc.) are outside the v1.0.8
frontend migration and deferred to v1.0.9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 01:09:05 +02:00
senke
72c5381c73 feat(openapi): annotate playlist handler gap — 12 endpoints (v1.0.8 B-annot)
Third batch. Fills the playlist_handler.go gap (was 8/24 annotated,
now 20/24). Covers the functionality consumed by the frontend
playlists service: import, favoris, share tokens, collaborators,
analytics, search, recommendations, duplication.

Handlers annotated:
- ImportPlaylist              — POST /playlists/import
- GetFavorisPlaylist          — GET  /playlists/favoris
- GetPlaylistByShareToken     — GET  /playlists/shared/{token}
- SearchPlaylists             — GET  /playlists/search
- GetRecommendations          — GET  /playlists/recommendations
- GetPlaylistStats            — GET  /playlists/{id}/analytics
- AddCollaborator             — POST /playlists/{id}/collaborators
- GetCollaborators            — GET  /playlists/{id}/collaborators
- UpdateCollaboratorPermission — PUT /playlists/{id}/collaborators/{userId}
- RemoveCollaborator          — DELETE /playlists/{id}/collaborators/{userId}
- CreateShareLink             — POST /playlists/{id}/share
- DuplicatePlaylist           — POST /playlists/{id}/duplicate

Not annotated (unrouted, survey false positives): FollowPlaylist,
UnfollowPlaylist — no route references in internal/api/routes_*.go.
Left unannotated to avoid polluting the spec with dead handlers.

Marketplace gap originally planned for this batch is deferred to
v1.0.9: the 13 remaining handlers (UploadProductPreview, reviews,
licenses, sell stats, refund, invoice) don't block the B-2 frontend
migration (auth/users/tracks/playlists only), so they will be done
after v1.0.8 ships. Task #48 updated to reflect.

Spec coverage:
  /playlists/* paths: 5 → 15
  make openapi:  valid
  go build ./...: 

Next: profile_handler.go + auth/handler.go to finish the B-2 spec
surface (users endpoints), then regen orval and migrate 4 services.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 01:04:15 +02:00
senke
3dc0654a52 feat(openapi): annotate track subsystem (social/analytics/search/hls/waveform) — v1.0.8 B-annot
Second batch of the Veza backend OpenAPI annotation campaign. Completes
the track/ handler subtree — 22 more handlers annotated across 5 files —
so the orval-generated frontend client now covers the full track API
surface (stream, download, like, repost, share, search, recommendations,
stats, history, play, waveform, version restore).

Handlers annotated:

- internal/core/track/track_social_handler.go (11):
  LikeTrack, UnlikeTrack, GetTrackLikes, GetUserLikedTracks,
  GetUserRepostedTracks, CreateShare, GetSharedTrack, RevokeShare,
  RepostTrack, UnrepostTrack, GetRepostStatus

- internal/core/track/track_analytics_handler.go (4):
  GetTrackStats, GetTrackHistory, RecordPlay, RestoreVersion

- internal/core/track/track_search_handler.go (3):
  GetRecommendations, GetSuggestedTags, SearchTracks

- internal/core/track/track_hls_handler.go (3):
  HandleStreamCallback (internal), DownloadTrack, StreamTrack
  — both user-facing endpoints document the v1.0.8 P2 302-to-signed-URL
  behavior for S3-backed tracks alongside the local-FS path.

- internal/core/track/track_waveform_handler.go (1): GetWaveform

All comment blocks converge on the established template:
Summary / Description / Tags / Accept/Produce / Security (BearerAuth
when required) / typed Param path|query|body / Success envelope
handlers.APIResponse{data=...} / Failure 400/401/403/404/500 / Router.

track_hls_handler.go + track_waveform_handler.go receive a blank
import of internal/handlers so swaggo's type resolver can locate
handlers.APIResponse without forcing the file to call that package
at runtime.

Spec coverage:
  /tracks/*  paths: 13 → 29
  make openapi:  valid (Swagger 2.0)
  go build ./...: 
  openapi.yaml: +780 lines describing 16 new track endpoints.

Leaves /internal/core/ subsystems still blank: admin, moderation,
analytics/*, auth/handler.go (duplicates routes handled elsewhere),
discover, feed. Batch 2b next will cover playlists + marketplace gap
so the 4 frontend services (auth/users/tracks/playlists) become
fully orval-migratable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 00:58:08 +02:00
11 changed files with 10780 additions and 0 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,16 @@ type RecordPlayRequest struct {
}
// GetTrackStats returns track statistics (plays, likes, views, etc.)
// @Summary Get track statistics
// @Description Aggregated track stats: views, likes, comments, play time, downloads, average duration.
// @Tags Track
// @Produce json
// @Param id path string true "Track UUID"
// @Success 200 {object} handlers.APIResponse{data=object{stats=object}}
// @Failure 400 {object} handlers.APIResponse "Invalid track id"
// @Failure 404 {object} handlers.APIResponse "Track not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/{id}/stats [get]
func (h *TrackHandler) GetTrackStats(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
@ -62,6 +72,18 @@ func (h *TrackHandler) GetTrackStats(c *gin.Context) {
}
// GetTrackHistory returns modification history for a track
// @Summary Get track history
// @Description Paginated audit log of modifications (metadata updates, version changes) for a track.
// @Tags Track
// @Produce json
// @Param id path string true "Track UUID"
// @Param limit query int false "Items per page" default(50)
// @Param offset query int false "Offset" default(0)
// @Success 200 {object} handlers.APIResponse{data=object{history=[]object,total=integer,limit=integer,offset=integer}}
// @Failure 400 {object} handlers.APIResponse "Invalid track id"
// @Failure 404 {object} handlers.APIResponse "Track not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/{id}/history [get]
func (h *TrackHandler) GetTrackHistory(c *gin.Context) {
if h.historyService == nil {
h.respondWithError(c, http.StatusInternalServerError, "history service not available")
@ -124,6 +146,20 @@ func (h *TrackHandler) GetTrackHistory(c *gin.Context) {
}
// RecordPlay enregistre un événement de lecture pour un track
// @Summary Record play event
// @Description Persist a playback event with optional play_time so the creator's analytics dashboard tracks listening behaviour.
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track UUID"
// @Param request body RecordPlayRequest false "Playback metadata (optional)"
// @Success 200 {object} handlers.APIResponse{data=object{message=string,id=string}}
// @Failure 400 {object} handlers.APIResponse "Invalid track id / body"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 404 {object} handlers.APIResponse "Track not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/{id}/play [post]
func (h *TrackHandler) RecordPlay(c *gin.Context) {
if h.playbackAnalyticsService == nil {
h.respondWithError(c, http.StatusInternalServerError, "playback analytics service not available")
@ -183,6 +219,20 @@ func (h *TrackHandler) RecordPlay(c *gin.Context) {
}
// RestoreVersion restaure une version spécifique d'un track
// @Summary Restore track version
// @Description Rollback a track to a previous version. Only the track owner can restore.
// @Tags Track
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track UUID"
// @Param versionId path string true "Version UUID"
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
// @Failure 400 {object} handlers.APIResponse "Invalid id"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Not owner"
// @Failure 404 {object} handlers.APIResponse "Track or version not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/{id}/versions/{versionId}/restore [post]
func (h *TrackHandler) RestoreVersion(c *gin.Context) {
if h.versionService == nil {
h.respondWithError(c, http.StatusInternalServerError, "version service not available")

View file

@ -11,6 +11,9 @@ import (
"github.com/google/uuid"
"veza-backend-api/internal/common"
// handlers is imported to let swaggo resolve handlers.APIResponse refs in
// doc comments (@Failure / @Success); not called directly from this file.
_ "veza-backend-api/internal/handlers"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
@ -26,6 +29,17 @@ type StreamCallbackRequest struct {
}
// HandleStreamCallback handles the callback from stream server
// @Summary Stream server callback
// @Description Internal endpoint called by the Rust stream server when HLS transcoding completes or fails. Updates the track's stream_status and stream_manifest_url. Requires internal API key (not user-facing).
// @Tags Track
// @Accept json
// @Produce json
// @Param id path string true "Track UUID"
// @Param request body StreamCallbackRequest true "Callback payload"
// @Success 200 {object} object{message=string}
// @Failure 400 {object} handlers.APIResponse "Validation / invalid id"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /internal/tracks/{id}/stream-ready [post]
func (h *TrackHandler) HandleStreamCallback(c *gin.Context) {
trackIDStr := c.Param("id")
// MIGRATION UUID: TrackID is UUID
@ -52,6 +66,19 @@ func (h *TrackHandler) HandleStreamCallback(c *gin.Context) {
}
// DownloadTrack gère le téléchargement d'un track
// @Summary Download a track
// @Description Serve the original audio file. For S3-backed tracks returns a 302 redirect to a signed URL (TTL 30min). For local-backed tracks streams the file with Range support. Public tracks or share_token access; paid tracks require a license.
// @Tags Track
// @Produce application/octet-stream
// @Param id path string true "Track UUID"
// @Param share_token query string false "Grants access without authentication for a limited time"
// @Success 200 {file} binary
// @Success 302 {string} string "Location header points to signed S3 URL (s3-backed tracks)"
// @Failure 400 {object} handlers.APIResponse "Invalid track id"
// @Failure 403 {object} handlers.APIResponse "No permission / license required"
// @Failure 404 {object} handlers.APIResponse "Track or file not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/{id}/download [get]
func (h *TrackHandler) DownloadTrack(c *gin.Context) {
// Récupérer l'utilisateur s'il est authentifié
var userID uuid.UUID
@ -193,6 +220,19 @@ func (h *TrackHandler) DownloadTrack(c *gin.Context) {
// by HLSEnabled), /stream is always available and is the default playback path when
// HLS transcoding is off. The file is served via http.ServeContent which handles
// Range, If-Modified-Since and If-None-Match automatically.
// @Summary Stream a track (raw audio + Range)
// @Description Default playback path. S3-backed tracks return a 302 redirect to a signed URL (TTL 15min). Local-backed tracks are streamed via http.ServeContent with Range support. Always available, unlike /hls/* which is gated by HLSEnabled.
// @Tags Track
// @Produce audio/*
// @Param id path string true "Track UUID"
// @Param share_token query string false "Grants access without authentication"
// @Success 200 {file} binary
// @Success 302 {string} string "Location header points to signed S3 URL (s3-backed tracks)"
// @Failure 400 {object} handlers.APIResponse "Invalid track id"
// @Failure 403 {object} handlers.APIResponse "No permission"
// @Failure 404 {object} handlers.APIResponse "Track or file not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/{id}/stream [get]
func (h *TrackHandler) StreamTrack(c *gin.Context) {
var userID uuid.UUID
if userIDInterface, exists := c.Get("user_id"); exists {

View file

@ -29,6 +29,16 @@ var tagSuggestionsByGenre = map[string][]string{
}
// GetRecommendations returns personalized track recommendations (D2 autoplay)
// @Summary Get track recommendations
// @Description Personalized tracks for D2 autoplay. If seed_track_id is given, returns tracks similar to that seed. Otherwise, uses the caller's history (chronological, no behavioural ranking — CLAUDE.md rule 7).
// @Tags Track
// @Produce json
// @Security BearerAuth
// @Param limit query int false "Max items (max 100)" default(20)
// @Param seed_track_id query string false "Start from this track's similarity neighbours"
// @Success 200 {object} response.APIResponse{data=object{tracks=[]models.Track}}
// @Failure 500 {object} response.APIResponse "Internal Error"
// @Router /tracks/recommendations [get]
func (h *TrackHandler) GetRecommendations(c *gin.Context) {
if h.trackRecommendationService == nil {
response.InternalServerError(c, "recommendations unavailable")
@ -72,6 +82,13 @@ func (h *TrackHandler) GetRecommendations(c *gin.Context) {
}
// GetSuggestedTags returns tag suggestions based on genre and BPM (E4)
// @Summary Get suggested tags
// @Description Returns a static tag suggestion list for a genre — useful for upload autocomplete and filter chips.
// @Tags Track
// @Produce json
// @Param genre query string false "Genre slug (pop, rock, electronic, hip-hop, jazz, classical, ambient, default)" default(default)
// @Success 200 {object} response.APIResponse{data=object{tags=[]string}}
// @Router /tracks/suggested-tags [get]
func (h *TrackHandler) GetSuggestedTags(c *gin.Context) {
genre := strings.ToLower(strings.TrimSpace(c.DefaultQuery("genre", "")))
if genre == "" {
@ -85,6 +102,29 @@ func (h *TrackHandler) GetSuggestedTags(c *gin.Context) {
}
// SearchTracks gère la recherche avancée de tracks
// @Summary Advanced track search
// @Description Full-text + faceted search on tracks (genre, BPM, duration, tags, musical key, dates). Sort-by and order configurable.
// @Tags Track
// @Produce json
// @Param q query string false "Full-text query (title/artist/album)"
// @Param tags query string false "Comma-separated tag list"
// @Param tag_mode query string false "Tag combinator (OR / AND)" default(OR)
// @Param min_duration query int false "Minimum duration (seconds)"
// @Param max_duration query int false "Maximum duration (seconds)"
// @Param min_bpm query int false "Minimum BPM"
// @Param max_bpm query int false "Maximum BPM"
// @Param genre query string false "Genre filter"
// @Param format query string false "Audio format filter"
// @Param musical_key query string false "Musical key filter"
// @Param min_date query string false "Created-after (RFC3339)"
// @Param max_date query string false "Created-before (RFC3339)"
// @Param page query int false "Page (1-based)" default(1)
// @Param limit query int false "Items per page (max 100)" default(20)
// @Param sort_by query string false "Sort column" default(created_at)
// @Param sort_order query string false "asc / desc" default(desc)
// @Success 200 {object} response.APIResponse{data=object{tracks=[]models.Track,pagination=object}}
// @Failure 500 {object} response.APIResponse "Internal Error"
// @Router /tracks/search [get]
func (h *TrackHandler) SearchTracks(c *gin.Context) {
if h.searchService == nil {
h.respondWithError(c, http.StatusInternalServerError, "search service not available")

View file

@ -25,6 +25,18 @@ type CreateShareRequest struct {
}
// LikeTrack gère l'ajout d'un like sur un track
// @Summary Like a track
// @Description Record a like from the authenticated user. Creates a grouped notification for the creator (F554).
// @Tags Track
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track UUID"
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
// @Failure 400 {object} handlers.APIResponse "Invalid track id"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 404 {object} handlers.APIResponse "Track not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/{id}/like [post]
func (h *TrackHandler) LikeTrack(c *gin.Context) {
userID, ok := h.getUserID(c)
if !ok {
@ -67,6 +79,17 @@ func (h *TrackHandler) LikeTrack(c *gin.Context) {
}
// UnlikeTrack gère la suppression d'un like sur un track
// @Summary Unlike a track
// @Description Remove the authenticated user's like on the track (idempotent).
// @Tags Track
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track UUID"
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
// @Failure 400 {object} handlers.APIResponse "Invalid track id"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/{id}/like [delete]
func (h *TrackHandler) UnlikeTrack(c *gin.Context) {
userID, ok := h.getUserID(c)
if !ok {
@ -95,6 +118,18 @@ func (h *TrackHandler) UnlikeTrack(c *gin.Context) {
// GetTrackLikes gère la récupération du nombre de likes d'un track.
// v0.10.3 F202: count visible only by track creator (or admin); others get is_liked only.
// @Summary Get track like status
// @Description Returns whether the current user has liked the track. The total like count is returned ONLY to the creator or an admin (privacy per ORIGIN_UI_UX_SYSTEM §13).
// @Tags Track
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track UUID"
// @Success 200 {object} handlers.APIResponse{data=object{is_liked=boolean,count=integer}} "count is omitted for non-owners"
// @Failure 400 {object} handlers.APIResponse "Invalid track id"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 404 {object} handlers.APIResponse "Track not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/{id}/likes [get]
func (h *TrackHandler) GetTrackLikes(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
@ -137,6 +172,19 @@ func (h *TrackHandler) GetTrackLikes(c *gin.Context) {
}
// GetUserLikedTracks gère la récupération des tracks likés par un utilisateur
// @Summary List tracks liked by a user
// @Description Returns paginated tracks the given user has liked. Used for profile "Likes" tab.
// @Tags User
// @Produce json
// @Security BearerAuth
// @Param id path string true "User UUID"
// @Param limit query int false "Items per page (max 100)" default(20)
// @Param offset query int false "Offset for pagination" default(0)
// @Success 200 {object} handlers.APIResponse{data=object{tracks=[]models.Track,total=integer,limit=integer,offset=integer}}
// @Failure 400 {object} handlers.APIResponse "Validation"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /users/{id}/likes [get]
func (h *TrackHandler) GetUserLikedTracks(c *gin.Context) {
userIDStr := c.Param("id")
if userIDStr == "" {
@ -188,6 +236,17 @@ func (h *TrackHandler) GetUserLikedTracks(c *gin.Context) {
}
// GetUserRepostedTracks returns tracks reposted by the user (v0.10.3 F203).
// @Summary List tracks reposted by a user
// @Description Returns paginated tracks the user has reposted. Used for profile "Reposts" tab.
// @Tags User
// @Produce json
// @Param id path string true "User UUID"
// @Param limit query int false "Items per page (max 100)" default(20)
// @Param offset query int false "Offset for pagination" default(0)
// @Success 200 {object} handlers.APIResponse{data=object{tracks=[]models.Track,total=integer,limit=integer,offset=integer}}
// @Failure 400 {object} handlers.APIResponse "Validation"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /users/{id}/reposts [get]
func (h *TrackHandler) GetUserRepostedTracks(c *gin.Context) {
userIDStr := c.Param("id")
if userIDStr == "" {
@ -243,6 +302,21 @@ func (h *TrackHandler) GetUserRepostedTracks(c *gin.Context) {
}
// CreateShare crée un nouveau lien de partage pour un track
// @Summary Create share link
// @Description Generate a tokenized share link for a track with given permission level and optional expiry.
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track UUID"
// @Param request body CreateShareRequest true "Share parameters"
// @Success 200 {object} handlers.APIResponse{data=object{share=object}}
// @Failure 400 {object} handlers.APIResponse "Validation"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Not owner"
// @Failure 404 {object} handlers.APIResponse "Track not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/{id}/share [post]
func (h *TrackHandler) CreateShare(c *gin.Context) {
userID, ok := h.getUserID(c)
if !ok {
@ -289,6 +363,17 @@ func (h *TrackHandler) CreateShare(c *gin.Context) {
}
// GetSharedTrack récupère un track via son token de partage
// @Summary Get track by share token
// @Description Public endpoint that resolves a share token and returns the track + share metadata. No auth required; the token IS the auth.
// @Tags Track
// @Produce json
// @Param token path string true "Opaque share token issued by CreateShare"
// @Success 200 {object} handlers.APIResponse{data=object{track=models.Track,share=object}}
// @Failure 400 {object} handlers.APIResponse "Missing token"
// @Failure 403 {object} handlers.APIResponse "Share link expired"
// @Failure 404 {object} handlers.APIResponse "Share or track not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/shared/{token} [get]
func (h *TrackHandler) GetSharedTrack(c *gin.Context) {
token := c.Param("token")
if token == "" {
@ -332,6 +417,19 @@ func (h *TrackHandler) GetSharedTrack(c *gin.Context) {
}
// RevokeShare révoque un lien de partage
// @Summary Revoke share link
// @Description Permanently disable a share token. Only the share issuer (or admin) can revoke.
// @Tags Track
// @Produce json
// @Security BearerAuth
// @Param id path string true "Share UUID"
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
// @Failure 400 {object} handlers.APIResponse "Validation"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Not issuer"
// @Failure 404 {object} handlers.APIResponse "Share not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/share/{id} [delete]
func (h *TrackHandler) RevokeShare(c *gin.Context) {
userID, ok := h.getUserID(c)
if !ok {
@ -373,6 +471,18 @@ func (h *TrackHandler) RevokeShare(c *gin.Context) {
}
// RepostTrack adds a track repost to the user's profile (v0.10.3 F203).
// @Summary Repost a track
// @Description Add a track to the authenticated user's profile as a repost. Notifies the creator (F204) unless self-repost.
// @Tags Track
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track UUID"
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
// @Failure 400 {object} handlers.APIResponse "Invalid track id"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 404 {object} handlers.APIResponse "Track not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/{id}/repost [post]
func (h *TrackHandler) RepostTrack(c *gin.Context) {
userID, ok := h.getUserID(c)
if !ok {
@ -418,6 +528,17 @@ func (h *TrackHandler) RepostTrack(c *gin.Context) {
}
// UnrepostTrack removes a track repost (v0.10.3 F203).
// @Summary Remove track repost
// @Description Remove the authenticated user's repost of the track (idempotent).
// @Tags Track
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track UUID"
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
// @Failure 400 {object} handlers.APIResponse "Invalid track id"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/{id}/repost [delete]
func (h *TrackHandler) UnrepostTrack(c *gin.Context) {
userID, ok := h.getUserID(c)
if !ok {
@ -451,6 +572,14 @@ func (h *TrackHandler) UnrepostTrack(c *gin.Context) {
// GetRepostStatus returns whether the current user has reposted the track (v0.10.3 F203).
// Works with OptionalAuth: if not authenticated, returns is_reposted: false.
// @Summary Get repost status
// @Description Returns whether the current user has reposted the track. Public (optional auth); unauthenticated callers get is_reposted=false.
// @Tags Track
// @Produce json
// @Param id path string true "Track UUID"
// @Success 200 {object} handlers.APIResponse{data=object{is_reposted=boolean}}
// @Failure 400 {object} handlers.APIResponse "Invalid track id"
// @Router /tracks/{id}/repost [get]
func (h *TrackHandler) GetRepostStatus(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {

View file

@ -6,11 +6,25 @@ import (
"github.com/google/uuid"
// handlers imported so swaggo resolves handlers.APIResponse refs in
// doc comments; no direct call.
_ "veza-backend-api/internal/handlers"
"github.com/gin-gonic/gin"
)
// GetWaveform returns the waveform JSON data for a track (S1-06)
// GET /api/v1/tracks/:id/waveform
// @Summary Get track waveform
// @Description Returns a JSON peaks array used by the client to draw the audio waveform preview. 404 if waveform extraction is not complete yet.
// @Tags Track
// @Produce json
// @Param id path string true "Track UUID"
// @Success 200 {object} object "Waveform peaks JSON (tool-specific shape)"
// @Failure 400 {object} handlers.APIResponse "Invalid track id"
// @Failure 404 {object} handlers.APIResponse "Waveform not generated / track not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /tracks/{id}/waveform [get]
func (h *TrackHandler) GetWaveform(c *gin.Context) {
if h.waveformService == nil {
h.respondWithError(c, http.StatusInternalServerError, "waveform service not available")

View file

@ -140,6 +140,18 @@ type ImportPlaylistRequest struct {
}
// ImportPlaylist gère l'import d'une playlist depuis JSON (v0.10.4 F145)
// @Summary Import playlist
// @Description Create a playlist from a JSON payload (title, description, is_public, ordered track IDs). Useful for bulk seed / migration.
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body ImportPlaylistRequest true "Playlist + tracks"
// @Success 201 {object} handlers.APIResponse{data=object{playlist=models.Playlist}}
// @Failure 400 {object} handlers.APIResponse "Validation"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /playlists/import [post]
func (h *PlaylistHandler) ImportPlaylist(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
@ -188,6 +200,15 @@ func (h *PlaylistHandler) ImportPlaylist(c *gin.Context) {
}
// GetFavorisPlaylist returns the current user's Favoris playlist, creating it if needed (v0.10.4 F136)
// @Summary Get Favoris playlist
// @Description Returns the authenticated user's "Favoris" playlist. Auto-created on first call. Used by the like-as-save pattern.
// @Tags Playlist
// @Produce json
// @Security BearerAuth
// @Success 200 {object} handlers.APIResponse{data=object{playlist=models.Playlist}}
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /playlists/favoris [get]
func (h *PlaylistHandler) GetFavorisPlaylist(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
@ -316,6 +337,18 @@ func (h *PlaylistHandler) GetPlaylist(c *gin.Context) {
// GetPlaylistByShareToken returns a playlist by its public share token (v0.10.4 F143).
// No authentication required.
// GetPlaylistByShareToken returns a playlist via its share token (no auth required).
// @Summary Get playlist by share token
// @Description Public endpoint resolving a share token. Allows unauthenticated access to the playlist snapshot + tracks.
// @Tags Playlist
// @Produce json
// @Param token path string true "Share token"
// @Success 200 {object} handlers.APIResponse{data=object{playlist=models.Playlist}}
// @Failure 400 {object} handlers.APIResponse "Missing token"
// @Failure 403 {object} handlers.APIResponse "Share expired"
// @Failure 404 {object} handlers.APIResponse "Share or playlist not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /playlists/shared/{token} [get]
func (h *PlaylistHandler) GetPlaylistByShareToken(c *gin.Context) {
token := c.Param("token")
if token == "" {
@ -635,6 +668,22 @@ type UpdateCollaboratorPermissionRequest struct {
// AddCollaborator gère l'ajout d'un collaborateur à une playlist
// T0479: POST /api/v1/playlists/:id/collaborators
// AddCollaborator adds a collaborator with a permission level to the playlist.
// @Summary Add playlist collaborator
// @Description Invite a user as collaborator. Only the owner (or admin) can add.
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist UUID"
// @Param request body AddCollaboratorRequest true "Collaborator + permission"
// @Success 200 {object} handlers.APIResponse{data=object{collaborator=object}}
// @Failure 400 {object} handlers.APIResponse "Validation"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Not owner"
// @Failure 404 {object} handlers.APIResponse "Playlist not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /playlists/{id}/collaborators [post]
func (h *PlaylistHandler) AddCollaborator(c *gin.Context) {
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
@ -703,6 +752,21 @@ func (h *PlaylistHandler) AddCollaborator(c *gin.Context) {
// RemoveCollaborator gère la suppression d'un collaborateur d'une playlist
// T0479: DELETE /api/v1/playlists/:id/collaborators/:userId
// RemoveCollaborator removes a collaborator from a playlist.
// @Summary Remove playlist collaborator
// @Description Revoke a collaborator's access. Only the owner (or admin) can remove.
// @Tags Playlist
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist UUID"
// @Param userId path string true "Collaborator user UUID"
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
// @Failure 400 {object} handlers.APIResponse "Validation"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Not owner"
// @Failure 404 {object} handlers.APIResponse "Playlist or collaborator not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /playlists/{id}/collaborators/{userId} [delete]
func (h *PlaylistHandler) RemoveCollaborator(c *gin.Context) {
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
@ -748,6 +812,23 @@ func (h *PlaylistHandler) RemoveCollaborator(c *gin.Context) {
// UpdateCollaboratorPermission gère la mise à jour de la permission d'un collaborateur
// T0479: PUT /api/v1/playlists/:id/collaborators/:userId
// UpdateCollaboratorPermission changes a collaborator's permission level.
// @Summary Update collaborator permission
// @Description Change a collaborator's permission level (read / write / admin). Only the owner can update.
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist UUID"
// @Param userId path string true "Collaborator user UUID"
// @Param request body UpdateCollaboratorPermissionRequest true "New permission"
// @Success 200 {object} handlers.APIResponse{data=object{collaborator=object}}
// @Failure 400 {object} handlers.APIResponse "Validation"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Not owner"
// @Failure 404 {object} handlers.APIResponse "Playlist or collaborator not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /playlists/{id}/collaborators/{userId} [put]
func (h *PlaylistHandler) UpdateCollaboratorPermission(c *gin.Context) {
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
@ -819,6 +900,19 @@ func (h *PlaylistHandler) UpdateCollaboratorPermission(c *gin.Context) {
// GetCollaborators gère la récupération des collaborateurs d'une playlist
// T0479: GET /api/v1/playlists/:id/collaborators
// GetCollaborators lists a playlist's collaborators.
// @Summary List playlist collaborators
// @Description Returns the collaborators of a playlist with their permission level.
// @Tags Playlist
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist UUID"
// @Success 200 {object} handlers.APIResponse{data=object{collaborators=[]object}}
// @Failure 400 {object} handlers.APIResponse "Invalid id"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 404 {object} handlers.APIResponse "Playlist not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /playlists/{id}/collaborators [get]
func (h *PlaylistHandler) GetCollaborators(c *gin.Context) {
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
@ -854,6 +948,20 @@ func (h *PlaylistHandler) GetCollaborators(c *gin.Context) {
// CreateShareLink gère la création d'un lien de partage public pour une playlist
// T0488: Create Playlist Public Share Link
// CreateShareLink generates a tokenised share link for a playlist.
// @Summary Create playlist share link
// @Description Generate a tokenised link to share a playlist (read-only). Only owner / admin can issue. No body required.
// @Tags Playlist
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist UUID"
// @Success 200 {object} handlers.APIResponse{data=object{share=object}}
// @Failure 400 {object} handlers.APIResponse "Validation"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Not owner"
// @Failure 404 {object} handlers.APIResponse "Playlist not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /playlists/{id}/share [post]
func (h *PlaylistHandler) CreateShareLink(c *gin.Context) {
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
@ -957,6 +1065,20 @@ func (h *PlaylistHandler) UnfollowPlaylist(c *gin.Context) {
// GetPlaylistStats gère la récupération des statistiques d'une playlist
// T0491: Create Playlist Analytics Backend
// GetPlaylistStats returns aggregated stats for a playlist.
// @Summary Get playlist statistics
// @Description Returns aggregated stats for a playlist (plays, follows, tracks count, etc.). Visible to the owner, collaborators and admins.
// @Tags Playlist
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist UUID"
// @Success 200 {object} handlers.APIResponse{data=object{stats=object}}
// @Failure 400 {object} handlers.APIResponse "Invalid id"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Forbidden"
// @Failure 404 {object} handlers.APIResponse "Playlist not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /playlists/{id}/analytics [get]
func (h *PlaylistHandler) GetPlaylistStats(c *gin.Context) {
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
@ -1038,6 +1160,20 @@ type DuplicatePlaylistRequest struct {
// DuplicatePlaylist gère la duplication d'une playlist
// T0495: Create Playlist Duplicate Feature
// DuplicatePlaylist duplicates a playlist into a new one owned by the caller.
// @Summary Duplicate playlist
// @Description Copy a playlist's track list into a new playlist owned by the authenticated user. Cover/description copied; original unchanged.
// @Tags Playlist
// @Produce json
// @Security BearerAuth
// @Param id path string true "Source playlist UUID"
// @Success 201 {object} handlers.APIResponse{data=object{playlist=models.Playlist}}
// @Failure 400 {object} handlers.APIResponse "Invalid id"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Source not visible"
// @Failure 404 {object} handlers.APIResponse "Source playlist not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /playlists/{id}/duplicate [post]
func (h *PlaylistHandler) DuplicatePlaylist(c *gin.Context) {
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
@ -1095,6 +1231,18 @@ func (h *PlaylistHandler) DuplicatePlaylist(c *gin.Context) {
// SearchPlaylists gère la recherche de playlists
// T0496: Create Playlist Search Backend
// SearchPlaylists searches public playlists by query, sort, filters.
// @Summary Search playlists
// @Description Full-text search on public playlists (title + description). Paginated.
// @Tags Playlist
// @Produce json
// @Param q query string false "Full-text query"
// @Param page query int false "Page number" default(1)
// @Param limit query int false "Items per page (max 100)" default(20)
// @Success 200 {object} handlers.APIResponse{data=object{playlists=[]models.Playlist,pagination=object}}
// @Failure 400 {object} handlers.APIResponse "Validation"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /playlists/search [get]
func (h *PlaylistHandler) SearchPlaylists(c *gin.Context) {
// Get current user ID
var currentUserID *uuid.UUID
@ -1161,6 +1309,17 @@ func (h *PlaylistHandler) SearchPlaylists(c *gin.Context) {
// GetRecommendations gère la récupération des recommandations de playlists
// T0498: Create Playlist Recommendations
// GetRecommendations returns playlist recommendations for the caller.
// @Summary Get playlist recommendations
// @Description Suggested playlists for the authenticated user. Chronological / declarative discovery — no behavioural ranking (CLAUDE.md rule 7).
// @Tags Playlist
// @Produce json
// @Security BearerAuth
// @Param limit query int false "Max items (max 100)" default(20)
// @Success 200 {object} handlers.APIResponse{data=object{playlists=[]models.Playlist}}
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /playlists/recommendations [get]
func (h *PlaylistHandler) GetRecommendations(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {

View file

@ -6,6 +6,8 @@ import (
"time"
apperrors "veza-backend-api/internal/errors"
// models imported so swaggo can resolve models.User in doc comments.
_ "veza-backend-api/internal/models"
"veza-backend-api/internal/services"
"veza-backend-api/internal/types"
"veza-backend-api/internal/utils"
@ -267,6 +269,17 @@ func (h *ProfileHandler) ListUsers(c *gin.Context) {
// SearchUsers gère la recherche d'utilisateurs
// BE-API-008: Implement user search endpoint
// @Summary Search users
// @Description Full-text search on users (username, display_name). Paginated. Public.
// @Tags User
// @Produce json
// @Param q query string false "Full-text query"
// @Param page query int false "Page" default(1)
// @Param limit query int false "Items per page (max 100)" default(20)
// @Success 200 {object} handlers.APIResponse{data=object{users=[]models.User,pagination=object}}
// @Failure 400 {object} handlers.APIResponse "Validation (bounds)"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /users/search [get]
func (h *ProfileHandler) SearchUsers(c *gin.Context) {
// Récupérer les paramètres de recherche
query := c.Query("q")
@ -308,6 +321,18 @@ func (h *ProfileHandler) SearchUsers(c *gin.Context) {
// FollowUser gère le suivi d'un utilisateur
// POST /api/v1/users/:id/follow
// BE-API-017: Implement user follow/unfollow endpoints
// @Summary Follow user
// @Description Authenticated user follows target user. Creates a notification (F554 grouped) for the target.
// @Tags User
// @Produce json
// @Security BearerAuth
// @Param id path string true "Target user UUID"
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
// @Failure 400 {object} handlers.APIResponse "Invalid id"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 404 {object} handlers.APIResponse "User not found"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /users/{id}/follow [post]
func (h *ProfileHandler) FollowUser(c *gin.Context) {
// Récupérer l'ID de l'utilisateur à suivre depuis l'URL
userIDStr := c.Param("id")
@ -366,6 +391,16 @@ func (h *ProfileHandler) FollowUser(c *gin.Context) {
// GetFollowSuggestions returns users to follow (v0.10.0 F211)
// GET /api/v1/users/suggestions?limit=10
// @Summary Get follow suggestions
// @Description Returns suggested users to follow for the authenticated user. Declarative discovery — no behavioural scoring (CLAUDE.md rule 7).
// @Tags User
// @Produce json
// @Security BearerAuth
// @Param limit query int false "Max items (max 50)" default(10)
// @Success 200 {object} handlers.APIResponse{data=object{users=[]models.User}}
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /users/suggestions [get]
func (h *ProfileHandler) GetFollowSuggestions(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
@ -393,6 +428,17 @@ func (h *ProfileHandler) GetFollowSuggestions(c *gin.Context) {
// UnfollowUser gère l'arrêt du suivi d'un utilisateur
// DELETE /api/v1/users/:id/follow
// BE-API-017: Implement user follow/unfollow endpoints
// @Summary Unfollow user
// @Description Authenticated user stops following target user (idempotent).
// @Tags User
// @Produce json
// @Security BearerAuth
// @Param id path string true "Target user UUID"
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
// @Failure 400 {object} handlers.APIResponse "Invalid id"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /users/{id}/follow [delete]
func (h *ProfileHandler) UnfollowUser(c *gin.Context) {
// Récupérer l'ID de l'utilisateur à ne plus suivre depuis l'URL
userIDStr := c.Param("id")
@ -435,6 +481,17 @@ func (h *ProfileHandler) UnfollowUser(c *gin.Context) {
// BlockUser gère le blocage d'un utilisateur
// POST /api/v1/users/:id/block
// BE-API-018: Implement user block/unblock endpoints
// @Summary Block user
// @Description Authenticated user blocks target user (hides their content, prevents follows). Cannot self-block.
// @Tags User
// @Produce json
// @Security BearerAuth
// @Param id path string true "Target user UUID"
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
// @Failure 400 {object} handlers.APIResponse "Validation / self-block"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /users/{id}/block [post]
func (h *ProfileHandler) BlockUser(c *gin.Context) {
// Récupérer l'ID de l'utilisateur à bloquer depuis l'URL
userIDStr := c.Param("id")
@ -487,6 +544,17 @@ func (h *ProfileHandler) BlockUser(c *gin.Context) {
// UnblockUser gère le déblocage d'un utilisateur
// DELETE /api/v1/users/:id/block
// BE-API-018: Implement user block/unblock endpoints
// @Summary Unblock user
// @Description Authenticated user unblocks target user (idempotent).
// @Tags User
// @Produce json
// @Security BearerAuth
// @Param id path string true "Target user UUID"
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
// @Failure 400 {object} handlers.APIResponse "Invalid id"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /users/{id}/block [delete]
func (h *ProfileHandler) UnblockUser(c *gin.Context) {
// Récupérer l'ID de l'utilisateur à débloquer depuis l'URL
userIDStr := c.Param("id")

File diff suppressed because it is too large Load diff