From 7a543877222b087ca6fbb7d66ccc38d599be2e38 Mon Sep 17 00:00:00 2001 From: Amir Tavakolian Date: Tue, 19 May 2026 09:42:22 +0330 Subject: [PATCH] add store role feature --- authorizationapp/app.go | 69 +++++++++++++++++++ authorizationapp/delivery/http/handler.go | 35 ++++++++++ authorizationapp/delivery/http/server.go | 26 +++++++ deploy/authorization/config.yaml | 1 + domain/authorization/entity/role.go | 13 ++++ .../migrations/1779100307_add_roles_table.sql | 13 ++++ domain/authorization/repository/mysql_repo.go | 34 +++++++++ domain/authorization/service/authorization.go | 45 ++++++++++++ domain/authorization/service/param.go | 6 ++ domain/authorization/service/repo_contract.go | 10 +++ 10 files changed, 252 insertions(+) create mode 100644 authorizationapp/app.go create mode 100644 authorizationapp/delivery/http/handler.go create mode 100644 authorizationapp/delivery/http/server.go create mode 100644 deploy/authorization/config.yaml create mode 100644 domain/authorization/entity/role.go create mode 100644 domain/authorization/repository/migrations/1779100307_add_roles_table.sql create mode 100644 domain/authorization/repository/mysql_repo.go create mode 100644 domain/authorization/service/authorization.go create mode 100644 domain/authorization/service/param.go create mode 100644 domain/authorization/service/repo_contract.go diff --git a/authorizationapp/app.go b/authorizationapp/app.go new file mode 100644 index 00000000..aed312ff --- /dev/null +++ b/authorizationapp/app.go @@ -0,0 +1,69 @@ +package authorizationapp + +import ( + "git.gocasts.ir/ebhomengo/niki/authorizationapp/delivery/http" + "git.gocasts.ir/ebhomengo/niki/domain/authorization/repository" + "git.gocasts.ir/ebhomengo/niki/domain/authorization/service" + cfgloader "git.gocasts.ir/ebhomengo/niki/pkg/cfg_loader" + mySql "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql" + "git.gocasts.ir/ebhomengo/niki/pkg/httpserver" + "git.gocasts.ir/ebhomengo/niki/pkg/path" + "log" + "os" + "path/filepath" +) + +type Config struct { + HTTPServer httpserver.Config `koanf:"http_server"` + MySQLDB mySql.Config `koanf:"mariadb"` +} +type Application struct { + RoleRepo *repository.DB + AuthorizationService service.Authorization + RoleHandler http.RoleHandler + RoleServer http.RoleServer +} + +func (a Application) Setup() Application { + appConfig := a.getYamlConfigPath() + db := mySql.New(appConfig.MySQLDB) + roleRepo := repository.New(db) + authorizationService := service.NewAuthorization(roleRepo) + roleHandler := http.NewRoleHandler(authorizationService) + server := http.NewRoleServer(appConfig.HTTPServer, roleHandler) + + return Application{ + RoleRepo: roleRepo, + AuthorizationService: authorizationService, + RoleHandler: roleHandler, + RoleServer: server, + } +} + +func (a Application) getYamlConfigPath() Config { + var appConfig Config + + projectRoot, err := path.PathProjectRoot() + if err != nil { + log.Fatalf("Error finding project root: %v", err) + } + + defaultConfig := filepath.Join(projectRoot, "deploy", "authorization", "config.yml") + _, err = os.Stat(defaultConfig) + if err != nil { + log.Fatal(err.Error()) + } + + options := cfgloader.Option{ + Prefix: "AUTHORIZATION_", + Delimiter: ".", + Separator: "__", + YamlFilePath: defaultConfig, + CallbackEnv: nil, + } + err = cfgloader.Load(options, &appConfig) + if err != nil { + log.Fatalf("failed loading authorization config: %v", err) + } + return appConfig +} diff --git a/authorizationapp/delivery/http/handler.go b/authorizationapp/delivery/http/handler.go new file mode 100644 index 00000000..d4cdc2a1 --- /dev/null +++ b/authorizationapp/delivery/http/handler.go @@ -0,0 +1,35 @@ +package http + +import ( + "fmt" + "git.gocasts.ir/ebhomengo/niki/domain/authorization/service" + "github.com/gin-gonic/gin" + "net/http" +) + +type RoleHandler struct { + service service.Authorization +} + +func NewRoleHandler(service service.Authorization) RoleHandler { + return RoleHandler{service: service} +} + +func (r RoleHandler) Store(c *gin.Context) { + var request service.StoreRoleRequest + err := c.ShouldBindJSON(&request) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + role, err := r.service.Store(c, request) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("role %s created successfully", role.Title_fa)}) + return +} diff --git a/authorizationapp/delivery/http/server.go b/authorizationapp/delivery/http/server.go new file mode 100644 index 00000000..6e1c002b --- /dev/null +++ b/authorizationapp/delivery/http/server.go @@ -0,0 +1,26 @@ +package http + +import ( + "fmt" + "git.gocasts.ir/ebhomengo/niki/pkg/httpserver" + "github.com/gin-gonic/gin" +) + +type RoleServer struct { + cfg httpserver.Config + handler RoleHandler +} + +func NewRoleServer(cfg httpserver.Config, handler RoleHandler) RoleServer { + return RoleServer{cfg: cfg, handler: handler} +} + +func (s RoleServer) Start() { + r := gin.Default() + r.POST("/role/create", s.handler.Store) + + err := r.Run(fmt.Sprintf("%s:%s", s.cfg.Host, s.cfg.Port)) + if err != nil { + panic(err.Error()) + } +} diff --git a/deploy/authorization/config.yaml b/deploy/authorization/config.yaml new file mode 100644 index 00000000..456382e2 --- /dev/null +++ b/deploy/authorization/config.yaml @@ -0,0 +1 @@ +database: diff --git a/domain/authorization/entity/role.go b/domain/authorization/entity/role.go new file mode 100644 index 00000000..dd60489d --- /dev/null +++ b/domain/authorization/entity/role.go @@ -0,0 +1,13 @@ +package entity + +import ( + "git.gocasts.ir/ebhomengo/niki/types" + "time" +) + +type Role struct { + ID types.ID `json:"id"` + Title string `json:"title"` + Title_fa string `json:"title_Fa"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/domain/authorization/repository/migrations/1779100307_add_roles_table.sql b/domain/authorization/repository/migrations/1779100307_add_roles_table.sql new file mode 100644 index 00000000..fa0d33a8 --- /dev/null +++ b/domain/authorization/repository/migrations/1779100307_add_roles_table.sql @@ -0,0 +1,13 @@ +-- +migrate Up +CREATE TABLE IF NOT EXISTS `roles` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `title` VARCHAR(255) NOT NULL, + `title_fa` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_roles_title` (`title`), + UNIQUE KEY `idx_roles_title_fa` (`title_fa`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +migrate Down +DROP TABLE IF EXISTS `roles`; \ No newline at end of file diff --git a/domain/authorization/repository/mysql_repo.go b/domain/authorization/repository/mysql_repo.go new file mode 100644 index 00000000..4c97e821 --- /dev/null +++ b/domain/authorization/repository/mysql_repo.go @@ -0,0 +1,34 @@ +package repository + +import ( + "context" + "git.gocasts.ir/ebhomengo/niki/domain/authorization/service" + + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql" + "git.gocasts.ir/ebhomengo/niki/types" +) + +type DB struct { + conn *mysql.DB +} + +func New(conn *mysql.DB) *DB { + return &DB{conn: conn} +} + +func (m DB) Store(ctx context.Context, req service.StoreRoleRequest) (types.ID, error) { + const op = "domain.authorization.repository.role.store" + + result, err := m.conn.Conn().ExecContext(ctx, "INSERT INTO roles VALUES (`?,?`)", req.Title, req.TitleFa) + if err != nil { + return 0, richerror.New(op).WithErr(err) + } + + roleID, err := result.LastInsertId() + if err != nil { + return 0, richerror.New(op).WithErr(err) + } + + return types.ID(roleID), nil +} diff --git a/domain/authorization/service/authorization.go b/domain/authorization/service/authorization.go new file mode 100644 index 00000000..8a877713 --- /dev/null +++ b/domain/authorization/service/authorization.go @@ -0,0 +1,45 @@ +package service + +import ( + "context" + "git.gocasts.ir/ebhomengo/niki/domain/authorization/entity" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "github.com/go-ozzo/ozzo-validation/v4" + "regexp" +) + +type Authorization struct { + roleRepo RoleRepo +} + +func NewAuthorization(roleRepo RoleRepo) Authorization { + return Authorization{roleRepo: roleRepo} +} + +func (a Authorization) Store(ctx context.Context, req StoreRoleRequest) (entity.Role, error) { + const op = "authorizationservice.Store" + + if err := a.validateRole(req); err != nil { + return entity.Role{}, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected) + } + + role, err := a.roleRepo.Store(ctx, req) + if err != nil { + return entity.Role{}, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected) + } + return role, nil +} + +func (s Authorization) validateRole(req StoreRoleRequest) error { + return validation.ValidateStruct(&req, + validation.Field(&req.Title, + validation.Required, + validation.Length(4, 0), + validation.Match(regexp.MustCompile(`^[a-zA-Z\s]+$`))), + + validation.Field(&req.TitleFa, + validation.Required, + validation.Length(4, 0), + validation.Match(regexp.MustCompile(`^[\x{0600}-\x{06FF}\s]+$`))), + ) +} diff --git a/domain/authorization/service/param.go b/domain/authorization/service/param.go new file mode 100644 index 00000000..179c4ea4 --- /dev/null +++ b/domain/authorization/service/param.go @@ -0,0 +1,6 @@ +package service + +type StoreRoleRequest struct { + Title string `json:"title"` + TitleFa string `json:"title_fa"` +} diff --git a/domain/authorization/service/repo_contract.go b/domain/authorization/service/repo_contract.go new file mode 100644 index 00000000..70bae722 --- /dev/null +++ b/domain/authorization/service/repo_contract.go @@ -0,0 +1,10 @@ +package service + +import ( + "context" +"git.gocasts.ir/ebhomengo/niki/types" +) + +type RoleRepo interface { + Store(ctx context.Context, req StoreRoleRequest) (types.ID, error) +}