package controllers import ( "context" "errors" "fmt" "net/http" "strings" "clapclap/internal/apperr" "clapclap/internal/auth/tfa" "clapclap/internal/avatar" "clapclap/internal/dtos" "clapclap/internal/env" "clapclap/internal/ginctx" "clapclap/internal/models" "clapclap/internal/upload" "clapclap/internal/utils" "clapclap/internal/uuid" "clapclap/internal/validate" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // GetAllUsers godoc // @Summary Get all users // @Description Retrieves a list of all registered users. Supports preloading relational data. // @Tags users // @Produce json // @Param preload query string false "Relation to preload" Enums(PreferredGenres, OwnedTracks, Comments) // @Success 200 {array} models.User // @Failure 500 {object} swaggError500 // @Router /users [get] func GetAllUsers(c *gin.Context) { repo, _ := ginctx.GetRepo(c, "repo") var users []models.User preloads := utils.GetReqPreloads(c.Request, utils.GetUserPreloads()) if err := repo.GetAllUsers(&users, preloads); err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err)) return } c.JSON(http.StatusOK, users) } // GetUserByID godoc // @Summary Get a user by ID // @Description Retrieves a user's details using their UUID. Supports preloading relational data. // @Tags users // @Produce json // @Param id path string true "User UUID" // @Param preload query string false "Relation to preload" Enums(PreferredGenres, OwnedTracks, Comments) // @Success 200 {object} models.User // @Failure 404 {object} swaggError404 // @Router /users/id/{id} [get] func GetUserByID(c *gin.Context) { repo, _ := ginctx.GetRepo(c, "repo") id, err := uuid.Parse(c.Param("id")) if err != nil { apperr.AbortWithError(c, apperr.NewBadRequestError(err, "Invalid user ID format")) return } preloads := utils.GetReqPreloads(c.Request, utils.GetUserPreloads()) user, err := repo.GetUserByID(id, preloads) if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "User not found")) return } c.JSON(http.StatusOK, user) } // GetUserByName godoc // @Summary Get a user by username // @Description Retrieves a user's details using their exact username. Supports preloading relational data. // @Tags users // @Produce json // @Param username path string true "Username" // @Param preload query string false "Relation to preload" Enums(PreferredGenres, OwnedTracks, Comments) // @Success 200 {object} models.User // @Failure 404 {object} swaggError404 // @Router /users/{username} [get] func GetUserByName(c *gin.Context) { repo, _ := ginctx.GetRepo(c, "repo") username := c.Param("username") preloads := utils.GetReqPreloads(c.Request, utils.GetUserPreloads()) if strings.HasSuffix(username, "@ft") { user, err := repo.Find42UserByUsername(username, preloads) if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "User not found")) return } c.JSON(http.StatusOK, *user) return } user, err := repo.FindUserByUsername(username, preloads) if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "User not found")) return } c.JSON(http.StatusOK, *user) } func createAvatarInS3(ctx context.Context, s3Client *s3.Client, userUUID uuid.UUID) (string, error) { avatarBuf, err := avatar.Generate(userUUID, 600, 5) if err != nil { return "", err } bucket := env.C.BUCKET_NAME objectKey := fmt.Sprintf("avatars_generated/%s.png", userUUID.String()) // Upload the buffer to S3 err = upload.UploadBufferToS3(ctx, s3Client, bucket, objectKey, avatarBuf, "image/png") if err != nil { return "", err } // Construct the final URL using your existing logic pattern return fmt.Sprintf("%s/%s/%s", strings.TrimRight(env.C.AWS_ENDPOINT_URL_S3, "/"), bucket, objectKey), nil } // CreateUser godoc // @Summary Create a new user // @Description Registers a new user in the database // @Tags users // @Accept json // @Produce json // @Param user body dtos.SignUpRequest true "User Registration Info" // @Success 201 {object} models.User // @Failure 422 {object} swaggError422 // @Failure 500 {object} swaggError500 // @Router /auth/sign-up [post] func CreateUser(c *gin.Context) { s3ic, _ := ginctx.GetS3(c, "s3-internal") repo, _ := ginctx.GetRepo(c, "repo") req, _ := ginctx.GetDto[dtos.SignUpRequest](c) if !validate.Username(req.Username) { apperr.AbortWithError(c, apperr.NewBadRequestError(nil, "Username does not match the required format")) return } if !validate.Password(req.Password) { apperr.AbortWithError(c, apperr.NewBadRequestError(nil, "Password does not match the required format")) return } userUUID := uuid.New() var err error if req.Avatar == "" { req.Avatar, err = createAvatarInS3(c.Request.Context(), s3ic, userUUID) if err != nil { apperr.AbortWithError(c, apperr.NewInternalServerError(err, "Failed to generate avatar")) return } } isVerified := false userModel := models.User{ Base: models.Base{ID: userUUID}, Username: req.Username, Email: req.Email, Avatar: req.Avatar, IsFt: false, IsVerified: isVerified, } err = repo.CreateUser(&userModel, req.Password) if errors.Is(err, gorm.ErrDuplicatedKey) { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Username or Email already taken.")) return } else if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Failed to create user")) return } go func(uID uuid.UUID, uEmail string) { defer func() { if r := recover(); r != nil { utils.LogError(fmt.Errorf("Recovered from panic in email background task: %v", r)) } }() if err := tfa.DispatchVerificationEmail(uID, uEmail); err != nil { utils.LogError(fmt.Errorf("Background Task Failed: %v", err)) } }(userModel.ID, userModel.Email) c.JSON(http.StatusCreated, userModel) } // DeleteUserByID godoc // @Summary Delete a user by ID // @Description Deletes a user from the database using their UUID // @Tags users // @Produce json // @Param id path string true "User UUID" // @Success 200 {object} swaggMessageResponse // @Failure 401 {object} swaggError401 // @Failure 403 {object} swaggError403 // @Failure 500 {object} swaggError500 // @Security BearerAuth // @Router /users/id/{id} [delete] func DeleteUserByID(c *gin.Context) { repo, _ := ginctx.GetRepo(c, "repo") userID, _ := ginctx.GetUUID(c, "userID") id := c.Param("id") if id != userID.String() { apperr.AbortWithError(c, apperr.NewForbiddenError(nil, "You can only delete your own profile")) return } if err := repo.DeleteUser(id); err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Failed to delete user")) return } c.JSON(http.StatusOK, gin.H{"message": "User successfully deleted"}) } // DeleteUserByName godoc // @Summary Delete a user by username // @Description Deletes a user from the database using their username // @Tags users // @Produce json // @Param username path string true "Username" // @Success 200 {object} swaggMessageResponse // @Failure 401 {object} swaggError401 // @Failure 403 {object} swaggError403 // @Failure 404 {object} swaggError404 // @Failure 500 {object} swaggError500 // @Security BearerAuth // @Router /users/{username} [delete] func DeleteUserByName(c *gin.Context) { userID, _ := ginctx.GetUUID(c, "userID") repo, _ := ginctx.GetRepo(c, "repo") username := c.Param("username") user, err := repo.FindUserByUsername(username, nil) if err != nil { user, err = repo.Find42UserByUsername(username, nil) if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "User not found")) } return } if user.ID != userID { apperr.AbortWithError(c, apperr.NewForbiddenError(nil, "You can only delete your own profile")) return } if err = repo.DeleteUserByUsername(username); err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Failed to delete user")) return } c.JSON(http.StatusOK, gin.H{"message": "User successfully deleted"}) } // UpdateUser godoc // @Summary Update user profile // @Description Updates the authenticated user's username or avatar. // @Tags users // @Accept json // @Produce json // @Param request body dtos.UpdateUserRequest true "Update payload" // @Success 200 {object} swaggMessageResponse // @Failure 401 {object} swaggError401 // @Failure 404 {object} swaggError404 // @Failure 422 {object} swaggError422 // @Failure 500 {object} swaggError500 // @Security BearerAuth // @Router /users [put] func UpdateUser(c *gin.Context) { userUUID, _ := ginctx.GetUUID(c, "userID") repo, _ := ginctx.GetRepo(c, "repo") req, _ := ginctx.GetDto[dtos.UpdateUserRequest](c) user, err := repo.GetUserByID(userUUID, nil) if err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "User not found")) return } if user.IsFt && user.Username != req.Username { apperr.AbortWithError(c, apperr.NewForbiddenError(nil, "@ft users are not allowed to change their username")) return } if !user.IsFt && !validate.Username(req.Username) { apperr.AbortWithError(c, apperr.NewBadRequestError(nil, "Username is not valid")) return } userModel := models.User{ Base: models.Base{ID: userUUID}, Username: req.Username, Avatar: req.Avatar, } if err = repo.UpdateUser(&userModel); err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Failed to update user")) return } c.JSON(http.StatusOK, gin.H{"message": "User successfully updated"}) } // ResetAvatar godoc // @Summary Reset user avatar // @Description Deletes the user's current avatar from S3 and replaces it with a freshly generated deterministic avatar. // @Tags users // @Produce json // @Success 200 {object} dtos.AvatarResetResponse // @Failure 401 {object} swaggError401 // @Failure 500 {object} swaggError500 // @Security BearerAuth // @Router /users/avatar [delete] func ResetAvatar(c *gin.Context) { userID, _ := ginctx.GetUUID(c, "userID") repo, _ := ginctx.GetRepo(c, "repo") s3ic, _ := ginctx.GetS3(c, "s3-internal") // NOTE: (loic) I guess we wont even delete that old avatar newAvatarUrl, err := createAvatarInS3(c.Request.Context(), s3ic, userID) if err != nil { apperr.AbortWithError(c, apperr.NewInternalServerError(err, "Failed to generate new avatar")) return } userUpdate := models.User{ Base: models.Base{ID: userID}, Avatar: newAvatarUrl, } if err := repo.UpdateUser(&userUpdate); err != nil { apperr.AbortWithError(c, apperr.NewDatabaseError(err, "Failed to save new avatar to database")) return } c.JSON(http.StatusOK, dtos.AvatarResetResponse{ Message: "Avatar successfully reset", Avatar: newAvatarUrl, }) }