veza/veza-backend-api/internal/api/user/handler.go

400 lines
10 KiB
Go

// veza-backend-api/internal/api/user/handler.go
package user
import (
"context"
"net/http"
"strconv"
"time"
"veza-backend-api/internal/common"
"veza-backend-api/internal/response"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid" // Added import
)
// UserServiceInterface defines the interface for user operations
// This allows for easier testing with mocks
type UserServiceInterface interface {
GetUserByID(userID uuid.UUID) (*UserResponse, error)
GetUsers(page, limit int, search string) ([]UserResponse, int, error)
UpdateUser(userID uuid.UUID, req UpdateUserRequest) (*UserResponse, error)
ChangePassword(userID uuid.UUID, currentPassword, newPassword string) error
GetUserPreferences(userID uuid.UUID) (*UserPreferencesResponse, error)
UpdateUserPreferences(userID uuid.UUID, req UserPreferencesRequest) (*UserPreferencesResponse, error)
DeleteAccount(userID uuid.UUID, password, reason string) error
RecoverAccount(email, password string) error
RequestDataDeletion(userID uuid.UUID, password, reason string) error
GetAccountStatus(userID uuid.UUID) (*AccountStatus, error)
}
// DataExportServiceInterface defines the interface for data export operations
type DataExportServiceInterface interface {
ExportUserDataAsJSON(ctx context.Context, userID uuid.UUID) ([]byte, error)
}
type Handler struct {
service UserServiceInterface
dataExportService DataExportServiceInterface // BE-SVC-022: Service d'export de données
}
func NewHandler(service *Service, dataExportService *services.DataExportService) *Handler {
return &Handler{
service: service,
dataExportService: dataExportService,
}
}
// NewHandlerWithInterfaces creates a new handler with interfaces (for testing)
func NewHandlerWithInterfaces(service UserServiceInterface, dataExportService DataExportServiceInterface) *Handler {
return &Handler{
service: service,
dataExportService: dataExportService,
}
}
// GetMe récupère le profil de l'utilisateur connecté
func (h *Handler) GetMe(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Unauthorized(c, "User ID not found")
return
}
user, err := h.service.GetUserByID(userID)
if err != nil {
response.NotFound(c, "User not found")
return
}
response.Success(c, user)
}
// UpdateMe met à jour le profil de l'utilisateur connecté
func (h *Handler) UpdateMe(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Unauthorized(c, "User ID not found")
return
}
var req UpdateUserRequest
if !common.BindAndValidate(c, &req) {
return
}
user, err := h.service.UpdateUser(userID, req)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Success(c, user)
}
// ChangePassword change le mot de passe de l'utilisateur
func (h *Handler) ChangePassword(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Unauthorized(c, "User ID not found")
return
}
var req struct {
CurrentPassword string `json:"current_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=8"`
}
if !common.BindAndValidate(c, &req) {
return
}
err := h.service.ChangePassword(userID, req.CurrentPassword, req.NewPassword)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Success(c, nil)
}
// GetUsers liste tous les utilisateurs
func (h *Handler) GetUsers(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
search := c.Query("search")
users, total, err := h.service.GetUsers(page, limit, search)
if err != nil {
response.InternalServerError(c, "Failed to retrieve users")
return
}
response.Success(c, gin.H{
"data": users,
"pagination": gin.H{
"page": page,
"limit": limit,
"total": total,
"total_pages": (total + limit - 1) / limit,
},
})
}
// GetUsersExceptMe liste tous les utilisateurs sauf l'utilisateur connecté
func (h *Handler) GetUsersExceptMe(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Unauthorized(c, "User ID not found")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
search := c.Query("search")
// Ajouter le filtre pour exclure l'utilisateur actuel
users, total, err := h.service.GetUsers(page, limit, search)
if err != nil {
response.InternalServerError(c, "Failed to retrieve users")
return
}
// Filtrer l'utilisateur connecté
filteredUsers := []UserResponse{}
for _, user := range users {
if user.ID != userID { // Direct comparison of uuid.UUID
filteredUsers = append(filteredUsers, user)
}
}
response.Success(c, gin.H{
"data": filteredUsers,
"pagination": gin.H{
"page": page,
"limit": limit,
"total": total - 1, // -1 car on exclut l'utilisateur connecté
"total_pages": (total + limit - 2) / limit,
},
})
}
// SearchUsers recherche des utilisateurs
func (h *Handler) SearchUsers(c *gin.Context) {
query := c.Query("q")
if query == "" {
response.BadRequest(c, "Query parameter 'q' is required")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
users, total, err := h.service.GetUsers(page, limit, query)
if err != nil {
response.InternalServerError(c, "Failed to search users")
return
}
response.Success(c, gin.H{
"data": users,
"pagination": gin.H{
"page": page,
"limit": limit,
"total": total,
"total_pages": (total + limit - 1) / limit,
},
})
}
func (h *Handler) GetUserAvatar(c *gin.Context) {
idStr := c.Param("id")
userID, err := uuid.Parse(idStr)
if err != nil {
response.BadRequest(c, "Invalid user ID")
return
}
user, err := h.service.GetUserByID(userID)
if err != nil {
response.NotFound(c, "User not found")
return
}
// ✅ Correct way to handle sql.NullString
if !user.Avatar.Valid || user.Avatar.String == "" {
response.NotFound(c, "No avatar found")
return
}
// Rediriger vers l'URL de l'avatar ou servir le fichier
c.Redirect(http.StatusFound, user.Avatar.String)
}
// GetPreferences récupère les préférences de l'utilisateur connecté
func (h *Handler) GetPreferences(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Unauthorized(c, "User ID not found")
return
}
preferences, err := h.service.GetUserPreferences(userID)
if err != nil {
response.InternalServerError(c, "Failed to get preferences")
return
}
response.Success(c, preferences)
}
// UpdatePreferences met à jour les préférences de l'utilisateur connecté
func (h *Handler) UpdatePreferences(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Unauthorized(c, "User ID not found")
return
}
var req UserPreferencesRequest
if !common.BindAndValidate(c, &req) {
return
}
preferences, err := h.service.UpdateUserPreferences(userID, req)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Success(c, preferences)
}
// DeleteAccount supprime le compte de l'utilisateur (soft delete)
func (h *Handler) DeleteAccount(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Unauthorized(c, "User ID not found")
return
}
var req struct {
Password string `json:"password" binding:"required"`
Reason string `json:"reason"`
ConfirmText string `json:"confirm_text" binding:"required"`
}
if !common.BindAndValidate(c, &req) {
return
}
// Vérifier le texte de confirmation
if req.ConfirmText != "DELETE" {
response.BadRequest(c, "Confirmation text must be 'DELETE'")
return
}
err := h.service.DeleteAccount(userID, req.Password, req.Reason)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Success(c, nil)
}
// RecoverAccount récupère un compte supprimé
func (h *Handler) RecoverAccount(c *gin.Context) {
var req struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
if !common.BindAndValidate(c, &req) {
return
}
err := h.service.RecoverAccount(req.Email, req.Password)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Success(c, nil)
}
// ExportData exporte les données de l'utilisateur (RGPD) - BE-SVC-022
func (h *Handler) ExportData(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Unauthorized(c, "User ID not found")
return
}
// Utiliser le nouveau service d'export de données
if h.dataExportService == nil {
response.InternalServerError(c, "Data export service not available")
return
}
// Exporter les données au format JSON
jsonData, err := h.dataExportService.ExportUserDataAsJSON(c.Request.Context(), userID)
if err != nil {
response.InternalServerError(c, "Failed to export user data: "+err.Error())
return
}
// Définir les headers pour le téléchargement
filename := "veza-data-export-" + time.Now().Format("2006-01-02T15-04-05") + ".json"
c.Header("Content-Type", "application/json")
c.Header("Content-Disposition", `attachment; filename="`+filename+`"`)
c.Header("Content-Length", strconv.Itoa(len(jsonData)))
// Envoyer le fichier JSON
c.Data(http.StatusOK, "application/json", jsonData)
}
// RequestDataDeletion demande la suppression définitive des données (RGPD)
func (h *Handler) RequestDataDeletion(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Unauthorized(c, "User ID not found")
return
}
var req struct {
Password string `json:"password" binding:"required"`
Reason string `json:"reason"`
}
if !common.BindAndValidate(c, &req) {
return
}
err := h.service.RequestDataDeletion(userID, req.Password, req.Reason)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Success(c, nil)
}
// GetAccountStatus récupère le statut du compte
func (h *Handler) GetAccountStatus(c *gin.Context) {
userID, exists := common.GetUserIDFromContext(c)
if !exists {
response.Unauthorized(c, "User ID not found")
return
}
status, err := h.service.GetAccountStatus(userID)
if err != nil {
response.InternalServerError(c, "Failed to get account status")
return
}
response.Success(c, status)
}