package controllers import ( "errors" "strconv" "time" "github.com/gofiber/fiber/v3" "github.com/google/uuid" "gorm.io/gorm" "server/internal/auth" "server/internal/models" ) type UserController struct{} func NewUserController() *UserController { return &UserController{} } // Typescript: interface type UpdateUserRequest struct { Name string `json:"name" validate:"required,min=1,max=255"` Email string `json:"email" validate:"required,email"` Password string `json:"password" validate:"omitempty,min=8,max=128"` Roles models.UserRoles `json:"roles"` Status models.UserStatus `json:"status"` Types models.UserTypes `json:"types"` Avatar *string `json:"avatar"` Details *models.UserDetailsShort `json:"details"` Preferences *models.UserPreferencesShort `json:"preferences"` } // GetUser returns a single user by UUID. func (uc *UserController) GetUser(c fiber.Ctx) error { user, err := loadUserByUUID(c) if err != nil { return err } return c.JSON(success(models.ToUserProfile(user))) } // CreateUser creates a user together with optional details and preferences. func (uc *UserController) CreateUser(c fiber.Ctx) error { var req models.UserCreateInput if err := c.Bind().Body(&req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "invalid payload") } if err := validateStruct(&req); err != nil { return err } db, err := dbFromCtx(c) if err != nil { return err } var existing models.User if err := db.Where("email = ?", req.Email).First(&existing).Error; err == nil { return fiber.NewError(fiber.StatusConflict, "user already exists") } else if !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, "failed to check user") } hashedPassword, err := auth.HashPassword(req.Password) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password") } now := time.Now().UTC() user := models.User{ Email: req.Email, Name: req.Name, Password: hashedPassword, Roles: func() models.UserRoles { if len(req.Roles) == 0 { return models.UserRoles{"user"} } return req.Roles }(), Status: func() models.UserStatus { if req.Status == "" { return models.UserStatusPending } return req.Status }(), Types: func() models.UserTypes { if len(req.Types) == 0 { return models.UserTypes{"internal"} } return req.Types }(), Avatar: req.Avatar, UUID: uuid.NewString(), Details: toUserDetails(req.Details), Preferences: toUserPreferences(req.Preferences), CreatedAt: now, UpdatedAt: now, } if err := db.Create(&user).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to create user") } if err := db.Preload("Details").Preload("Preferences").First(&user, user.ID).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to reload user") } return c.Status(fiber.StatusCreated).JSON(success(models.ToUserProfile(&user))) } // UpdateUser replaces user fields and synchronizes details/preferences. func (uc *UserController) UpdateUser(c fiber.Ctx) error { var req UpdateUserRequest if err := c.Bind().Body(&req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "invalid payload") } if err := validateStruct(&req); err != nil { return err } db, err := dbFromCtx(c) if err != nil { return err } user, err := loadUserByUUID(c) if err != nil { return err } if req.Email != user.Email { var existing models.User if err := db.Select("id").Where("email = ?", req.Email).First(&existing).Error; err == nil && existing.ID != user.ID { return fiber.NewError(fiber.StatusConflict, "user already exists") } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, "failed to check user") } } now := time.Now().UTC() user.Name = req.Name user.Email = req.Email user.Avatar = req.Avatar user.UpdatedAt = now if req.Status != "" { user.Status = req.Status } if len(req.Roles) > 0 { user.Roles = req.Roles } if len(req.Types) > 0 { user.Types = req.Types } if err := db.Transaction(func(tx *gorm.DB) error { if err := tx.Save(user).Error; err != nil { return err } if err := syncUserDetails(tx, user.ID, req.Details); err != nil { return err } if err := syncUserPreferences(tx, user.ID, req.Preferences); err != nil { return err } return nil }); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to update user") } if err := db.Preload("Details").Preload("Preferences").First(user, user.ID).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to reload user") } return c.JSON(success(models.ToUserProfile(user))) } // DeleteUser removes a user and linked details/preferences through cascading delete rules. func (uc *UserController) DeleteUser(c fiber.Ctx) error { db, err := dbFromCtx(c) if err != nil { return err } user, err := loadUserByID(c) if err != nil { return err } if err := db.Transaction(func(tx *gorm.DB) error { if err := tx.Where("user_id = ?", user.ID).Delete(&models.UserDetails{}).Error; err != nil { return err } if err := tx.Where("user_id = ?", user.ID).Delete(&models.UserPreferences{}).Error; err != nil { return err } return tx.Delete(user).Error }); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to delete user") } return c.JSON(success(SimpleResponse{Message: "user deleted"})) } func loadUserByID(c fiber.Ctx) (*models.User, error) { id, err := strconv.Atoi(c.Params("id")) if err != nil || id <= 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "invalid user id") } db, err := dbFromCtx(c) if err != nil { return nil, err } var user models.User if err := db.Preload("Details").Preload("Preferences").First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "user not found") } return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load user") } return &user, nil } func loadUserByUUID(c fiber.Ctx) (*models.User, error) { uuid := c.Params("uuid") if uuid == "" { return nil, fiber.NewError(fiber.StatusBadRequest, "invalid user uuid") } db, err := dbFromCtx(c) if err != nil { return nil, err } var user models.User if err := db.Preload("Details").Preload("Preferences").First(&user, "uuid = ?", uuid).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "user not found") } return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load user") } return &user, nil } func syncUserDetails(tx *gorm.DB, userID int, input *models.UserDetailsShort) error { if input == nil { return tx.Where("user_id = ?", userID).Delete(&models.UserDetails{}).Error } var details models.UserDetails if err := tx.Where("user_id = ?", userID).First(&details).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { details = models.UserDetails{UserID: userID} } else { return err } } details.Title = input.Title details.FirstName = input.FirstName details.LastName = input.LastName details.Address = input.Address details.City = input.City details.ZipCode = input.ZipCode details.Country = input.Country details.Phone = input.Phone if details.ID == 0 { return tx.Create(&details).Error } return tx.Save(&details).Error } func syncUserPreferences(tx *gorm.DB, userID int, input *models.UserPreferencesShort) error { if input == nil { return tx.Where("user_id = ?", userID).Delete(&models.UserPreferences{}).Error } var preferences models.UserPreferences if err := tx.Where("user_id = ?", userID).First(&preferences).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { preferences = models.UserPreferences{UserID: userID} } else { return err } } preferences.UseIdle = input.UseIdle preferences.IdleTimeout = input.IdleTimeout preferences.UseIdlePassword = input.UseIdlePassword preferences.IdlePin = input.IdlePin preferences.UseDirectLogin = input.UseDirectLogin preferences.UseQuadcodeLogin = input.UseQuadcodeLogin preferences.SendNoticesMail = input.SendNoticesMail preferences.Language = input.Language if preferences.ID == 0 { return tx.Create(&preferences).Error } return tx.Save(&preferences).Error }