go-quasar-partial-ssr/backend/internal/http/controllers/user.go

307 lines
8.5 KiB
Go

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
}