feat(api): remediate missing openapi spec and annotate handlers

This commit is contained in:
okinrev 2025-12-06 17:34:18 +01:00
parent 65af2570a8
commit 88a8bfdce0
9 changed files with 7823 additions and 78 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

@ -16,7 +16,9 @@ import (
"gorm.io/gorm"
"veza-backend-api/internal/models"
"veza-backend-api/internal/services"
"veza-backend-api/internal/services"
"veza-backend-api/internal/validators"
"veza-backend-api/internal/response"
)
// TrackHandler gère les opérations sur les tracks
@ -70,6 +72,19 @@ func (h *TrackHandler) SetHistoryService(historyService *services.TrackHistorySe
}
// UploadTrack gère l'upload d'un fichier audio
// @Summary Upload Track
// @Description Upload a new track (audio file)
// @Tags Track
// @Accept multipart/form-data
// @Produce json
// @Security BearerAuth
// @Param file formData file true "Audio File (MP3, WAV, FLAC, OGG)"
// @Success 201 {object} response.APIResponse{data=object{track=models.Track}}
// @Failure 400 {object} response.APIResponse "No file or validation error"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 403 {object} response.APIResponse "Quota exceeded"
// @Failure 500 {object} response.APIResponse "Internal Error"
// @Router /tracks [post]
func (h *TrackHandler) UploadTrack(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
@ -107,6 +122,18 @@ func (h *TrackHandler) UploadTrack(c *gin.Context) {
}
// GetUploadStatus récupère le statut d'upload d'un track
// @Summary Get Upload Status
// @Description Get the processing status of an uploaded track
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track ID"
// @Success 200 {object} response.APIResponse{data=object{progress=int}}
// @Failure 400 {object} response.APIResponse "Invalid ID"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 404 {object} response.APIResponse "Track not found"
// @Router /tracks/{id}/status [get]
func (h *TrackHandler) GetUploadStatus(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
@ -168,6 +195,17 @@ type InitiateChunkedUploadRequest struct {
}
// InitiateChunkedUpload initialise un nouvel upload par chunks
// @Summary Initiate Chunked Upload
// @Description Start a new chunked upload session
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body InitiateChunkedUploadRequest true "Upload Metadata"
// @Success 200 {object} response.APIResponse{data=object{upload_id=string,message=string}}
// @Failure 400 {object} response.APIResponse "Validation Error"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Router /tracks/initiate [post]
func (h *TrackHandler) InitiateChunkedUpload(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
@ -214,6 +252,22 @@ type UploadChunkRequest struct {
}
// UploadChunk gère l'upload d'un chunk
// @Summary Upload Chunk
// @Description Upload a single chunk of a file
// @Tags Track
// @Accept multipart/form-data
// @Produce json
// @Security BearerAuth
// @Param chunk formData file true "Chunk Data"
// @Param upload_id formData string true "Upload ID"
// @Param chunk_number formData int true "Chunk Number"
// @Param total_chunks formData int true "Total Chunks"
// @Param total_size formData int64 true "Total Size"
// @Param filename formData string true "Filename"
// @Success 200 {object} response.APIResponse{data=object{message=string,upload_id=string,received_chunks=int,progress=float64}}
// @Failure 400 {object} response.APIResponse "Validation Error"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Router /tracks/chunk [post]
func (h *TrackHandler) UploadChunk(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
@ -261,6 +315,17 @@ type CompleteChunkedUploadRequest struct {
}
// CompleteChunkedUpload assemble tous les chunks et crée le track final
// @Summary Complete Chunked Upload
// @Description Finish upload session and assemble file
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body CompleteChunkedUploadRequest true "Upload ID"
// @Success 201 {object} response.APIResponse{data=object{message=string,track=models.Track,md5=string}}
// @Failure 400 {object} response.APIResponse "Validation or Assemblage Error"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Router /tracks/complete [post]
func (h *TrackHandler) CompleteChunkedUpload(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
@ -441,6 +506,17 @@ func (h *TrackHandler) getErrorStatusCode(err error) int {
}
// GetUploadQuota récupère les informations de quota d'upload pour un utilisateur
// @Summary Get Upload Quota
// @Description Get remaining upload quota for the user
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string false "User ID (optional, defaults to current user)"
// @Success 200 {object} response.APIResponse{data=object{quota=object}}
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 403 {object} response.APIResponse "Forbidden"
// @Router /tracks/quota/{id} [get]
func (h *TrackHandler) GetUploadQuota(c *gin.Context) {
// Récupérer l'ID utilisateur depuis l'URL ou depuis le contexte d'authentification
userIDParam := c.Param("id")
@ -489,6 +565,16 @@ func (h *TrackHandler) GetUploadQuota(c *gin.Context) {
}
// ResumeUpload récupère l'état d'un upload pour permettre la reprise
// @Summary Resume Upload
// @Description Get state of an interrupted upload
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param uploadId path string true "Upload ID"
// @Success 200 {object} response.APIResponse{data=object{upload_id=string,chunks_received=int}}
// @Failure 404 {object} response.APIResponse "Upload session not found"
// @Router /tracks/resume/{uploadId} [get]
func (h *TrackHandler) ResumeUpload(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
@ -531,6 +617,21 @@ func (h *TrackHandler) ResumeUpload(c *gin.Context) {
}
// ListTracks gère la liste des tracks avec pagination, filtres et tri
// @Summary List Tracks
// @Description Get a paginated list of tracks with filters
// @Tags Track
// @Accept json
// @Produce json
// @Param page query int false "Page number" default(1)
// @Param limit query int false "Items per page" default(20)
// @Param user_id query string false "Filter by User ID"
// @Param genre query string false "Filter by Genre"
// @Param format query string false "Filter by Format"
// @Param sort_by query string false "Sort field" default(created_at)
// @Param sort_order query string false "Sort order (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 [get]
func (h *TrackHandler) ListTracks(c *gin.Context) {
// Récupérer les paramètres de query
page := c.DefaultQuery("page", "1")
@ -608,6 +709,16 @@ func (h *TrackHandler) ListTracks(c *gin.Context) {
}
// GetTrack gère la récupération d'un track par son ID
// @Summary Get Track by ID
// @Description Get detailed information about a track
// @Tags Track
// @Accept json
// @Produce json
// @Param id path string true "Track ID"
// @Success 200 {object} response.APIResponse{data=object{track=models.Track}}
// @Failure 400 {object} response.APIResponse "Invalid ID"
// @Failure 404 {object} response.APIResponse "Track not found"
// @Router /tracks/{id} [get]
func (h *TrackHandler) GetTrack(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
@ -652,6 +763,20 @@ type UpdateTrackRequest struct {
}
// UpdateTrack gère la mise à jour d'un track
// @Summary Update Track
// @Description Update track metadata
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track ID"
// @Param track body UpdateTrackRequest true "Track Metadata"
// @Success 200 {object} response.APIResponse{data=object{track=models.Track}}
// @Failure 400 {object} response.APIResponse "Validation Error"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 403 {object} response.APIResponse "Forbidden"
// @Failure 404 {object} response.APIResponse "Track not found"
// @Router /tracks/{id} [put]
func (h *TrackHandler) UpdateTrack(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
@ -711,6 +836,18 @@ func (h *TrackHandler) UpdateTrack(c *gin.Context) {
}
// DeleteTrack gère la suppression d'un track
// @Summary Delete Track
// @Description Permanently delete a track
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track ID"
// @Success 200 {object} response.APIResponse{data=object{message=string}}
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 403 {object} response.APIResponse "Forbidden"
// @Failure 404 {object} response.APIResponse "Track not found"
// @Router /tracks/{id} [delete]
func (h *TrackHandler) DeleteTrack(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
@ -754,6 +891,17 @@ type BatchDeleteRequest struct {
}
// BatchDeleteTracks gère la suppression en lot de plusieurs tracks
// @Summary Batch Delete Tracks
// @Description Delete multiple tracks at once
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body BatchDeleteRequest true "List of Track IDs"
// @Success 200 {object} response.APIResponse{data=object{deleted=[]string,failed=object}}
// @Failure 400 {object} response.APIResponse "Validation Error"
// @Failure 500 {object} response.APIResponse "Internal Error"
// @Router /tracks/batch/delete [post]
func (h *TrackHandler) BatchDeleteTracks(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {

View file

@ -17,8 +17,17 @@ import (
)
// Login gère la connexion des utilisateurs
// T0203: Intègre création de session après login avec IP et User-Agent
// P0: JSON Hardening - Utilise BindAndValidateJSON pour une gestion robuste des erreurs
// @Summary User Login
// @Description Authenticate user and return access/refresh tokens
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body dto.LoginRequest true "Login Credentials"
// @Success 200 {object} dto.LoginResponse
// @Failure 400 {object} handlers.APIResponse "Validation or Bad Request"
// @Failure 401 {object} handlers.APIResponse "Invalid credentials"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /auth/login [post]
func Login(authService *auth.AuthService, sessionService *services.SessionService, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
commonHandler := NewCommonHandler(logger)
@ -94,8 +103,17 @@ func Login(authService *auth.AuthService, sessionService *services.SessionServic
}
// Register gère l'inscription des utilisateurs
// GO-013: Utilise validator centralisé pour validation améliorée
// P0: JSON Hardening - Utilise BindAndValidateJSON pour une gestion robuste des erreurs
// @Summary User Registration
// @Description Register a new user account
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body dto.RegisterRequest true "Registration Data"
// @Success 201 {object} dto.RegisterResponse
// @Failure 400 {object} handlers.APIResponse "Validation Error"
// @Failure 409 {object} handlers.APIResponse "User already exists"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /auth/register [post]
func Register(authService *auth.AuthService, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
commonHandler := NewCommonHandler(logger)
@ -131,8 +149,17 @@ func Register(authService *auth.AuthService, logger *zap.Logger) gin.HandlerFunc
}
// Refresh gère le rafraîchissement d'un access token
// GO-013: Utilise validator centralisé pour validation améliorée
// P0: JSON Hardening - Utilise BindAndValidateJSON pour une gestion robuste des erreurs
// @Summary Refresh Token
// @Description Get a new access token using a refresh token
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body dto.RefreshRequest true "Refresh Token"
// @Success 200 {object} dto.TokenResponse
// @Failure 400 {object} handlers.APIResponse "Validation Error"
// @Failure 401 {object} handlers.APIResponse "Invalid/Expired Refresh Token"
// @Failure 500 {object} handlers.APIResponse "Internal Error"
// @Router /auth/refresh [post]
func Refresh(authService *auth.AuthService, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
commonHandler := NewCommonHandler(logger)
@ -164,7 +191,17 @@ func Refresh(authService *auth.AuthService, logger *zap.Logger) gin.HandlerFunc
}
// Logout gère la déconnexion des utilisateurs
// P0: JSON Hardening - Utilise BindAndValidateJSON pour une gestion robuste des erreurs
// @Summary Logout
// @Description Revoke refresh token and current session
// @Tags Auth
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body object{refresh_token=string} true "Refresh Token to revoke"
// @Success 200 {object} handlers.APIResponse "Success message"
// @Failure 400 {object} handlers.APIResponse "Validation Error"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Router /auth/logout [post]
func Logout(authService *auth.AuthService, sessionService *services.SessionService, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
commonHandler := NewCommonHandler(logger)
@ -208,6 +245,15 @@ func Logout(authService *auth.AuthService, sessionService *services.SessionServi
}
// VerifyEmail gère la vérification de l'email
// @Summary Verify Email
// @Description Verify user email address using a token
// @Tags Auth
// @Accept json
// @Produce json
// @Param token query string true "Verification Token"
// @Success 200 {object} handlers.APIResponse "Success message"
// @Failure 400 {object} handlers.APIResponse "Invalid Token"
// @Router /auth/verify-email [post]
func VerifyEmail(authService *auth.AuthService) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.Query("token")
@ -226,7 +272,15 @@ func VerifyEmail(authService *auth.AuthService) gin.HandlerFunc {
}
// ResendVerification gère la demande de renvoi d'email de vérification
// P0: JSON Hardening - Utilise BindAndValidateJSON pour une gestion robuste des erreurs
// @Summary Resend Verification Email
// @Description Resend the email verification link
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body dto.ResendVerificationRequest true "Email"
// @Success 200 {object} handlers.APIResponse "Success message"
// @Failure 400 {object} handlers.APIResponse "Validation Error"
// @Router /auth/resend-verification [post]
func ResendVerification(authService *auth.AuthService, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
commonHandler := NewCommonHandler(logger)
@ -248,6 +302,15 @@ func ResendVerification(authService *auth.AuthService, logger *zap.Logger) gin.H
}
// CheckUsername vérifie la disponibilité d'un nom d'utilisateur
// @Summary Check Username Availability
// @Description Check if a username is already taken
// @Tags Auth
// @Accept json
// @Produce json
// @Param username query string true "Username to check"
// @Success 200 {object} handlers.APIResponse{data=object{available=boolean,username=string}}
// @Failure 400 {object} handlers.APIResponse "Missing Username"
// @Router /auth/check-username [get]
func CheckUsername(authService *auth.AuthService) gin.HandlerFunc {
return func(c *gin.Context) {
username := c.Query("username")
@ -267,6 +330,15 @@ func CheckUsername(authService *auth.AuthService) gin.HandlerFunc {
}
// GetMe retourne les informations de l'utilisateur connecté
// @Summary Get Current User
// @Description Get profile information of the currently logged-in user
// @Tags Auth
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} handlers.APIResponse{data=object{id=string,email=string,role=string}}
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Router /auth/me [get]
func GetMe() gin.HandlerFunc {
return func(c *gin.Context) {
userID, exists := c.Get("user_id")

View file

@ -24,6 +24,17 @@ func NewChatHandler(chatService *services.ChatService, userService *services.Use
}
}
// GetToken generates a JWT token for the chat service
// @Summary Get Chat Token
// @Description Generate a short-lived token for chat authentication
// @Tags Chat
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} APIResponse{data=object{token=string}}
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 500 {object} APIResponse "Internal Error"
// @Router /chat/token [get]
func (h *ChatHandler) GetToken(c *gin.Context) {
userIDVal, exists := c.Get("user_id")
if !exists {

View file

@ -64,7 +64,18 @@ type ReorderTracksRequest struct {
}
// CreatePlaylist gère la création d'une playlist
// GO-013: Utilise validator centralisé pour validation améliorée
// @Summary Create Playlist
// @Description Create a new playlist
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body CreatePlaylistRequest true "Playlist Metadata"
// @Success 201 {object} APIResponse{data=object{playlist=models.Playlist}}
// @Failure 400 {object} APIResponse "Validation Error"
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 500 {object} APIResponse "Internal Error"
// @Router /playlists [post]
func (h *PlaylistHandler) CreatePlaylist(c *gin.Context) {
userIDVal, exists := c.Get("user_id")
if !exists {
@ -93,6 +104,18 @@ func (h *PlaylistHandler) CreatePlaylist(c *gin.Context) {
}
// GetPlaylists gère la récupération des playlists avec pagination
// @Summary Get Playlists
// @Description Get a paginated list of playlists
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query int false "Page number" default(1)
// @Param limit query int false "Items per page" default(20)
// @Param user_id query string false "Filter by User ID"
// @Success 200 {object} APIResponse{data=object{playlists=[]models.Playlist,pagination=object}}
// @Failure 500 {object} APIResponse "Internal Error"
// @Router /playlists [get]
func (h *PlaylistHandler) GetPlaylists(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
@ -138,6 +161,17 @@ func (h *PlaylistHandler) GetPlaylists(c *gin.Context) {
}
// GetPlaylist gère la récupération d'une playlist
// @Summary Get Playlist by ID
// @Description Get detailed information about a playlist
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist ID"
// @Success 200 {object} APIResponse{data=object{playlist=models.Playlist}}
// @Failure 400 {object} APIResponse "Invalid ID"
// @Failure 404 {object} APIResponse "Playlist not found"
// @Router /playlists/{id} [get]
func (h *PlaylistHandler) GetPlaylist(c *gin.Context) {
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
@ -167,6 +201,20 @@ func (h *PlaylistHandler) GetPlaylist(c *gin.Context) {
}
// UpdatePlaylist gère la mise à jour d'une playlist
// @Summary Update Playlist
// @Description Update playlist metadata
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist ID"
// @Param playlist body UpdatePlaylistRequest true "Playlist Metadata"
// @Success 200 {object} APIResponse{data=object{playlist=models.Playlist}}
// @Failure 400 {object} APIResponse "Validation Error"
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 403 {object} APIResponse "Forbidden"
// @Failure 404 {object} APIResponse "Playlist not found"
// @Router /playlists/{id} [put]
func (h *PlaylistHandler) UpdatePlaylist(c *gin.Context) {
userIDVal, exists := c.Get("user_id")
if !exists {
@ -210,6 +258,18 @@ func (h *PlaylistHandler) UpdatePlaylist(c *gin.Context) {
}
// DeletePlaylist gère la suppression d'une playlist
// @Summary Delete Playlist
// @Description Permanently delete a playlist
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist ID"
// @Success 200 {object} APIResponse{data=object{message=string}}
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 403 {object} APIResponse "Forbidden"
// @Failure 404 {object} APIResponse "Playlist not found"
// @Router /playlists/{id} [delete]
func (h *PlaylistHandler) DeletePlaylist(c *gin.Context) {
userIDVal, exists := c.Get("user_id")
if !exists {
@ -246,6 +306,18 @@ func (h *PlaylistHandler) DeletePlaylist(c *gin.Context) {
}
// AddTrack gère l'ajout d'un track à une playlist
// @Summary Add Track to Playlist
// @Description Add a track to the playlist
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist ID"
// @Param trackId body object{track_id=string} true "Track ID (in body)"
// @Success 200 {object} APIResponse{data=object{message=string}}
// @Failure 400 {object} APIResponse "Track already present or invalid ID"
// @Failure 404 {object} APIResponse "Playlist or Track not found"
// @Router /playlists/{id}/tracks [post]
func (h *PlaylistHandler) AddTrack(c *gin.Context) {
userIDVal, exists := c.Get("user_id")
if !exists {
@ -297,6 +369,17 @@ func (h *PlaylistHandler) AddTrack(c *gin.Context) {
}
// RemoveTrack gère la suppression d'un track d'une playlist
// @Summary Remove Track from Playlist
// @Description Remove a track from the playlist
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist ID"
// @Param trackId path string true "Track ID"
// @Success 200 {object} APIResponse{data=object{message=string}}
// @Failure 404 {object} APIResponse "Playlist or Track not found"
// @Router /playlists/{id}/tracks/{trackId} [delete]
func (h *PlaylistHandler) RemoveTrack(c *gin.Context) {
userIDVal, exists := c.Get("user_id")
if !exists {
@ -344,6 +427,17 @@ func (h *PlaylistHandler) RemoveTrack(c *gin.Context) {
}
// ReorderTracks gère la réorganisation des tracks d'une playlist
// @Summary Reorder Tracks
// @Description Reorder tracks in the playlist
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist ID"
// @Param order body ReorderTracksRequest true "New Track Order"
// @Success 200 {object} APIResponse{data=object{message=string}}
// @Failure 400 {object} APIResponse "Validation Error"
// @Router /playlists/{id}/tracks/reorder [put]
func (h *PlaylistHandler) ReorderTracks(c *gin.Context) {
userIDVal, exists := c.Get("user_id")
if !exists {

View file

@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/services"
"veza-backend-api/internal/types"
)
@ -26,11 +27,21 @@ func NewProfileHandler(userService *services.UserService, logger *zap.Logger) *P
}
// GetProfile retrieves a public user profile by ID
// @Summary Get Profile by ID
// @Description Get public profile information for a user
// @Tags User
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} handlers.APIResponse{data=object{profile=object}}
// @Failure 400 {object} handlers.APIResponse "Invalid ID"
// @Failure 404 {object} handlers.APIResponse "User not found"
// @Router /users/{id} [get]
func (h *ProfileHandler) GetProfile(c *gin.Context) {
userIDStr := c.Param("id")
userID, err := uuid.Parse(userIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid user id"))
return
}
@ -45,18 +56,28 @@ func (h *ProfileHandler) GetProfile(c *gin.Context) {
// Get user profile with privacy check
profile, err := h.userService.GetProfile(userID, requesterID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeNotFound, "user not found"))
return
}
c.JSON(http.StatusOK, gin.H{"profile": profile})
RespondSuccess(c, http.StatusOK, gin.H{"profile": profile})
}
// GetProfileByUsername retrieves a public profile by username
// @Summary Get Profile by Username
// @Description Get public profile information for a user by username
// @Tags User
// @Accept json
// @Produce json
// @Param username path string true "Username"
// @Success 200 {object} handlers.APIResponse{data=object{profile=object}}
// @Failure 400 {object} handlers.APIResponse "Missing username"
// @Failure 404 {object} handlers.APIResponse "User not found"
// @Router /users/by-username/{username} [get]
func (h *ProfileHandler) GetProfileByUsername(c *gin.Context) {
username := c.Param("username")
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "username required"})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "username required"))
return
}
@ -71,20 +92,31 @@ func (h *ProfileHandler) GetProfileByUsername(c *gin.Context) {
// Get profile with privacy check
profile, err := h.userService.GetProfileByUsername(username, requesterID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeNotFound, "user not found"))
return
}
c.JSON(http.StatusOK, gin.H{"profile": profile})
RespondSuccess(c, http.StatusOK, gin.H{"profile": profile})
}
// GetProfileCompletion retrieves the profile completion status
// T0220: Returns percentage and missing fields
// @Summary Get Profile Completion
// @Description Get profile completion percentage and missing fields
// @Tags User
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} handlers.APIResponse{data=object}
// @Failure 400 {object} handlers.APIResponse "Invalid ID"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Forbidden"
// @Router /users/{id}/completion [get]
func (h *ProfileHandler) GetProfileCompletion(c *gin.Context) {
userIDStr := c.Param("id")
userID, err := uuid.Parse(userIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid user id"))
return
}
@ -94,28 +126,28 @@ func (h *ProfileHandler) GetProfileCompletion(c *gin.Context) {
if reqUUID, ok := reqID.(uuid.UUID); ok {
authenticatedUserID = reqUUID
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
RespondWithAppError(c, apperrors.NewUnauthorizedError("user not authenticated"))
return
}
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
RespondWithAppError(c, apperrors.NewUnauthorizedError("user not authenticated"))
return
}
// Verify that user_id corresponds to authenticated user
if userID != authenticatedUserID {
c.JSON(http.StatusForbidden, gin.H{"error": "cannot access other user's profile completion"})
RespondWithAppError(c, apperrors.NewForbiddenError("cannot access other user's profile completion"))
return
}
// Calculate profile completion
completion, err := h.userService.CalculateProfileCompletion(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to calculate profile completion"})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "failed to calculate profile completion"))
return
}
c.JSON(http.StatusOK, completion)
RespondSuccess(c, http.StatusOK, completion)
}
// UpdateProfileRequest represents the request body for updating a user profile
@ -130,11 +162,24 @@ type UpdateProfileRequest struct {
}
// UpdateProfile updates a user profile
// @Summary Update Profile
// @Description Update user profile details
// @Tags User
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "User ID"
// @Param profile body UpdateProfileRequest true "Profile Data"
// @Success 200 {object} handlers.APIResponse{data=object{profile=object}}
// @Failure 400 {object} handlers.APIResponse "Validation Error"
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Forbidden"
// @Router /users/{id} [put]
func (h *ProfileHandler) UpdateProfile(c *gin.Context) {
userIDStr := c.Param("id")
userID, err := uuid.Parse(userIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid user id"))
return
}
@ -144,17 +189,17 @@ func (h *ProfileHandler) UpdateProfile(c *gin.Context) {
if reqUUID, ok := reqID.(uuid.UUID); ok {
authenticatedUserID = reqUUID
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
RespondWithAppError(c, apperrors.NewUnauthorizedError("user not authenticated"))
return
}
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
RespondWithAppError(c, apperrors.NewUnauthorizedError("user not authenticated"))
return
}
// Verify that user_id corresponds to authenticated user
if userID != authenticatedUserID {
c.JSON(http.StatusForbidden, gin.H{"error": "cannot update other user's profile"})
RespondWithAppError(c, apperrors.NewForbiddenError("cannot update other user's profile"))
return
}
@ -168,24 +213,24 @@ func (h *ProfileHandler) UpdateProfile(c *gin.Context) {
if req.Username != "" {
// Validate username format (alphanumeric + underscore, 3-30 chars)
if !isValidUsername(req.Username) {
c.JSON(http.StatusBadRequest, gin.H{"error": "username must be 3-30 characters, alphanumeric and underscore only"})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "username must be 3-30 characters, alphanumeric and underscore only"))
return
}
// Validate username uniqueness if modified
if err := h.userService.ValidateUsername(userID, req.Username); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, err.Error()))
return
}
// Check if username can be modified (once per month)
canChange, err := h.userService.CanChangeUsername(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check username change eligibility"})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "failed to check username change eligibility"))
return
}
if !canChange {
c.JSON(http.StatusBadRequest, gin.H{"error": "username can only be changed once per month"})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "username can only be changed once per month"))
return
}
}
@ -194,7 +239,7 @@ func (h *ProfileHandler) UpdateProfile(c *gin.Context) {
if req.Birthdate != "" {
birthdate, err := time.Parse("2006-01-02", req.Birthdate)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid birthdate format, expected YYYY-MM-DD"})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid birthdate format, expected YYYY-MM-DD"))
return
}
@ -202,7 +247,7 @@ func (h *ProfileHandler) UpdateProfile(c *gin.Context) {
age := time.Since(birthdate)
minAge := 13 * 365 * 24 * time.Hour // 13 years
if age < minAge {
c.JSON(http.StatusBadRequest, gin.H{"error": "user must be at least 13 years old"})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "user must be at least 13 years old"))
return
}
}
@ -226,11 +271,11 @@ func (h *ProfileHandler) UpdateProfile(c *gin.Context) {
// Update profile using the new UpdateProfile method
profile, err := h.userService.UpdateProfile(userID, serviceReq)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update profile"})
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "failed to update profile"))
return
}
c.JSON(http.StatusOK, gin.H{"profile": profile})
RespondSuccess(c, http.StatusOK, gin.H{"profile": profile})
}
// isValidUsername validates username format (alphanumeric + underscore, 3-30 chars)

View file

@ -6,6 +6,13 @@ import (
"github.com/gin-gonic/gin"
)
// APIResponse is the unified response envelope
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error interface{} `json:"error,omitempty"`
}
// Success sends a successful JSON response
func Success(c *gin.Context, data interface{}, message ...string) {
response := gin.H{