Compare commits
3 Commits
f35fcbc875
...
c8784320cf
| Author | SHA1 | Date |
|---|---|---|
|
|
c8784320cf | |
|
|
0a7cc993d4 | |
|
|
3b5c39ffc0 |
Binary file not shown.
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,56 +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
|
||||
)
|
||||
|
||||
type AllRoles struct {
|
||||
Roles map[string]string `json:"roles"`
|
||||
}
|
||||
|
||||
func GetRoles(c fiber.Ctx) error {
|
||||
a := AllRoles{
|
||||
Roles: make(map[string]string),
|
||||
}
|
||||
a.Roles["RoleSuperAdmin"] = "superadmin"
|
||||
a.Roles["RoleAdmin"] = "admin"
|
||||
a.Roles["RoleManager"] = "manager"
|
||||
a.Roles["RoleContentCreator"] = "content_creator"
|
||||
a.Roles["RoleUser"] = "user"
|
||||
a.Roles["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
|
||||
|
|
@ -87,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
|
||||
|
|
@ -95,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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"server/internal/auth"
|
||||
"server/internal/config"
|
||||
|
||||
"time"
|
||||
|
|
@ -23,8 +22,9 @@ type TockenService struct {
|
|||
}
|
||||
|
||||
type Claims struct {
|
||||
Username string `json:"username"`
|
||||
Permission int `json:"permission"`
|
||||
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,12 +158,12 @@ 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,
|
||||
ID: id,
|
||||
Impersonator: "",
|
||||
Permission: permission,
|
||||
TokenType: tokenType,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: s.cfg.Issuer,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,77 +1,75 @@
|
|||
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 `json:"id" 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<Date>"`
|
||||
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"`
|
||||
CreatedAt *time.Time `json:"createdAt" ts:"type=Nullable<Date>"`
|
||||
UpdatedAt *time.Time `json:"updatedAt" ts:"type=Nullable<Date>"`
|
||||
DeletedAt *gorm.DeletedAt `json:"-" gorm:"index" ts:"type=Nullable<Date>"`
|
||||
ActivatedAt *time.Time `json:"activatedAt" ts:"type=Nullable<Date>"`
|
||||
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"`
|
||||
type UpdateUserProfileRequest struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Permission auth.Permission `json:"permission"`
|
||||
Types UserTypes `json:"types"`
|
||||
Permission string `json:"permission"`
|
||||
Type UserType `json:"type"`
|
||||
Status UserStatus `json:"status"`
|
||||
ActivatedAt *time.Time `json:"activatedAt" ts:"type=Nullable<Date>"`
|
||||
UUID string `json:"uuid"`
|
||||
Details *UserDetails `json:"details"`
|
||||
Preferences *UserPreferences `json:"preferences"`
|
||||
Avatar *string `json:"avatar"`
|
||||
CreatedAt *time.Time `json:"createdAt" ts:"type=Nullable<Date>"`
|
||||
UpdatedAt *time.Time `json:"updatedAt" ts:"type=Nullable<Date>"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
IdlePin string `json:"idlePin"`
|
||||
UseDirectLogin bool `json:"useDirectLogin"`
|
||||
UseQuadcodeLogin bool `json:"useQuadcodeLogin"`
|
||||
SendNoticesMail bool `json:"sendNoticesMail"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt *time.Time `json:"createdAt,omitempty" ts:"type=Date"`
|
||||
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"`
|
||||
|
|
@ -80,15 +78,28 @@ type UserPreferences struct {
|
|||
UseQuadcodeLogin bool `json:"useQuadcodeLogin"`
|
||||
SendNoticesMail bool `json:"sendNoticesMail"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt *time.Time `json:"createdAt" ts:"type=Nullable<Date>"`
|
||||
UpdatedAt *time.Time `json:"updatedAt" ts:"type=Nullable<Date>"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Address string `json:"address"`
|
||||
City string `json:"city"`
|
||||
ZipCode string `json:"zipCode"`
|
||||
Country string `json:"country"`
|
||||
Phone string `json:"phone"`
|
||||
CreatedAt *time.Time `json:"createdAt,omitempty" ts:"type=Date"`
|
||||
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"`
|
||||
|
|
@ -97,8 +108,6 @@ type UserDetails struct {
|
|||
ZipCode string `json:"zipCode"`
|
||||
Country string `json:"country"`
|
||||
Phone string `json:"phone"`
|
||||
CreatedAt *time.Time `json:"createdAt" ts:"type=Nullable<Date>"`
|
||||
UpdatedAt *time.Time `json:"updatedAt" ts:"type=Nullable<Date>"`
|
||||
}
|
||||
|
||||
// UserDetails holds optional profile data.
|
||||
|
|
@ -106,27 +115,27 @@ type UserDetails struct {
|
|||
// 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"`
|
||||
ExpiresAt time.Time `json:"expiresAt" ts:"type=Nullable<Date>" gorm:"index"`
|
||||
IPAddress string `json:"ipAddress" gorm:"size:64"`
|
||||
UserAgent string `json:"userAgent" gorm:"size:512"`
|
||||
CreatedAt time.Time `json:"createdAt" ts:"type=Nullable<Date>"`
|
||||
UpdatedAt *time.Time `json:"updatedAt" ts:"type=Nullable<Date>"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty" ts:"type=Date"`
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty" ts:"type=Date"`
|
||||
DeletedAt *gorm.DeletedAt `json:"-" gorm:"index" `
|
||||
}
|
||||
|
||||
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" ts:"type=Nullable<Date>" gorm:"index"`
|
||||
UsedAt *time.Time `json:"usedAt" ts:"type=Nullable<Date>"`
|
||||
CreatedAt *time.Time `json:"createdAt" ts:"type=Nullable<Date>"`
|
||||
UpdatedAt *time.Time `json:"updatedAt" ts:"type=Nullable<Date>"`
|
||||
ExpiresAt time.Time `json:"expiresAt,omitempty" ts:"type=Date" gorm:"index"`
|
||||
UsedAt *time.Time `json:"usedAt,omitempty" ts:"type=Date"`
|
||||
CreatedAt *time.Time `json:"createdAt" ts:"type=Date"`
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty" ts:"type=Date"`
|
||||
DeletedAt *gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
}
|
||||
|
||||
|
|
@ -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"`
|
||||
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 auth.Permission `json:"permission"`
|
||||
Permission string `json:"permission"`
|
||||
Status UserStatus `json:"status"`
|
||||
Types UserTypes `json:"types"`
|
||||
Avatar *string `json:"avatar"`
|
||||
Details *UserDetails `json:"details"`
|
||||
Preferences *UserPreferences `json:"preferences"`
|
||||
Type UserType `json:"type"`
|
||||
}
|
||||
|
||||
// UserCreateRequest captures the minimal payload to create a user.
|
||||
|
|
@ -181,10 +198,10 @@ 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"`
|
||||
Avatar *string `json:"avatar"`
|
||||
Details *UserDetails `json:"details" `
|
||||
Preferences *UserPreferences `json:"preferences" `
|
||||
Type UserType `json:"type"`
|
||||
Avatar *string `json:"avatar,omitempty"`
|
||||
Details *UserDetails `json:"details,omitempty"`
|
||||
Preferences *UserPreferences `json:"preferences,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
|
|
@ -79,6 +80,285 @@ type TSSourceFile struct {
|
|||
Len int
|
||||
}
|
||||
|
||||
func (i *TSInfo) Populate(path string, excludedPackages ExcudedPackages) {
|
||||
i.Packages = make(map[string]TSInfoPakage)
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.NeedName |
|
||||
packages.NeedFiles |
|
||||
packages.NeedCompiledGoFiles |
|
||||
packages.NeedSyntax,
|
||||
Dir: path,
|
||||
}
|
||||
|
||||
pkgs, err := packages.Load(cfg, "./...")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if packages.PrintErrors(pkgs) > 0 {
|
||||
log.Fatal("package loading failed")
|
||||
}
|
||||
|
||||
//to get line info for endpoints and typescript declarations
|
||||
for _, loadedPkg := range pkgs {
|
||||
pkg := loadedPkg.Name
|
||||
// skip packages
|
||||
skip := false
|
||||
for _, excluded := range excludedPackages {
|
||||
if pkg == excluded {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
// initialize package info if not exists
|
||||
if _, ok := i.Packages[pkg]; !ok {
|
||||
ip := TSInfoPakage{}
|
||||
ip.MakeMaps()
|
||||
i.Packages[pkg] = ip
|
||||
}
|
||||
|
||||
// start parsing files
|
||||
for _, n := range loadedPkg.CompiledGoFiles {
|
||||
// search for declarative // Typescript: declarations and endpoints
|
||||
i.parseTypescriptDeclarations(pkg, n)
|
||||
}
|
||||
// parse doc to get all types and consts
|
||||
docPkg, err := doc.NewFromFiles(loadedPkg.Fset, loadedPkg.Syntax, loadedPkg.PkgPath)
|
||||
if err != nil {
|
||||
exitOnError(err)
|
||||
}
|
||||
|
||||
for _, t := range docPkg.Types {
|
||||
i.getType(pkg, t)
|
||||
}
|
||||
|
||||
for _, c := range docPkg.Consts {
|
||||
i.getConst(pkg, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// estrae le definizioni typescript // Typescript: ...
|
||||
func (ts *TSInfo) parseTypescriptDeclarations(p string, n string) {
|
||||
pkg := ts.Packages[p]
|
||||
f, err := os.OpenFile(n, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Fatalf("open file error: %v", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
l := 1
|
||||
dat := ""
|
||||
lines := []TSSourceLine{}
|
||||
rd := bufio.NewReader(f)
|
||||
// scansione del file per trovare le dichiarazioni typescript
|
||||
for {
|
||||
line, err := rd.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
log.Fatalf("read file line error: %v", err)
|
||||
return
|
||||
}
|
||||
lines = append(lines, TSSourceLine{Pos: l, End: l + len(line), Line: l, Source: line})
|
||||
l++
|
||||
dat += line
|
||||
// è una definizione typescript?
|
||||
if strings.Contains(line, "// Typescript:") {
|
||||
// typescript type, ad esempio: // Typescript: TStype=MyType=string | number
|
||||
if strings.Contains(line, "TStype=") {
|
||||
p := strings.Index(line, "TStype=")
|
||||
s := strings.Trim(line[p+len("TStype="):], " ")
|
||||
a := strings.Split(s, "=")
|
||||
if len(a) == 2 {
|
||||
t := TSType{
|
||||
Name: strings.Trim(a[0], " "),
|
||||
Typescript: true,
|
||||
Type: "UserDefined",
|
||||
TsType: strings.Trim(a[1], " "),
|
||||
dependOn: false,
|
||||
}
|
||||
pkg.types[strings.Trim(a[0], " ")] = t
|
||||
}
|
||||
}
|
||||
// dicchiarazione typescript, ad esempio: // Typescript: TSDeclaration=MyType=string | number
|
||||
if strings.Contains(line, "TSDeclaration=") {
|
||||
p := strings.Index(line, "TSDeclaration=")
|
||||
s := strings.Trim(line[p+len("TSDeclaration="):], " ")
|
||||
a := strings.Split(s, "=")
|
||||
if len(a) == 2 {
|
||||
t := TSDec{
|
||||
Name: strings.Trim(a[0], " "),
|
||||
Value: strings.Trim(a[1], " "),
|
||||
}
|
||||
pkg.decs[strings.Trim(a[0], " ")] = t
|
||||
}
|
||||
}
|
||||
// dichiarazione endpoint, ad esempio: // Typescript: TSEndpoint= path=/api/auth/login; name=login; method=POST; request=users.LoginRequest; response=tokens.TokenPair
|
||||
if strings.Contains(line, "TSEndpoint= ") {
|
||||
e := ParseEndpoint(line, n, l)
|
||||
if _, ok := pkg.endpoints[e.Name]; ok {
|
||||
exitOnError(fmt.Errorf("enpoint name %s allready in use: %s", e.Name, line))
|
||||
}
|
||||
pkg_info := pkg
|
||||
pkg_info.endpoints[e.Name] = e
|
||||
pkg_info.imports = e.Imports
|
||||
pkg_info.isUsed = true
|
||||
pkg = pkg_info
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *TSInfo) getType(p string, t *doc.Type) {
|
||||
var isTypescript = strings.HasPrefix(t.Doc, "Typescript:")
|
||||
if isTypescript {
|
||||
command := strings.TrimPrefix(t.Doc, "Typescript:")
|
||||
command = strings.TrimSpace(command)
|
||||
command = strings.Trim(command, "\n")
|
||||
}
|
||||
for _, spec := range t.Decl.Specs {
|
||||
if len(t.Consts) > 0 {
|
||||
i.getConst(p, t.Consts[0])
|
||||
}
|
||||
switch spec.(type) {
|
||||
case *ast.TypeSpec:
|
||||
typeSpec := spec.(*ast.TypeSpec)
|
||||
switch typeSpec.Type.(type) {
|
||||
case *ast.StructType:
|
||||
v := TSStruct{
|
||||
Name: typeSpec.Name.Name,
|
||||
Typescript: false,
|
||||
Fields: []TSSField{},
|
||||
}
|
||||
v.getStruct(typeSpec)
|
||||
if len(v.Fields) == 0 {
|
||||
continue
|
||||
}
|
||||
i.Packages[p].structs[typeSpec.Name.Name] = v
|
||||
for _, imp := range v.Imports {
|
||||
a := strings.Split(imp, ".")
|
||||
if len(a) == 2 {
|
||||
if _, ok := i.Packages[p].imports[a[0]]; !ok {
|
||||
i.Packages[p].imports[a[0]] = []string{}
|
||||
}
|
||||
if !slices.Contains(i.Packages[p].imports[a[0]], a[1]) {
|
||||
i.Packages[p].imports[a[0]] = append(i.Packages[p].imports[a[0]], a[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.Ident:
|
||||
tsInfo := getFieldTsInfo(typeSpec.Type)
|
||||
t := TSType{
|
||||
Name: typeSpec.Name.Name,
|
||||
Typescript: true,
|
||||
Type: getFieldInfo(typeSpec.Type),
|
||||
TsType: tsInfo,
|
||||
dependOn: toBeImported(typeSpec.Type),
|
||||
}
|
||||
//fmt.Printf("Type found: %s Type: %s TsType: %s AT: %s\n", t.Name, t.Type, t.TsType, t.SourceInfo)
|
||||
i.Packages[p].types[typeSpec.Name.Name] = t
|
||||
default:
|
||||
// if isType && command != "interface" {
|
||||
// exitOnError(fmt.Errorf("mismatch delaration for interface %s AT: %s", t.Doc, getSourceInfo(int(typeSpec.Name.NamePos), src)))
|
||||
// }
|
||||
tsInfo := getFieldTsInfo(typeSpec.Type)
|
||||
t := TSType{
|
||||
Name: typeSpec.Name.Name,
|
||||
Typescript: true,
|
||||
Type: getFieldInfo(typeSpec.Type),
|
||||
TsType: tsInfo,
|
||||
dependOn: toBeImported(typeSpec.Type),
|
||||
}
|
||||
//fmt.Printf("Type found: %s Type: %s TsType: %s AT: %s\n", t.Name, t.Type, t.TsType, t.SourceInfo)
|
||||
i.Packages[p].types[typeSpec.Name.Name] = t
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *TSInfo) getConst(p string, c *doc.Value) {
|
||||
var isTypescript = strings.HasPrefix(c.Doc, "Typescript:")
|
||||
if isTypescript {
|
||||
command := strings.TrimPrefix(c.Doc, "Typescript:")
|
||||
command = strings.TrimSpace(command)
|
||||
command = strings.Trim(command, "\n")
|
||||
|
||||
if strings.Contains(command, "enum=") {
|
||||
enumName := strings.TrimPrefix(command, "enum=")
|
||||
enum := TSEnum{
|
||||
Name: enumName,
|
||||
Info: []TSEnumInfo{},
|
||||
}
|
||||
d := c.Decl
|
||||
iota := false
|
||||
iotaValue := 0
|
||||
for _, s := range d.Specs {
|
||||
v := s.(*ast.ValueSpec) // safe because decl.Tok == token.CONST
|
||||
if len(v.Values) > 0 {
|
||||
be, ok := v.Values[0].(*ast.BinaryExpr)
|
||||
if ok {
|
||||
x := be.X.(*ast.BasicLit)
|
||||
exitOnError(fmt.Errorf("enum binary expression not implemented %s %s %s", x.Value, be.Op.String(), be.Y))
|
||||
}
|
||||
ident, ok := v.Values[0].(*ast.Ident)
|
||||
if ok {
|
||||
if ident.Name == "iota" {
|
||||
iota = true
|
||||
iotaValue = v.Names[0].Obj.Data.(int)
|
||||
enum.Info = append(enum.Info, TSEnumInfo{Key: v.Names[0].Name, Value: fmt.Sprintf("%d", iotaValue)})
|
||||
}
|
||||
}
|
||||
list, ok := v.Values[0].(*ast.BasicLit)
|
||||
if ok {
|
||||
enum.Info = append(enum.Info, TSEnumInfo{Key: v.Names[0].Name, Value: list.Value})
|
||||
}
|
||||
} else {
|
||||
for _, name := range v.Names {
|
||||
if iota {
|
||||
iotaValue++
|
||||
enum.Info = append(enum.Info, TSEnumInfo{Key: name.Name, Value: fmt.Sprintf("%d", iotaValue)})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
i.Packages[p].enums[enumName] = enum
|
||||
t1 := TSType{
|
||||
Name: enumName,
|
||||
Typescript: true,
|
||||
Type: "",
|
||||
TsType: fmt.Sprintf("typeof Enum%s[keyof typeof Enum%s] ", enumName, enumName), //getFieldTsInfo(expr.Type),
|
||||
dependOn: false,
|
||||
}
|
||||
i.Packages[p].types[enumName] = t1
|
||||
}
|
||||
|
||||
if strings.Contains(command, "const") {
|
||||
d := c.Decl
|
||||
for _, s := range d.Specs {
|
||||
v := s.(*ast.ValueSpec)
|
||||
if len(v.Names) == 0 || len(v.Values) == 0 {
|
||||
continue
|
||||
}
|
||||
c := TSConst{
|
||||
Name: v.Names[0].Name,
|
||||
Value: v.Values[0].(*ast.BasicLit).Value,
|
||||
}
|
||||
i.Packages[p].consts[c.Name] = c
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(command, "type") {
|
||||
fmt.Printf("TypeScript type declaration found in const s, but type parsing is not implemented yet\n")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TSInfo) findStruct(p string, n string) bool {
|
||||
|
||||
if _, ok := ts.Packages[p]; ok {
|
||||
|
|
@ -127,269 +407,6 @@ func (ts TSInfo) find(p string, n string) bool {
|
|||
|
||||
// popola TsInfo con tutte le definizioni dei tipi
|
||||
|
||||
func (i *TSInfo) getConst(p string, c *doc.Value) {
|
||||
var isTypescript = strings.HasPrefix(c.Doc, "Typescript:")
|
||||
if isTypescript {
|
||||
command := strings.TrimPrefix(c.Doc, "Typescript:")
|
||||
command = strings.TrimSpace(command)
|
||||
command = strings.Trim(command, "\n")
|
||||
|
||||
if strings.Contains(command, "type") {
|
||||
fmt.Printf("TypeScript type declaration found in const s, but type parsing is not implemented yet\n")
|
||||
}
|
||||
|
||||
if strings.Contains(command, "enum=") {
|
||||
enumName := strings.TrimPrefix(command, "enum=")
|
||||
enum := TSEnum{
|
||||
Name: enumName,
|
||||
Info: []TSEnumInfo{},
|
||||
}
|
||||
d := c.Decl
|
||||
iota := false
|
||||
iotaValue := 0
|
||||
for _, s := range d.Specs {
|
||||
v := s.(*ast.ValueSpec) // safe because decl.Tok == token.CONST
|
||||
if len(v.Values) > 0 {
|
||||
be, ok := v.Values[0].(*ast.BinaryExpr)
|
||||
if ok {
|
||||
x := be.X.(*ast.BasicLit)
|
||||
exitOnError(fmt.Errorf("enum binary expression not implemented %s %s %s", x.Value, be.Op.String(), be.Y))
|
||||
}
|
||||
ident, ok := v.Values[0].(*ast.Ident)
|
||||
if ok {
|
||||
if ident.Name == "iota" {
|
||||
iota = true
|
||||
iotaValue = v.Names[0].Obj.Data.(int)
|
||||
enum.Info = append(enum.Info, TSEnumInfo{Key: v.Names[0].Name, Value: fmt.Sprintf("%d", iotaValue)})
|
||||
}
|
||||
}
|
||||
list, ok := v.Values[0].(*ast.BasicLit)
|
||||
if ok {
|
||||
enum.Info = append(enum.Info, TSEnumInfo{Key: v.Names[0].Name, Value: list.Value})
|
||||
}
|
||||
} else {
|
||||
for _, name := range v.Names {
|
||||
if iota {
|
||||
iotaValue++
|
||||
enum.Info = append(enum.Info, TSEnumInfo{Key: name.Name, Value: fmt.Sprintf("%d", iotaValue)})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
i.Packages[p].enums[enumName] = enum
|
||||
t1 := TSType{
|
||||
Name: enumName,
|
||||
Typescript: false,
|
||||
Type: "",
|
||||
TsType: fmt.Sprintf("typeof Enum%s[keyof typeof Enum%s] ", enumName, enumName), //getFieldTsInfo(expr.Type),
|
||||
dependOn: false,
|
||||
}
|
||||
i.Packages[p].types[enumName] = t1
|
||||
}
|
||||
|
||||
if strings.Contains(command, "const") {
|
||||
d := c.Decl
|
||||
for _, s := range d.Specs {
|
||||
v := s.(*ast.ValueSpec)
|
||||
if len(v.Names) == 0 || len(v.Values) == 0 {
|
||||
continue
|
||||
}
|
||||
c := TSConst{
|
||||
Name: v.Names[0].Name,
|
||||
Value: v.Values[0].(*ast.BasicLit).Value,
|
||||
}
|
||||
i.Packages[p].consts[c.Name] = c
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(command, "type") {
|
||||
fmt.Printf("TypeScript type declaration found in const s, but type parsing is not implemented yet\n")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (i *TSInfo) getType(p string, t *doc.Type) {
|
||||
var isTypescript = strings.HasPrefix(t.Doc, "Typescript:")
|
||||
if isTypescript {
|
||||
command := strings.TrimPrefix(t.Doc, "Typescript:")
|
||||
command = strings.TrimSpace(command)
|
||||
command = strings.Trim(command, "\n")
|
||||
|
||||
if strings.Contains(command, "interface") {
|
||||
fmt.Printf("TypeScript interface declaration found in type s, but interface parsing is not implemented yet\n")
|
||||
}
|
||||
|
||||
if strings.Contains(command, "type") {
|
||||
fmt.Printf("TypeScript type declaration found in type s, but type parsing is not implemented yet\n")
|
||||
}
|
||||
|
||||
if strings.Contains(command, "enum=") {
|
||||
fmt.Printf("TypeScript enum declaration found in type s, but enum parsing is not implemented yet\n")
|
||||
}
|
||||
}
|
||||
for _, spec := range t.Decl.Specs {
|
||||
/* if len(t.Consts) > 0 {
|
||||
i.getConst(p, t.Consts[0])
|
||||
continue
|
||||
} */
|
||||
switch spec.(type) {
|
||||
case *ast.TypeSpec:
|
||||
typeSpec := spec.(*ast.TypeSpec)
|
||||
switch typeSpec.Type.(type) {
|
||||
case *ast.StructType:
|
||||
// if isType && command != "interface" {
|
||||
// exitOnError(fmt.Errorf("mismatch delaration for interface %s AT: %s", t.Doc, getSourceInfo(int(typeSpec.Name.NamePos), src)))
|
||||
// }
|
||||
v := TSStruct{
|
||||
Name: typeSpec.Name.Name,
|
||||
Typescript: false,
|
||||
Fields: []TSSField{},
|
||||
SourceInfo: "",
|
||||
}
|
||||
v.getStruct(typeSpec)
|
||||
i.Packages[p].structs[typeSpec.Name.Name] = v
|
||||
default:
|
||||
// if isType && command != "interface" {
|
||||
// exitOnError(fmt.Errorf("mismatch delaration for interface %s AT: %s", t.Doc, getSourceInfo(int(typeSpec.Name.NamePos), src)))
|
||||
// }
|
||||
tsInfo := getFieldTsInfo(typeSpec.Type)
|
||||
t := TSType{
|
||||
Name: typeSpec.Name.Name,
|
||||
Typescript: false,
|
||||
Type: getFieldInfo(typeSpec.Type),
|
||||
TsType: tsInfo,
|
||||
dependOn: toBeImported(typeSpec.Type),
|
||||
}
|
||||
//fmt.Printf("Type found: %s Type: %s TsType: %s AT: %s\n", t.Name, t.Type, t.TsType, t.SourceInfo)
|
||||
i.Packages[p].types[typeSpec.Name.Name] = t
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseTypescriptDeclarations(n string, pkg TSInfoPakage) {
|
||||
f, err := os.OpenFile(n, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Fatalf("open file error: %v", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
l := 1
|
||||
dat := ""
|
||||
lines := []TSSourceLine{}
|
||||
rd := bufio.NewReader(f)
|
||||
for {
|
||||
line, err := rd.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
log.Fatalf("read file line error: %v", err)
|
||||
return
|
||||
}
|
||||
lines = append(lines, TSSourceLine{Pos: l, End: l + len(line), Line: l, Source: line})
|
||||
l++
|
||||
dat += line
|
||||
|
||||
if strings.Contains(line, "// Typescript:") {
|
||||
if strings.Contains(line, "TStype=") {
|
||||
p := strings.Index(line, "TStype=")
|
||||
s := strings.Trim(line[p+len("TStype="):], " ")
|
||||
a := strings.Split(s, "=")
|
||||
if len(a) == 2 {
|
||||
t := TSType{
|
||||
Name: strings.Trim(a[0], " "),
|
||||
Typescript: true,
|
||||
Type: "UserDefined",
|
||||
TsType: strings.Trim(a[1], " "),
|
||||
dependOn: false,
|
||||
}
|
||||
pkg.types[strings.Trim(a[0], " ")] = t
|
||||
}
|
||||
|
||||
}
|
||||
if strings.Contains(line, "TSDeclaration=") {
|
||||
p := strings.Index(line, "TSDeclaration=")
|
||||
s := strings.Trim(line[p+len("TSDeclaration="):], " ")
|
||||
a := strings.Split(s, "=")
|
||||
if len(a) == 2 {
|
||||
t := TSDec{
|
||||
Name: strings.Trim(a[0], " "),
|
||||
Value: strings.Trim(a[1], " "),
|
||||
}
|
||||
pkg.decs[strings.Trim(a[0], " ")] = t
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(line, "TSEndpoint= ") {
|
||||
e := ParseEndpoint(line, n, l)
|
||||
if _, ok := pkg.endpoints[e.Name]; ok {
|
||||
exitOnError(fmt.Errorf("enpoint name %s allready in use: %s", e.Name, line))
|
||||
}
|
||||
pkg_info := pkg
|
||||
pkg_info.endpoints[e.Name] = e
|
||||
pkg_info.imports = e.Imports
|
||||
pkg_info.isUsed = true
|
||||
pkg = pkg_info
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (i *TSInfo) Populate(path string) {
|
||||
i.Packages = make(map[string]TSInfoPakage)
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.NeedName |
|
||||
packages.NeedFiles |
|
||||
packages.NeedCompiledGoFiles |
|
||||
packages.NeedSyntax,
|
||||
Dir: path,
|
||||
}
|
||||
|
||||
pkgs, err := packages.Load(cfg, "./...")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if packages.PrintErrors(pkgs) > 0 {
|
||||
log.Fatal("package loading failed")
|
||||
}
|
||||
|
||||
//to get line info for endpoints and typescript declarations
|
||||
for _, loadedPkg := range pkgs {
|
||||
pkg := loadedPkg.Name
|
||||
// skip packages
|
||||
if pkg == "typescript" || pkg == "tsrpc" {
|
||||
continue
|
||||
}
|
||||
// initialize package info if not exists
|
||||
if _, ok := i.Packages[pkg]; !ok {
|
||||
i.Packages[pkg] = TSInfoPakage{structs: make(map[string]TSStruct), types: make(map[string]TSType), enums: make(map[string]TSEnum), consts: make(map[string]TSConst), decs: make(map[string]TSDec), endpoints: make(map[string]TSEndpoint)}
|
||||
}
|
||||
// start parsing files
|
||||
for _, n := range loadedPkg.CompiledGoFiles {
|
||||
// search for declarative // Typescript: declarations and endpoints
|
||||
parseTypescriptDeclarations(n, i.Packages[pkg])
|
||||
}
|
||||
// parse doc to get all types and consts
|
||||
docPkg, err := doc.NewFromFiles(loadedPkg.Fset, loadedPkg.Syntax, loadedPkg.PkgPath)
|
||||
if err != nil {
|
||||
exitOnError(err)
|
||||
}
|
||||
|
||||
for _, t := range docPkg.Types {
|
||||
i.getType(pkg, t)
|
||||
}
|
||||
|
||||
for _, c := range docPkg.Consts {
|
||||
i.getConst(pkg, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TSInfo) findAvailableStruct(pkg string, n string, deep int) (int, bool) {
|
||||
a := strings.Split(n, ".")
|
||||
if n == "" || IsNativeType(n) {
|
||||
|
|
@ -446,11 +463,11 @@ func (i *TSInfo) TestEndpoints() {
|
|||
for _, v1 := range i.Packages[p].endpoints {
|
||||
_, f := i.findAvailableStruct(p, v1.Request, 0)
|
||||
if !f {
|
||||
fmt.Printf("??Endpoint: request %s not found\n", v1.Request)
|
||||
fmt.Printf("\n??Endpoint: request %s not found\n", v1.Request)
|
||||
}
|
||||
_, f = i.findAvailableStruct(p, v1.Response, 0)
|
||||
if !f {
|
||||
fmt.Printf("??Endpoint: response %s not found\n", v1.Response)
|
||||
fmt.Printf("\n??Endpoint: response %s not found\n", v1.Response)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ package tsrpc
|
|||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type TSSField struct {
|
||||
|
|
@ -15,14 +17,13 @@ type TSSField struct {
|
|||
Json TSTagJson
|
||||
Ts TSTagTs
|
||||
DependOn bool
|
||||
SourceInfo string
|
||||
}
|
||||
|
||||
type TSStruct struct {
|
||||
Name string
|
||||
Typescript bool
|
||||
Fields []TSSField
|
||||
SourceInfo string
|
||||
Imports []string
|
||||
}
|
||||
|
||||
func IsNativeType(t string) bool {
|
||||
|
|
@ -137,7 +138,15 @@ func (s *TSStruct) getStruct(ts *ast.TypeSpec) {
|
|||
Type: getFieldInfo(field.Type),
|
||||
TsType: tsType,
|
||||
DependOn: toBeImported(field.Type),
|
||||
SourceInfo: "",
|
||||
}
|
||||
if len(strings.Split(f.Type, ".")) == 2 && tagTs.Type == "" {
|
||||
a := strings.Split(f.Type, ".")
|
||||
a[1] = strings.Trim(a[1], "[]*")
|
||||
a[0] = strings.Trim(a[0], "[]*")
|
||||
imp := fmt.Sprintf("%s.%s", a[0], a[1])
|
||||
if !slices.Contains(s.Imports, imp) {
|
||||
s.Imports = append(s.Imports, imp)
|
||||
}
|
||||
}
|
||||
s.Fields = append(s.Fields, f)
|
||||
} else {
|
||||
|
|
@ -149,7 +158,6 @@ func (s *TSStruct) getStruct(ts *ast.TypeSpec) {
|
|||
Type: getFieldInfo(field.Type),
|
||||
TsType: tsType,
|
||||
DependOn: toBeImported(field.Type),
|
||||
SourceInfo: "",
|
||||
}
|
||||
s.Fields = append(s.Fields, f)
|
||||
} else {
|
||||
|
|
@ -161,7 +169,6 @@ func (s *TSStruct) getStruct(ts *ast.TypeSpec) {
|
|||
Type: se.Name,
|
||||
TsType: tsType,
|
||||
DependOn: false,
|
||||
SourceInfo: "",
|
||||
}
|
||||
s.Fields = append(s.Fields, f)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package tsrpc
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -59,6 +60,9 @@ func (ts *TSSouces) ensurePackage(p string) {
|
|||
if pkg.Endpoints == nil {
|
||||
pkg.Endpoints = make(map[string]string)
|
||||
}
|
||||
if pkg.Imports == nil {
|
||||
pkg.Imports = make(map[string][]string)
|
||||
}
|
||||
|
||||
ts.Pakages[p] = pkg
|
||||
}
|
||||
|
|
@ -147,7 +151,7 @@ func structToTs(info TSInfo, p string, k string) (string, []string, error) {
|
|||
result += "}\n"
|
||||
}
|
||||
if fields == 0 {
|
||||
return result, dependencies, fmt.Errorf("struct %s not export fields AT: %s", k, info.Packages[p].structs[k].SourceInfo)
|
||||
return result, dependencies, fmt.Errorf("struct %s not export fields", k)
|
||||
}
|
||||
return result, dependencies, nil
|
||||
}
|
||||
|
|
@ -193,7 +197,7 @@ func (ts *TSSouces) AddDependencies(info TSInfo, p string, s string, dependencie
|
|||
|
||||
if info.findStruct(pk, st) {
|
||||
if emptySrtuct(info, pk, st) {
|
||||
ts.Errors = append(ts.Errors, fmt.Sprintf("Empty struct %s.%s AT: %s", pk, st, info.Packages[p].structs[s].SourceInfo))
|
||||
ts.Errors = append(ts.Errors, fmt.Sprintf("Empty struct %s.%s ", pk, st))
|
||||
}
|
||||
s, d, err := structToTs(info, pk, st)
|
||||
if err != nil {
|
||||
|
|
@ -209,14 +213,14 @@ func (ts *TSSouces) AddDependencies(info TSInfo, p string, s string, dependencie
|
|||
ts.ensurePackage(pk)
|
||||
ts.Pakages[pk].Types[st] = s
|
||||
} else {
|
||||
ts.Errors = append(ts.Errors, fmt.Sprintf("Dipendence not found %s.%s AT: %s", pk, st, info.Packages[p].structs[s].SourceInfo))
|
||||
ts.Errors = append(ts.Errors, fmt.Sprintf("Dipendence not found %s.%s", pk, st))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (ts *TSSouces) Populate(info TSInfo) {
|
||||
func (ts *TSSouces) BuildTSSources(info TSInfo) {
|
||||
ts.Pakages = make(map[string]TSModule)
|
||||
ts.Errors = []string{}
|
||||
for p := range info.Packages {
|
||||
|
|
@ -227,7 +231,7 @@ func (ts *TSSouces) Populate(info TSInfo) {
|
|||
for _, st := range info.Packages[p].structs {
|
||||
if st.Typescript {
|
||||
if len(st.Fields) == 0 {
|
||||
ts.Errors = append(ts.Errors, fmt.Sprintf("Empty struct %s.%s AT: %s", p, st.Name, info.Packages[p].structs[st.Name].SourceInfo))
|
||||
ts.Errors = append(ts.Errors, fmt.Sprintf("Empty struct %s.%s ", p, st.Name))
|
||||
}
|
||||
s, dependencies, err := structToTs(info, p, st.Name)
|
||||
if err != nil {
|
||||
|
|
@ -253,9 +257,9 @@ func (ts *TSSouces) Populate(info TSInfo) {
|
|||
}
|
||||
|
||||
for _, t := range info.Packages[p].types {
|
||||
if t.Typescript {
|
||||
|
||||
ts.Pakages[p].Types[t.Name] = fmt.Sprintf("export type %s = %s\n", t.Name, t.TsType)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, e := range info.Packages[p].endpoints {
|
||||
|
|
@ -280,5 +284,16 @@ func (ts *TSSouces) Populate(info TSInfo) {
|
|||
pkg.Endpoints[e.Name] = e.ToTs()
|
||||
ts.Pakages[p] = pkg
|
||||
}
|
||||
|
||||
for pk, t := range info.Packages[p].imports {
|
||||
pkg := ts.Pakages[p]
|
||||
if _, ok := pkg.Imports[pk]; !ok {
|
||||
for _, v := range t {
|
||||
if !slices.Contains(pkg.Imports[pk], v) {
|
||||
pkg.Imports[pk] = append(pkg.Imports[pk], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ var TSReport = ""
|
|||
|
||||
var tsFiles = TSFiles{}
|
||||
|
||||
type ExcudedPackages []string
|
||||
|
||||
func (ip *TSInfoPakage) MakeMaps() {
|
||||
*ip = TSInfoPakage{structs: make(map[string]TSStruct), types: make(map[string]TSType), enums: make(map[string]TSEnum), consts: make(map[string]TSConst), decs: make(map[string]TSDec), endpoints: make(map[string]TSEndpoint), imports: make(map[string][]string), isUsed: false}
|
||||
}
|
||||
|
||||
func GetTSSource() error {
|
||||
path := ""
|
||||
if value, exists := os.LookupEnv("TS_GENERATOR_PATH"); exists {
|
||||
|
|
@ -26,9 +32,9 @@ func GetTSSource() error {
|
|||
|
||||
var tsInfoData = TSInfo{}
|
||||
var tsSourcesData = TSSouces{}
|
||||
tsInfoData.Populate(path)
|
||||
tsInfoData.Populate(path, ExcudedPackages{"Typescript", "tsrpc"})
|
||||
tsInfoData.TestEndpoints()
|
||||
tsSourcesData.Populate(tsInfoData)
|
||||
tsSourcesData.BuildTSSources(tsInfoData)
|
||||
|
||||
if len(tsSourcesData.Errors) != 0 {
|
||||
err := ""
|
||||
|
|
@ -119,6 +125,7 @@ func GetTSSource() error {
|
|||
for f := range tsSourcesData.Pakages[p].Imports {
|
||||
imports += "import type * as " + f + " from './" + f + ".ts'\n"
|
||||
}
|
||||
|
||||
tmp += imports
|
||||
tmp += source
|
||||
tsFiles.Add(p+".ts", tmp)
|
||||
|
|
@ -128,7 +135,6 @@ func GetTSSource() error {
|
|||
err = tsFiles.Save()
|
||||
if err != nil {
|
||||
fmt.Printf("save ts files: %s\n", err)
|
||||
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,11 +5,14 @@
|
|||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useQuasar } from 'quasar'
|
||||
import { resolveI18nLocale, usePreferencesStore } from 'src/stores/preferences-store';
|
||||
import { useUserStore } from 'src/stores/user-store';
|
||||
|
||||
const { locale } = useI18n();
|
||||
const preferencesStore = usePreferencesStore();
|
||||
const userStore = useUserStore();
|
||||
const $q = useQuasar()
|
||||
|
||||
watch(
|
||||
() => preferencesStore.language,
|
||||
|
|
@ -18,4 +21,16 @@ watch(
|
|||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => $q.appVisible,
|
||||
(state) => {
|
||||
console.log(`App became ${state ? 'visible' : 'hidden'}`)
|
||||
if (state) {
|
||||
void userStore.getUser();
|
||||
// App is visible, you can perform actions here if needed
|
||||
} else {
|
||||
// App is hidden, you can perform actions here if needed
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ 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: 11
|
||||
// /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<string> }> => {
|
||||
|
|
@ -16,7 +16,7 @@ export const listUsers = async (
|
|||
|
||||
// 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<string> }> => {
|
||||
|
|
@ -26,6 +26,42 @@ export const blockUser = async (
|
|||
};
|
||||
};
|
||||
|
||||
// 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: 23
|
||||
export const updateUser = async (
|
||||
data: users.UpdateUserRequest,
|
||||
): Promise<{ data: users.User; error: Nullable<string> }> => {
|
||||
return (await api.PUT("/api/admin/updateUser", data)) as {
|
||||
data: users.User;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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<string> }> => {
|
||||
return (await api.PUT("/api/admin/updateuserdetails", data)) as {
|
||||
data: users.User;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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<string> }> => {
|
||||
return (await api.PUT("/api/admin/updateuserpreferences", data)) as {
|
||||
data: users.User;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
export interface ListUsersResponse {
|
||||
items: users.User[];
|
||||
page: number;
|
||||
|
|
@ -33,7 +69,7 @@ export interface ListUsersResponse {
|
|||
}
|
||||
|
||||
export interface BlockUserRequest {
|
||||
uuid: string;
|
||||
id: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//
|
||||
// This file was generated by github.com/millevolte/ts-rpc
|
||||
//
|
||||
// Apr 27, 2026 22:35:43 UTC
|
||||
// May 08, 2026 12:28:15 UTC
|
||||
//
|
||||
|
||||
export interface ApiRestResponse {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// API Declarations
|
||||
export type Record<K extends string | number | symbol, T> = { [P in K]: T };
|
||||
|
||||
export type Nullable<T> = T | null;
|
||||
|
||||
export type Record<K extends string | number | symbol, T> = { [P in K]: T };
|
||||
|
|
|
|||
|
|
@ -1,21 +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<string>;
|
||||
}> => {
|
||||
return (await api.GET("/api/roles")) as {
|
||||
data: AllRoles;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
export interface AllRoles {
|
||||
roles: Record<string, string>;
|
||||
}
|
||||
|
||||
export type Permission = string;
|
||||
export const EnumUserRole = {
|
||||
SuperAdminRole: "superadmin",
|
||||
AdminRole: "admin",
|
||||
ManagerRole: "manager",
|
||||
ContentCreatorRole: "content_creator",
|
||||
UserRole: "user",
|
||||
GuestRole: "guest",
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { api } from "./api";
|
||||
import type { Nullable } from "./apiTypes.ts";
|
||||
import type * as responses from "./responses.ts";
|
||||
import type * as tokens from "./tokens.ts";
|
||||
import type * as auth from "./auth.ts";
|
||||
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<string>;
|
||||
|
|
@ -17,57 +16,9 @@ export const me = async (): Promise<{
|
|||
};
|
||||
};
|
||||
|
||||
// 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<string> }> => {
|
||||
return (await api.POST("/api/auth/refresh", data)) as {
|
||||
data: tokens.TokenPair;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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<string> }> => {
|
||||
return (await api.POST("/api/auth/password/reset", data)) as {
|
||||
data: responses.SimpleResponse;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/api/users/:uuid; 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,
|
||||
): Promise<{ data: User; error: Nullable<string> }> => {
|
||||
return (await api.GET(`/api/users/${uuid}`)) as {
|
||||
data: User;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/api/users/update; name=updateUser; method=PUT; request=users.UpdateUserRequest; response=users.User
|
||||
|
||||
// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 34
|
||||
export const updateUser = async (
|
||||
data: UpdateUserRequest,
|
||||
): Promise<{ data: User; error: Nullable<string> }> => {
|
||||
return (await api.PUT("/api/users/update", data)) as {
|
||||
data: User;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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<string> }> => {
|
||||
|
|
@ -77,30 +28,6 @@ export const updatePassword = 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<string> }> => {
|
||||
return (await api.DELETE(`/api/users/${uuid}`)) as {
|
||||
data: responses.SimpleResponse;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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<string> }> => {
|
||||
return (await api.POST("/api/auth/password/forgot", data)) as {
|
||||
data: responses.SimpleResponse;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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
|
||||
|
|
@ -113,21 +40,45 @@ export const createUser = async (
|
|||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/api/auth/login; name=login; method=POST; request=users.LoginRequest; response=tokens.TokenPair
|
||||
// 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: 43
|
||||
export const login = async (
|
||||
data: LoginRequest,
|
||||
// /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<string> }> => {
|
||||
return (await api.PUT("/api/users/update/details", data)) as {
|
||||
data: User;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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<string> }> => {
|
||||
return (await api.POST("/api/auth/login", data)) as {
|
||||
return (await api.POST("/api/auth/refresh", data)) as {
|
||||
data: tokens.TokenPair;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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<string> }> => {
|
||||
return (await api.POST("/api/auth/password/reset", data)) as {
|
||||
data: responses.SimpleResponse;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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
|
||||
// /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<string> }> => {
|
||||
|
|
@ -137,9 +88,45 @@ export const register = async (
|
|||
};
|
||||
};
|
||||
|
||||
// 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<string> }> => {
|
||||
return (await api.DELETE(`/api/users/${id}`)) as {
|
||||
data: responses.SimpleResponse;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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<string> }> => {
|
||||
return (await api.POST("/api/auth/login", data)) as {
|
||||
data: tokens.TokenPair;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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<string> }> => {
|
||||
return (await api.POST("/api/auth/password/forgot", data)) as {
|
||||
data: responses.SimpleResponse;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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
|
||||
// /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<string> }> => {
|
||||
|
|
@ -149,43 +136,58 @@ export const validToken = async (
|
|||
};
|
||||
};
|
||||
|
||||
export interface ForgotPasswordRequest {
|
||||
email: string;
|
||||
}
|
||||
// Typescript: TSEndpoint= path=/api/users/:id; name=getUser; method=GET; response=users.User
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 28
|
||||
export const getUser = async (
|
||||
id: string,
|
||||
): Promise<{ data: User; error: Nullable<string> }> => {
|
||||
return (await api.GET(`/api/users/${id}`)) as {
|
||||
data: User;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
export interface UpdateUserRequest {
|
||||
uuid: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
permission: auth.Permission;
|
||||
status: UserStatus;
|
||||
types: UserTypes;
|
||||
avatar: Nullable<string>;
|
||||
details: Nullable<UserDetails>;
|
||||
preferences: Nullable<UserPreferences>;
|
||||
}
|
||||
// Typescript: TSEndpoint= path=/api/users/update; name=updateUser; method=PUT; request=users.UpdateUserRequest; response=users.User
|
||||
|
||||
export interface UserCreateRequest {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
permission: auth.Permission;
|
||||
status: UserStatus;
|
||||
types: UserTypes;
|
||||
avatar: Nullable<string>;
|
||||
details: Nullable<UserDetails>;
|
||||
preferences: Nullable<UserPreferences>;
|
||||
}
|
||||
// /Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/user/routes.go Line: 34
|
||||
export const updateUser = async (
|
||||
data: UpdateUserRequest,
|
||||
): Promise<{ data: User; error: Nullable<string> }> => {
|
||||
return (await api.PUT("/api/users/update", data)) as {
|
||||
data: User;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
export interface UserDetails {
|
||||
id: number;
|
||||
userId: number;
|
||||
userId: string;
|
||||
title: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
address: string;
|
||||
city: string;
|
||||
zipCode: string;
|
||||
country: string;
|
||||
phone: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface UpdateUserRequest {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
permission: string;
|
||||
status: UserStatus;
|
||||
type: UserType;
|
||||
}
|
||||
|
||||
export interface UpdateUserDetailsRequest {
|
||||
id: number;
|
||||
userId: string;
|
||||
title: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
|
|
@ -194,42 +196,15 @@ export interface UserDetails {
|
|||
zipCode: string;
|
||||
country: string;
|
||||
phone: string;
|
||||
createdAt: Nullable<Date>;
|
||||
updatedAt: Nullable<Date>;
|
||||
}
|
||||
|
||||
export interface RefreshRequest {
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface ResetPasswordRequest {
|
||||
token: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
export interface UpdateUserPreferencesRequest {
|
||||
id: number;
|
||||
email: string;
|
||||
name: string;
|
||||
permission: auth.Permission;
|
||||
types: UserTypes;
|
||||
status: UserStatus;
|
||||
activatedAt: Nullable<Date>;
|
||||
uuid: string;
|
||||
details: Nullable<UserDetails>;
|
||||
preferences: Nullable<UserPreferences>;
|
||||
avatar: Nullable<string>;
|
||||
createdAt: Nullable<Date>;
|
||||
updatedAt: Nullable<Date>;
|
||||
}
|
||||
|
||||
export interface UpdatePasswordRequest {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
id: number;
|
||||
userId: number;
|
||||
userId: string;
|
||||
useIdle: boolean;
|
||||
idleTimeout: number;
|
||||
useIdlePassword: boolean;
|
||||
|
|
@ -238,10 +213,83 @@ export interface UserPreferences {
|
|||
useQuadcodeLogin: boolean;
|
||||
sendNoticesMail: boolean;
|
||||
language: string;
|
||||
createdAt: Nullable<Date>;
|
||||
updatedAt: Nullable<Date>;
|
||||
}
|
||||
|
||||
export interface UserCreateRequest {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
permission: string;
|
||||
status: UserStatus;
|
||||
type: UserType;
|
||||
avatar?: Nullable<string>;
|
||||
details?: Nullable<UserDetails>;
|
||||
preferences?: Nullable<UserPreferences>;
|
||||
}
|
||||
|
||||
export interface UpdatePasswordRequest {
|
||||
id: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
permission: string;
|
||||
type: UserType;
|
||||
status: UserStatus;
|
||||
details: Nullable<UserDetails>;
|
||||
preferences: Nullable<UserPreferences>;
|
||||
avatar: Nullable<string>;
|
||||
activatedAt: Nullable<Date>;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@
|
|||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<div class="q-pa-md">
|
||||
{{userStore.userData ? `Logged in as: ${userStore.userData} ` : 'Not logged in'}}<br>
|
||||
<q-btn flat color="primary" label="Logout" @click="userStore.clearUser()" />
|
||||
</div>
|
||||
</q-drawer>
|
||||
|
||||
<q-page-container>
|
||||
|
|
@ -55,13 +59,28 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from 'src/stores/user-store';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const leftDrawerOpen = ref(false);
|
||||
const userStore = useUserStore();
|
||||
|
||||
function toggleLeftDrawer() {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => userStore.userCounter,
|
||||
() => {
|
||||
if (!userStore.userData) {
|
||||
void router.push('/')
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -117,25 +117,25 @@
|
|||
<q-btn flat round dense icon="more_vert" color="grey-8">
|
||||
<q-menu anchor="bottom right" self="top right">
|
||||
<q-list dense class="user-action-menu">
|
||||
<q-item clickable v-close-popup @click="openViewDialog(props.row.uuid)">
|
||||
<q-item clickable v-close-popup @click="openViewDialog(props.row.id)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="visibility" />
|
||||
</q-item-section>
|
||||
<q-item-section>Show</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="openEditDialog(props.row.uuid)">
|
||||
<q-item clickable v-close-popup @click="openEditDialog(props.row.id)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="edit" />
|
||||
</q-item-section>
|
||||
<q-item-section>Edit</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="openAvatarDialog(props.row.uuid)">
|
||||
<q-item clickable v-close-popup @click="openAvatarDialog(props.row.id)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="add_a_photo" />
|
||||
</q-item-section>
|
||||
<q-item-section>Edit avatar</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="openPasswordDialog(props.row.uuid)">
|
||||
<q-item clickable v-close-popup @click="openPasswordDialog(props.row.id)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="password" />
|
||||
</q-item-section>
|
||||
|
|
@ -167,415 +167,68 @@
|
|||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="dialogOpen">
|
||||
<q-card class="editor-card modal-card">
|
||||
<q-form class="form-grid" @submit.prevent="saveUser">
|
||||
<q-card-section class="editor-header">
|
||||
<div class="editor-toolbar">
|
||||
<div class="editor-headline">
|
||||
<div class="text-overline">{{ dialogMode === 'create' ? 'Nuovo utente' : dialogMode === 'edit' ? 'Modifica utente' : 'Dettaglio utente' }}</div>
|
||||
<div class="text-h5">{{ form.name || 'Profilo utente' }}</div>
|
||||
<div class="text-caption">{{ form.email || 'Compila i dati di base' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-toolbar-actions">
|
||||
<q-btn flat color="white" label="Chiudi" v-close-popup />
|
||||
<q-btn
|
||||
v-if="dialogMode !== 'view'"
|
||||
color="white"
|
||||
text-color="primary"
|
||||
unelevated
|
||||
:loading="saving"
|
||||
:label="dialogMode === 'create' ? 'Crea utente' : 'Salva modifiche'"
|
||||
type="submit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-tabs
|
||||
v-model="editorTab"
|
||||
dense
|
||||
align="left"
|
||||
inline-label
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
class="editor-tabs"
|
||||
>
|
||||
<q-tab name="account" icon="person" label="Account" />
|
||||
<q-tab name="details" icon="badge" label="Details" />
|
||||
<q-tab name="preferences" icon="tune" label="Preferences" />
|
||||
</q-tabs>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="editor-body">
|
||||
<q-tab-panels v-model="editorTab" animated class="editor-panels">
|
||||
<q-tab-panel name="account">
|
||||
<section class="form-section">
|
||||
<h2>Account</h2>
|
||||
<div class="section-grid">
|
||||
<q-input v-model="form.name" outlined label="Nome" :readonly="dialogMode === 'view'" />
|
||||
<q-input v-model="form.email" outlined label="Email" type="email" :readonly="dialogMode === 'view'" />
|
||||
<q-input
|
||||
v-if="dialogMode === 'create'"
|
||||
v-model="form.password"
|
||||
outlined
|
||||
label="Password"
|
||||
type="password"
|
||||
hint="Minimo 8 caratteri"
|
||||
/>
|
||||
<div v-if="dialogMode !== 'create'" class="avatar-inline-card span-2">
|
||||
<div class="avatar-inline-preview">
|
||||
<img v-if="form.avatar" :src="form.avatar" :alt="form.name" />
|
||||
<span v-else>{{ avatarInitials(form) }}</span>
|
||||
</div>
|
||||
<div class="avatar-inline-meta">
|
||||
<div class="text-subtitle2">Avatar</div>
|
||||
<div class="text-caption text-grey-7">
|
||||
{{ form.avatar ? 'Avatar profilo impostato' : 'Nessun avatar impostato' }}
|
||||
</div>
|
||||
</div>
|
||||
<q-btn
|
||||
v-if="dialogMode === 'edit'"
|
||||
flat
|
||||
color="primary"
|
||||
icon="add_a_photo"
|
||||
label="Modifica"
|
||||
@click="openAvatarDialog(form.uuid)"
|
||||
/>
|
||||
</div>
|
||||
<q-input v-else v-model="form.avatar" outlined label="Avatar URL" />
|
||||
<q-select
|
||||
v-model="form.status"
|
||||
outlined
|
||||
label="Status"
|
||||
:options="statusOptions"
|
||||
:readonly="dialogMode === 'view'"
|
||||
/>
|
||||
<q-select
|
||||
v-model="form.permission"
|
||||
outlined
|
||||
multiple
|
||||
use-input
|
||||
use-chips
|
||||
new-value-mode="add-unique"
|
||||
label="Roles"
|
||||
:options="roleOptions"
|
||||
:readonly="dialogMode === 'view'"
|
||||
/>
|
||||
<q-select
|
||||
v-model="form.types"
|
||||
outlined
|
||||
multiple
|
||||
use-input
|
||||
use-chips
|
||||
new-value-mode="add-unique"
|
||||
label="Types"
|
||||
:options="typeOptions"
|
||||
:readonly="dialogMode === 'view'"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="details">
|
||||
<section class="form-section">
|
||||
<div class="section-heading">
|
||||
<h2>Details</h2>
|
||||
<q-toggle
|
||||
v-model="detailsEnabled"
|
||||
label="Abilita details"
|
||||
:disable="dialogMode === 'view'"
|
||||
/>
|
||||
</div>
|
||||
<div class="section-grid" :class="{ disabled: !detailsEnabled }">
|
||||
<q-input v-model="form.details.title" outlined label="Title" :disable="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="form.details.firstName" outlined label="First name" :disable="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="form.details.lastName" outlined label="Last name" :disable="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="form.details.phone" outlined label="Phone" :disable="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="form.details.address" outlined label="Address" class="span-2" :disable="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="form.details.city" outlined label="City" :disable="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="form.details.zipCode" outlined label="Zip code" :disable="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="form.details.country" outlined label="Country" :disable="!detailsEnabled || dialogMode === 'view'" />
|
||||
</div>
|
||||
</section>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="preferences">
|
||||
<section class="form-section">
|
||||
<div class="section-heading">
|
||||
<h2>Preferences</h2>
|
||||
<q-toggle
|
||||
v-model="preferencesEnabled"
|
||||
label="Abilita preferences"
|
||||
:disable="dialogMode === 'view'"
|
||||
/>
|
||||
</div>
|
||||
<div class="section-grid" :class="{ disabled: !preferencesEnabled }">
|
||||
<q-input v-model="form.preferences.language" outlined label="Language" :disable="!preferencesEnabled || dialogMode === 'view'" />
|
||||
<q-input
|
||||
v-model.number="form.preferences.idleTimeout"
|
||||
outlined
|
||||
type="number"
|
||||
label="Idle timeout"
|
||||
:disable="!preferencesEnabled || dialogMode === 'view'"
|
||||
/>
|
||||
<q-input v-model="form.preferences.idlePin" outlined label="Idle pin" :disable="!preferencesEnabled || dialogMode === 'view'" />
|
||||
<q-toggle v-model="form.preferences.useIdle" label="Use idle" :disable="!preferencesEnabled || dialogMode === 'view'" />
|
||||
<q-toggle v-model="form.preferences.useIdlePassword" label="Use idle password" :disable="!preferencesEnabled || dialogMode === 'view'" />
|
||||
<q-toggle v-model="form.preferences.useDirectLogin" label="Use direct login" :disable="!preferencesEnabled || dialogMode === 'view'" />
|
||||
<q-toggle v-model="form.preferences.useQuadcodeLogin" label="Use quadcode login" :disable="!preferencesEnabled || dialogMode === 'view'" />
|
||||
<q-toggle v-model="form.preferences.sendNoticesMail" label="Send notices mail" :disable="!preferencesEnabled || dialogMode === 'view'" />
|
||||
</div>
|
||||
</section>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-card-section>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="passwordDialogOpen">
|
||||
<q-card class="password-card modal-card">
|
||||
<q-card-section>
|
||||
<div class="text-overline text-primary">Change password</div>
|
||||
<div class="text-h6">{{ passwordDialogUserEmail || 'User' }}</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="password-grid">
|
||||
<q-input
|
||||
v-model="passwordForm.password"
|
||||
outlined
|
||||
type="password"
|
||||
label="New password"
|
||||
hint="Minimo 8 caratteri"
|
||||
/>
|
||||
<q-input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
outlined
|
||||
type="password"
|
||||
label="Confirm password"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat color="grey-7" label="Chiudi" v-close-popup />
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Salva password"
|
||||
:loading="saving"
|
||||
@click="savePasswordChange"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="blockDialogOpen">
|
||||
<q-card class="password-card modal-card">
|
||||
<q-card-section>
|
||||
<div class="text-overline text-primary">User access</div>
|
||||
<div class="text-h6">{{ blockDialogUser.email || 'User' }}</div>
|
||||
<div class="text-caption">
|
||||
Stato attuale: {{ blockDialogUser.status || 'n/a' }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="password-grid">
|
||||
<q-toggle
|
||||
v-model="blockDialogBlocked"
|
||||
checked-icon="block"
|
||||
unchecked-icon="lock_open"
|
||||
color="negative"
|
||||
:label="blockDialogBlocked ? 'Utente bloccato' : 'Utente attivo'"
|
||||
/>
|
||||
<div class="text-body2 text-grey-7">
|
||||
{{
|
||||
blockDialogBlocked
|
||||
? 'L’utente non potra piu accedere finche non verra sbloccato.'
|
||||
: 'L’utente potra accedere normalmente.'
|
||||
}}
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat color="grey-7" label="Chiudi" v-close-popup />
|
||||
<q-btn
|
||||
color="primary"
|
||||
:loading="saving"
|
||||
:label="blockDialogBlocked ? 'Salva blocco' : 'Salva sblocco'"
|
||||
@click="saveBlockState"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="avatarDialogOpen">
|
||||
<q-card class="editor-card modal-card">
|
||||
<q-card-section class="editor-header">
|
||||
<div class="editor-toolbar">
|
||||
<div class="editor-headline">
|
||||
<div class="text-overline">Avatar editor</div>
|
||||
<div class="text-h5">{{ avatarDialogUser.email || 'User avatar' }}</div>
|
||||
<div class="text-caption">Ritaglio circolare per il profilo utente</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-toolbar-actions">
|
||||
<q-btn flat color="white" label="Chiudi" v-close-popup />
|
||||
<q-btn
|
||||
color="white"
|
||||
text-color="primary"
|
||||
unelevated
|
||||
:disable="!avatarSource"
|
||||
:loading="saving"
|
||||
label="Salva avatar"
|
||||
@click="saveAvatar"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="avatar-editor-body">
|
||||
<div class="avatar-toolbar">
|
||||
<input
|
||||
ref="avatarFileInputRef"
|
||||
class="visually-hidden"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="onAvatarFileSelected"
|
||||
<UserEditorDialog
|
||||
v-model="dialogOpen"
|
||||
:dialog-mode="dialogMode"
|
||||
:user-id="editorDialogUserId"
|
||||
@saved="loadUsers"
|
||||
@open-avatar="openAvatarDialog"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="upload"
|
||||
label="Carica immagine"
|
||||
@click="openAvatarFilePicker"
|
||||
<UserPasswordDialog
|
||||
v-model="passwordDialogOpen"
|
||||
:user-id="passwordDialogUserId"
|
||||
@saved="loadUsers"
|
||||
/>
|
||||
|
||||
<div class="avatar-file-name">
|
||||
{{ avatarFile?.name || 'Nessun file selezionato' }}
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
flat
|
||||
color="primary"
|
||||
icon="restart_alt"
|
||||
label="Reset crop"
|
||||
:disable="!avatarSource"
|
||||
@click="resetAvatarCrop"
|
||||
<UserBlockDialog
|
||||
v-model="blockDialogOpen"
|
||||
:user-id="blockDialogUserId"
|
||||
@saved="loadUsers"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="avatar-editor-grid">
|
||||
<div class="avatar-cropper-shell">
|
||||
<div v-if="avatarSource" class="avatar-cropper-box">
|
||||
<VuePictureCropper
|
||||
ref="avatarCropperRef"
|
||||
:img="avatarSource"
|
||||
:box-style="avatarCropperBoxStyle"
|
||||
:options="avatarCropperOptions"
|
||||
:preset-mode="avatarPresetMode"
|
||||
<UserAvatarDialog
|
||||
v-model="avatarDialogOpen"
|
||||
:user-id="avatarDialogUserId"
|
||||
@saved="loadUsers"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="avatar-empty-state">
|
||||
Seleziona un’immagine per modificare l’avatar.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="avatar-preview-shell">
|
||||
<div class="avatar-preview-title">Anteprima</div>
|
||||
<div class="avatar-preview-disc">
|
||||
<img v-if="avatarPreview" :src="avatarPreview" alt="Avatar preview" />
|
||||
<span v-else>No avatar</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useQuasar, type QTableColumn, type QTableProps } from 'quasar';
|
||||
import VuePictureCropper from 'vue-picture-cropper';
|
||||
|
||||
import UserAvatarDialog from './dialogs/UserAvatarDialog.vue';
|
||||
import UserBlockDialog from './dialogs/UserBlockDialog.vue';
|
||||
import UserEditorDialog from './dialogs/UserEditorDialog.vue';
|
||||
import UserPasswordDialog from './dialogs/UserPasswordDialog.vue';
|
||||
import type { DialogMode } from './dialogs/types';
|
||||
|
||||
import {
|
||||
blockUser,
|
||||
listUsers,
|
||||
type BlockUserRequest,
|
||||
type ListUsersRequest,
|
||||
}from 'src/api/admin';
|
||||
|
||||
import {
|
||||
createUser,
|
||||
getUser,
|
||||
updateUser,
|
||||
EnumUserStatus,
|
||||
type UpdateUserRequest,
|
||||
type UserCreateRequest,
|
||||
type User,
|
||||
type UserStatus,
|
||||
type UserDetails,
|
||||
type UserPreferences,
|
||||
} from 'src/api/users';
|
||||
|
||||
|
||||
type DialogMode = 'create' | 'edit' | 'view';
|
||||
|
||||
type UserDetailsForm = Omit<UserDetails, 'id' | 'userId' | 'createdAt' | 'updatedAt'>;
|
||||
type UserPreferencesForm = Omit<UserPreferences, 'id' | 'userId' | 'createdAt' | 'updatedAt'>;
|
||||
|
||||
interface UserFormState {
|
||||
uuid: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
status: UserStatus;
|
||||
permission: string;
|
||||
types: string[];
|
||||
avatar: string;
|
||||
details: UserDetailsForm;
|
||||
preferences: UserPreferencesForm;
|
||||
}
|
||||
|
||||
const $q = useQuasar();
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const dialogOpen = ref(false);
|
||||
const passwordDialogOpen = ref(false);
|
||||
const blockDialogOpen = ref(false);
|
||||
const avatarDialogOpen = ref(false);
|
||||
const dialogMode = ref<DialogMode>('create');
|
||||
const editorTab = ref<'account' | 'details' | 'preferences'>('account');
|
||||
const editorDialogUserId = ref('');
|
||||
const passwordDialogUserId = ref('');
|
||||
const blockDialogUserId = ref('');
|
||||
const avatarDialogUserId = ref('');
|
||||
const filter = ref('');
|
||||
const rows = ref<User[]>([]);
|
||||
const detailsEnabled = ref(true);
|
||||
const preferencesEnabled = ref(true);
|
||||
const passwordDialogUserUuid = ref('');
|
||||
const passwordDialogUserEmail = ref('');
|
||||
const blockDialogUser = reactive({
|
||||
uuid: '',
|
||||
email: '',
|
||||
status: '' as UserStatus | '',
|
||||
});
|
||||
const blockDialogBlocked = ref(false);
|
||||
const avatarDialogUser = reactive({
|
||||
uuid: '',
|
||||
email: '',
|
||||
});
|
||||
const avatarFile = ref<File | null>(null);
|
||||
const avatarFileInputRef = ref<HTMLInputElement | null>(null);
|
||||
const avatarSource = ref('');
|
||||
const avatarPreview = ref('');
|
||||
const avatarCropperRef = ref<{ cropper?: { getDataURL?: (options?: { width?: number; height?: number; rounded?: boolean }) => string; reset?: () => void } } | null>(null);
|
||||
const pagination = ref<QTableProps['pagination']>({
|
||||
sortBy: 'name',
|
||||
descending: false,
|
||||
|
|
@ -584,217 +237,25 @@ const pagination = ref<QTableProps['pagination']>({
|
|||
rowsNumber: 0,
|
||||
});
|
||||
|
||||
const statusOptions = Object.values(EnumUserStatus);
|
||||
const roleOptions = ['admin', 'manager', 'user'];
|
||||
const typeOptions = ['internal', 'external'];
|
||||
const avatarCropperBoxStyle = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: '#f3f7fb',
|
||||
margin: '0 auto',
|
||||
};
|
||||
const avatarPresetMode = {
|
||||
mode: 'round',
|
||||
width: 320,
|
||||
height: 320,
|
||||
} as const;
|
||||
|
||||
const columns: QTableColumn<User>[] = [
|
||||
{ name: 'name', label: 'Utente', field: 'name', align: 'left', sortable: true },
|
||||
{ name: 'status', label: 'Status', field: 'status', align: 'left', sortable: true },
|
||||
{ name: 'permission', label: 'Roles', field: (row) => row.permission, align: 'left' },
|
||||
{ name: 'details', label: 'Details', field: (row) => fullName(row), align: 'left' },
|
||||
{ name: 'preferences', label: 'Preferences', field: (row) => row.preferences?.language ?? '', align: 'left' },
|
||||
{ name: 'actions', label: '', field: 'uuid', align: 'right' },
|
||||
{ name: 'actions', label: '', field: 'id', align: 'right' },
|
||||
];
|
||||
|
||||
const form = reactive<UserFormState>(emptyForm());
|
||||
const passwordForm = reactive({
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
const payload = computed<UserCreateRequest | UpdateUserRequest>(() => ({
|
||||
name: form.name.trim(),
|
||||
email: form.email.trim(),
|
||||
password: dialogMode.value === 'create' ? form.password : '',
|
||||
status: form.status,
|
||||
// TODO: permission management
|
||||
//permission: sanitizeList(form.permission),
|
||||
permission: form.permission,
|
||||
types: sanitizeList(form.types),
|
||||
avatar: normalizeNullableString(form.avatar),
|
||||
details: detailsEnabled.value ? sanitizeDetails(form.details) : null,
|
||||
preferences: preferencesEnabled.value ? sanitizePreferences(form.preferences) : null,
|
||||
}));
|
||||
|
||||
onMounted(async () => {
|
||||
await loadUsers();
|
||||
const err = await loadUsers();
|
||||
if (!err) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Errore durante il caricamento degli utenti',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function emptyForm(): UserFormState {
|
||||
return {
|
||||
uuid: '',
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
status: EnumUserStatus.UserStatusPending,
|
||||
permission: "user",
|
||||
types: ['internal'],
|
||||
avatar: '',
|
||||
details: {
|
||||
title: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
address: '',
|
||||
city: '',
|
||||
zipCode: '',
|
||||
country: '',
|
||||
phone: '',
|
||||
},
|
||||
preferences: {
|
||||
useIdle: false,
|
||||
idleTimeout: 0,
|
||||
useIdlePassword: false,
|
||||
idlePin: '',
|
||||
useDirectLogin: false,
|
||||
useQuadcodeLogin: false,
|
||||
sendNoticesMail: false,
|
||||
language: 'it',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function resetForm(user?: User): void {
|
||||
const next = user ? mapUserToForm(user) : emptyForm();
|
||||
Object.assign(form, next);
|
||||
detailsEnabled.value = !!user?.details || !user;
|
||||
preferencesEnabled.value = !!user?.preferences || !user;
|
||||
}
|
||||
|
||||
function mapUserToForm(user: User): UserFormState {
|
||||
return {
|
||||
uuid: user.uuid,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
password: '',
|
||||
status: user.status,
|
||||
permission: user.permission,
|
||||
types: [...user.types],
|
||||
avatar: user.avatar ?? '',
|
||||
details: {
|
||||
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: {
|
||||
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',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeList(values: string[]): string[] {
|
||||
return values.map((value) => value.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeNullableString(value: string): string | null {
|
||||
const normalized = value.trim();
|
||||
return normalized === '' ? null : normalized;
|
||||
}
|
||||
|
||||
function sanitizeDetails(value: UserDetailsForm): UserDetails | null {
|
||||
const normalized: UserDetailsForm = {
|
||||
title: value.title.trim(),
|
||||
firstName: value.firstName.trim(),
|
||||
lastName: value.lastName.trim(),
|
||||
address: value.address.trim(),
|
||||
city: value.city.trim(),
|
||||
zipCode: value.zipCode.trim(),
|
||||
country: value.country.trim(),
|
||||
phone: value.phone.trim(),
|
||||
};
|
||||
|
||||
return Object.values(normalized).some(Boolean) ? normalized as UserDetails : null;
|
||||
}
|
||||
|
||||
function sanitizePreferences(value: UserPreferencesForm): UserPreferences | null {
|
||||
const normalized: UserPreferencesForm = {
|
||||
useIdle: value.useIdle,
|
||||
idleTimeout: Number(value.idleTimeout) || 0,
|
||||
useIdlePassword: value.useIdlePassword,
|
||||
idlePin: value.idlePin.trim(),
|
||||
useDirectLogin: value.useDirectLogin,
|
||||
useQuadcodeLogin: value.useQuadcodeLogin,
|
||||
sendNoticesMail: value.sendNoticesMail,
|
||||
language: value.language.trim(),
|
||||
};
|
||||
|
||||
const hasData =
|
||||
normalized.useIdle ||
|
||||
normalized.idleTimeout > 0 ||
|
||||
normalized.useIdlePassword ||
|
||||
normalized.idlePin !== '' ||
|
||||
normalized.useDirectLogin ||
|
||||
normalized.useQuadcodeLogin ||
|
||||
normalized.sendNoticesMail ||
|
||||
normalized.language !== '';
|
||||
|
||||
return hasData ? normalized as UserPreferences : null;
|
||||
}
|
||||
|
||||
function syncAvatarPreview(): void {
|
||||
avatarPreview.value =
|
||||
avatarCropperRef.value?.cropper?.getDataURL?.({
|
||||
width: 220,
|
||||
height: 220,
|
||||
rounded: true,
|
||||
}) || avatarSource.value;
|
||||
}
|
||||
|
||||
const avatarCropperOptions = {
|
||||
viewMode: 1 as const,
|
||||
dragMode: 'move' as const,
|
||||
aspectRatio: 1,
|
||||
autoCropArea: 0.9,
|
||||
background: false,
|
||||
movable: true,
|
||||
zoomable: true,
|
||||
scalable: false,
|
||||
guides: false,
|
||||
ready: () => {
|
||||
syncAvatarPreview();
|
||||
},
|
||||
crop: () => {
|
||||
syncAvatarPreview();
|
||||
},
|
||||
};
|
||||
|
||||
function buildUpdatePayload(user: User, password = ''): UpdateUserRequest {
|
||||
return {
|
||||
uuid: user.uuid,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
password,
|
||||
status: user.status,
|
||||
permission: user.permission,
|
||||
types: [...user.types],
|
||||
avatar: user.avatar ?? null,
|
||||
details: user.details ? { ...user.details } : null,
|
||||
preferences: user.preferences ? { ...user.preferences } : null,
|
||||
};
|
||||
}
|
||||
|
||||
function fullName(user: Pick<User, 'details'>): string {
|
||||
const parts = [user.details?.title, user.details?.firstName, user.details?.lastName].filter(Boolean);
|
||||
|
|
@ -826,7 +287,7 @@ function statusColor(status: UserStatus): string {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadUsers(): Promise<void> {
|
||||
async function loadUsers(): Promise<boolean> {
|
||||
loading.value = true;
|
||||
try {
|
||||
const request: ListUsersRequest = {
|
||||
|
|
@ -848,9 +309,11 @@ async function loadUsers(): Promise<void> {
|
|||
};
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
return false;
|
||||
}
|
||||
loading.value = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async function onTableRequest(props: { pagination: NonNullable<QTableProps['pagination']> }): Promise<void> {
|
||||
|
|
@ -858,246 +321,49 @@ async function onTableRequest(props: { pagination: NonNullable<QTableProps['pagi
|
|||
...pagination.value,
|
||||
...props.pagination,
|
||||
};
|
||||
await loadUsers();
|
||||
const err = await loadUsers();
|
||||
if (!err) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Errore durante il caricamento degli utenti',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog(): void {
|
||||
dialogMode.value = 'create';
|
||||
editorTab.value = 'account';
|
||||
resetForm();
|
||||
editorDialogUserId.value = '';
|
||||
dialogOpen.value = true;
|
||||
}
|
||||
|
||||
async function openEditDialog(uuid: string): Promise<void> {
|
||||
await openUserDialog('edit', uuid);
|
||||
function openEditDialog(id: string): void {
|
||||
dialogMode.value = 'edit';
|
||||
editorDialogUserId.value = id;
|
||||
dialogOpen.value = true;
|
||||
}
|
||||
|
||||
async function openViewDialog(uuid: string): Promise<void> {
|
||||
await openUserDialog('view', uuid);
|
||||
function openViewDialog(id: string): void {
|
||||
dialogMode.value = 'view';
|
||||
editorDialogUserId.value = id;
|
||||
dialogOpen.value = true;
|
||||
}
|
||||
|
||||
async function openPasswordDialog(uuid: string): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
const profile = await resolveUserProfile(uuid);
|
||||
passwordDialogUserUuid.value = profile.uuid;
|
||||
passwordDialogUserEmail.value = profile.email;
|
||||
passwordForm.password = '';
|
||||
passwordForm.confirmPassword = '';
|
||||
function openPasswordDialog(id: string): void {
|
||||
passwordDialogUserId.value = id;
|
||||
passwordDialogOpen.value = true;
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openAvatarDialog(uuid: string): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
const profile = await resolveUserProfile(uuid);
|
||||
avatarDialogUser.uuid = profile.uuid;
|
||||
avatarDialogUser.email = profile.email;
|
||||
avatarFile.value = null;
|
||||
avatarSource.value = profile.avatar ?? '';
|
||||
avatarPreview.value = profile.avatar ?? '';
|
||||
function openAvatarDialog(id: string): void {
|
||||
avatarDialogUserId.value = id;
|
||||
avatarDialogOpen.value = true;
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openUserDialog(mode: DialogMode, uuid: string): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
const profile = await resolveUserProfile(uuid);
|
||||
dialogMode.value = mode;
|
||||
editorTab.value = 'account';
|
||||
resetForm(profile);
|
||||
dialogOpen.value = true;
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUser(): Promise<void> {
|
||||
if (dialogMode.value === 'view') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!form.name.trim() || !form.email.trim()) {
|
||||
$q.notify({ type: 'negative', message: 'Nome ed email sono obbligatori.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (dialogMode.value === 'create' && form.password.trim().length < 8) {
|
||||
$q.notify({ type: 'negative', message: 'La password deve contenere almeno 8 caratteri.' });
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
if (dialogMode.value === 'create') {
|
||||
const response = await createUser(payload.value as UserCreateRequest);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
$q.notify({ type: 'positive', message: `Utente ${response.data.email} creato.` });
|
||||
} else if (form.uuid) {
|
||||
const response = await updateUser(payload.value as UpdateUserRequest);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
$q.notify({ type: 'positive', message: `Utente ${response.data.email} aggiornato.` });
|
||||
}
|
||||
|
||||
dialogOpen.value = false;
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePasswordChange(): Promise<void> {
|
||||
if (passwordForm.password.trim().length < 8) {
|
||||
$q.notify({ type: 'negative', message: 'La password deve contenere almeno 8 caratteri.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordForm.password !== passwordForm.confirmPassword) {
|
||||
$q.notify({ type: 'negative', message: 'Le password non coincidono.' });
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const profile = await resolveUserProfile(passwordDialogUserUuid.value);
|
||||
const response = await updateUser(
|
||||
buildUpdatePayload(profile, passwordForm.password),
|
||||
);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
passwordDialogOpen.value = false;
|
||||
$q.notify({ type: 'positive', message: `Password aggiornata per ${response.data.email}.` });
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openAvatarFilePicker(): void {
|
||||
avatarFileInputRef.value?.click();
|
||||
}
|
||||
|
||||
function onAvatarFileSelected(event: Event): void {
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
const selected = target?.files?.[0] ?? null;
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
avatarFile.value = selected;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
avatarSource.value = typeof reader.result === 'string' ? reader.result : '';
|
||||
avatarPreview.value = avatarSource.value;
|
||||
};
|
||||
reader.readAsDataURL(selected);
|
||||
}
|
||||
|
||||
function resetAvatarCrop(): void {
|
||||
avatarCropperRef.value?.cropper?.reset?.();
|
||||
}
|
||||
|
||||
async function saveAvatar(): Promise<void> {
|
||||
if (!avatarSource.value) {
|
||||
$q.notify({ type: 'negative', message: 'Seleziona un’immagine prima di salvare.' });
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const profile = await resolveUserProfile(avatarDialogUser.uuid);
|
||||
const avatarDataUrl =
|
||||
avatarCropperRef.value?.cropper?.getDataURL?.({
|
||||
width: 256,
|
||||
height: 256,
|
||||
rounded: true,
|
||||
}) || avatarSource.value;
|
||||
|
||||
const response = await updateUser(
|
||||
{
|
||||
...buildUpdatePayload(profile),
|
||||
avatar: avatarDataUrl,
|
||||
},
|
||||
);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
avatarPreview.value = avatarDataUrl;
|
||||
avatarDialogOpen.value = false;
|
||||
$q.notify({ type: 'positive', message: `Avatar aggiornato per ${response.data.email}.` });
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openBlockDialog(user: User
|
||||
): void {
|
||||
blockDialogUser.uuid = user.uuid;
|
||||
blockDialogUser.email = user.email;
|
||||
blockDialogUser.status = user.status;
|
||||
blockDialogBlocked.value = user.status === EnumUserStatus.UserStatusDisabled;
|
||||
blockDialogUserId.value = user.id;
|
||||
blockDialogOpen.value = true;
|
||||
}
|
||||
|
||||
async function saveBlockState(): Promise<void> {
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload: BlockUserRequest = {
|
||||
uuid: blockDialogUser.uuid,
|
||||
action: blockDialogBlocked.value ? 'block' : 'unblock',
|
||||
};
|
||||
const response = await blockUser(payload);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
blockDialogOpen.value = false;
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: blockDialogBlocked.value
|
||||
? `Utente ${response.data.email} bloccato.`
|
||||
: `Utente ${response.data.email} sbloccato.`,
|
||||
});
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveUserProfile(uuid: string): Promise<User> {
|
||||
const response = await getUser(uuid);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
function notifyError(error: unknown): void {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,444 @@
|
|||
<template>
|
||||
<q-dialog v-model="isOpen">
|
||||
<q-card class="editor-card modal-card">
|
||||
<q-card-section class="editor-header">
|
||||
<div class="editor-toolbar">
|
||||
<div class="editor-headline">
|
||||
<div class="text-overline">Avatar editor</div>
|
||||
<div class="text-h5">{{ avatarState.email || 'User avatar' }}</div>
|
||||
<div class="text-caption">Ritaglio circolare per il profilo utente</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-toolbar-actions">
|
||||
<q-btn flat color="white" label="Chiudi" @click="isOpen = false" />
|
||||
<q-btn
|
||||
color="white"
|
||||
text-color="primary"
|
||||
unelevated
|
||||
:disable="!avatarSource"
|
||||
:loading="saving"
|
||||
label="Salva avatar"
|
||||
@click="emitSave"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="avatar-editor-body">
|
||||
<div class="avatar-toolbar">
|
||||
<input
|
||||
ref="avatarFileInputRef"
|
||||
class="visually-hidden"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="onAvatarFileSelected"
|
||||
/>
|
||||
|
||||
<q-btn color="primary" icon="upload" label="Carica immagine" @click="openAvatarFilePicker" />
|
||||
|
||||
<div class="avatar-file-name">
|
||||
{{ avatarFile?.name || 'Nessun file selezionato' }}
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
flat
|
||||
color="primary"
|
||||
icon="restart_alt"
|
||||
label="Reset crop"
|
||||
:disable="!avatarSource"
|
||||
@click="resetAvatarCrop"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="avatar-editor-grid">
|
||||
<div class="avatar-cropper-shell">
|
||||
<div v-if="avatarSource" class="avatar-cropper-box">
|
||||
<VuePictureCropper
|
||||
ref="avatarCropperRef"
|
||||
:img="avatarSource"
|
||||
:box-style="avatarCropperBoxStyle"
|
||||
:options="avatarCropperOptions"
|
||||
:preset-mode="avatarPresetMode"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="avatar-empty-state">
|
||||
Seleziona un’immagine per modificare l’avatar.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="avatar-preview-shell">
|
||||
<div class="avatar-preview-title">Anteprima</div>
|
||||
<div class="avatar-preview-disc">
|
||||
<img v-if="avatarPreview" :src="avatarPreview" alt="Avatar preview" />
|
||||
<span v-else>No avatar</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import VuePictureCropper from 'vue-picture-cropper';
|
||||
|
||||
import { getUser } from 'src/api/users';
|
||||
|
||||
import type { AvatarDialogState } from './types';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
userUuid?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
saved: [];
|
||||
}>();
|
||||
|
||||
const $q = useQuasar();
|
||||
const saving = ref(false);
|
||||
const avatarState = reactive<AvatarDialogState>({
|
||||
id: '',
|
||||
email: '',
|
||||
avatar: '',
|
||||
});
|
||||
|
||||
const avatarFile = ref<File | null>(null);
|
||||
const avatarFileInputRef = ref<HTMLInputElement | null>(null);
|
||||
const avatarSource = ref('');
|
||||
const avatarPreview = ref('');
|
||||
const avatarCropperRef = ref<{
|
||||
cropper?: {
|
||||
getDataURL?: (options?: { width?: number; height?: number; rounded?: boolean }) => string;
|
||||
reset?: () => void;
|
||||
};
|
||||
} | null>(null);
|
||||
|
||||
const avatarCropperBoxStyle = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: '#f3f7fb',
|
||||
margin: '0 auto',
|
||||
};
|
||||
|
||||
const avatarPresetMode = {
|
||||
mode: 'round',
|
||||
width: 320,
|
||||
height: 320,
|
||||
} as const;
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value),
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.userUuid] as const,
|
||||
([open, userUuid]) => {
|
||||
if (!open || !userUuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
void prepareDialog(userUuid);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function prepareDialog(userUuid: string): Promise<void> {
|
||||
try {
|
||||
const response = await getUser(userUuid);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
avatarState.id = response.data.id;
|
||||
avatarState.email = response.data.email;
|
||||
avatarState.avatar = response.data.avatar ?? '';
|
||||
avatarFile.value = null;
|
||||
avatarSource.value = avatarState.avatar;
|
||||
avatarPreview.value = avatarState.avatar;
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
isOpen.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function syncAvatarPreview(): void {
|
||||
avatarPreview.value =
|
||||
avatarCropperRef.value?.cropper?.getDataURL?.({
|
||||
width: 220,
|
||||
height: 220,
|
||||
rounded: true,
|
||||
}) || avatarSource.value;
|
||||
}
|
||||
|
||||
const avatarCropperOptions = {
|
||||
viewMode: 1 as const,
|
||||
dragMode: 'move' as const,
|
||||
aspectRatio: 1,
|
||||
autoCropArea: 0.9,
|
||||
background: false,
|
||||
movable: true,
|
||||
zoomable: true,
|
||||
scalable: false,
|
||||
guides: false,
|
||||
ready: () => {
|
||||
syncAvatarPreview();
|
||||
},
|
||||
crop: () => {
|
||||
syncAvatarPreview();
|
||||
},
|
||||
};
|
||||
|
||||
function openAvatarFilePicker(): void {
|
||||
avatarFileInputRef.value?.click();
|
||||
}
|
||||
|
||||
function onAvatarFileSelected(event: Event): void {
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
const selected = target?.files?.[0] ?? null;
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
avatarFile.value = selected;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
avatarSource.value = typeof reader.result === 'string' ? reader.result : '';
|
||||
avatarPreview.value = avatarSource.value;
|
||||
};
|
||||
reader.readAsDataURL(selected);
|
||||
}
|
||||
|
||||
function resetAvatarCrop(): void {
|
||||
avatarCropperRef.value?.cropper?.reset?.();
|
||||
}
|
||||
|
||||
function emitSave(): void {
|
||||
const avatarDataUrl =
|
||||
avatarCropperRef.value?.cropper?.getDataURL?.({
|
||||
width: 256,
|
||||
height: 256,
|
||||
rounded: true,
|
||||
}) || avatarSource.value;
|
||||
|
||||
void saveAvatar(avatarDataUrl);
|
||||
}
|
||||
|
||||
function saveAvatar(avatarDataUrl: string): void {
|
||||
if (!avatarDataUrl) {
|
||||
$q.notify({ type: 'negative', message: 'Seleziona un’immagine prima di salvare.' });
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
// TODO: manca un endpoint backend/frontend dedicato per aggiornare l'avatar utente.
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Endpoint per il salvataggio avatar non ancora disponibile.',
|
||||
});
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function notifyError(error: unknown): void {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editor-card {
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
width: min(96vw, 800px);
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
background: linear-gradient(135deg, #0d47a1, #00897b);
|
||||
color: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
padding-top: 20px;
|
||||
padding-right: 24px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
padding-right: 260px;
|
||||
}
|
||||
|
||||
.editor-headline {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.editor-toolbar-actions {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 24px;
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.avatar-editor-body {
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.avatar-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.avatar-editor-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.3fr) 280px;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.avatar-file-name {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
color: #617487;
|
||||
font-size: 0.92rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.avatar-cropper-shell,
|
||||
.avatar-preview-shell {
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(180deg, #f7fafd 0%, #edf3f9 100%);
|
||||
border: 1px solid rgba(13, 71, 161, 0.08);
|
||||
}
|
||||
|
||||
.avatar-cropper-shell {
|
||||
min-height: 520px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.avatar-cropper-box {
|
||||
width: 100%;
|
||||
height: 480px;
|
||||
overflow: hidden;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.avatar-empty-state {
|
||||
min-height: 480px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #617487;
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.avatar-preview-shell {
|
||||
padding: 20px;
|
||||
position: sticky;
|
||||
top: 112px;
|
||||
}
|
||||
|
||||
.avatar-preview-title {
|
||||
margin-bottom: 14px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #0d47a1;
|
||||
}
|
||||
|
||||
.avatar-preview-disc {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
margin: 0 auto;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(circle at 30% 30%, #ffffff 0%, #dce6f1 100%);
|
||||
color: #617487;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar-preview-disc img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.editor-toolbar {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.editor-toolbar-actions {
|
||||
position: static;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.avatar-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.avatar-editor-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.avatar-cropper-shell {
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.avatar-cropper-box,
|
||||
.avatar-empty-state {
|
||||
height: 380px;
|
||||
min-height: 380px;
|
||||
}
|
||||
|
||||
.avatar-preview-shell {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
<template>
|
||||
<q-dialog v-model="isOpen">
|
||||
<q-card class="password-card modal-card">
|
||||
<q-card-section>
|
||||
<div class="text-overline text-primary">User access</div>
|
||||
<div class="text-h6">{{ blockState.email || 'User' }}</div>
|
||||
<div class="text-caption">
|
||||
Stato attuale: {{ blockState.status || 'n/a' }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="password-grid">
|
||||
<q-toggle
|
||||
v-model="blockedModel"
|
||||
checked-icon="block"
|
||||
unchecked-icon="lock_open"
|
||||
color="negative"
|
||||
:label="blockState.blocked ? 'Utente bloccato' : 'Utente attivo'"
|
||||
/>
|
||||
<div class="text-body2 text-grey-7">
|
||||
{{
|
||||
blockState.blocked
|
||||
? 'L’utente non potra piu accedere finche non verra sbloccato.'
|
||||
: 'L’utente potra accedere normalmente.'
|
||||
}}
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat color="grey-7" label="Chiudi" @click="isOpen = false" />
|
||||
<q-btn
|
||||
color="primary"
|
||||
:loading="saving"
|
||||
:label="blockState.blocked ? 'Salva blocco' : 'Salva sblocco'"
|
||||
@click="saveBlockState"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
import { blockUser } from 'src/api/admin';
|
||||
import { EnumUserStatus, getUser } from 'src/api/users';
|
||||
|
||||
import type { BlockDialogState } from './types';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
userUuid?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
saved: [];
|
||||
}>();
|
||||
|
||||
const $q = useQuasar();
|
||||
const saving = ref(false);
|
||||
const blockState = reactive<BlockDialogState>({
|
||||
id: '',
|
||||
email: '',
|
||||
status: 'pending',
|
||||
blocked: false,
|
||||
});
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value),
|
||||
});
|
||||
|
||||
const blockedModel = computed({
|
||||
get: () => blockState.blocked,
|
||||
set: (value: boolean) => {
|
||||
blockState.blocked = value;
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.userUuid] as const,
|
||||
([open, userUuid]) => {
|
||||
if (!open || !userUuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
void prepareDialog(userUuid);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function prepareDialog(userUuid: string): Promise<void> {
|
||||
try {
|
||||
const response = await getUser(userUuid);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
blockState.id = response.data.id;
|
||||
blockState.email = response.data.email;
|
||||
blockState.status = response.data.status;
|
||||
blockState.blocked = response.data.status === EnumUserStatus.UserStatusDisabled;
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
isOpen.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBlockState(): Promise<void> {
|
||||
saving.value = true;
|
||||
try {
|
||||
const response = await blockUser({
|
||||
id: blockState.id,
|
||||
action: blockState.blocked ? 'block' : 'unblock',
|
||||
});
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
isOpen.value = false;
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: blockState.blocked
|
||||
? `Utente ${response.data.email} bloccato.`
|
||||
: `Utente ${response.data.email} sbloccato.`,
|
||||
});
|
||||
emit('saved');
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function notifyError(error: unknown): void {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-card {
|
||||
width: min(96vw, 460px);
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
.password-card {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.password-grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,636 @@
|
|||
<template>
|
||||
<q-dialog v-model="isOpen">
|
||||
<q-card class="editor-card modal-card">
|
||||
<q-form class="form-grid" @submit.prevent="saveUser">
|
||||
<q-card-section class="editor-header">
|
||||
<div class="editor-toolbar">
|
||||
<div class="editor-headline">
|
||||
<div class="text-overline">
|
||||
{{
|
||||
dialogMode === 'create'
|
||||
? 'Nuovo utente'
|
||||
: dialogMode === 'edit'
|
||||
? 'Modifica utente'
|
||||
: 'Dettaglio utente'
|
||||
}}
|
||||
</div>
|
||||
<div class="text-h5">{{ formModel.name || 'Profilo utente' }}</div>
|
||||
<div class="text-caption">{{ formModel.email || 'Compila i dati di base' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-toolbar-actions">
|
||||
<q-btn flat color="white" label="Chiudi" @click="isOpen = false" />
|
||||
<q-btn
|
||||
v-if="dialogMode !== 'view'"
|
||||
color="white"
|
||||
text-color="primary"
|
||||
unelevated
|
||||
:loading="saving"
|
||||
:label="dialogMode === 'create' ? 'Crea utente' : 'Salva modifiche'"
|
||||
type="submit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-tabs
|
||||
v-model="tabModel"
|
||||
dense
|
||||
align="left"
|
||||
inline-label
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
class="editor-tabs"
|
||||
>
|
||||
<q-tab name="account" icon="person" label="Account" />
|
||||
<q-tab name="details" icon="badge" label="Details" />
|
||||
<q-tab name="preferences" icon="tune" label="Preferences" />
|
||||
</q-tabs>
|
||||
</q-card-section>
|
||||
<q-card-section class="editor-body">
|
||||
<q-tab-panels v-model="tabModel" animated class="editor-panels">
|
||||
<q-tab-panel name="account">
|
||||
<section class="form-section">
|
||||
<h2>Account</h2>
|
||||
<div class="section-grid">
|
||||
<q-input v-model="formModel.name" outlined label="Nome" :readonly="dialogMode === 'view'" />
|
||||
<q-input v-model="formModel.email" outlined label="Email" type="email" :readonly="dialogMode === 'view'" />
|
||||
<q-input
|
||||
v-if="dialogMode === 'create'"
|
||||
v-model="formModel.password"
|
||||
outlined
|
||||
label="Password"
|
||||
type="password"
|
||||
hint="Minimo 8 caratteri"
|
||||
/>
|
||||
<div v-if="dialogMode !== 'create'" class="avatar-inline-card span-2">
|
||||
<div class="avatar-inline-preview">
|
||||
<img v-if="formModel.avatar" :src="formModel.avatar" :alt="formModel.name" />
|
||||
<span v-else>{{ avatarInitials(formModel) }}</span>
|
||||
</div>
|
||||
<div class="avatar-inline-meta">
|
||||
<div class="text-subtitle2">Avatar</div>
|
||||
<div class="text-caption text-grey-7">
|
||||
{{ formModel.avatar ? 'Avatar profilo impostato' : 'Nessun avatar impostato' }}
|
||||
</div>
|
||||
</div>
|
||||
<q-btn
|
||||
v-if="dialogMode === 'edit'"
|
||||
flat
|
||||
color="primary"
|
||||
icon="add_a_photo"
|
||||
label="Modifica"
|
||||
@click="emit('open-avatar', formModel.id)"
|
||||
/>
|
||||
</div>
|
||||
<q-input v-else v-model="formModel.avatar" outlined label="Avatar URL" />
|
||||
<q-select
|
||||
v-model="formModel.status"
|
||||
outlined
|
||||
label="Status"
|
||||
:options="statusOptions"
|
||||
:readonly="dialogMode === 'view'"
|
||||
/>
|
||||
<q-select
|
||||
v-model="formModel.permission"
|
||||
outlined
|
||||
label="Roles"
|
||||
:options="rolesOptions"
|
||||
:readonly="dialogMode === 'view'"
|
||||
/>
|
||||
<q-select
|
||||
v-model="formModel.type"
|
||||
outlined
|
||||
label="Types"
|
||||
:options="typeOptions"
|
||||
:readonly="dialogMode === 'view'"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="details">
|
||||
<section class="form-section">
|
||||
<div class="section-heading">
|
||||
<h2>Details</h2>
|
||||
<q-toggle
|
||||
v-model="detailsEnabledModel"
|
||||
label="Abilita details"
|
||||
:disable="dialogMode === 'view'"
|
||||
/>
|
||||
</div>
|
||||
<div class="section-grid" :class="{ readonlyd: !detailsEnabled }">
|
||||
<q-input v-model="formModel.details.title" outlined label="Title" :readonly="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="formModel.details.firstName" outlined label="First name" :readonly="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="formModel.details.lastName" outlined label="Last name" :readonly="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="formModel.details.phone" outlined label="Phone" :readonly="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="formModel.details.address" outlined label="Address" class="span-2" :readonly="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="formModel.details.city" outlined label="City" :readonly="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="formModel.details.zipCode" outlined label="Zip code" :readonly="!detailsEnabled || dialogMode === 'view'" />
|
||||
<q-input v-model="formModel.details.country" outlined label="Country" :readonly="!detailsEnabled || dialogMode === 'view'" />
|
||||
</div>
|
||||
</section>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="preferences">
|
||||
<section class="form-section">
|
||||
<div class="section-heading">
|
||||
<h2>Preferences</h2>
|
||||
<q-toggle
|
||||
v-model="preferencesEnabledModel"
|
||||
label="Abilita preferences"
|
||||
:disable="dialogMode === 'view'"
|
||||
/>
|
||||
</div>
|
||||
<div class="section-grid" :class="{ readonlyd: !preferencesEnabled }">
|
||||
<q-select
|
||||
v-model="formModel.preferences.language"
|
||||
emit-value
|
||||
outlined
|
||||
label="Language"
|
||||
:options="languageOptions"
|
||||
:readonly="!preferencesEnabled || dialogMode === 'view'"
|
||||
/>
|
||||
<q-input
|
||||
v-model.number="formModel.preferences.idleTimeout"
|
||||
outlined
|
||||
type="number"
|
||||
label="Idle timeout"
|
||||
:readonly="!preferencesEnabled || dialogMode === 'view'"
|
||||
/>
|
||||
<q-input v-model="formModel.preferences.idlePin" outlined label="Idle pin" :readonly="!preferencesEnabled || dialogMode === 'view'" />
|
||||
<q-toggle v-model="formModel.preferences.useIdle" label="Use idle" :disable="!preferencesEnabled || dialogMode === 'view'" />
|
||||
<q-toggle v-model="formModel.preferences.useIdlePassword" label="Use idle password" :disable="!preferencesEnabled || dialogMode === 'view'" />
|
||||
<q-toggle v-model="formModel.preferences.useDirectLogin" label="Use direct login" :disable="!preferencesEnabled || dialogMode === 'view'" />
|
||||
<q-toggle v-model="formModel.preferences.useQuadcodeLogin" label="Use quadcode login" :disable="!preferencesEnabled || dialogMode === 'view'" />
|
||||
<q-toggle v-model="formModel.preferences.sendNoticesMail" label="Send notices mail" :disable="!preferencesEnabled || dialogMode === 'view'" />
|
||||
</div>
|
||||
</section>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-card-section>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
import { EnumUserRole } from 'src/api/auth';
|
||||
import { updateUser } from 'src/api/admin';
|
||||
import { EnumUserStatus, EnumUserTypes, createUser, getUser, type UpdateUserRequest, type UserCreateRequest, type UserDetails, type UserPreferences, type UpdateUserDetailsRequest , type UpdateUserPreferencesRequest } from 'src/api/users';
|
||||
import { adminUpdateUserPreferences, adminUpdateUserDetails } from 'src/api/admin';
|
||||
import { createEmptyUserForm, mapUserToForm, type DialogMode, type EditorTab, type UserDetailsForm, type UserFormState, type UserPreferencesForm } from './types';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const langs = [
|
||||
{
|
||||
code: 'it',
|
||||
short_name: 'IT',
|
||||
},
|
||||
{
|
||||
code: 'en',
|
||||
short_name: 'EN',
|
||||
},
|
||||
{
|
||||
code: 'en_us',
|
||||
short_name: 'EN',
|
||||
},
|
||||
{
|
||||
code: 'de',
|
||||
short_name: 'DE',
|
||||
},
|
||||
{
|
||||
code: 'de_ch',
|
||||
short_name: 'DE',
|
||||
},
|
||||
{
|
||||
code: 'fr',
|
||||
short_name: 'FR',
|
||||
},
|
||||
{
|
||||
code: 'fr_ch',
|
||||
short_name: 'FR',
|
||||
},
|
||||
];
|
||||
|
||||
const languageOptions = computed(() =>
|
||||
langs.map((lang) => ({
|
||||
label: t(`language.${lang.code}`),
|
||||
value: t(`language.${lang.code}`),
|
||||
})),
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
dialogMode: DialogMode;
|
||||
userId?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
saved: [];
|
||||
'open-avatar': [id: string];
|
||||
}>();
|
||||
|
||||
const $q = useQuasar();
|
||||
const saving = ref(false);
|
||||
const loading = ref(false);
|
||||
const editorTab = ref<EditorTab>('account');
|
||||
const detailsEnabled = ref(true);
|
||||
const preferencesEnabled = ref(true);
|
||||
const statusOptions = Object.values(EnumUserStatus);
|
||||
const rolesOptions = Object.values(EnumUserRole);
|
||||
const typeOptions = Object.values(EnumUserTypes);
|
||||
const formModel = reactive<UserFormState>(createEmptyUserForm());
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value),
|
||||
});
|
||||
|
||||
const tabModel = computed({
|
||||
get: () => editorTab.value,
|
||||
set: (value: EditorTab) => {
|
||||
editorTab.value = value;
|
||||
},
|
||||
});
|
||||
|
||||
const detailsEnabledModel = computed({
|
||||
get: () => detailsEnabled.value,
|
||||
set: (value: boolean) => {
|
||||
detailsEnabled.value = value;
|
||||
},
|
||||
});
|
||||
|
||||
const preferencesEnabledModel = computed({
|
||||
get: () => preferencesEnabled.value,
|
||||
set: (value: boolean) => {
|
||||
preferencesEnabled.value = value;
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.userId, props.dialogMode] as const,
|
||||
([open, userId, dialogMode]) => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
void prepareDialog(dialogMode, userId);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function prepareDialog(dialogMode: DialogMode, userId?: string): Promise<void> {
|
||||
debugger
|
||||
loading.value = true;
|
||||
editorTab.value = 'account';
|
||||
|
||||
try {
|
||||
if (dialogMode === 'create') {
|
||||
resetForm();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
throw new Error('ID utente mancante.');
|
||||
}
|
||||
|
||||
const response = await getUser(userId);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
resetForm(response.data);
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
isOpen.value = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm(user?: Parameters<typeof mapUserToForm>[0]): void {
|
||||
const next = user ? mapUserToForm(user) : createEmptyUserForm();
|
||||
Object.assign(formModel, next);
|
||||
detailsEnabled.value = !!user?.details || !user;
|
||||
preferencesEnabled.value = !!user?.preferences || !user;
|
||||
}
|
||||
|
||||
function normalizeNullableString(value: string): string | null {
|
||||
const normalized = value.trim();
|
||||
return normalized === '' ? null : normalized;
|
||||
}
|
||||
|
||||
function sanitizeDetails(value: UserDetailsForm): UserDetails | null {
|
||||
const normalized: UserDetailsForm = {
|
||||
id: value.id,
|
||||
userId: value.userId,
|
||||
title: value.title.trim(),
|
||||
firstName: value.firstName.trim(),
|
||||
lastName: value.lastName.trim(),
|
||||
address: value.address.trim(),
|
||||
city: value.city.trim(),
|
||||
zipCode: value.zipCode.trim(),
|
||||
country: value.country.trim(),
|
||||
phone: value.phone.trim(),
|
||||
};
|
||||
|
||||
return Object.values(normalized).some(Boolean) ? normalized as UserDetails : null;
|
||||
}
|
||||
|
||||
function sanitizePreferences(value: UserPreferencesForm): UserPreferences | null {
|
||||
const normalized: UserPreferencesForm = {
|
||||
id: value.id,
|
||||
userId: value.userId,
|
||||
useIdle: value.useIdle,
|
||||
idleTimeout: Number(value.idleTimeout) || 0,
|
||||
useIdlePassword: value.useIdlePassword,
|
||||
idlePin: value.idlePin.trim(),
|
||||
useDirectLogin: value.useDirectLogin,
|
||||
useQuadcodeLogin: value.useQuadcodeLogin,
|
||||
sendNoticesMail: value.sendNoticesMail,
|
||||
language: value.language,
|
||||
};
|
||||
|
||||
const hasData =
|
||||
normalized.useIdle ||
|
||||
normalized.idleTimeout > 0 ||
|
||||
normalized.useIdlePassword ||
|
||||
normalized.idlePin !== '' ||
|
||||
normalized.useDirectLogin ||
|
||||
normalized.useQuadcodeLogin ||
|
||||
normalized.sendNoticesMail ||
|
||||
normalized.language !== '';
|
||||
|
||||
return hasData ? normalized as UserPreferences : null;
|
||||
}
|
||||
|
||||
async function saveUser(): Promise<void> {
|
||||
if (props.dialogMode === 'view') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formModel.name.trim() || !formModel.email.trim()) {
|
||||
$q.notify({ type: 'negative', message: 'Nome ed email sono obbligatori.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.dialogMode === 'create' && formModel.password.trim().length < 8) {
|
||||
$q.notify({ type: 'negative', message: 'La password deve contenere almeno 8 caratteri.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
id: formModel.id,
|
||||
name: formModel.name.trim(),
|
||||
email: formModel.email.trim(),
|
||||
password: props.dialogMode === 'create' ? formModel.password : '',
|
||||
status: formModel.status,
|
||||
permission: formModel.permission,
|
||||
type: formModel.type,
|
||||
avatar: normalizeNullableString(formModel.avatar),
|
||||
details: detailsEnabled.value ? sanitizeDetails(formModel.details) : null,
|
||||
preferences: preferencesEnabled.value ? sanitizePreferences(formModel.preferences) : null,
|
||||
};
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
if (props.dialogMode === 'create') {
|
||||
const response = await createUser(payload as UserCreateRequest);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
$q.notify({ type: 'positive', message: `Utente ${response.data.email} creato.` });
|
||||
} else {
|
||||
console.log('Updating user with payload:', editorTab.value, payload);
|
||||
let response;
|
||||
switch (editorTab.value) {
|
||||
case 'account':
|
||||
response = await updateUser(payload as UpdateUserRequest);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
break;
|
||||
case 'details':
|
||||
response = await adminUpdateUserDetails(payload.details as unknown as UpdateUserDetailsRequest);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
break;
|
||||
case 'preferences':
|
||||
response = await adminUpdateUserPreferences(payload.preferences as unknown as UpdateUserPreferencesRequest);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!response) {
|
||||
response = await updateUser(payload as UpdateUserRequest);
|
||||
}
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
$q.notify({ type: 'positive', message: `Utente ${response.data.email} aggiornato.` });
|
||||
}
|
||||
|
||||
isOpen.value = false;
|
||||
emit('saved');
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function avatarInitials(user: Pick<UserFormState, 'name' | 'email'>): string {
|
||||
const source = user.name.trim() || user.email.trim();
|
||||
const parts = source.split(/\s+/).filter(Boolean);
|
||||
const first = parts[0] ?? '';
|
||||
const second = parts[1] ?? '';
|
||||
if (parts.length === 0) {
|
||||
return '?';
|
||||
}
|
||||
if (parts.length === 1) {
|
||||
return first.slice(0, 2).toUpperCase();
|
||||
}
|
||||
return `${first.charAt(0)}${second.charAt(0)}`.toUpperCase();
|
||||
}
|
||||
|
||||
function notifyError(error: unknown): void {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.readonly, .readonly *, [readonly], [readonly] * {
|
||||
outline: 0 !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.disabled, .disabled *, [disabled], [disabled] * {
|
||||
outline: 0 !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.editor-card {
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.modal-card {
|
||||
width: min(96vw, 800px);
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
background: linear-gradient(135deg, #0d47a1, #00897b);
|
||||
color: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
padding-top: 20px;
|
||||
padding-right: 24px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
padding-right: 260px;
|
||||
}
|
||||
|
||||
.editor-headline {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.editor-toolbar-actions {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 24px;
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.editor-body {
|
||||
padding: 24px;
|
||||
padding-top: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.editor-tabs {
|
||||
border-radius: 16px;
|
||||
background: rgba(13, 71, 161, 0.06);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.editor-panels {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.form-section h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.section-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.avatar-inline-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(13, 71, 161, 0.12);
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #eef4fa 100%);
|
||||
}
|
||||
|
||||
.avatar-inline-preview {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0d47a1, #26a69a);
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.avatar-inline-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-inline-meta {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.span-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.section-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.span-2 {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.editor-toolbar-actions {
|
||||
position: static;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
<template>
|
||||
<q-dialog v-model="isOpen">
|
||||
<q-card class="password-card modal-card">
|
||||
<q-card-section>
|
||||
<div class="text-overline text-primary">Change password</div>
|
||||
<div class="text-h6">{{ userEmail || 'User' }}</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="password-grid">
|
||||
<q-input
|
||||
v-model="formModel.password"
|
||||
outlined
|
||||
type="password"
|
||||
label="New password"
|
||||
hint="Minimo 8 caratteri"
|
||||
/>
|
||||
<q-input
|
||||
v-model="formModel.confirmPassword"
|
||||
outlined
|
||||
type="password"
|
||||
label="Confirm password"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat color="grey-7" label="Chiudi" @click="isOpen = false" />
|
||||
<q-btn color="primary" label="Salva password" :loading="saving" @click="savePasswordChange" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
import { getUser, updatePassword } from 'src/api/users';
|
||||
|
||||
import type { PasswordFormState } from './types';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
userId?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
saved: [];
|
||||
}>();
|
||||
|
||||
const $q = useQuasar();
|
||||
const saving = ref(false);
|
||||
const userEmail = ref('');
|
||||
const formModel = reactive<PasswordFormState>({
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value),
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.userId] as const,
|
||||
([open, userId]) => {
|
||||
if (!open || !userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
void prepareDialog(userId);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function prepareDialog(userId: string): Promise<void> {
|
||||
try {
|
||||
const response = await getUser(userId);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
userEmail.value = response.data.email;
|
||||
formModel.password = '';
|
||||
formModel.confirmPassword = '';
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
isOpen.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePasswordChange(): Promise<void> {
|
||||
if (!props.userId) {
|
||||
$q.notify({ type: 'negative', message: 'ID utente mancante.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (formModel.password.trim().length < 8) {
|
||||
$q.notify({ type: 'negative', message: 'La password deve contenere almeno 8 caratteri.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (formModel.password !== formModel.confirmPassword) {
|
||||
$q.notify({ type: 'negative', message: 'Le password non coincidono.' });
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const response = await updatePassword({
|
||||
id: props.userId,
|
||||
password: formModel.password,
|
||||
});
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
isOpen.value = false;
|
||||
$q.notify({ type: 'positive', message: `Password aggiornata per ${userEmail.value || 'utente'}.` });
|
||||
emit('saved');
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function notifyError(error: unknown): void {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-card {
|
||||
width: min(96vw, 460px);
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
.password-card {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.password-grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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<UserDetails, 'createdAt' | 'updatedAt'>;
|
||||
|
||||
export type UserPreferencesForm = Omit<UserPreferences, 'createdAt' | 'updatedAt'>;
|
||||
|
||||
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',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { EnumUserStatus, register, type UserCreateRequest } from 'src/api/users';
|
||||
import { EnumUserStatus, EnumUserTypes, register, type UserCreateRequest } from 'src/api/users';
|
||||
|
||||
const $q = useQuasar();
|
||||
const loading = ref(false);
|
||||
|
|
@ -113,9 +113,11 @@ async function submit(): Promise<void> {
|
|||
password: form.password,
|
||||
permission: "user",
|
||||
status: EnumUserStatus.UserStatusPending,
|
||||
types: ['internal'],
|
||||
type: EnumUserTypes.UserTypeInternal,
|
||||
avatar: null,
|
||||
details: {
|
||||
id: 0,
|
||||
userId: '',
|
||||
title: '',
|
||||
firstName: form.firstName.trim(),
|
||||
lastName: form.lastName.trim(),
|
||||
|
|
@ -124,10 +126,6 @@ async function submit(): Promise<void> {
|
|||
zipCode: '',
|
||||
country: '',
|
||||
phone: '',
|
||||
id: 0,
|
||||
userId: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
preferences: null,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import { defineStore, acceptHMRUpdate } from 'pinia';
|
||||
import { me, type User } from 'src/api/users';;
|
||||
import { type Nullable } from 'src/api/apiTypes';
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
user: null as Nullable<User>,
|
||||
error: null as Nullable<string>,
|
||||
counter: 0,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
userData: (state) => state.user,
|
||||
userError: (state) => state.error,
|
||||
userCounter: (state) => state.counter,
|
||||
},
|
||||
|
||||
actions: {
|
||||
getUser: async function () {
|
||||
try {
|
||||
const response = await me();
|
||||
console.log('Current user:', response);
|
||||
this.setUser(response.data);
|
||||
this.error = null;
|
||||
this.counter++;
|
||||
} catch (error) {
|
||||
this.clearUser();
|
||||
this.counter++;
|
||||
this.error = 'Failed to fetch user data from server: ' + (error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
},
|
||||
setUser(u: User) {
|
||||
this.user = u;
|
||||
},
|
||||
clearUser() {
|
||||
this.user = null;
|
||||
this.error = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot));
|
||||
}
|
||||
Loading…
Reference in New Issue