diff --git a/backend/data/data.db b/backend/data/data.db index b5b6544..1eb939c 100644 Binary files a/backend/data/data.db and b/backend/data/data.db differ diff --git a/backend/internal/admin/controller.go b/backend/internal/admin/controller.go index 9c01c68..cc38e60 100644 --- a/backend/internal/admin/controller.go +++ b/backend/internal/admin/controller.go @@ -115,3 +115,65 @@ func (ac *AdminController) BlockUser(c fiber.Ctx) error { return c.JSON(responses.Success(u)) } + +func (ac *AdminController) UpdateUser(c fiber.Ctx) error { + var req users.UpdateUserRequest + if err := c.Bind().Body(&req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "invalid payload") + } + if err := validation.ValidateStruct(&req); err != nil { + return err + } + + user, err := users.UpdateUser(req) + if err != nil { + return err + } + + return c.JSON(responses.Success(user)) + +} + +// UpdateUserDetails replaces user fields and synchronizes details/preferences. +func (ac *AdminController) UpdateUserDetails(c fiber.Ctx) error { + var req users.UpdateUserDetailsRequest + if err := c.Bind().Body(&req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "invalid payload") + } + if err := validation.ValidateStruct(&req); err != nil { + return err + } + err := users.UpdateUserDetails(req) + if err != nil { + return err + } + + user, err := users.GetUserByID(req.UserID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "invalid user id") + } + + return c.JSON(responses.Success(user)) +} + +// UpdateUserPreferences replaces user fields and synchronizes details/preferences. +func (ac *AdminController) UpdateUserPreferences(c fiber.Ctx) error { + var req users.UpdateUserPreferencesRequest + if err := c.Bind().Body(&req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "invalid payload") + } + if err := validation.ValidateStruct(&req); err != nil { + return err + } + err := users.UpdateUserPreferences(req) + if err != nil { + return err + } + + user, err := users.GetUserByID(req.UserID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "invalid user id") + } + + return c.JSON(responses.Success(user)) +} diff --git a/backend/internal/admin/routes.go b/backend/internal/admin/routes.go index 2ab7a55..af6fdf4 100644 --- a/backend/internal/admin/routes.go +++ b/backend/internal/admin/routes.go @@ -1,6 +1,8 @@ package admin import ( + "server/internal/auth" + "github.com/gofiber/fiber/v3" ) @@ -8,9 +10,27 @@ func RegisterAdminRoutes(app fiber.Router) { adminController := NewAdminController() // Typescript: TSEndpoint= path=/api/admin/users; name=listUsers; method=POST; request=admin.ListUsersRequest; response=admin.ListUsersResponse - app.Post("/admin/users", adminController.ListUsers) + app.Post("/admin/users", func(c fiber.Ctx) error { + return auth.IsPermitted(c, auth.AdminPermission|auth.SuperAdminPermission) + }, adminController.ListUsers) // Typescript: TSEndpoint= path=/api/admin/users/block; name=blockUser; method=PUT; request=admin.BlockUserRequest; response=users.User - app.Put("/admin/users/block", adminController.BlockUser) + app.Put("/admin/users/block", func(c fiber.Ctx) error { + return auth.IsPermitted(c, auth.AdminPermission|auth.SuperAdminPermission) + }, adminController.BlockUser) + // Typescript: TSEndpoint= path=/api/admin/updateUser; name=updateUser; method=PUT; request=users.UpdateUserRequest; response=users.User + app.Put("/admin/updateUser", func(c fiber.Ctx) error { + return auth.IsPermitted(c, auth.AdminPermission|auth.SuperAdminPermission) + }, adminController.UpdateUser) + + // Typescript: TSEndpoint= path=/api/admin/updateuserdetails; name=adminUpdateUserDetails; method=PUT; request=users.UpdateUserDetailsRequest; response=users.User + app.Put("/admin/updateuserdetails", func(c fiber.Ctx) error { + return auth.IsPermitted(c, auth.AdminPermission|auth.SuperAdminPermission) + }, adminController.UpdateUserDetails) + + // Typescript: TSEndpoint= path=/api/admin/updateuserpreferences; name=adminUpdateUserPreferences; method=PUT; request=users.UpdateUserPreferencesRequest; response=users.User + app.Put("/admin/updateuserpreferences", func(c fiber.Ctx) error { + return auth.IsPermitted(c, auth.AdminPermission|auth.SuperAdminPermission) + }, adminController.UpdateUserPreferences) } diff --git a/backend/internal/auth/roles.go b/backend/internal/auth/roles.go index f4115b0..29acb0e 100644 --- a/backend/internal/auth/roles.go +++ b/backend/internal/auth/roles.go @@ -1,23 +1,14 @@ package auth import ( - "server/internal/responses" + "server/internal/tokens" "github.com/gofiber/fiber/v3" ) -/* const ( - "superadmin" = SuperAdminPermission - "admin" = AdminPermission - "manager" = ManagerPermission - "content_creator" = ContentCreatorPermission - "user" = UserPermission - "guest" = GuestPermission -) */ - type Role struct { - Name Permission `json:"name"` - Permission int `json:"permission"` + Name string `json:"name"` + Permission uint `json:"permission"` } var Roles = []Role{ @@ -29,53 +20,29 @@ var Roles = []Role{ {"guest", GuestPermission}, } -// Typescript: type -type Permission string +// RolesData represents permissions of a user. +type RolesData string -// Typescript: enum=EnumPermission +// Typescript: enum=UserRole const ( - RoleSuperAdmin Permission = "superadmin" - RoleAdmin Permission = "admin" - RoleManager Permission = "manager" - RoleContentCreator Permission = "content_creator" - RoleUser Permission = "user" - RoleGuest Permission = "guest" + SuperAdminRole RolesData = "superadmin" + AdminRole RolesData = "admin" + ManagerRole RolesData = "manager" + ContentCreatorRole RolesData = "content_creator" + UserRole RolesData = "user" + GuestRole RolesData = "guest" ) -type Permissions struct { - SuperAdmin Permission `json:"superadmin"` - Admin Permission `json:"admin"` - Manager Permission `json:"manager"` - ContentCreator Permission `json:"content_creator"` - User Permission `json:"user"` - Guest Permission `json:"guest"` -} - const ( - SuperAdminPermission = 0b1111111111111111 - AdminPermission = 0b0111111111111111 - ManagerPermission = 0b0010111111111111 - ContentCreatorPermission = 0b0001111111111111 - UserPermission = 0b0000000000000011 - GuestPermission = 0b0000000000000001 + SuperAdminPermission uint = 0b1111111111111111 + AdminPermission uint = 0b0111111111111111 + ManagerPermission uint = 0b0010111111111111 + ContentCreatorPermission uint = 0b0001111111111111 + UserPermission uint = 0b0000000000000011 + GuestPermission uint = 0b0000000000000001 ) -// Typescript: type -type AllRoles map[string]string - -func GetRoles(c fiber.Ctx) error { - a := make(AllRoles) - a["RoleSuperAdmin"] = "superadmin" - a["RoleAdmin"] = "admin" - a["RoleManager"] = "manager" - a["RoleContentCreator"] = "content_creator" - a["RoleUser"] = "user" - a["RoleGuest"] = "guest" - - return c.JSON(responses.Success(a)) -} - -func PermissionToString(p int) Permission { +func PermissionToString(p uint) string { for _, role := range Roles { if role.Permission == p { return role.Name @@ -84,7 +51,7 @@ func PermissionToString(p int) Permission { return "unknown" } -func RoleToPermission(s Permission) int { +func RoleToPermission(s string) uint { for _, role := range Roles { if role.Name == s { return role.Permission @@ -92,3 +59,20 @@ func RoleToPermission(s Permission) int { } return 0 } + +func IsPermitted(c fiber.Ctx, permission uint) error { + claims := c.Locals("authClaims") + if claims == nil { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "authenticated": false, + }) + } + p := claims.(*tokens.Claims).Permission + if p&permission == 0 { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "authenticated": true, + "authorized": false, + }) + } + return c.Next() +} diff --git a/backend/internal/auth/routes.go b/backend/internal/auth/routes.go index d4a05f8..c36f1cf 100644 --- a/backend/internal/auth/routes.go +++ b/backend/internal/auth/routes.go @@ -3,6 +3,5 @@ package auth import "github.com/gofiber/fiber/v3" func RegisterAuthRoutes(app fiber.Router) { - // Typescript: TSEndpoint= path=/api/roles; name=getRoles; method=GET; response=auth.AllRoles - app.Get("/roles", GetRoles) + } diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go index ef195c2..df00ea3 100644 --- a/backend/internal/middleware/auth.go +++ b/backend/internal/middleware/auth.go @@ -28,7 +28,7 @@ func GetAuthClaims(dbConn *gorm.DB, tokenService *tokens.TockenService) fiber.Ha func AuthMe(c fiber.Ctx) error { claims := c.Locals("authClaims") if claims == nil { - return c.Status(fiber.StatusOK).JSON(fiber.Map{ + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ "authenticated": false, }) } diff --git a/backend/internal/seed/seed.go b/backend/internal/seed/seed.go index 77a40dd..b585a20 100644 --- a/backend/internal/seed/seed.go +++ b/backend/internal/seed/seed.go @@ -33,7 +33,7 @@ func SeedUsers(db *gorm.DB, n int) ([]users.User, []Credential, error) { creds := make([]Credential, 0, n) for i := 0; i < n; i++ { now := time.Now().UTC() - uuid := gofakeit.UUID() + //id := gofakeit.UUID() email := gofakeit.Email() pw, err := randomPassword() @@ -51,8 +51,7 @@ func SeedUsers(db *gorm.DB, n int) ([]users.User, []Credential, error) { Password: passwordHash, Permission: auth.PermissionToString(auth.SuperAdminPermission), Status: users.UserStatusActive, - Types: users.UserTypes{"internal"}, - UUID: uuid, + Type: users.UserType("internal"), Details: &users.UserDetails{ Title: gofakeit.JobTitle(), FirstName: gofakeit.FirstName(), diff --git a/backend/internal/tokens/services.go b/backend/internal/tokens/services.go index fa458d1..a657681 100644 --- a/backend/internal/tokens/services.go +++ b/backend/internal/tokens/services.go @@ -6,7 +6,6 @@ import ( "encoding/base64" "encoding/hex" "errors" - "server/internal/auth" "server/internal/config" "time" @@ -23,9 +22,10 @@ type TockenService struct { } type Claims struct { - Username string `json:"username"` - Permission int `json:"permission"` - TokenType string `json:"type"` + ID string `json:"id"` + Impersonator string `json:"impersonator,omitempty"` + Permission uint `json:"permission"` + TokenType string `json:"type"` jwt.RegisteredClaims } @@ -84,13 +84,13 @@ func HashToken(token string) string { return hashToken(token) } -func (s *TockenService) GenerateTokenPair(user string, permission auth.Permission) (TokenPair, error) { - access, err := s.GenerateToken(user, permission, TokenTypeAccess, s.accessExpiry) +func (s *TockenService) GenerateTokenPair(id string, permission uint) (TokenPair, error) { + access, err := s.GenerateToken(id, permission, TokenTypeAccess, s.accessExpiry) if err != nil { return TokenPair{}, err } - refresh, err := s.GenerateToken(user, permission, TokenTypeRefresh, s.refreshExpiry) + refresh, err := s.GenerateToken(id, permission, TokenTypeRefresh, s.refreshExpiry) if err != nil { return TokenPair{}, err } @@ -117,7 +117,7 @@ func (s *TockenService) Refresh(refreshToken string) (TokenPair, error) { if claims.TokenType != TokenTypeRefresh { return TokenPair{}, errors.New("refresh token required") } - return s.GenerateTokenPair(claims.Username, auth.PermissionToString(claims.Permission)) + return s.GenerateTokenPair(claims.ID, claims.Permission) } func (s *TockenService) ValidateToken(tokenString string) (*Claims, error) { @@ -158,13 +158,13 @@ func (s *TockenService) ParseToken(tokenString string) (*Claims, error) { return claims, nil } -func (s *TockenService) GenerateToken(user string, permission auth.Permission, tokenType string, expiry time.Duration) (string, error) { - p := auth.RoleToPermission(permission) +func (s *TockenService) GenerateToken(id string, permission uint, tokenType string, expiry time.Duration) (string, error) { claims := Claims{ - Username: user, - Permission: p, - TokenType: tokenType, + ID: id, + Impersonator: "", + Permission: permission, + TokenType: tokenType, RegisteredClaims: jwt.RegisteredClaims{ Issuer: s.cfg.Issuer, ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiry)), diff --git a/backend/internal/user/controller.go b/backend/internal/user/controller.go index 196fdc0..15712ce 100644 --- a/backend/internal/user/controller.go +++ b/backend/internal/user/controller.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "log" - "strconv" "strings" "time" @@ -33,13 +32,13 @@ func NewUserController(tockenService *tokens.TockenService) *UserController { } } -// GetUser returns a single user by UUID. +// GetUser returns a single user by ID. func (uc *UserController) GetUser(c fiber.Ctx) error { - user, err := loadUserByUUID(c, c.Params("uuid")) + user, err := GetUserByID(c.Params("id")) if err != nil { return err } - return c.JSON(responses.Success(ToUserProfile(user))) + return c.JSON(responses.Success(user)) } // CreateUser creates a user together with optional details and preferences. @@ -52,58 +51,12 @@ func (uc *UserController) CreateUser(c fiber.Ctx) error { return err } - db, err := db.DBFromCtx(c) + user, err := CreateUser(req) if err != nil { return err } - var existing 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 := systemUtils.HashPassword(req.Password) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password") - } - - now := time.Now().UTC() - user := User{ - Email: req.Email, - Name: req.Name, - Password: hashedPassword, - Permission: req.Permission, - Status: func() UserStatus { - if req.Status == "" { - return UserStatusPending - } - return req.Status - }(), - Types: func() UserTypes { - if len(req.Types) == 0 { - return UserTypes{"internal"} - } - return req.Types - }(), - Avatar: req.Avatar, - UUID: uuid.NewString(), - Details: req.Details, - Preferences: 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(responses.Success(ToUserProfile(&user))) + return c.Status(fiber.StatusCreated).JSON(responses.Success(user)) } // UpdateUser replaces user fields and synchronizes details/preferences. @@ -116,84 +69,42 @@ func (uc *UserController) UpdateUser(c fiber.Ctx) error { return err } - db, err := db.DBFromCtx(c) + user, err := UpdateUser(req) if err != nil { return err } - user, err := loadUserByUUID(c, req.UUID) + return c.JSON(responses.Success(user)) +} + +// UpdateUserDetails replaces user fields and synchronizes details/preferences. +func (uc *UserController) UpdateUserDetails(c fiber.Ctx) error { + var req UpdateUserDetailsRequest + if err := c.Bind().Body(&req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "invalid payload") + } + if err := validation.ValidateStruct(&req); err != nil { + return err + } + err := UpdateUserDetails(req) if err != nil { return err } - if req.Email != user.Email { - var existing 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") - } + user, err := GetUserByID(req.UserID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "invalid user id") } - now := time.Now().UTC() - user.Name = req.Name - user.Email = req.Email - user.Avatar = req.Avatar - user.UpdatedAt = &now - user.Permission = req.Permission - if req.Status != "" { - user.Status = req.Status - } - 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(responses.Success(ToUserProfile(user))) + return c.JSON(responses.Success(user)) } // DeleteUser removes a user and linked details/preferences through cascading delete rules. func (uc *UserController) DeleteUser(c fiber.Ctx) error { - db, err := db.DBFromCtx(c) - if err != nil { + if err := DeleteUser(c.Params("uuid")); 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(&UserDetails{}).Error; err != nil { - return err - } - if err := tx.Where("user_id = ?", user.ID).Delete(&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(responses.Success(responses.SimpleResponse{Message: "user deleted"})) } @@ -232,7 +143,7 @@ func (uc *UserController) Login(c fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "invalid user role") } - token, err := uc.TockenService.GenerateTokenPair(user.Email, auth.PermissionToString(permission)) + token, err := uc.TockenService.GenerateTokenPair(user.ID, permission) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to issue token") } @@ -300,14 +211,14 @@ func (uc *UserController) Register(c fiber.Ctx) error { } return req.Status }(), - Types: func() UserTypes { - if len(req.Types) == 0 { - return UserTypes{"internal"} + Type: func() UserType { + if len(req.Type) == 0 { + return UserType("internal") } - return req.Types + return req.Type }(), Avatar: req.Avatar, - UUID: uuid.NewString(), + ID: uuid.NewString(), Details: req.Details, Preferences: func() *UserPreferences { if req.Preferences == nil { @@ -527,107 +438,6 @@ func (uc *UserController) ValidToken(c fiber.Ctx) error { return c.JSON(responses.Success(responses.SimpleResponse{Message: "valid reset token"})) } -func loadUserByID(c fiber.Ctx) (*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 := db.DBFromCtx(c) - if err != nil { - return nil, err - } - - var user 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, uuid string) (*User, error) { - if uuid == "" { - return nil, fiber.NewError(fiber.StatusBadRequest, "invalid user uuid") - } - - db, err := db.DBFromCtx(c) - if err != nil { - return nil, err - } - - var user 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 *UserDetails) error { - if input == nil { - return tx.Where("user_id = ?", userID).Delete(&UserDetails{}).Error - } - - var details UserDetails - if err := tx.Where("user_id = ?", userID).First(&details).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - details = 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 *UserPreferences) error { - if input == nil { - return tx.Where("user_id = ?", userID).Delete(&UserPreferences{}).Error - } - - var preferences UserPreferences - if err := tx.Where("user_id = ?", userID).First(&preferences).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - preferences = 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 -} - // Me returns the authenticated user's profile (short format). func (uc *UserController) Me(c fiber.Ctx) error { @@ -638,25 +448,25 @@ func (uc *UserController) Me(c fiber.Ctx) error { tokenString := c.Get("Auth-Token") if tokenString == "" { - return c.JSON(responses.Success("missing token header")) + return fiber.NewError(fiber.StatusForbidden, "missing token header") } claims, err := tokenService.ValidateAccessToken(tokenString) if err != nil { - return c.JSON(responses.Success("bad token")) + return fiber.NewError(fiber.StatusUnauthorized, "bad token") } db, err := db.DBFromCtx(c) if err != nil { - return c.JSON(responses.Success("failed to load db")) + return fiber.NewError(fiber.StatusInternalServerError, "failed to load db") } var user User - if err := db.Preload("Details").Preload("Preferences").Where("email = ?", claims.Username).First(&user).Error; err != nil { + if err := db.Preload("Details").Preload("Preferences").Where("id = ?", claims.ID).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return c.JSON(responses.Success("user not found")) + return fiber.NewError(fiber.StatusNotFound, "user not found") } - return c.JSON(responses.Success("failed to load user")) + return fiber.NewError(fiber.StatusInternalServerError, "failed to load user") } return c.JSON(responses.Success(&user)) @@ -678,7 +488,7 @@ func (us *UserController) Refresh(c fiber.Ctx) error { if claims.TokenType != tokens.TokenTypeRefresh { return fiber.NewError(fiber.StatusUnauthorized, "refresh token required") } - tokens, err := us.TockenService.GenerateTokenPair(claims.Username, auth.PermissionToString(claims.Permission)) + tokens, err := us.TockenService.GenerateTokenPair(claims.ID, claims.Permission) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } @@ -688,21 +498,18 @@ func (us *UserController) Refresh(c fiber.Ctx) error { // update user password by Claims func (us *UserController) UpdatePassword(c fiber.Ctx) error { var req UpdatePasswordRequest + claims := c.Locals("authClaims") + if claims == nil { + return fiber.NewError(fiber.StatusForbidden, "forbidden") + } + if err := c.Bind().Body(&req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "invalid payload") } if err := validation.ValidateStruct(&req); err != nil { return err } - - db, err := db.DBFromCtx(c) - if err != nil { - return err - } - - err = UpdateUserPassword(db, req, c.Params("uuid")) - - if err != nil { + if err := UpdateUserPassword(req, claims.(tokens.Claims).ID); err != nil { return err } diff --git a/backend/internal/user/model.go b/backend/internal/user/model.go index 7b76d4a..d5985b1 100644 --- a/backend/internal/user/model.go +++ b/backend/internal/user/model.go @@ -1,77 +1,60 @@ package users import ( - "server/internal/auth" "time" + "github.com/google/uuid" "gorm.io/gorm" ) +// BeforeCreate will set a UUID rather than numeric ID. +func (base *User) BeforeCreate(tx *gorm.DB) error { + if base.ID == "" { + id, err := uuid.NewRandom() + if err != nil { + return err + } + base.ID = id.String() + } + return nil +} + type User struct { - ID int `gorm:"primaryKey"` + ID string `json:"id" gorm:"type:uuid;primary_key;"` Email string `json:"email" gorm:"uniqueIndex;size:255"` Name string `json:"name" gorm:"size:255"` Password string `json:"-" gorm:"size:255"` - Permission auth.Permission `json:"permission"` - Types UserTypes `json:"types" gorm:"type:text;serializer:json"` + Permission string `json:"permission"` + Type UserType `json:"type"` Status UserStatus `json:"status" gorm:"type:text;default:'pending'"` - ActivatedAt *time.Time `json:"activatedAt" ts:"type=Nullable"` - UUID string `json:"uuid" gorm:"size:36"` Details *UserDetails `json:"details" gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` Preferences *UserPreferences `json:"preferences" gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` Avatar *string `json:"avatar" gorm:"size:512"` + ActivatedAt *time.Time `json:"activatedAt" ts:"type=Nullable"` CreatedAt *time.Time `json:"createdAt,omitempty" ts:"type=Date"` UpdatedAt *time.Time `json:"updatedAt,omitempty" ts:"type=Date"` DeletedAt *gorm.DeletedAt `json:"-" gorm:"index" ` } -// UserTypes is stored as JSON array (e.g. ["internal","external"]). -type UserTypes []string - -// UserProfile is the safe full representation of a user returned by CRUD endpoints. -type UserProfile struct { - ID int `json:"id"` - Email string `json:"email"` - Name string `json:"name"` - Permission auth.Permission `json:"permission"` - Types UserTypes `json:"types"` - Status UserStatus `json:"status"` - ActivatedAt *time.Time `json:"activatedAt" ts:"type=Nullable"` - UUID string `json:"uuid"` - Details *UserDetails `json:"details"` - Preferences *UserPreferences `json:"preferences"` - Avatar *string `json:"avatar"` - CreatedAt *time.Time `json:"createdAt,omitempty" ts:"type=Date"` - UpdatedAt *time.Time `json:"updatedAt,omitempty" ts:"type=Date"` +type UpdateUserProfileRequest struct { + ID string `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + Permission string `json:"permission"` + Type UserType `json:"type"` + Status UserStatus `json:"status"` } -// ToUserProfile maps a User to a full response without exposing the password hash. -func ToUserProfile(u *User) UserProfile { - if u == nil { - return UserProfile{} - } - return UserProfile{ - ID: u.ID, - Email: u.Email, - Name: u.Name, - Permission: u.Permission, - Types: u.Types, - Status: u.Status, - ActivatedAt: u.ActivatedAt, - UUID: u.UUID, - Details: u.Details, - Preferences: u.Preferences, - Avatar: u.Avatar, - CreatedAt: u.CreatedAt, - UpdatedAt: u.UpdatedAt, - } +type UpdateUserAvatarRequest struct { + ID string `json:"id" validate:"required,uuid4"` + Img []byte `json:"img"` } // UserPreferences holds per-user settings stored as JSON. type UserPreferences struct { ID int `json:"id" gorm:"primaryKey"` - UserID int `json:"userId" gorm:"index"` + UserID string `json:"userId" gorm:"type:uuid;column:user_foreign_key;not null;"` UseIdle bool `json:"useIdle"` IdleTimeout int `json:"idleTimeout"` UseIdlePassword bool `json:"useIdlePassword"` @@ -84,11 +67,24 @@ type UserPreferences struct { UpdatedAt *time.Time `json:"updatedAt,omitempty" ts:"type=Date"` } +type UpdateUserPreferencesRequest struct { + ID int `json:"id"` + UserID string `json:"userId"` + UseIdle bool `json:"useIdle"` + IdleTimeout int `json:"idleTimeout"` + UseIdlePassword bool `json:"useIdlePassword"` + IdlePin string `json:"idlePin"` + UseDirectLogin bool `json:"useDirectLogin"` + UseQuadcodeLogin bool `json:"useQuadcodeLogin"` + SendNoticesMail bool `json:"sendNoticesMail"` + Language string `json:"language"` +} + // UserDetails holds optional profile data. type UserDetails struct { ID int `json:"id" gorm:"primaryKey"` - UserID int `json:"userId" gorm:"index"` + UserID string `json:"userId" gorm:"type:uuid;column:user_foreign_key;not null;"` Title string `json:"title"` FirstName string `json:"firstName"` LastName string `json:"lastName"` @@ -101,13 +97,26 @@ type UserDetails struct { UpdatedAt *time.Time `json:"updatedAt,omitempty" ts:"type=Date"` } +type UpdateUserDetailsRequest struct { + ID int `json:"id"` + UserID string `json:"userId" gorm:"index"` + Title string `json:"title"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Address string `json:"address"` + City string `json:"city"` + ZipCode string `json:"zipCode"` + Country string `json:"country"` + Phone string `json:"phone"` +} + // UserDetails holds optional profile data. // Session tracks logins with browser metadata. type Session struct { - ID int `json:"id" gorm:"primaryKey"` - UserID *int `json:"userId" gorm:"index"` + ID int `gorm:"primaryKey"` + UserID *string `json:"userId" gorm:"index"` Username string `json:"username" gorm:"size:255"` AccessTokenHash string `json:"-" gorm:"size:128;index"` RefreshTokenHash string `json:"-" gorm:"size:128;index"` @@ -120,8 +129,8 @@ type Session struct { } type PasswordResetToken struct { - ID int `json:"id" gorm:"primaryKey"` - UserID int `json:"userId" gorm:"index"` + ID int `gorm:"primaryKey"` + UserID string `json:"userId" gorm:"index"` TokenHash string `json:"-" gorm:"size:64;uniqueIndex"` ExpiresAt time.Time `json:"expiresAt,omitempty" ts:"type=Date" gorm:"index"` UsedAt *time.Time `json:"usedAt,omitempty" ts:"type=Date"` @@ -140,6 +149,16 @@ const ( UserStatusDisabled UserStatus = "disabled" ) +// UserTypes represents different types of a user. +type UserType string + +// Typescript: enum=UserTypes +const ( + UserTypeInternal UserType = "internal" + UserTypeExternal UserType = "external" + UserTypeSurveyor UserType = "surveyor" +) + type LoginRequest struct { Username string `json:"username" validate:"required,email"` Password string `json:"password" validate:"required,min=8,max=128"` @@ -159,20 +178,18 @@ type ResetPasswordRequest struct { } type UpdatePasswordRequest struct { + ID string `json:"id" validate:"required,uuid4"` Password string `json:"password" validate:"required,min=8,max=128"` } type UpdateUserRequest struct { - UUID string `json:"uuid" validate:"required,uuid4"` - 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"` - Permission auth.Permission `json:"permission"` - Status UserStatus `json:"status"` - Types UserTypes `json:"types"` - Avatar *string `json:"avatar,omitempty"` - Details *UserDetails `json:"details,omitempty"` - Preferences *UserPreferences `json:"preferences,omitempty"` + ID string `json:"id" validate:"required,uuid4"` + 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"` + Permission string `json:"permission"` + Status UserStatus `json:"status"` + Type UserType `json:"type"` } // UserCreateRequest captures the minimal payload to create a user. @@ -181,9 +198,9 @@ type UserCreateRequest struct { Name string `json:"name" validate:"required,min=1,max=255"` Email string `json:"email" validate:"required,email"` Password string `json:"password" validate:"required,min=8,max=128"` - Permission auth.Permission `json:"permission"` + Permission string `json:"permission"` Status UserStatus `json:"status"` - Types UserTypes `json:"types"` + Type UserType `json:"type"` Avatar *string `json:"avatar,omitempty"` Details *UserDetails `json:"details,omitempty"` Preferences *UserPreferences `json:"preferences,omitempty"` diff --git a/backend/internal/user/repository.go b/backend/internal/user/repository.go new file mode 100644 index 0000000..8a10019 --- /dev/null +++ b/backend/internal/user/repository.go @@ -0,0 +1,338 @@ +package users + +import ( + "encoding/base64" + "errors" + "server/internal/db" + "server/internal/systemUtils" + "time" + + "github.com/gofiber/fiber/v3" + "github.com/google/uuid" + "gorm.io/gorm" +) + +func repositoryDB() (*gorm.DB, error) { + database, err := db.GetDB() + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "database unavailable") + } + return database, nil +} + +func CreateUser(createUserRequest UserCreateRequest) (*User, error) { + database, err := repositoryDB() + if err != nil { + return nil, err + } + + var existing User + if err := database.Where("email = ?", createUserRequest.Email).First(&existing).Error; err == nil { + return nil, fiber.NewError(fiber.StatusConflict, "user already exists") + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to check user") + } + + hashedPassword, err := systemUtils.HashPassword(createUserRequest.Password) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to secure password") + } + + now := time.Now().UTC() + user := User{ + Email: createUserRequest.Email, + Name: createUserRequest.Name, + Password: hashedPassword, + Permission: createUserRequest.Permission, + Status: func() UserStatus { + if createUserRequest.Status == "" { + return UserStatusPending + } + return createUserRequest.Status + }(), + Type: func() UserType { + if createUserRequest.Type == "" { + return UserType("internal") + } + return createUserRequest.Type + }(), + Avatar: func() *string { + if createUserRequest.Avatar == nil { + return nil + } + return createUserRequest.Avatar + }(), + ID: uuid.NewString(), + Details: func() *UserDetails { + if createUserRequest.Details == nil { + return nil + } + return createUserRequest.Details + }(), + Preferences: func() *UserPreferences { + if createUserRequest.Preferences == nil { + return nil + } + return createUserRequest.Preferences + }(), + CreatedAt: &now, + UpdatedAt: &now, + } + + if err := database.Create(&user).Error; err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to create user") + } + + return GetUserByID(user.ID) +} + +func UpdateUser(updateUserRequest UpdateUserRequest) (*User, error) { + database, err := repositoryDB() + if err != nil { + return nil, err + } + + user, err := GetUserByID(updateUserRequest.ID) + if err != nil { + return nil, err + } + + now := time.Now().UTC() + user.Name = updateUserRequest.Name + user.Email = updateUserRequest.Email + user.UpdatedAt = &now + user.Permission = updateUserRequest.Permission + if updateUserRequest.Status != "" { + user.Status = updateUserRequest.Status + } + if updateUserRequest.Type != "" { + user.Type = updateUserRequest.Type + } + user.Permission = updateUserRequest.Permission + + if err := database.Transaction(func(tx *gorm.DB) error { + if err := tx.Save(user).Error; err != nil { + return err + } + return nil + }); err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to update user") + } + + return GetUserByID(user.ID) +} + +func UpdateUserDetails(updateUserDetails UpdateUserDetailsRequest) error { + database, err := repositoryDB() + if err != nil { + return err + } + if updateUserDetails.UserID == "" { + return fiber.NewError(fiber.StatusBadRequest, "invalid user id") + } + + input := &UserDetails{ + UserID: updateUserDetails.UserID, + Title: updateUserDetails.Title, + FirstName: updateUserDetails.FirstName, + LastName: updateUserDetails.LastName, + Address: updateUserDetails.Address, + City: updateUserDetails.City, + ZipCode: updateUserDetails.ZipCode, + Country: updateUserDetails.Country, + Phone: updateUserDetails.Phone, + } + + if err := database.Transaction(func(tx *gorm.DB) error { + return syncUserDetails(tx, updateUserDetails.UserID, input) + }); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to update user details") + } + return nil +} + +func UpdateUserPreferences(updateUserPreferences UpdateUserPreferencesRequest) error { + database, err := repositoryDB() + if err != nil { + return err + } + if updateUserPreferences.UserID == "" { + return fiber.NewError(fiber.StatusBadRequest, "invalid user id") + } + + input := &UserPreferences{ + UserID: updateUserPreferences.UserID, + UseIdle: updateUserPreferences.UseIdle, + IdleTimeout: updateUserPreferences.IdleTimeout, + UseIdlePassword: updateUserPreferences.UseIdlePassword, + IdlePin: updateUserPreferences.IdlePin, + UseDirectLogin: updateUserPreferences.UseDirectLogin, + UseQuadcodeLogin: updateUserPreferences.UseQuadcodeLogin, + SendNoticesMail: updateUserPreferences.SendNoticesMail, + Language: updateUserPreferences.Language, + } + + if err := database.Transaction(func(tx *gorm.DB) error { + return syncUserPreferences(tx, updateUserPreferences.UserID, input) + }); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to update user preferences") + } + return nil +} + +func UpdateUserAvatar(updateUserAvatarRequest UpdateUserAvatarRequest) (*User, error) { + database, err := repositoryDB() + if err != nil { + return nil, err + } + + user, err := GetUserByID(updateUserAvatarRequest.ID) + if err != nil { + return nil, err + } + + avatar := base64.StdEncoding.EncodeToString(updateUserAvatarRequest.Img) + now := time.Now().UTC() + if err := database.Model(user).Updates(map[string]any{ + "avatar": avatar, + "updated_at": now, + }).Error; err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to update user avatar") + } + + return GetUserByID(user.ID) +} + +func UpdateUserPassword(req UpdatePasswordRequest, id string) error { + database, err := repositoryDB() + if err != nil { + return err + } + user := User{} + if err := database.Where("uuid = ?", id).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "user not found") + } + return fiber.NewError(fiber.StatusInternalServerError, "failed to load user") + } + hashedPassword, err := systemUtils.HashPassword(req.Password) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password") + } + now := time.Now().UTC() + if err := database.Model(&user).Updates(map[string]any{ + "password": hashedPassword, + "updated_at": now, + }).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to update password") + } + if err := database.Where("user_id = ?", user.ID).Delete(&Session{}).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to revoke sessions") + } + return nil +} + +func GetUserByID(id string) (*User, error) { + if id == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "invalid user id") + } + + database, err := repositoryDB() + if err != nil { + return nil, err + } + + var user User + if err := database.Preload("Details").Preload("Preferences").First(&user, "id = ?", 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 DeleteUser(uuid string) error { + database, err := repositoryDB() + if err != nil { + return err + } + + user, err := GetUserByID(uuid) + if err != nil { + return err + } + + if err := database.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("user_id = ?", user.ID).Delete(&UserDetails{}).Error; err != nil { + return err + } + if err := tx.Where("user_id = ?", user.ID).Delete(&UserPreferences{}).Error; err != nil { + return err + } + return tx.Delete(user).Error + }); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to delete user") + } + + return nil +} + +func syncUserDetails(tx *gorm.DB, userID string, input *UserDetails) error { + if input == nil { + return tx.Where("user_id = ?", userID).Delete(&UserDetails{}).Error + } + + var details UserDetails + if err := tx.Where("user_id = ?", userID).First(&details).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + details = 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 string, input *UserPreferences) error { + if input == nil { + return tx.Where("user_id = ?", userID).Delete(&UserPreferences{}).Error + } + + var preferences UserPreferences + if err := tx.Where("user_id = ?", userID).First(&preferences).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + preferences = 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 +} diff --git a/backend/internal/user/repository.md b/backend/internal/user/repository.md new file mode 100644 index 0000000..87670aa --- /dev/null +++ b/backend/internal/user/repository.md @@ -0,0 +1,8 @@ +CreateUser(createUserRequest CreateUserRequest) +UpdateUser(updateUserRequest UpdateUserRequest) +UpadteUserDetails(upadteUserDetails UpadteUserDetailsRequest) +UpdateUserPreferences(updateUserPreferences UpdateUserPreferencesRequest) +UpdateUserAvatar(updateUserAvatarRequest UpdateUserAvatarRequest) +UpdatePassword(updatePasswordRequest UpdatePasswordRequest) +GetUserByUUID(uuid string) +DeleteUser(uuid string) \ No newline at end of file diff --git a/backend/internal/user/rerpository.go b/backend/internal/user/rerpository.go deleted file mode 100644 index 0eb4137..0000000 --- a/backend/internal/user/rerpository.go +++ /dev/null @@ -1,35 +0,0 @@ -package users - -import ( - "errors" - "server/internal/systemUtils" - "time" - - "github.com/gofiber/fiber/v3" - "gorm.io/gorm" -) - -func UpdateUserPassword(db *gorm.DB, req UpdatePasswordRequest, uuid string) error { - user := User{} - if err := db.Where("uuid = ?", uuid).First(&user).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "user not found") - } - return fiber.NewError(fiber.StatusInternalServerError, "failed to load user") - } - hashedPassword, err := systemUtils.HashPassword(req.Password) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password") - } - now := time.Now().UTC() - if err := db.Model(&user).Updates(map[string]any{ - "password": hashedPassword, - "updated_at": now, - }).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "failed to update password") - } - if err := db.Where("user_id = ?", user.ID).Delete(&Session{}).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "failed to revoke sessions") - } - return nil -} diff --git a/backend/internal/user/routes.go b/backend/internal/user/routes.go index 199ad8a..a5ac3b1 100644 --- a/backend/internal/user/routes.go +++ b/backend/internal/user/routes.go @@ -24,8 +24,8 @@ func RegisterUserRoutes(app fiber.Router) { userController := NewUserController(tockenService) - // Typescript: TSEndpoint= path=/api/users/:uuid; name=getUser; method=GET; response=users.User - app.Get("/users/:uuid", userController.GetUser) + // Typescript: TSEndpoint= path=/api/users/:id; name=getUser; method=GET; response=users.User + app.Get("/users/:id", userController.GetUser) // Typescript: TSEndpoint= path=/api/users; name=createUser; method=POST; request=users.UserCreateRequest; response=users.User app.Post("/users", userController.CreateUser) @@ -33,8 +33,11 @@ func RegisterUserRoutes(app fiber.Router) { // Typescript: TSEndpoint= path=/api/users/update; name=updateUser; method=PUT; request=users.UpdateUserRequest; response=users.User app.Put("/users/update", userController.UpdateUser) - // Typescript: TSEndpoint= path=/api/users/:uuid; name=deleteUser; method=DELETE; response=responses.SimpleResponse - app.Delete("/users/:uuid", userController.DeleteUser) + // Typescript: TSEndpoint= path=/api/users/update/details; name=updateUserDetails; method=PUT; request=users.UpdateUserDetailsRequest; response=users.User + app.Put("/users/update/details", userController.UpdateUserDetails) + + // Typescript: TSEndpoint= path=/api/users/:id; name=deleteUser; method=DELETE; response=responses.SimpleResponse + app.Delete("/users/:id", userController.DeleteUser) // Typescript: TSEndpoint= path=/api/auth/me; name=me; method=GET; response=users.User app.Get("/auth/me", middleware.AuthMe, userController.Me) diff --git a/frontend/.env b/frontend/.env index f985471..b0f22de 100644 --- a/frontend/.env +++ b/frontend/.env @@ -2,3 +2,4 @@ GO_PROJECT_DIR=/Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/http/static # Option B (overrides GO_PROJECT_DIR): explicit target dir where dist/spa is copied # GO_SPA_TARGET_DIR=/absolute/path/to/your/go/project/spa +SEED=0 diff --git a/frontend/quasar.config.ts b/frontend/quasar.config.ts index 168d51e..ee93a36 100644 --- a/frontend/quasar.config.ts +++ b/frontend/quasar.config.ts @@ -116,7 +116,7 @@ export default defineConfig((ctx) => { // directives: [], // Quasar plugins - plugins: ['Notify', 'Dialog', 'Loading'], + plugins: ['Notify', 'Dialog', 'Loading', 'AppVisibility'], }, // animations: 'all', // --- includes all animations diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a43423a..82f3c56 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -5,11 +5,14 @@ diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts index 89fb1ee..0eecc50 100644 --- a/frontend/src/api/admin.ts +++ b/frontend/src/api/admin.ts @@ -2,9 +2,21 @@ import { api } from "./api"; import type { Nullable } from "./apiTypes.ts"; import type * as users from "./users.ts"; +// Typescript: TSEndpoint= path=/api/admin/users; name=listUsers; method=POST; request=admin.ListUsersRequest; response=admin.ListUsersResponse + +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/admin/routes.go Line: 13 +export const listUsers = async ( + data: ListUsersRequest, +): Promise<{ data: ListUsersResponse; error: Nullable }> => { + return (await api.POST("/api/admin/users", data)) as { + data: ListUsersResponse; + error: Nullable; + }; +}; + // Typescript: TSEndpoint= path=/api/admin/users/block; name=blockUser; method=PUT; request=admin.BlockUserRequest; response=users.User -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/admin/routes.go Line: 14 +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/admin/routes.go Line: 18 export const blockUser = async ( data: BlockUserRequest, ): Promise<{ data: users.User; error: Nullable }> => { @@ -14,14 +26,38 @@ export const blockUser = async ( }; }; -// Typescript: TSEndpoint= path=/api/admin/users; name=listUsers; method=POST; request=admin.ListUsersRequest; response=admin.ListUsersResponse +// Typescript: TSEndpoint= path=/api/admin/updateUser; name=updateUser; method=PUT; request=users.UpdateUserRequest; response=users.User -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/admin/routes.go Line: 11 -export const listUsers = async ( - data: ListUsersRequest, -): Promise<{ data: ListUsersResponse; error: Nullable }> => { - return (await api.POST("/api/admin/users", data)) as { - data: ListUsersResponse; +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/admin/routes.go Line: 23 +export const updateUser = async ( + data: users.UpdateUserRequest, +): Promise<{ data: users.User; error: Nullable }> => { + return (await api.PUT("/api/admin/updateUser", data)) as { + data: users.User; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/api/admin/updateuserdetails; name=adminUpdateUserDetails; method=PUT; request=users.UpdateUserDetailsRequest; response=users.User + +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/admin/routes.go Line: 28 +export const adminUpdateUserDetails = async ( + data: users.UpdateUserDetailsRequest, +): Promise<{ data: users.User; error: Nullable }> => { + return (await api.PUT("/api/admin/updateuserdetails", data)) as { + data: users.User; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/api/admin/updateuserpreferences; name=adminUpdateUserPreferences; method=PUT; request=users.UpdateUserPreferencesRequest; response=users.User + +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/admin/routes.go Line: 33 +export const adminUpdateUserPreferences = async ( + data: users.UpdateUserPreferencesRequest, +): Promise<{ data: users.User; error: Nullable }> => { + return (await api.PUT("/api/admin/updateuserpreferences", data)) as { + data: users.User; error: Nullable; }; }; @@ -33,7 +69,7 @@ export interface ListUsersResponse { } export interface BlockUserRequest { - uuid: string; + id: string; action: string; } diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 8e12ec8..d90cedf 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -4,7 +4,7 @@ // // This file was generated by github.com/millevolte/ts-rpc // -// May 05, 2026 14:14:16 UTC +// May 08, 2026 12:28:15 UTC // export interface ApiRestResponse { diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index c951d35..cf33dfd 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -1,30 +1,12 @@ -import { api } from "./api"; -import type { Nullable, Record } from "./apiTypes.ts"; +export type UserRole = (typeof EnumUserRole)[keyof typeof EnumUserRole]; -// Typescript: TSEndpoint= path=/api/roles; name=getRoles; method=GET; response=auth.AllRoles +export type RolesData = string; -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/auth/routes.go Line: 7 -export const getRoles = async (): Promise<{ - data: AllRoles; - error: Nullable; -}> => { - return (await api.GET("/api/roles")) as { - data: AllRoles; - error: Nullable; - }; -}; -export type EnumPermission = - (typeof EnumEnumPermission)[keyof typeof EnumEnumPermission]; - -export type Permission = string; - -export type AllRoles = Record; - -export const EnumEnumPermission = { - RoleSuperAdmin: "superadmin", - RoleAdmin: "admin", - RoleManager: "manager", - RoleContentCreator: "content_creator", - RoleUser: "user", - RoleGuest: "guest", +export const EnumUserRole = { + SuperAdminRole: "superadmin", + AdminRole: "admin", + ManagerRole: "manager", + ContentCreatorRole: "content_creator", + UserRole: "user", + GuestRole: "guest", } as const; diff --git a/frontend/src/api/systemUtils.ts b/frontend/src/api/systemUtils.ts index 6c3bda3..3a86047 100644 --- a/frontend/src/api/systemUtils.ts +++ b/frontend/src/api/systemUtils.ts @@ -1,6 +1,19 @@ import { api } from "./api"; import type { Nullable } from "./apiTypes.ts"; +// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string + +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/systemUtils/routes.go Line: 36 +export const health = async (): Promise<{ + data: string; + error: Nullable; +}> => { + return (await api.GET("/health")) as { + data: string; + error: Nullable; + }; +}; + // Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string // /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/systemUtils/routes.go Line: 39 @@ -27,19 +40,6 @@ export const mailDebug = async (): Promise<{ }; }; -// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string - -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/systemUtils/routes.go Line: 36 -export const health = async (): Promise<{ - data: string; - error: Nullable; -}> => { - return (await api.GET("/health")) as { - data: string; - error: Nullable; - }; -}; - export interface MailDebugItem { name: string; content: string; diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 0fdf12a..3df1009 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -1,36 +1,11 @@ import { api } from "./api"; import type { Nullable } from "./apiTypes.ts"; -import type * as auth from "./auth.ts"; -import type * as responses from "./responses.ts"; import type * as tokens from "./tokens.ts"; - -// Typescript: TSEndpoint= path=/api/auth/password/reset; name=resetPassword; method=POST; request=users.ResetPasswordRequest; response=responses.SimpleResponse - -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 55 -export const resetPassword = async ( - data: ResetPasswordRequest, -): Promise<{ data: responses.SimpleResponse; error: Nullable }> => { - return (await api.POST("/api/auth/password/reset", data)) as { - data: responses.SimpleResponse; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/api/auth/password/valid; name=validToken; method=POST; request=string; response=responses.SimpleResponse - -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 58 -export const validToken = async ( - data: string, -): Promise<{ data: responses.SimpleResponse; error: Nullable }> => { - return (await api.POST("/api/auth/password/valid", data)) as { - data: responses.SimpleResponse; - error: Nullable; - }; -}; +import type * as responses from "./responses.ts"; // Typescript: TSEndpoint= path=/api/auth/me; name=me; method=GET; response=users.User -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 40 +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 43 export const me = async (): Promise<{ data: User; error: Nullable; @@ -41,21 +16,9 @@ export const me = async (): Promise<{ }; }; -// Typescript: TSEndpoint= path=/api/auth/password/forgot; name=forgotPassword; method=POST; request=users.ForgotPasswordRequest; response=responses.SimpleResponse - -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 52 -export const forgotPassword = async ( - data: ForgotPasswordRequest, -): Promise<{ data: responses.SimpleResponse; error: Nullable }> => { - return (await api.POST("/api/auth/password/forgot", data)) as { - data: responses.SimpleResponse; - error: Nullable; - }; -}; - // Typescript: TSEndpoint= path=/api/auth/password/update; name=updatePassword; method=PUT; request=users.UpdatePasswordRequest; response=responses.SimpleResponse -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 61 +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 64 export const updatePassword = async ( data: UpdatePasswordRequest, ): Promise<{ data: responses.SimpleResponse; error: Nullable }> => { @@ -65,13 +28,121 @@ export const updatePassword = async ( }; }; -// Typescript: TSEndpoint= path=/api/users/:uuid; name=getUser; method=GET; response=users.User +// Typescript: TSEndpoint= path=/api/users; name=createUser; method=POST; request=users.UserCreateRequest; response=users.User + +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 31 +export const createUser = async ( + data: UserCreateRequest, +): Promise<{ data: User; error: Nullable }> => { + return (await api.POST("/api/users", data)) as { + data: User; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/api/users/update/details; name=updateUserDetails; method=PUT; request=users.UpdateUserDetailsRequest; response=users.User + +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 37 +export const updateUserDetails = async ( + data: UpdateUserDetailsRequest, +): Promise<{ data: User; error: Nullable }> => { + return (await api.PUT("/api/users/update/details", data)) as { + data: User; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/api/auth/refresh; name=refresh; method=POST; request=users.RefreshRequest; response=tokens.TokenPair + +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 49 +export const refresh = async ( + data: RefreshRequest, +): Promise<{ data: tokens.TokenPair; error: Nullable }> => { + return (await api.POST("/api/auth/refresh", data)) as { + data: tokens.TokenPair; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/api/auth/password/reset; name=resetPassword; method=POST; request=users.ResetPasswordRequest; response=responses.SimpleResponse + +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 58 +export const resetPassword = async ( + data: ResetPasswordRequest, +): Promise<{ data: responses.SimpleResponse; error: Nullable }> => { + return (await api.POST("/api/auth/password/reset", data)) as { + data: responses.SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/api/auth/register; name=register; method=POST; request=users.UserCreateRequest; response=users.User + +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 52 +export const register = async ( + data: UserCreateRequest, +): Promise<{ data: User; error: Nullable }> => { + return (await api.POST("/api/auth/register", data)) as { + data: User; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/api/users/:id; name=deleteUser; method=DELETE; response=responses.SimpleResponse + +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 40 +export const deleteUser = async ( + id: string, +): Promise<{ data: responses.SimpleResponse; error: Nullable }> => { + return (await api.DELETE(`/api/users/${id}`)) as { + data: responses.SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/api/auth/login; name=login; method=POST; request=users.LoginRequest; response=tokens.TokenPair + +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 46 +export const login = async ( + data: LoginRequest, +): Promise<{ data: tokens.TokenPair; error: Nullable }> => { + return (await api.POST("/api/auth/login", data)) as { + data: tokens.TokenPair; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/api/auth/password/forgot; name=forgotPassword; method=POST; request=users.ForgotPasswordRequest; response=responses.SimpleResponse + +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 55 +export const forgotPassword = async ( + data: ForgotPasswordRequest, +): Promise<{ data: responses.SimpleResponse; error: Nullable }> => { + return (await api.POST("/api/auth/password/forgot", data)) as { + data: responses.SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/api/auth/password/valid; name=validToken; method=POST; request=string; response=responses.SimpleResponse + +// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 61 +export const validToken = async ( + data: string, +): Promise<{ data: responses.SimpleResponse; error: Nullable }> => { + return (await api.POST("/api/auth/password/valid", data)) as { + data: responses.SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/api/users/:id; name=getUser; method=GET; response=users.User // /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 28 export const getUser = async ( - uuid: string, + id: string, ): Promise<{ data: User; error: Nullable }> => { - return (await api.GET(`/api/users/${uuid}`)) as { + return (await api.GET(`/api/users/${id}`)) as { data: User; error: Nullable; }; @@ -89,109 +160,9 @@ export const updateUser = async ( }; }; -// Typescript: TSEndpoint= path=/api/users/:uuid; name=deleteUser; method=DELETE; response=responses.SimpleResponse - -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 37 -export const deleteUser = async ( - uuid: string, -): Promise<{ data: responses.SimpleResponse; error: Nullable }> => { - return (await api.DELETE(`/api/users/${uuid}`)) as { - data: responses.SimpleResponse; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/api/auth/login; name=login; method=POST; request=users.LoginRequest; response=tokens.TokenPair - -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 43 -export const login = async ( - data: LoginRequest, -): Promise<{ data: tokens.TokenPair; error: Nullable }> => { - return (await api.POST("/api/auth/login", data)) as { - data: tokens.TokenPair; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/api/auth/register; name=register; method=POST; request=users.UserCreateRequest; response=users.User - -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 49 -export const register = async ( - data: UserCreateRequest, -): Promise<{ data: User; error: Nullable }> => { - return (await api.POST("/api/auth/register", data)) as { - data: User; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/api/users; name=createUser; method=POST; request=users.UserCreateRequest; response=users.User - -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 31 -export const createUser = async ( - data: UserCreateRequest, -): Promise<{ data: User; error: Nullable }> => { - return (await api.POST("/api/users", data)) as { - data: User; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/api/auth/refresh; name=refresh; method=POST; request=users.RefreshRequest; response=tokens.TokenPair - -// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 46 -export const refresh = async ( - data: RefreshRequest, -): Promise<{ data: tokens.TokenPair; error: Nullable }> => { - return (await api.POST("/api/auth/refresh", data)) as { - data: tokens.TokenPair; - error: Nullable; - }; -}; - -export interface UserCreateRequest { - name: string; - email: string; - password: string; - permission: auth.Permission; - status: UserStatus; - types: UserTypes; - avatar: Nullable; - details: Nullable; - preferences: Nullable; -} - -export interface UserPreferences { - id: number; - userId: number; - useIdle: boolean; - idleTimeout: number; - useIdlePassword: boolean; - idlePin: string; - useDirectLogin: boolean; - useQuadcodeLogin: boolean; - sendNoticesMail: boolean; - language: string; - createdAt: Nullable; - updatedAt: Nullable; -} - -export interface RefreshRequest { - refresh_token: string; -} - -export interface ResetPasswordRequest { - token: string; - password: string; -} - -export interface UpdatePasswordRequest { - password: string; -} - export interface UserDetails { id: number; - userId: number; + userId: string; title: string; firstName: string; lastName: string; @@ -200,39 +171,78 @@ export interface UserDetails { zipCode: string; country: string; phone: string; - createdAt: Nullable; - updatedAt: Nullable; -} - -export interface LoginRequest { - username: string; - password: string; + createdAt?: Date; + updatedAt?: Date; } export interface UpdateUserRequest { - uuid: string; + id: string; name: string; email: string; password: string; - permission: auth.Permission; + permission: string; status: UserStatus; - types: UserTypes; - avatar: Nullable; - details: Nullable; - preferences: Nullable; + type: UserType; +} + +export interface UpdateUserDetailsRequest { + id: number; + userId: string; + title: string; + firstName: string; + lastName: string; + address: string; + city: string; + zipCode: string; + country: string; + phone: string; +} + +export interface RefreshRequest { + refresh_token: string; +} + +export interface UpdateUserPreferencesRequest { + id: number; + userId: string; + useIdle: boolean; + idleTimeout: number; + useIdlePassword: boolean; + idlePin: string; + useDirectLogin: boolean; + useQuadcodeLogin: boolean; + sendNoticesMail: boolean; + language: string; +} + +export interface UserCreateRequest { + name: string; + email: string; + password: string; + permission: string; + status: UserStatus; + type: UserType; + avatar?: Nullable; + details?: Nullable; + preferences?: Nullable; +} + +export interface UpdatePasswordRequest { + id: string; + password: string; } export interface User { + id: string; email: string; name: string; - permission: auth.Permission; - types: UserTypes; + permission: string; + type: UserType; status: UserStatus; - activatedAt: Nullable; - uuid: string; details: Nullable; preferences: Nullable; avatar: Nullable; + activatedAt: Nullable; createdAt?: Date; updatedAt?: Date; } @@ -241,12 +251,45 @@ export interface ForgotPasswordRequest { email: string; } +export interface UserPreferences { + id: number; + userId: string; + useIdle: boolean; + idleTimeout: number; + useIdlePassword: boolean; + idlePin: string; + useDirectLogin: boolean; + useQuadcodeLogin: boolean; + sendNoticesMail: boolean; + language: string; + createdAt?: Date; + updatedAt?: Date; +} + +export interface LoginRequest { + username: string; + password: string; +} + +export interface ResetPasswordRequest { + token: string; + password: string; +} + +export type UserTypes = (typeof EnumUserTypes)[keyof typeof EnumUserTypes]; + export type UserStatus = string; -export type UserTypes = string[]; +export type UserType = string; export const EnumUserStatus = { UserStatusPending: "pending", UserStatusActive: "active", UserStatusDisabled: "disabled", } as const; + +export const EnumUserTypes = { + UserTypeInternal: "internal", + UserTypeExternal: "external", + UserTypeSurveyor: "surveyor", +} as const; diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue index 161404d..5350e7e 100644 --- a/frontend/src/layouts/AdminLayout.vue +++ b/frontend/src/layouts/AdminLayout.vue @@ -46,6 +46,10 @@ +
+ {{userStore.userData ? `Logged in as: ${userStore.userData} ` : 'Not logged in'}}
+ +
@@ -55,13 +59,28 @@ diff --git a/frontend/src/pages/admin/UsersPage.vue b/frontend/src/pages/admin/UsersPage.vue index 6191301..34c8d28 100644 --- a/frontend/src/pages/admin/UsersPage.vue +++ b/frontend/src/pages/admin/UsersPage.vue @@ -117,25 +117,25 @@ - + Show - + Edit - + Edit avatar - + @@ -167,415 +167,68 @@ - - - - -
-
-
{{ dialogMode === 'create' ? 'Nuovo utente' : dialogMode === 'edit' ? 'Modifica utente' : 'Dettaglio utente' }}
-
{{ form.name || 'Profilo utente' }}
-
{{ form.email || 'Compila i dati di base' }}
-
-
+ -
- - -
+ - - - - - -
+ - - - - - -
-

