Merge pull request 'feature/responses-v2' (#27) from feature/responses-v2 into dev

Reviewed-on: winter-access/backend_nam#27
This commit is contained in:
areeqakbr 2025-06-22 11:58:48 +00:00
commit bcb9ac21a0
7 changed files with 98 additions and 20 deletions

View File

@ -20,6 +20,7 @@ type NearestDeviceResponse struct {
Province *string `json:"province,omitempty"`
City *string `json:"city,omitempty"`
District *string `json:"district,omitempty"`
ImageURL *string `json:"image_url"`
// Connection counts
BackboneCount int `json:"backbone_count"`
@ -44,7 +45,8 @@ type NearestDeviceDetailResponse struct {
Province *string `json:"province,omitempty"`
City *string `json:"city,omitempty"`
District *string `json:"district,omitempty"`
ImageURL *string `json:"image_url,omitempty"`
ImageURL *string `json:"image_url"`
ImageURLs []string `json:"image_urls"`
// Detailed connection information
Backbones []BackboneConnectionInfo `json:"backbones"`

View File

@ -16,10 +16,10 @@ const (
)
type UserRegisterDTO struct {
Name string `json:"name" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required,min=6"`
NomorInduk string `json:"nomor_induk" validate:"required"`
Name string `json:"name" validate:"required,min=2,max=100,alphaspace"`
Username string `json:"username" validate:"required,min=3,max=50,alphanumunderscore,nospace"`
Password string `json:"password" validate:"required,min=6,max=100"`
NomorInduk string `json:"nomor_induk" validate:"required,min=3,max=50,alphanum"`
}
type UserLoginDTO struct {

View File

@ -16,13 +16,13 @@ const (
type User struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
NomorInduk *string `json:"nomor_induk,omitempty" gorm:"unique"` // Add this field
NomorInduk *string `json:"nomor_induk,omitempty" gorm:"unique"`
RoleID uuid.UUID `json:"role_id" gorm:"type:uuid"`
Role Role `json:"role" gorm:"foreignKey:RoleID"`
Name string `json:"name"`
Name string `json:"name" gorm:"unique"`
Username string `json:"username" gorm:"unique"`
Password string `json:"password"`
Status UserStatus `json:"status" gorm:"type:varchar(20);default:'pending'"` // Add status field
Status UserStatus `json:"status" gorm:"type:varchar(20);default:'pending'"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -11,10 +11,10 @@ type UsersRepo interface {
Post(user entity.User) error
GetRoleByDepartment(departmentName string) (entity.Role, error)
GetUserByUsername(username string) (entity.User, error)
GetUserByNomorInduk(nomorInduk string) (entity.User, error) // Add this
CreateUserFromExternal(user entity.User) error // Add this
UpdateUserRole(nomorInduk string, roleID uuid.UUID) error // Add this
GetAllUsers() ([]entity.User, error) // Add this
GetUserByNomorInduk(nomorInduk string) (entity.User, error)
CreateUserFromExternal(user entity.User) error
UpdateUserRole(nomorInduk string, roleID uuid.UUID) error
GetAllUsers() ([]entity.User, error)
RegisterUser(user entity.User) error
GetPendingUsers() ([]entity.User, error)
UpdateUserStatus(userID uuid.UUID, status entity.UserStatus) error
@ -23,6 +23,7 @@ type UsersRepo interface {
CreateSuperAdminBypass(user entity.User) error
UpdateUserRoleByID(userID uuid.UUID, roleID uuid.UUID) error
UpdateUserRoleByUsername(username string, roleID uuid.UUID) error
GetUserByName(name string) (entity.User, error)
}
type usersRepo struct {
@ -35,6 +36,13 @@ func NewUsersRepo(db *gorm.DB) UsersRepo {
}
}
func (r *usersRepo) GetUserByName(name string) (entity.User, error) {
var user entity.User
// Use LOWER() function for case-insensitive comparison
err := r.db.Where("LOWER(name) = LOWER(?)", name).Preload("Role").First(&user).Error
return user, err
}
func (r *usersRepo) UpdateUserRoleByID(userID uuid.UUID, roleID uuid.UUID) error {
return r.db.Model(&entity.User{}).Where("id = ?", userID).Update("role_id", roleID).Error
}

View File

@ -7,6 +7,7 @@ import (
"users_management/m/model/entity"
"users_management/m/repository"
"users_management/m/utils"
"users_management/m/utils/validation"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
@ -15,10 +16,10 @@ import (
type UsersUsecase interface {
GetRoleByDepartment(departmentName string) (uuid.UUID, error)
GetUserByUsername(username string) (entity.User, error)
GetUserByNomorInduk(nomorInduk string) (entity.User, error) // Add this
CreateUserFromExternal(nomorInduk, name, roleName string) error // Add this
UpdateUserRole(nomorInduk, roleName string) error // Add this
GetAllUsers() ([]entity.User, error) // Add this
GetUserByNomorInduk(nomorInduk string) (entity.User, error)
CreateUserFromExternal(nomorInduk, name, roleName string) error
UpdateUserRole(nomorInduk, roleName string) error
GetAllUsers() ([]entity.User, error)
RegisterUser(registerDTO dto.UserRegisterDTO) error
GetPendingUsers() ([]dto.PendingUserResponse, error)
ApproveUser(userID uuid.UUID) error
@ -28,6 +29,7 @@ type UsersUsecase interface {
UpdateUserRoleByID(userID uuid.UUID, newRoleName string) error
UpdateUserRoleByUsername(username, newRoleName string) error
GetUserByID(userID uuid.UUID) (entity.User, error)
GetUserByName(name string) (entity.User, error)
}
@ -37,12 +39,19 @@ type usersUsecase struct {
}
func NewUsersUsecase(userRepo repository.UsersRepo) UsersUsecase {
validate := validator.New()
validation.RegisterCustomValidators(validate)
return &usersUsecase{
userRepo: userRepo,
validate: validator.New(),
validate: validate,
}
}
func (u *usersUsecase) GetUserByName(name string) (entity.User, error) {
return u.userRepo.GetUserByName(name)
}
func (u *usersUsecase) CreateSuperAdminBypass(user entity.User) error {
return u.userRepo.CreateSuperAdminBypass(user)
@ -92,7 +101,6 @@ func (u *usersUsecase) RegisterUser(registerDTO dto.UserRegisterDTO) error {
if err := u.validate.Struct(registerDTO); err != nil {
return err
}
// Check if username already exists
existingUser, err := u.userRepo.GetUserByUsernameWithStatus(registerDTO.Username)
if err == nil && existingUser.ID != uuid.Nil {
@ -107,6 +115,11 @@ func (u *usersUsecase) RegisterUser(registerDTO dto.UserRegisterDTO) error {
}
}
existingUserByName, err := u.userRepo.GetUserByName(registerDTO.Name)
if err == nil && existingUserByName.ID != uuid.Nil {
return errors.New("name already exists, please use a different name")
}
// Hash password
hashedPassword, err := utils.HashPassword(registerDTO.Password)
if err != nil {
@ -119,7 +132,6 @@ func (u *usersUsecase) RegisterUser(registerDTO dto.UserRegisterDTO) error {
}
// Create user with pending status
user := entity.User{
ID: uuid.New(),

View File

@ -49,6 +49,7 @@ func ConvertToNearestDeviceResponses(devices []entity.DeviceWithDistance, repo r
Province: device.Province,
City: device.City,
District: device.District,
ImageURL: device.ImageURL,
BackboneCount: backboneCount,
FishboneCount: fishboneCount,
TowerCount: towerCount,
@ -94,6 +95,13 @@ func ConvertToNearestDeviceDetailResponse(device entity.Device, distance float64
backboneInfos := convertToBackboneConnectionInfos(backbones, device.ID)
fishboneInfos := convertToFishboneConnectionInfos(fishbones, device.ID)
towerInfos := convertToTowerConnectionInfos(towers, device.Latitude, device.Longitude)
allImageURLs := device.GetAllImageURLs()
if len(allImageURLs) == 0 {
allImageURLs = []string{}
}
log.Printf("Device %s has %d images", device.DeviceCode, len(allImageURLs))
response := res.NearestDeviceDetailResponse{
ID: device.ID,
@ -108,7 +116,7 @@ func ConvertToNearestDeviceDetailResponse(device entity.Device, distance float64
Province: device.Province,
City: device.City,
District: device.District,
ImageURL: device.ImageURL,
ImageURLs: device.GetAllImageURLs(),
Backbones: backboneInfos,
Fishbones: fishboneInfos,
Towers: towerInfos,

View File

@ -0,0 +1,48 @@
package validation
import (
"regexp"
"strings"
"unicode"
"github.com/go-playground/validator/v10"
)
// RegisterCustomValidators registers custom validation rules
func RegisterCustomValidators(validate *validator.Validate) {
validate.RegisterValidation("nospace", validateNoSpace)
validate.RegisterValidation("alphanumunderscore", validateAlphaNumUnderscore)
validate.RegisterValidation("alphaspace", validateAlphaSpace) // Add this
validate.RegisterValidation("alphanum", validateAlphaNum) // Add this
}
// validateNoSpace checks that the field contains no spaces
func validateNoSpace(fl validator.FieldLevel) bool {
value := fl.Field().String()
return !strings.Contains(value, " ")
}
// validateAlphaNumUnderscore allows alphanumeric characters and underscores only
func validateAlphaNumUnderscore(fl validator.FieldLevel) bool {
value := fl.Field().String()
matched, _ := regexp.MatchString("^[a-zA-Z0-9_]+$", value)
return matched
}
// validateAlphaSpace allows alphabetic characters and spaces only (for names)
func validateAlphaSpace(fl validator.FieldLevel) bool {
value := fl.Field().String()
for _, char := range value {
if !unicode.IsLetter(char) && char != ' ' {
return false
}
}
return true
}
// validateAlphaNum allows alphanumeric characters only
func validateAlphaNum(fl validator.FieldLevel) bool {
value := fl.Field().String()
matched, _ := regexp.MatchString("^[a-zA-Z0-9]+$", value)
return matched
}