Skip to content

Commit 5e52a44

Browse files
author
Diyor Khaydarov
committed
Merge branch 'staging' of https://github.com/iota-uz/iota-sdk into staging
2 parents c68b0e3 + 37df787 commit 5e52a44

File tree

12 files changed

+155
-15
lines changed

12 files changed

+155
-15
lines changed

Diff for: modules/core/domain/aggregates/user/user_repository.go

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type Repository interface {
4040
GetByPhone(ctx context.Context, phone string) (User, error)
4141
GetPaginated(ctx context.Context, params *FindParams) ([]User, error)
4242
GetByID(ctx context.Context, id uint) (User, error)
43+
PhoneExists(ctx context.Context, phone string) (bool, error)
4344
Create(ctx context.Context, user User) (User, error)
4445
Update(ctx context.Context, user User) error
4546
UpdateLastAction(ctx context.Context, id uint) error
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package user
2+
3+
import (
4+
"context"
5+
)
6+
7+
type Validator interface {
8+
ValidateCreate(ctx context.Context, u User) error
9+
}

Diff for: modules/core/infrastructure/persistence/user_repository.go

+18
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ const (
4040

4141
userCountQuery = `SELECT COUNT(u.id) FROM users u`
4242

43+
userExistsQuery = `SELECT 1 FROM users u`
44+
4345
userUpdateLastLoginQuery = `UPDATE users SET last_login = NOW() WHERE id = $1`
4446

4547
userUpdateLastActionQuery = `UPDATE users SET last_action = NOW() WHERE id = $1`
@@ -260,6 +262,22 @@ func (g *PgUserRepository) GetByPhone(ctx context.Context, phone string) (user.U
260262
return users[0], nil
261263
}
262264

265+
func (g *PgUserRepository) PhoneExists(ctx context.Context, phone string) (bool, error) {
266+
tx, err := composables.UseTx(ctx)
267+
if err != nil {
268+
return false, errors.Wrap(err, "failed to get transaction")
269+
}
270+
271+
base := repo.Join(userExistsQuery, "WHERE u.phone = $1")
272+
query := repo.Exists(base)
273+
274+
exists := false
275+
if err := tx.QueryRow(ctx, query, phone).Scan(&exists); err != nil {
276+
return false, errors.Wrap(err, "checking phone existence failed")
277+
}
278+
return exists, nil
279+
}
280+
263281
func (g *PgUserRepository) Create(ctx context.Context, data user.User) (user.User, error) {
264282
tx, err := composables.UseTx(ctx)
265283
if err != nil {

Diff for: modules/core/module.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package core
22

33
import (
44
"embed"
5-
5+
"github.com/iota-uz/iota-sdk/modules/core/validators"
66
"github.com/iota-uz/iota-sdk/pkg/spotlight"
77

88
icons "github.com/iota-uz/icons/phosphor"
@@ -50,9 +50,12 @@ func (m *Module) Register(app application.Application) error {
5050
roleRepo := persistence.NewRoleRepository()
5151
permRepo := persistence.NewPermissionRepository()
5252

53+
// custom validations
54+
userValidator := validators.NewUserValidator(userRepo)
55+
5356
app.RegisterServices(
5457
services.NewUploadService(uploadRepo, fsStorage, app.EventPublisher()),
55-
services.NewUserService(userRepo, app.EventPublisher()),
58+
services.NewUserService(userRepo, userValidator, app.EventPublisher()),
5659
services.NewSessionService(persistence.NewSessionRepository(), app.EventPublisher()),
5760
)
5861
app.RegisterServices(

Diff for: modules/core/presentation/controllers/user_controller.go

+26-12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package controllers
33
import (
44
"bytes"
55
"context"
6+
"errors"
67
"net/http"
78
"net/url"
89
"sort"
@@ -28,6 +29,7 @@ import (
2829
"github.com/iota-uz/iota-sdk/pkg/server"
2930
"github.com/iota-uz/iota-sdk/pkg/shared"
3031
"github.com/iota-uz/iota-sdk/pkg/types"
32+
"github.com/iota-uz/iota-sdk/pkg/validators"
3133
"github.com/nicksnyder/go-i18n/v2/i18n"
3234
"github.com/sirupsen/logrus"
3335
"golang.org/x/text/language"
@@ -412,22 +414,17 @@ func (c *UsersController) Create(
412414
roleService *services.RoleService,
413415
groupService *services.GroupService,
414416
) {
415-
dto, err := composables.UseForm(&user.CreateDTO{}, r)
416-
if err != nil {
417-
logger.Errorf("Error parsing form: %v", err)
418-
http.Error(w, err.Error(), http.StatusBadRequest)
419-
return
420-
}
417+
respondWithForm := func(errors map[string]string, dto *user.CreateDTO) {
418+
ctx := r.Context()
421419

422-
if errors, ok := dto.Ok(r.Context()); !ok {
423-
roles, err := roleService.GetAll(r.Context())
420+
roles, err := roleService.GetAll(ctx)
424421
if err != nil {
425422
logger.Errorf("Error retrieving roles: %v", err)
426423
http.Error(w, "Error retrieving roles", http.StatusInternalServerError)
427424
return
428425
}
429426

430-
groups, err := groupService.GetAll(r.Context())
427+
groups, err := groupService.GetAll(ctx)
431428
if err != nil {
432429
logger.Errorf("Error retrieving groups: %v", err)
433430
http.Error(w, "Error retrieving groups", http.StatusInternalServerError)
@@ -440,16 +437,27 @@ func (c *UsersController) Create(
440437
http.Error(w, err.Error(), http.StatusInternalServerError)
441438
return
442439
}
440+
443441
props := &users.CreateFormProps{
444442
User: *mappers.UserToViewModel(userEntity),
445443
Roles: mapping.MapViewModels(roles, mappers.RoleToViewModel),
446444
Groups: mapping.MapViewModels(groups, mappers.GroupToViewModel),
447445
PermissionGroups: c.permissionGroups(c.app.RBAC()),
448446
Errors: errors,
449447
}
450-
templ.Handler(
451-
users.CreateForm(props), templ.WithStreaming(),
452-
).ServeHTTP(w, r)
448+
449+
templ.Handler(users.CreateForm(props), templ.WithStreaming()).ServeHTTP(w, r)
450+
}
451+
452+
dto, err := composables.UseForm(&user.CreateDTO{}, r)
453+
if err != nil {
454+
logger.Errorf("Error parsing form: %v", err)
455+
http.Error(w, err.Error(), http.StatusBadRequest)
456+
return
457+
}
458+
459+
if errs, ok := dto.Ok(r.Context()); !ok {
460+
respondWithForm(errs, dto)
453461
return
454462
}
455463

@@ -487,6 +495,12 @@ func (c *UsersController) Create(
487495
}
488496

489497
if err := userService.Create(r.Context(), userEntity); err != nil {
498+
var errs *validators.ValidationError
499+
if errors.As(err, &errs) {
500+
respondWithForm(errs.Fields, dto)
501+
return
502+
}
503+
490504
logger.Errorf("Error creating user: %v", err)
491505
http.Error(w, err.Error(), http.StatusInternalServerError)
492506
return

Diff for: modules/core/presentation/locales/en.json

+3
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@
227227
"Delete": "Delete user",
228228
"DeleteConfirmation": "Are you sure you want to delete this user?",
229229
"Permissions": "User Permissions"
230+
},
231+
"Errors": {
232+
"PhoneUnique": "Phone number is already used"
230233
}
231234
},
232235
"Roles": {

Diff for: modules/core/presentation/locales/ru.json

+3
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,9 @@
299299
"Delete": "Удалить пользователя",
300300
"DeleteConfirmation": "Вы уверены что хотите удалить пользователя?",
301301
"Permissions": "Разрешения пользователя"
302+
},
303+
"Errors": {
304+
"PhoneUnique": "Номер телефона уже используется"
302305
}
303306
},
304307
"Projects": {

Diff for: modules/core/presentation/locales/uz.json

+3
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@
227227
"Delete": "Foydalanuvchini o'chirish",
228228
"DeleteConfirmation": "Haqiqatan ham bu foydalanuvchini o'chirmoqchimisiz?",
229229
"Permissions": "Foydalanuvchi ruxsatlari"
230+
},
231+
"Errors": {
232+
"PhoneUnique": "Telefon raqami allaqachon foydalanilgan"
230233
}
231234
},
232235
"Roles": {

Diff for: modules/core/services/user_service.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import (
1111

1212
type UserService struct {
1313
repo user.Repository
14+
validator user.Validator
1415
publisher eventbus.EventBus
1516
}
1617

17-
func NewUserService(repo user.Repository, publisher eventbus.EventBus) *UserService {
18+
func NewUserService(repo user.Repository, validator user.Validator, publisher eventbus.EventBus) *UserService {
1819
return &UserService{
1920
repo: repo,
21+
validator: validator,
2022
publisher: publisher,
2123
}
2224
}
@@ -66,6 +68,9 @@ func (s *UserService) Create(ctx context.Context, data user.User) error {
6668
if err := composables.CanUser(ctx, permissions.UserCreate); err != nil {
6769
return err
6870
}
71+
if err := s.validator.ValidateCreate(ctx, data); err != nil {
72+
return err
73+
}
6974
logger := composables.UseLogger(ctx)
7075
tx, err := composables.BeginTx(ctx)
7176
if err != nil {

Diff for: modules/core/validators/user_validator.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package validators
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/iota-uz/iota-sdk/modules/core/domain/aggregates/user"
7+
"github.com/iota-uz/iota-sdk/pkg/composables"
8+
"github.com/iota-uz/iota-sdk/pkg/validators"
9+
"github.com/nicksnyder/go-i18n/v2/i18n"
10+
)
11+
12+
type UserValidator struct {
13+
repo user.Repository
14+
}
15+
16+
func NewUserValidator(repo user.Repository) *UserValidator {
17+
return &UserValidator{repo: repo}
18+
}
19+
20+
func (v *UserValidator) ValidateCreate(ctx context.Context, u user.User) error {
21+
l, ok := composables.UseLocalizer(ctx)
22+
if !ok {
23+
panic(composables.ErrNoLocalizer)
24+
}
25+
26+
errors := map[string]string{}
27+
28+
if u.Phone().Value() != "" {
29+
exists, err := v.repo.PhoneExists(ctx, u.Phone().Value())
30+
if err != nil {
31+
return fmt.Errorf("failed to check phone existence: %w", err)
32+
}
33+
if exists {
34+
errors["Phone"] = l.MustLocalize(&i18n.LocalizeConfig{
35+
MessageID: "Users.Errors.PhoneUnique",
36+
})
37+
}
38+
}
39+
40+
if len(errors) > 0 {
41+
return validators.NewValidationError(errors)
42+
}
43+
44+
return nil
45+
}

Diff for: pkg/repo/repo.go

+10
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ func Join(expressions ...string) string {
6868
return strings.Join(expressions, " ")
6969
}
7070

71+
// Exists wraps a SELECT query inside SELECT EXISTS (...).
72+
//
73+
// Example usage:
74+
//
75+
// query := repo.Exists("SELECT 1 FROM users WHERE phone = $1")
76+
// // Returns: "SELECT EXISTS (SELECT 1 FROM users WHERE phone = $1)"
77+
func Exists(inner string) string {
78+
return "SELECT EXISTS (" + inner + ")"
79+
}
80+
7181
// OrderBy generates an SQL ORDER BY clause for the given fields and sort direction.
7282
// Returns an empty string if no fields are provided.
7383
//

Diff for: pkg/validators/validation_error.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package validators
2+
3+
type ValidationError struct {
4+
Fields map[string]string
5+
}
6+
7+
func (v *ValidationError) Error() string {
8+
return "validators failed"
9+
}
10+
11+
func NewValidationError(fields map[string]string) *ValidationError {
12+
return &ValidationError{Fields: fields}
13+
}
14+
15+
func (v *ValidationError) HasField(field string) bool {
16+
_, exists := v.Fields[field]
17+
return exists
18+
}
19+
20+
func (v *ValidationError) Field(field string) string {
21+
return v.Fields[field]
22+
}
23+
24+
func (v *ValidationError) FieldsMap() map[string]string {
25+
return v.Fields
26+
}

0 commit comments

Comments
 (0)