Account

-
- - - -
-
- - {{ avatarInitials(form) }} -
-
-
Avatar
-
- {{ form.avatar ? 'Avatar profilo impostato' : 'Nessun avatar impostato' }} -
-
- -
- - - - -
-
-
- - -
-
-

Details

- -
-
- - - - - - - - -
-
-
- - -
-
-

Preferences

- -
-
- - - - - - - - -
-
-
-
-
-
-
-
- - - - -
Change password
-
{{ passwordDialogUserEmail || 'User' }}
-
- - - - - - - - - - - - -
-
- - - - -
User access
-
{{ blockDialogUser.email || 'User' }}
-
- Stato attuale: {{ blockDialogUser.status || 'n/a' }} -
-
- - - - - -
- {{ - blockDialogBlocked - ? 'L’utente non potra piu accedere finche non verra sbloccato.' - : 'L’utente potra accedere normalmente.' - }} -
-
- - - - - -
-
- - - - -
-
-
Avatar editor
-
{{ avatarDialogUser.email || 'User avatar' }}
-
Ritaglio circolare per il profilo utente
-
-
- -
- - -
-
- - - - -
- - - - -
- {{ avatarFile?.name || 'Nessun file selezionato' }} -
- - -
- -
-
-
- -
-
- Seleziona un’immagine per modificare l’avatar. -
-
- -
-
Anteprima
-
- Avatar preview - No avatar -
-
-
-
-
-
+ + + diff --git a/frontend/src/pages/admin/dialogs/UserBlockDialog.vue b/frontend/src/pages/admin/dialogs/UserBlockDialog.vue new file mode 100644 index 0000000..9f579a8 --- /dev/null +++ b/frontend/src/pages/admin/dialogs/UserBlockDialog.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/frontend/src/pages/admin/dialogs/UserEditorDialog.vue b/frontend/src/pages/admin/dialogs/UserEditorDialog.vue new file mode 100644 index 0000000..ef75903 --- /dev/null +++ b/frontend/src/pages/admin/dialogs/UserEditorDialog.vue @@ -0,0 +1,636 @@ + + + + + + + diff --git a/frontend/src/pages/admin/dialogs/UserPasswordDialog.vue b/frontend/src/pages/admin/dialogs/UserPasswordDialog.vue new file mode 100644 index 0000000..268b5a3 --- /dev/null +++ b/frontend/src/pages/admin/dialogs/UserPasswordDialog.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/frontend/src/pages/admin/dialogs/types.ts b/frontend/src/pages/admin/dialogs/types.ts new file mode 100644 index 0000000..e349555 --- /dev/null +++ b/frontend/src/pages/admin/dialogs/types.ts @@ -0,0 +1,114 @@ +import type { User, UserDetails, UserPreferences, UserStatus } from 'src/api/users'; + +export type DialogMode = 'create' | 'edit' | 'view'; + +export type EditorTab = 'account' | 'details' | 'preferences'; + +export type UserDetailsForm = Omit; + +export type UserPreferencesForm = Omit; + +export interface UserFormState { + id: string; + name: string; + email: string; + password: string; + status: UserStatus; + permission: string; + type: string; + avatar: string; + details: UserDetailsForm; + preferences: UserPreferencesForm; +} + +export interface PasswordFormState { + password: string; + confirmPassword: string; +} + +export interface BlockDialogState { + id: string; + email: string; + status: UserStatus; + blocked: boolean; +} + +export interface AvatarDialogState { + id: string; + email: string; + avatar: string; +} + +export function createEmptyUserForm(): UserFormState { + return { + id: '', + name: '', + email: '', + password: '', + status: 'pending', + permission: 'user', + type: 'internal', + avatar: '', + details: { + id: 0, + userId: '', + title: '', + firstName: '', + lastName: '', + address: '', + city: '', + zipCode: '', + country: '', + phone: '', + }, + preferences: { + id: 0, + userId: '', + useIdle: false, + idleTimeout: 0, + useIdlePassword: false, + idlePin: '', + useDirectLogin: false, + useQuadcodeLogin: false, + sendNoticesMail: false, + language: 'it', + }, + }; +} + +export function mapUserToForm(user: User): UserFormState { + return { + id: user.id, + name: user.name, + email: user.email, + password: '', + status: user.status, + permission: user.permission, + type: user.type, + avatar: user.avatar ?? '', + details: { + id: user.details?.id ?? 0, + userId: user.details?.userId ?? '', + title: user.details?.title ?? '', + firstName: user.details?.firstName ?? '', + lastName: user.details?.lastName ?? '', + address: user.details?.address ?? '', + city: user.details?.city ?? '', + zipCode: user.details?.zipCode ?? '', + country: user.details?.country ?? '', + phone: user.details?.phone ?? '', + }, + preferences: { + id: user.preferences?.id ?? 0, + userId: user.preferences?.userId ?? '', + useIdle: user.preferences?.useIdle ?? false, + idleTimeout: user.preferences?.idleTimeout ?? 0, + useIdlePassword: user.preferences?.useIdlePassword ?? false, + idlePin: user.preferences?.idlePin ?? '', + useDirectLogin: user.preferences?.useDirectLogin ?? false, + useQuadcodeLogin: user.preferences?.useQuadcodeLogin ?? false, + sendNoticesMail: user.preferences?.sendNoticesMail ?? false, + language: user.preferences?.language ?? 'it', + }, + }; +} diff --git a/frontend/src/pages/dev/SignupPage.vue b/frontend/src/pages/dev/SignupPage.vue index b81f5ca..3232346 100644 --- a/frontend/src/pages/dev/SignupPage.vue +++ b/frontend/src/pages/dev/SignupPage.vue @@ -58,7 +58,7 @@