├── internal ├── auth │ ├── Dockerfile │ ├── controller │ │ ├── consumer │ │ │ ├── dto │ │ │ │ └── usercode.go │ │ │ └── user_verification.go │ │ ├── http │ │ │ ├── v1 │ │ │ │ └── router.go │ │ │ └── middleware │ │ │ │ └── middleware.go │ │ └── grpc │ │ │ ├── server.go │ │ │ └── service.go │ ├── entity │ │ ├── role.go │ │ ├── model.go │ │ └── user.go │ ├── repository │ │ ├── role.go │ │ ├── repo.go │ │ ├── interfaces.go │ │ └── user.go │ ├── service │ │ ├── service.go │ │ ├── interfaces.go │ │ └── user.go │ ├── storage │ │ └── storage.go │ ├── app │ │ └── app.go │ └── docs │ │ └── swagger.yaml ├── crm_core │ ├── entity │ │ ├── role.go │ │ ├── response.go │ │ ├── vote.go │ │ ├── ticket.go │ │ ├── contact.go │ │ ├── deal.go │ │ ├── company.go │ │ ├── task.go │ │ └── user.go │ ├── controller │ │ └── http │ │ │ ├── v1 │ │ │ ├── debug │ │ │ │ └── router.go │ │ │ ├── router.go │ │ │ ├── deal.go │ │ │ ├── ticket.go │ │ │ ├── company.go │ │ │ ├── contact.go │ │ │ └── task.go │ │ │ └── middleware │ │ │ └── middleware.go │ ├── repository │ │ ├── repo.go │ │ ├── deal.go │ │ ├── ticket.go │ │ ├── contact.go │ │ ├── company.go │ │ ├── interfaces.go │ │ └── task.go │ ├── service │ │ ├── service.go │ │ ├── company.go │ │ ├── deal.go │ │ ├── contact.go │ │ ├── ticket.go │ │ ├── interfaces.go │ │ └── task.go │ ├── metrics │ │ ├── http.go │ │ ├── metrics.go │ │ ├── buckets.go │ │ └── database.go │ ├── transport │ │ └── validate_grpc.go │ └── app │ │ └── app.go └── kafka │ ├── producer.go │ └── consumer.go ├── pkg ├── crm_core │ ├── img │ │ ├── crm-icon.png │ │ ├── architecture-of-project.png │ │ └── detailed-architecture.png │ ├── grafana │ │ ├── provisioning │ │ │ ├── datasources │ │ │ │ └── prometheus.yaml │ │ │ └── dashboards │ │ │ │ └── provider.yaml │ │ └── dashboards │ │ │ └── example.json │ ├── cache │ │ ├── redis.go │ │ └── contact.go │ ├── postgres │ │ └── postgres.go │ ├── httpserver │ │ ├── public │ │ │ ├── options.go │ │ │ └── server.go │ │ └── debug │ │ │ ├── options.go │ │ │ └── server.go │ ├── prometheus │ │ └── prometheus.yml │ └── logger │ │ └── logger.go └── auth │ ├── cache │ ├── redis.go │ └── user.go │ ├── postgres │ └── postgres.go │ ├── httpserver │ ├── options.go │ └── server.go │ ├── authservice │ ├── auth.proto │ └── gw │ │ ├── auth_grpc.pb.go │ │ └── auth.pb.gw.go │ ├── logger │ └── logger.go │ └── utils │ └── utils.go ├── .deepsource.toml ├── .gitignore ├── dockerfile-auth ├── dockerfile-crm ├── migrations ├── auth_down │ └── migrate_down.go ├── crm_core_down │ └── migrate_down.go ├── auth │ └── migrate.go ├── crm_core │ └── migrate.go ├── auth_mock │ └── auth_mock.go └── crm_mock │ └── crm_mock.go ├── cmd ├── auth │ └── main.go └── crm_core │ └── main.go ├── config ├── crm_core │ ├── test_config.yml │ └── config.go └── auth │ ├── test_config.yml │ └── config.go ├── LICENSE ├── Makefile ├── docker-compose.yml └── go.mod /internal/auth/Dockerfile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/auth/controller/consumer/dto/usercode.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type UserCode struct { 4 | Code string 5 | } 6 | -------------------------------------------------------------------------------- /pkg/crm_core/img/crm-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahugg/CRM-system-go-microservices/HEAD/pkg/crm_core/img/crm-icon.png -------------------------------------------------------------------------------- /pkg/crm_core/img/architecture-of-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahugg/CRM-system-go-microservices/HEAD/pkg/crm_core/img/architecture-of-project.png -------------------------------------------------------------------------------- /pkg/crm_core/img/detailed-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahugg/CRM-system-go-microservices/HEAD/pkg/crm_core/img/detailed-architecture.png -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "go" 5 | 6 | [analyzers.meta] 7 | import_root = "github.com/Rahugg/CRM-system-go-microservices" -------------------------------------------------------------------------------- /internal/auth/entity/role.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type Role struct { 4 | ID uint `gorm:"primary_key, AUTO_INCREMENT"` 5 | Name string `gorm:"unique"` 6 | } 7 | -------------------------------------------------------------------------------- /internal/crm_core/entity/role.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type Role struct { 4 | ID uint `gorm:"primary_key, AUTO_INCREMENT"` 5 | Name string `gorm:"unique"` 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | config/crm_core/config.yml 4 | config/auth/config.yml 5 | 6 | .idea 7 | 8 | vendor 9 | 10 | docker/data 11 | docker 12 | 13 | data/kafka1/ 14 | data/zookeeper/ 15 | -------------------------------------------------------------------------------- /dockerfile-auth: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY .. . 6 | 7 | EXPOSE 8081 8 | 9 | RUN go mod download 10 | RUN go build ./cmd/auth 11 | 12 | CMD ["go", "run", "./cmd/auth"] 13 | -------------------------------------------------------------------------------- /dockerfile-crm: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY .. . 6 | 7 | EXPOSE 8082 8 | 9 | RUN go mod download 10 | RUN go build ./cmd/crm_core 11 | 12 | CMD ["go", "run", "./cmd/crm_core"] -------------------------------------------------------------------------------- /pkg/crm_core/grafana/provisioning/datasources/prometheus.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | deleteDatasources: 4 | - name: Prometheus 5 | orgId: 1 6 | 7 | datasources: 8 | - name: Prometheus 9 | type: prometheus 10 | access: proxy 11 | orgId: 1 12 | url: http://prometheus:9090 13 | editable: true 14 | -------------------------------------------------------------------------------- /pkg/crm_core/grafana/provisioning/dashboards/provider.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'default' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | disableDeletion: false 9 | updateIntervalSeconds: 10 #how often Grafana will scan for changed dashboards 10 | options: 11 | path: /var/lib/grafana/dashboards 12 | -------------------------------------------------------------------------------- /internal/auth/repository/role.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "crm_system/internal/auth/entity" 5 | ) 6 | 7 | func (r *AuthRepo) GetRoleById(id uint) (*entity.Role, error) { 8 | var role entity.Role 9 | if err := r.DB.Where("id = ?", id).First(&role).Error; err != nil { 10 | return nil, err 11 | } 12 | return &role, nil 13 | } 14 | -------------------------------------------------------------------------------- /internal/crm_core/entity/response.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type CustomResponse struct { 4 | Status int `json:"status"` 5 | Message string `json:"message"` 6 | } 7 | 8 | type CustomResponseWithData struct { 9 | Status int `json:"status"` 10 | Message string `json:"message"` 11 | Data interface{} `json:"data"` 12 | } 13 | -------------------------------------------------------------------------------- /internal/crm_core/entity/vote.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type Vote struct { 9 | gorm.Model 10 | SenderID uuid.UUID `json:"sender_id"` 11 | TaskID uint `json:"task_id"` 12 | Task *Task `gorm:"foreignkey:TaskID" json:"todo"` 13 | } 14 | 15 | type VoteInput struct { 16 | TaskID uint `json:"task_id"` 17 | } 18 | -------------------------------------------------------------------------------- /internal/auth/repository/repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "crm_system/config/auth" 5 | "crm_system/pkg/auth/logger" 6 | "crm_system/pkg/auth/postgres" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type AuthRepo struct { 11 | DB *gorm.DB 12 | } 13 | 14 | func New(config *auth.Configuration, l *logger.Logger) *AuthRepo { 15 | db := postgres.ConnectDB(config, l) 16 | return &AuthRepo{ 17 | DB: db, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/crm_core/cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "github.com/redis/go-redis/v9" 6 | ) 7 | 8 | func NewRedisClient() (*redis.Client, error) { 9 | client := redis.NewClient(&redis.Options{ 10 | Addr: "localhost:6379", 11 | Password: "", 12 | DB: 0, 13 | }) 14 | err := client.Ping(context.Background()).Err() 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return client, nil 20 | } 21 | -------------------------------------------------------------------------------- /internal/crm_core/controller/http/v1/debug/router.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "github.com/gin-contrib/pprof" 5 | "github.com/gin-gonic/gin" 6 | "net/http" 7 | ) 8 | 9 | func NewDebugRouter(handler *gin.Engine) { 10 | 11 | // Health Check 12 | handler.GET("/health", func(c *gin.Context) { 13 | c.JSON(http.StatusOK, gin.H{ 14 | "message": "Hello to auth service", 15 | }) 16 | }) 17 | 18 | pprof.Register(handler) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /internal/crm_core/repository/repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "crm_system/config/crm_core" 5 | "crm_system/pkg/crm_core/logger" 6 | "crm_system/pkg/crm_core/postgres" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type CRMSystemRepo struct { 11 | DB *gorm.DB 12 | } 13 | 14 | func New(config *crm_core.Configuration, l *logger.Logger) *CRMSystemRepo { 15 | db := postgres.ConnectDB(config, l) 16 | return &CRMSystemRepo{ 17 | DB: db, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/auth/cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "github.com/redis/go-redis/v9" 6 | ) 7 | 8 | func NewRedisClient() (*redis.Client, error) { 9 | client := redis.NewClient(&redis.Options{ 10 | Addr: "localhost:6379", 11 | Password: "", // no password set 12 | DB: 0, // use default DB 13 | }) 14 | err := client.Ping(context.Background()).Err() 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return client, nil 20 | } 21 | -------------------------------------------------------------------------------- /internal/crm_core/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crm_system/config/crm_core" 5 | "crm_system/internal/crm_core/repository" 6 | "crm_system/pkg/crm_core/logger" 7 | ) 8 | 9 | type Service struct { 10 | Repo *repository.CRMSystemRepo 11 | Config *crm_core.Configuration 12 | } 13 | 14 | func New(config *crm_core.Configuration, repo *repository.CRMSystemRepo, l *logger.Logger) *Service { 15 | return &Service{ 16 | Repo: repo, 17 | Config: config, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /migrations/auth_down/migrate_down.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crm_system/config/auth" 5 | entityRepo "crm_system/internal/auth/entity" 6 | repoPkg "crm_system/internal/auth/repository" 7 | "crm_system/pkg/auth/logger" 8 | ) 9 | 10 | func main() { 11 | cfg := auth.NewConfig() 12 | l := logger.New(cfg.Gin.Mode) 13 | repo := repoPkg.New(cfg, l) 14 | err := repo.DB.Migrator().DropTable(&entityRepo.User{}, &entityRepo.Role{}) 15 | if err != nil { 16 | l.Error("error happened: %s", err) 17 | return 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/auth/controller/http/v1/router.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "crm_system/internal/auth/controller/http/middleware" 5 | "crm_system/internal/auth/service" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | func NewRouter(handler *gin.Engine, s *service.Service, MW *middleware.Middleware) { 11 | 12 | // Health Check 13 | handler.GET("/health", func(c *gin.Context) { 14 | c.JSON(http.StatusOK, gin.H{ 15 | "message": "Hello to auth service", 16 | }) 17 | }) 18 | 19 | h := handler.Group("/v1") 20 | { 21 | newUserRoutes(h, s, MW) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cmd/auth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crm_system/config/auth" 5 | "crm_system/internal/auth/app" 6 | ) 7 | 8 | // @title auth-service 9 | // @version 1.0 10 | // @description auth_service 11 | // @termsOfService http://swagger.io/terms/ 12 | // @license.name Apache 2.0 13 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 14 | // @schemes https http 15 | // @host localhost:8081 16 | // @securityDefinitions.apikey BearerAuth 17 | // @type apiKey 18 | // @name Authorization 19 | // @in header 20 | func main() { 21 | // Configuration 22 | cfg := auth.NewConfig() 23 | 24 | // Run 25 | app.Run(cfg) 26 | } 27 | -------------------------------------------------------------------------------- /internal/crm_core/entity/ticket.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type StatusTicket string 9 | 10 | const ( 11 | Open StatusTicket = "OPEN" 12 | InProgressTicket StatusTicket = "IN-PROGRESS" 13 | Closed StatusTicket = "CLOSED" 14 | ) 15 | 16 | type Ticket struct { 17 | gorm.Model 18 | IssueDescription string `gorm:"varchar(255);not null" json:"issue_description"` 19 | Status StatusTicket `gorm:"status_ticket"` 20 | ContactID uint `json:"contact_id"` 21 | AssignedTo uuid.UUID `json:"assigned_to"` 22 | } 23 | -------------------------------------------------------------------------------- /internal/crm_core/entity/contact.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "gorm.io/gorm" 4 | 5 | type Contact struct { 6 | gorm.Model 7 | FirstName string `gorm:"varchar(255);not null" json:"first_name"` 8 | LastName string `gorm:"varchar(255);not null" json:"last_name"` 9 | Email string `gorm:"varchar(255);not null" json:"email"` 10 | Phone string `gorm:"varchar(255);not null" json:"phone"` 11 | CompanyID uint `json:"company_id"` 12 | } 13 | 14 | /* 15 | { 16 | "first_name": "John", 17 | "last_name": "Doe", 18 | "email": "johndoe@example.com", 19 | "phone": "123-456-7890", 20 | "company_id": 789012 21 | } 22 | */ 23 | -------------------------------------------------------------------------------- /pkg/auth/postgres/postgres.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "crm_system/config/auth" 5 | "crm_system/pkg/auth/logger" 6 | "fmt" 7 | "gorm.io/driver/postgres" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | func ConnectDB(config *auth.Configuration, l *logger.Logger) *gorm.DB { 12 | connectionStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", 13 | config.DB.Host, 14 | config.DB.Port, 15 | config.DB.User, 16 | config.DB.Password, 17 | config.DB.Name, 18 | ) 19 | db, err := gorm.Open(postgres.Open(connectionStr), &gorm.Config{}) 20 | if err != nil { 21 | l.Fatal(err) 22 | } 23 | return db 24 | } 25 | -------------------------------------------------------------------------------- /internal/crm_core/entity/deal.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type StatusDeal string 8 | 9 | const ( 10 | Initiated StatusDeal = "INITIATED" 11 | InProgress StatusDeal = "IN-PROGRESS" 12 | ClosedWon StatusDeal = "CLOSED-WON" 13 | ClosedLost StatusDeal = "CLOSED-LOST" 14 | ) 15 | 16 | type Deal struct { 17 | gorm.Model 18 | Title string `gorm:"varchar(255);not null" json:"title"` 19 | Value uint `gorm:"varchar(255);not null" json:"value"` 20 | Status StatusDeal `gorm:"type:status_deal"` 21 | ContactID uint `json:"contact_id"` 22 | RepID uint `json:"rep_id"` 23 | } 24 | -------------------------------------------------------------------------------- /cmd/crm_core/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crm_system/config/crm_core" 5 | "crm_system/internal/crm_core/app" 6 | ) 7 | 8 | // @title crm-core-service 9 | // @version 1.0 10 | // @description crm-core-service 11 | // @termsOfService http://swagger.io/terms/ 12 | // @license.name Apache 2.0 13 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 14 | // @schemes https http 15 | // @host localhost:8082 16 | // @securityDefinitions.apikey BearerAuth 17 | // @type apiKey 18 | // @name Authorization 19 | // @in header 20 | func main() { 21 | // Configuration 22 | cfg := crm_core.NewConfig() 23 | 24 | // Run 25 | app.Run(cfg) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/crm_core/postgres/postgres.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "crm_system/config/crm_core" 5 | "crm_system/pkg/crm_core/logger" 6 | "fmt" 7 | "gorm.io/driver/postgres" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | func ConnectDB(config *crm_core.Configuration, l *logger.Logger) *gorm.DB { 12 | connectionStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", 13 | config.DB.Host, 14 | config.DB.Port, 15 | config.DB.User, 16 | config.DB.Password, 17 | config.DB.Name, 18 | ) 19 | db, err := gorm.Open(postgres.Open(connectionStr), &gorm.Config{}) 20 | if err != nil { 21 | l.Fatal(err) 22 | } 23 | return db 24 | } 25 | -------------------------------------------------------------------------------- /internal/crm_core/entity/company.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type Company struct { 9 | gorm.Model 10 | Name string `gorm:"type:varchar(255);not null" json:"name"` 11 | Address string `gorm:"type:varchar(255);not null" json:"address"` 12 | Phone string `gorm:"type:varchar(255);not null" json:"phone"` 13 | ManagerID uuid.UUID `json:"manager_id"` 14 | } 15 | 16 | type NewCompany struct { 17 | Name string `gorm:"type:varchar(255);not null" json:"name"` 18 | Address string `gorm:"type:varchar(255);not null" json:"address"` 19 | Phone string `gorm:"type:varchar(255);not null" json:"phone"` 20 | } 21 | -------------------------------------------------------------------------------- /migrations/crm_core_down/migrate_down.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crm_system/config/crm_core" 5 | entityRepo "crm_system/internal/crm_core/entity" 6 | repoPkg "crm_system/internal/crm_core/repository" 7 | "crm_system/pkg/crm_core/logger" 8 | ) 9 | 10 | func main() { 11 | cfg := crm_core.NewConfig() 12 | l := logger.New(cfg.Gin.Mode) 13 | repo := repoPkg.New(cfg, l) 14 | 15 | err := repo.DB.Migrator().DropTable(&entityRepo.Company{}, 16 | &entityRepo.Contact{}, 17 | &entityRepo.Deal{}, 18 | &entityRepo.Task{}, 19 | &entityRepo.Ticket{}, 20 | &entityRepo.Vote{}, 21 | &entityRepo.TaskChanges{}) 22 | if err != nil { 23 | l.Error("error happened: %s", err) 24 | return 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /migrations/auth/migrate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crm_system/config/auth" 5 | entityRepo "crm_system/internal/auth/entity" 6 | _ "crm_system/internal/auth/repository" 7 | repoPkg "crm_system/internal/auth/repository" 8 | "crm_system/pkg/auth/logger" 9 | "fmt" 10 | ) 11 | 12 | func main() { 13 | cfg := auth.NewConfig() 14 | l := logger.New(cfg.Gin.Mode) 15 | repo := repoPkg.New(cfg, l) 16 | repo.DB.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"") 17 | err := repo.DB.AutoMigrate( 18 | &entityRepo.User{}, 19 | &entityRepo.Role{}, 20 | &entityRepo.UserCode{}, 21 | ) 22 | if err != nil { 23 | l.Fatal("Automigration failed") 24 | } 25 | 26 | fmt.Println("👍 Migration complete - auth service") 27 | } 28 | -------------------------------------------------------------------------------- /internal/crm_core/metrics/http.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | const ( 4 | httpHandlerLabel = "handler" 5 | httpCodeLabel = "code" 6 | httpMethodLabel = "method" 7 | ) 8 | 9 | var ( 10 | HttpResponseTime = newHistogramVec( 11 | "response_time_seconds", 12 | "Histogram of application RT for any kind of requests seconds", 13 | TimeBucketsMedium, 14 | httpHandlerLabel, httpCodeLabel, httpMethodLabel, 15 | ) 16 | 17 | HttpRequestsTotalCollector = newCounterVec( 18 | "requests_total", 19 | "Counter of HTTP requests for any HTTP-based requests", 20 | httpHandlerLabel, httpCodeLabel, httpMethodLabel, 21 | ) 22 | ) 23 | 24 | // nolint: gochecknoinits 25 | func init() { 26 | mustRegister(HttpResponseTime, HttpRequestsTotalCollector) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/auth/httpserver/options.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | // Option -. 9 | type Option func(*Server) 10 | 11 | // Port -. 12 | func Port(port string) Option { 13 | return func(s *Server) { 14 | s.server.Addr = net.JoinHostPort("", port) 15 | } 16 | } 17 | 18 | // ReadTimeout -. 19 | func ReadTimeout(timeout time.Duration) Option { 20 | return func(s *Server) { 21 | s.server.ReadTimeout = timeout 22 | } 23 | } 24 | 25 | // WriteTimeout -. 26 | func WriteTimeout(timeout time.Duration) Option { 27 | return func(s *Server) { 28 | s.server.WriteTimeout = timeout 29 | } 30 | } 31 | 32 | // ShutdownTimeout -. 33 | func ShutdownTimeout(timeout time.Duration) Option { 34 | return func(s *Server) { 35 | s.shutdownTimeout = timeout 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/crm_core/httpserver/public/options.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | // Option -. 9 | type Option func(*Server) 10 | 11 | // Port -. 12 | func Port(port string) Option { 13 | return func(s *Server) { 14 | s.server.Addr = net.JoinHostPort("", port) 15 | } 16 | } 17 | 18 | // ReadTimeout -. 19 | func ReadTimeout(timeout time.Duration) Option { 20 | return func(s *Server) { 21 | s.server.ReadTimeout = timeout 22 | } 23 | } 24 | 25 | // WriteTimeout -. 26 | func WriteTimeout(timeout time.Duration) Option { 27 | return func(s *Server) { 28 | s.server.WriteTimeout = timeout 29 | } 30 | } 31 | 32 | // ShutdownTimeout -. 33 | func ShutdownTimeout(timeout time.Duration) Option { 34 | return func(s *Server) { 35 | s.shutdownTimeout = timeout 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/auth/authservice/auth.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package authservice; 4 | 5 | option go_package = "./;pb"; 6 | 7 | service AuthService { 8 | rpc Validate (ValidateRequest) returns (ValidateResponse) {} 9 | } 10 | 11 | message ValidateRequest{ 12 | string accessToken = 1; 13 | repeated string roles = 2; 14 | } 15 | 16 | message ValidateResponse { 17 | ResponseJSON response = 1; 18 | } 19 | 20 | message ResponseJSON { 21 | User user = 1; 22 | Role role = 2; 23 | } 24 | 25 | message User { 26 | string id = 1; 27 | string FirstName = 2; 28 | string LastName = 3; 29 | int64 Age = 4; 30 | string Phone = 5; 31 | int64 RoleID = 6; 32 | string Email = 7; 33 | string Provider = 8; 34 | string Password = 9; 35 | } 36 | 37 | message Role { 38 | int64 ID = 1; 39 | string Name = 2; 40 | } -------------------------------------------------------------------------------- /internal/auth/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crm_system/config/auth" 5 | "crm_system/internal/auth/repository" 6 | "crm_system/internal/auth/storage" 7 | "crm_system/internal/kafka" 8 | ) 9 | 10 | type Service struct { 11 | Repo *repository.AuthRepo 12 | Config *auth.Configuration 13 | userVerificationProducer *kafka.Producer 14 | Storage *storage.DataStorage 15 | } 16 | 17 | func New(config *auth.Configuration, repo *repository.AuthRepo, userVerificationProducer *kafka.Producer, storage *storage.DataStorage) *Service { 18 | return &Service{ 19 | Repo: repo, 20 | Config: config, 21 | userVerificationProducer: userVerificationProducer, 22 | Storage: storage, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/crm_core/httpserver/debug/options.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | // Option -. 9 | type Option func(*ServerDebug) 10 | 11 | // Port -. 12 | func Port(port string) Option { 13 | return func(s *ServerDebug) { 14 | s.server.Addr = net.JoinHostPort("", port) 15 | } 16 | } 17 | 18 | // ReadTimeout -. 19 | func ReadTimeout(timeout time.Duration) Option { 20 | return func(s *ServerDebug) { 21 | s.server.ReadTimeout = timeout 22 | } 23 | } 24 | 25 | // WriteTimeout -. 26 | func WriteTimeout(timeout time.Duration) Option { 27 | return func(s *ServerDebug) { 28 | s.server.WriteTimeout = timeout 29 | } 30 | } 31 | 32 | // ShutdownTimeout -. 33 | func ShutdownTimeout(timeout time.Duration) Option { 34 | return func(s *ServerDebug) { 35 | s.shutdownTimeout = timeout 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /migrations/crm_core/migrate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crm_system/config/crm_core" 5 | entityRepo "crm_system/internal/crm_core/entity" 6 | repoPkg "crm_system/internal/crm_core/repository" 7 | "crm_system/pkg/crm_core/logger" 8 | "fmt" 9 | ) 10 | 11 | func main() { 12 | cfg := crm_core.NewConfig() 13 | l := logger.New(cfg.Gin.Mode) 14 | repo := repoPkg.New(cfg, l) 15 | repo.DB.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"") 16 | err := repo.DB.AutoMigrate( 17 | &entityRepo.Company{}, 18 | &entityRepo.Contact{}, 19 | &entityRepo.Deal{}, 20 | &entityRepo.Task{}, 21 | &entityRepo.Ticket{}, 22 | &entityRepo.Vote{}, 23 | &entityRepo.TaskChanges{}, 24 | ) 25 | if err != nil { 26 | l.Fatal("Automigration failed") 27 | } 28 | 29 | fmt.Println("👍 Migration complete - crm_core service") 30 | } 31 | -------------------------------------------------------------------------------- /internal/auth/service/interfaces.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crm_system/internal/auth/entity" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | type ( 9 | AuthService interface { 10 | SignUp(payload *entity.SignUpInput, roleId uint, provider string) (string, error) 11 | SignIn(payload *entity.SignInInput) (*entity.SignInResult, error) 12 | } 13 | UserService interface { 14 | GetUsers(sortBy, sortOrder, age string) (*[]entity.User, error) 15 | GetUser(id string) (*entity.User, error) 16 | CreateUser(user *entity.User) error 17 | UpdateUser(newUser *entity.User, id string) error 18 | DeleteUser(id string) error 19 | GetMe(ctx *gin.Context) (interface{}, error) 20 | UpdateMe(ctx *gin.Context, newUser *entity.User) error 21 | SearchUser(query string) (*[]entity.User, error) 22 | CreateUserCode(id string) error 23 | ConfirmUser(code string) error 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /internal/kafka/producer.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import ( 4 | "crm_system/config/auth" 5 | "fmt" 6 | "github.com/IBM/sarama" 7 | ) 8 | 9 | type Producer struct { 10 | asyncProducer sarama.AsyncProducer 11 | topic string 12 | } 13 | 14 | func NewProducer(cfg *auth.Configuration) (*Producer, error) { 15 | samaraConfig := sarama.NewConfig() 16 | 17 | asyncProducer, err := sarama.NewAsyncProducer(cfg.Kafka.Brokers, samaraConfig) 18 | if err != nil { 19 | return nil, fmt.Errorf("failed to NewAsyncProducer err: %w", err) 20 | } 21 | 22 | return &Producer{ 23 | asyncProducer: asyncProducer, 24 | topic: cfg.Kafka.Producer.Topic, 25 | }, nil 26 | } 27 | 28 | func (p *Producer) ProduceMessage(message []byte) { 29 | msg := &sarama.ProducerMessage{ 30 | Topic: p.topic, 31 | Value: sarama.ByteEncoder(message), 32 | } 33 | 34 | p.asyncProducer.Input() <- msg 35 | } 36 | -------------------------------------------------------------------------------- /config/crm_core/test_config.yml: -------------------------------------------------------------------------------- 1 | app: 2 | name: 'crm_core' 3 | version: '1.0.0' 4 | 5 | http: 6 | port: '8082' 7 | debugPort: '8084' 8 | default_read_timeout: 5 9 | default_write_timeout: 5 10 | default_shutdown_timeout: 5 11 | 12 | log: 13 | level: 'debug' 14 | 15 | gin: 16 | mode: 'debug' 17 | 18 | db: 19 | pool_max: 2 20 | host: 'localhost' 21 | user: 'postgres' 22 | password: 'postgres' 23 | name: 'postgres' 24 | port: 5432 25 | 26 | Transport: 27 | Validate: 28 | Host: http://127.0.0.1:8081 29 | Timeout: 5s 30 | ValidateGrpc: 31 | Host: localhost:9090 32 | jwt: 33 | access_private_key: private_key 34 | access_public_key: private_key 35 | access_token_expired_in: 60m 36 | access_token_max_age: 60 37 | 38 | refresh_private_key: private_key 39 | refresh_public_key: private_key 40 | refresh_token_expired_in: 1440m 41 | refresh_token_max_age: 1440 -------------------------------------------------------------------------------- /internal/crm_core/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ) 6 | 7 | const ( 8 | statusError = "error" 9 | statusOk = "ok" 10 | ) 11 | 12 | func mustRegister(collectors ...prometheus.Collector) { 13 | prometheus.DefaultRegisterer.MustRegister(collectors...) 14 | } 15 | 16 | func newHistogramVec(name, help string, buckets []float64, labelValues ...string) *prometheus.HistogramVec { 17 | return prometheus.NewHistogramVec( 18 | prometheus.HistogramOpts{ 19 | Namespace: "crm_core", 20 | Name: name, 21 | Help: help, 22 | Buckets: buckets, 23 | }, 24 | labelValues, 25 | ) 26 | } 27 | 28 | func newCounterVec(name, help string, labelValues ...string) *prometheus.CounterVec { 29 | return prometheus.NewCounterVec( 30 | prometheus.CounterOpts{ 31 | Namespace: "crm_core", 32 | Name: name, 33 | Help: help, 34 | }, 35 | labelValues, 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /internal/crm_core/metrics/buckets.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | var ( 4 | // TimeBucketsFast suits if expected response time is very high: 1ms..100ms 5 | // This buckets suits for cache storages (in-memory cache, Memcache). 6 | TimeBucketsFast = []float64{0.001, 0.003, 0.007, 0.015, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 20} 7 | 8 | // TimeBucketsMedium suits for most of GO APIs, where response time is between 50ms..500ms. 9 | // Works for wide range of systems because provides near-logarithmic buckets distribution. 10 | //nolint:lll 11 | TimeBucketsMedium = []float64{0.001, 0.005, 0.015, 0.05, 0.1, 0.25, 0.5, 0.75, 1, 1.5, 2, 3.5, 5, 10, 15, 20, 30, 40, 50, 60} 12 | 13 | // TimeBucketsSlow suits for relatively slow services, where expected response time is > 500ms. 14 | TimeBucketsSlow = []float64{0.05, 0.1, 0.2, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5, 10, 15, 20} 15 | 16 | CBOpenDurationBuckets = []float64{15, 30, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600, 900, 1200} 17 | ) 18 | -------------------------------------------------------------------------------- /config/auth/test_config.yml: -------------------------------------------------------------------------------- 1 | app: 2 | name: 'auth_service' 3 | version: '1.0.0' 4 | 5 | http: 6 | port: '8081' 7 | default_read_timeout: 5 8 | default_write_timeout: 5 9 | default_shutdown_timeout: 5 10 | 11 | Grpc: 12 | Port: ":9090" 13 | 14 | log: 15 | level: 'debug' 16 | 17 | gin: 18 | mode: 'debug' 19 | 20 | db: 21 | pool_max: 2 22 | host: 'localhost' 23 | user: 'postgres' 24 | password: 'postgres' 25 | name: 'postgres' 26 | port: 5432 27 | 28 | jwt: 29 | access_private_key: private_key 30 | access_public_key: private_key 31 | access_token_expired_in: 60m 32 | access_token_max_age: 60m 33 | 34 | refresh_private_key: rahug 35 | refresh_public_key: rahug 36 | refresh_token_expired_in: 1440m 37 | refresh_token_max_age: 1440 38 | Kafka: 39 | Brokers: 40 | - 127.0.0.1:19092 41 | Producer: 42 | topic: "user-verification" 43 | Consumer: 44 | topics: 45 | - "user-verification" 46 | Storage: 47 | Interval: 10s 48 | ShutdownTimeout: 1m 49 | -------------------------------------------------------------------------------- /internal/crm_core/controller/http/v1/router.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "crm_system/internal/crm_core/controller/http/middleware" 5 | "crm_system/internal/crm_core/service" 6 | "crm_system/pkg/crm_core/cache" 7 | "github.com/gin-gonic/gin" 8 | "github.com/prometheus/client_golang/prometheus/promhttp" 9 | "net/http" 10 | ) 11 | 12 | func NewRouter(handler *gin.Engine, s *service.Service, MW *middleware.Middleware, cc cache.Contact) { 13 | 14 | // Health Check 15 | handler.GET("/health", func(c *gin.Context) { 16 | c.JSON(http.StatusOK, gin.H{ 17 | "message": "Hello to auth service", 18 | }) 19 | }) 20 | 21 | handler.GET("/metrics", gin.WrapH(promhttp.Handler())) 22 | 23 | h := handler.Group("/v1") 24 | { 25 | handler.Use(MW.MetricsHandler()) 26 | 27 | newCompanyRoutes(h, s, MW) 28 | newContactRoutes(h, s, MW, cc) 29 | newDealRoutes(h, s, MW) 30 | newTaskRoutes(h, s, MW) 31 | newTicketRoutes(h, s, MW) 32 | } 33 | } 34 | 35 | //customer relation 36 | //open source crm-system 37 | -------------------------------------------------------------------------------- /internal/auth/controller/consumer/user_verification.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "crm_system/internal/auth/controller/consumer/dto" 5 | "crm_system/pkg/auth/logger" 6 | "encoding/json" 7 | "github.com/IBM/sarama" 8 | ) 9 | 10 | type UserVerificationCallback struct { 11 | logger *logger.Logger 12 | } 13 | 14 | func NewUserVerificationCallback(logger *logger.Logger) *UserVerificationCallback { 15 | return &UserVerificationCallback{logger: logger} 16 | } 17 | 18 | func (c *UserVerificationCallback) Callback(message <-chan *sarama.ConsumerMessage, error <-chan *sarama.ConsumerError) { 19 | for { 20 | select { 21 | case msg := <-message: 22 | var userCode dto.UserCode 23 | 24 | err := json.Unmarshal(msg.Value, &userCode) 25 | if err != nil { 26 | c.logger.Error("failed to unmarshall record value err: %v", err) 27 | } else { 28 | c.logger.Info("user code: %s", userCode) 29 | } 30 | case err := <-error: 31 | c.logger.Error("failed consume err: %v", err) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/auth/repository/interfaces.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "crm_system/internal/auth/entity" 5 | ) 6 | 7 | type ( 8 | AuthRepository interface { 9 | CreateUserCode(id string, code string) error 10 | ConfirmUser(code string) error 11 | } 12 | UserRepository interface { 13 | CreateUser(user *entity.User) error 14 | SaveUser(user *entity.User) error 15 | SortUsers(users *[]entity.User, sortBy, sortOrder string) (*[]entity.User, error) 16 | GetUserByIdWithPreload(id string) (*entity.User, error) 17 | GetUser(id string) (*entity.User, error) 18 | GetAllUsers() (*[]entity.User, error) 19 | GetUserByEmail(email string) (*entity.User, error) 20 | GetUsersByRole(roleId uint) (*[]entity.User, error) 21 | DeleteUser(id string, user *entity.User) error 22 | SearchUser(query string) (*[]entity.User, error) 23 | FilterUsersByAge(users *[]entity.User, age string) (*[]entity.User, error) 24 | } 25 | RoleRepository interface { 26 | GetRoleById(id uint) (*entity.Role, error) 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /internal/crm_core/metrics/database.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "time" 4 | 5 | const ( 6 | repoMethodLabel = "method" 7 | repoStatusLabel = "status" 8 | ) 9 | 10 | var databaseResponseTime = newHistogramVec( 11 | "db_response_time_seconds", 12 | "Histogram of application RT for DB", 13 | TimeBucketsMedium, 14 | repoMethodLabel, repoStatusLabel, 15 | ) 16 | 17 | func DatabaseQueryTime(method string) (ok, fail func()) { 18 | start := time.Now() 19 | 20 | completed := false 21 | 22 | ok = func() { 23 | if completed { 24 | return 25 | } 26 | 27 | completed = true 28 | 29 | databaseResponseTime.WithLabelValues(method, statusOk).Observe(time.Since(start).Seconds()) 30 | } 31 | 32 | fail = func() { 33 | if completed { 34 | return 35 | } 36 | 37 | completed = true 38 | 39 | databaseResponseTime.WithLabelValues(method, statusError).Observe(time.Since(start).Seconds()) 40 | } 41 | 42 | return ok, fail 43 | } 44 | 45 | // nolint: gochecknoinits 46 | func init() { 47 | mustRegister(databaseResponseTime) 48 | } 49 | -------------------------------------------------------------------------------- /internal/auth/controller/grpc/server.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "fmt" 5 | "google.golang.org/grpc" 6 | "log" 7 | "net" 8 | 9 | pb "crm_system/pkg/auth/authservice/gw" 10 | ) 11 | 12 | type Server struct { 13 | port string 14 | service *Service 15 | grpcServer *grpc.Server 16 | } 17 | 18 | func NewServer( 19 | port string, 20 | service *Service, 21 | ) *Server { 22 | var opts []grpc.ServerOption 23 | 24 | grpcServer := grpc.NewServer(opts...) 25 | 26 | return &Server{ 27 | port: port, 28 | service: service, 29 | grpcServer: grpcServer, 30 | } 31 | } 32 | 33 | func (s *Server) Start() error { 34 | listener, err := net.Listen("tcp", s.port) 35 | if err != nil { 36 | return fmt.Errorf("failed to listen grpc port: %s", s.port) 37 | } 38 | 39 | pb.RegisterAuthServiceServer(s.grpcServer, s.service) 40 | 41 | go func() { 42 | err = s.grpcServer.Serve(listener) 43 | if err != nil { 44 | log.Fatalf("error happened in grpc: %s", err) 45 | } 46 | }() 47 | 48 | return nil 49 | } 50 | 51 | func (s *Server) Close() { 52 | s.grpcServer.GracefulStop() 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rahugg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/auth/cache/user.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "crm_system/internal/auth/entity" 6 | "encoding/json" 7 | "github.com/redis/go-redis/v9" 8 | "time" 9 | ) 10 | 11 | type User interface { 12 | Get(ctx context.Context, key string) (*entity.User, error) 13 | Set(ctx context.Context, key string, value *entity.User) error 14 | } 15 | 16 | type UserCache struct { 17 | Expiration time.Duration 18 | redisCli *redis.Client 19 | } 20 | 21 | func NewUserCache(redisCli *redis.Client) User { 22 | return &UserCache{ 23 | redisCli: redisCli, 24 | } 25 | } 26 | 27 | func (u *UserCache) Get(ctx context.Context, key string) (*entity.User, error) { 28 | value := u.redisCli.Get(ctx, key).Val() 29 | 30 | var user *entity.User 31 | 32 | err := json.Unmarshal([]byte(value), &user) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return user, nil 38 | } 39 | 40 | func (u *UserCache) Set(ctx context.Context, key string, value *entity.User) error { 41 | userJson, err := json.Marshal(value) 42 | if err != nil { 43 | return err 44 | } 45 | return u.redisCli.Set(ctx, key, string(userJson), u.Expiration).Err() 46 | } 47 | -------------------------------------------------------------------------------- /pkg/crm_core/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Alertmanager configuration 8 | alerting: 9 | alertmanagers: 10 | - static_configs: 11 | - targets: 12 | # - alertmanager:9093 13 | 14 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 15 | rule_files: 16 | # - "first_rules.yml" 17 | # - "second_rules.yml" 18 | 19 | # A scrape configuration containing exactly one endpoint to scrape: 20 | # Here it's Prometheus itself. 21 | scrape_configs: 22 | # The job name is added as a label `job=` to any timeseries scraped from this config. 23 | - job_name: 'prometheus' 24 | scrape_interval: 5s 25 | static_configs: 26 | - targets: ['localhost:9090'] 27 | 28 | - job_name: 'crm_core' 29 | scrape_interval: 5s 30 | static_configs: 31 | - targets: ['host.docker.internal:8082'] 32 | -------------------------------------------------------------------------------- /migrations/auth_mock/auth_mock.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crm_system/config/auth" 5 | entityRepo "crm_system/internal/auth/entity" 6 | _ "crm_system/internal/auth/repository" 7 | repoPkg "crm_system/internal/auth/repository" 8 | "crm_system/pkg/auth/logger" 9 | "fmt" 10 | ) 11 | 12 | func main() { 13 | cfg := auth.NewConfig() 14 | l := logger.New(cfg.Gin.Mode) 15 | repo := repoPkg.New(cfg, l) 16 | 17 | roles := []*entityRepo.Role{ 18 | {ID: 1, Name: "admin"}, 19 | {ID: 2, Name: "manager"}, 20 | {ID: 3, Name: "sales_rep"}, 21 | {ID: 4, Name: "support_rep"}, 22 | {ID: 5, Name: "guest"}, 23 | } 24 | for i, role := range roles { 25 | if repo.DB.Model(&role).Where("id = ?", i+1).Updates(&role).RowsAffected == 0 { 26 | repo.DB.Create(&role) 27 | } 28 | } 29 | 30 | newAdmin := entityRepo.User{ 31 | FirstName: "admin", 32 | LastName: "main", 33 | Email: "a_faizolla@kbtu.kz", 34 | Password: "$2a$12$/84UJA1OqAVDl.6BB9r5VegjczNvNXM.DlaFYF8uk9QoB6YK2LdoK", 35 | RoleID: 1, 36 | Provider: "admin", 37 | } 38 | 39 | if repo.DB.Model(&newAdmin).Where("email = ?", "a_faizolla@kbtu.kz").Updates(&newAdmin).RowsAffected == 0 { 40 | repo.DB.Create(&newAdmin) 41 | } 42 | 43 | fmt.Println("Mock data inserted successfully✅ auth-service") 44 | } 45 | -------------------------------------------------------------------------------- /internal/crm_core/transport/validate_grpc.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "crm_system/config/crm_core" 6 | pb "crm_system/pkg/auth/authservice/gw" 7 | "fmt" 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/credentials/insecure" 10 | ) 11 | 12 | type ValidateGrpcTransport struct { 13 | config crm_core.Configuration 14 | client pb.AuthServiceClient 15 | } 16 | 17 | func NewValidateGrpcTransport(config crm_core.Configuration) *ValidateGrpcTransport { 18 | opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} 19 | 20 | conn, _ := grpc.Dial(config.Transport.ValidateGrpc.Host, opts...) 21 | 22 | client := pb.NewAuthServiceClient(conn) 23 | 24 | return &ValidateGrpcTransport{ 25 | client: client, 26 | config: config, 27 | } 28 | } 29 | 30 | func (t *ValidateGrpcTransport) ValidateTransport(ctx context.Context, accessToken string, roles ...string) (*pb.ResponseJSON, error) { 31 | 32 | resp, err := t.client.Validate(ctx, &pb.ValidateRequest{ 33 | AccessToken: accessToken, 34 | Roles: roles, 35 | }) 36 | 37 | if err != nil { 38 | return nil, fmt.Errorf("cannot Validate: %w", err) 39 | } 40 | 41 | if resp == nil { 42 | return nil, fmt.Errorf("not found") 43 | } 44 | 45 | return resp.Response, nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/crm_core/cache/contact.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "crm_system/internal/crm_core/entity" 6 | "encoding/json" 7 | "github.com/redis/go-redis/v9" 8 | "time" 9 | ) 10 | 11 | type Contact interface { 12 | Get(ctx context.Context, key string) (*entity.Contact, error) 13 | Set(ctx context.Context, key string, value *entity.Contact) error 14 | } 15 | 16 | type ContactCache struct { 17 | Expiration time.Duration 18 | redisCli *redis.Client 19 | } 20 | 21 | func NewContactCache(redisCli *redis.Client, expiration time.Duration) Contact { 22 | return &ContactCache{ 23 | redisCli: redisCli, 24 | Expiration: expiration, 25 | } 26 | } 27 | 28 | func (c *ContactCache) Get(ctx context.Context, key string) (*entity.Contact, error) { 29 | value := c.redisCli.Get(ctx, key).Val() 30 | 31 | if value == "" { 32 | return nil, nil 33 | } 34 | 35 | var contact *entity.Contact 36 | 37 | err := json.Unmarshal([]byte(value), &contact) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return contact, nil 43 | } 44 | 45 | func (c *ContactCache) Set(ctx context.Context, key string, value *entity.Contact) error { 46 | contactJson, err := json.Marshal(value) 47 | if err != nil { 48 | return err 49 | } 50 | return c.redisCli.Set(ctx, key, string(contactJson), c.Expiration).Err() 51 | } 52 | -------------------------------------------------------------------------------- /pkg/auth/httpserver/server.go: -------------------------------------------------------------------------------- 1 | // Package httpserver implements HTTP server. 2 | package httpserver 3 | 4 | import ( 5 | "context" 6 | "crm_system/config/auth" 7 | "log" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | // Server -. 13 | type Server struct { 14 | server *http.Server 15 | config *auth.Configuration 16 | notify chan error 17 | shutdownTimeout time.Duration 18 | } 19 | 20 | // New -. 21 | func New(handler http.Handler, config *auth.Configuration, opts ...Option) *Server { 22 | httpServer := &http.Server{ 23 | Handler: handler, 24 | ReadTimeout: time.Duration(config.HTTP.DefaultReadTimeout), 25 | WriteTimeout: time.Duration(config.HTTP.DefaultWriteTimeout), 26 | Addr: config.HTTP.Port, 27 | } 28 | 29 | s := &Server{ 30 | server: httpServer, 31 | config: config, 32 | notify: make(chan error, 1), 33 | shutdownTimeout: time.Duration(config.HTTP.DefaultShutdownTimeout), 34 | } 35 | 36 | // Custom options 37 | for _, opt := range opts { 38 | opt(s) 39 | } 40 | 41 | s.start() 42 | 43 | return s 44 | } 45 | 46 | func (s *Server) start() { 47 | go func() { 48 | log.Printf("Listening on port:%s", s.config.HTTP.Port) 49 | s.notify <- s.server.ListenAndServe() 50 | close(s.notify) 51 | }() 52 | } 53 | 54 | // Notify -. 55 | func (s *Server) Notify() <-chan error { 56 | return s.notify 57 | } 58 | 59 | // Shutdown -. 60 | func (s *Server) Shutdown() error { 61 | ctx, cancel := context.WithTimeout(context.Background(), s.shutdownTimeout) 62 | defer cancel() 63 | 64 | return s.server.Shutdown(ctx) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/crm_core/httpserver/debug/server.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "context" 5 | "crm_system/config/crm_core" 6 | "log" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | // Server -. 12 | type ServerDebug struct { 13 | server *http.Server 14 | config *crm_core.Configuration 15 | notify chan error 16 | shutdownTimeout time.Duration 17 | } 18 | 19 | // New -. 20 | func New(handler http.Handler, config *crm_core.Configuration, opts ...Option) *ServerDebug { 21 | httpServer := &http.Server{ 22 | Handler: handler, 23 | ReadTimeout: time.Duration(config.HTTP.DefaultReadTimeout), 24 | WriteTimeout: time.Duration(config.HTTP.DefaultWriteTimeout), 25 | Addr: config.HTTP.DebugPort, 26 | } 27 | 28 | s := &ServerDebug{ 29 | server: httpServer, 30 | config: config, 31 | notify: make(chan error, 1), 32 | shutdownTimeout: time.Duration(config.HTTP.DefaultShutdownTimeout), 33 | } 34 | 35 | // Custom options 36 | for _, opt := range opts { 37 | opt(s) 38 | } 39 | 40 | s.start() 41 | 42 | return s 43 | } 44 | 45 | func (s *ServerDebug) start() { 46 | go func() { 47 | log.Printf("Listening debug server on port:%s", s.config.HTTP.DebugPort) 48 | s.notify <- s.server.ListenAndServe() 49 | close(s.notify) 50 | }() 51 | } 52 | 53 | // Notify -. 54 | func (s *ServerDebug) Notify() <-chan error { 55 | return s.notify 56 | } 57 | 58 | // Shutdown -. 59 | func (s *ServerDebug) Shutdown() error { 60 | ctx, cancel := context.WithTimeout(context.Background(), s.shutdownTimeout) 61 | defer cancel() 62 | 63 | return s.server.Shutdown(ctx) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/crm_core/httpserver/public/server.go: -------------------------------------------------------------------------------- 1 | // Package httpserver implements HTTP server. 2 | package public 3 | 4 | import ( 5 | "context" 6 | "crm_system/config/crm_core" 7 | "log" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | // Server -. 13 | type Server struct { 14 | server *http.Server 15 | config *crm_core.Configuration 16 | notify chan error 17 | shutdownTimeout time.Duration 18 | } 19 | 20 | // New -. 21 | func New(handler http.Handler, config *crm_core.Configuration, opts ...Option) *Server { 22 | httpServer := &http.Server{ 23 | Handler: handler, 24 | ReadTimeout: time.Duration(config.HTTP.DefaultReadTimeout), 25 | WriteTimeout: time.Duration(config.HTTP.DefaultWriteTimeout), 26 | Addr: config.HTTP.Port, 27 | } 28 | 29 | s := &Server{ 30 | server: httpServer, 31 | config: config, 32 | notify: make(chan error, 1), 33 | shutdownTimeout: time.Duration(config.HTTP.DefaultShutdownTimeout), 34 | } 35 | 36 | // Custom options 37 | for _, opt := range opts { 38 | opt(s) 39 | } 40 | 41 | s.start() 42 | 43 | return s 44 | } 45 | 46 | func (s *Server) start() { 47 | go func() { 48 | log.Printf("Listening server on port:%s", s.config.HTTP.Port) 49 | s.notify <- s.server.ListenAndServe() 50 | close(s.notify) 51 | }() 52 | } 53 | 54 | // Notify -. 55 | func (s *Server) Notify() <-chan error { 56 | return s.notify 57 | } 58 | 59 | // Shutdown -. 60 | func (s *Server) Shutdown() error { 61 | ctx, cancel := context.WithTimeout(context.Background(), s.shutdownTimeout) 62 | defer cancel() 63 | 64 | return s.server.Shutdown(ctx) 65 | } 66 | -------------------------------------------------------------------------------- /internal/auth/entity/model.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "time" 6 | ) 7 | 8 | type SignUpInput struct { 9 | FirstName string `gorm:"type:varchar(255);not null" json:"name" binding:"required"` 10 | LastName string `gorm:"type:varchar(255);not null" json:"surname" binding:"required"` 11 | Email string `gorm:"type:varchar(255);not null" json:"email" binding:"required"` 12 | Password string `gorm:"type:varchar(255);not null" json:"password" binding:"required,min=8"` 13 | PasswordConfirm string `gorm:"type:varchar(255);not null" json:"passwordConfirm" binding:"required"` 14 | } 15 | 16 | type SignInInput struct { 17 | Email string `gorm:"type:varchar(255);not null" json:"email" binding:"required"` 18 | Password string `gorm:"type:varchar(255);not null" json:"password" binding:"required"` 19 | } 20 | 21 | type SignInResult struct { 22 | Role string `json:"role"` 23 | AccessToken string `json:"access_token"` 24 | RefreshToken string `json:"refresh_token"` 25 | AccessTokenAge int `json:"access_token_age"` 26 | RefreshTokenAge int `json:"refresh_token_age"` 27 | } 28 | 29 | type SignUpResult struct { 30 | ID uuid.UUID `json:"id"` 31 | FirstName string `json:"name"` 32 | LastName string `json:"surname"` 33 | Email string `json:"email"` 34 | Role string `json:"role"` 35 | Provider string `json:"provider"` 36 | CreatedAt time.Time `json:"created_at"` 37 | UpdatedAt time.Time `json:"updated_at"` 38 | } 39 | 40 | type CustomResponse struct { 41 | Status int `json:"status"` 42 | Message string `json:"message"` 43 | } 44 | 45 | type CustomResponseWithData struct { 46 | Status int `json:"status"` 47 | Message string `json:"message"` 48 | Data interface{} `json:"data"` 49 | } 50 | -------------------------------------------------------------------------------- /internal/auth/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "crm_system/internal/auth/entity" 6 | "crm_system/internal/auth/repository" 7 | "crm_system/pkg/auth/logger" 8 | "fmt" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | type DataStorage struct { 14 | interval time.Duration 15 | service *repository.AuthRepo 16 | users map[string]*[]entity.User 17 | mu sync.RWMutex 18 | logger *logger.Logger 19 | } 20 | 21 | func NewDataStorage(interval time.Duration, service *repository.AuthRepo, logger *logger.Logger) *DataStorage { 22 | return &DataStorage{ 23 | interval: interval, 24 | service: service, 25 | users: make(map[string]*[]entity.User), 26 | mu: sync.RWMutex{}, 27 | logger: logger, 28 | } 29 | } 30 | 31 | func (ds *DataStorage) Run() { 32 | ticker := time.NewTicker(ds.interval) 33 | defer ticker.Stop() 34 | 35 | ctx := context.Background() 36 | 37 | for { 38 | select { 39 | case <-ctx.Done(): 40 | return 41 | case <-ticker.C: 42 | startTime := time.Now() 43 | fmt.Println(startTime) 44 | 45 | ds.LoadData() 46 | 47 | elapsedTime := time.Since(startTime) 48 | 49 | timeToNextTick := ds.interval - elapsedTime 50 | 51 | time.Sleep(timeToNextTick) 52 | } 53 | } 54 | 55 | } 56 | 57 | func (ds *DataStorage) LoadData() { 58 | ds.mu.RLock() 59 | defer ds.mu.RUnlock() 60 | 61 | allUsers, err := ds.service.GetAllUsers() 62 | if err != nil { 63 | ds.logger.Error("failed to GetAllUsers err: %v", err) 64 | } 65 | 66 | method := make(map[string]*[]entity.User) 67 | 68 | method["users"] = allUsers 69 | 70 | ds.users = method 71 | } 72 | 73 | func (ds *DataStorage) GetAllUsers() (*[]entity.User, error) { 74 | ds.mu.RLock() 75 | defer ds.mu.RUnlock() 76 | 77 | users, ok := ds.users["users"] 78 | if !ok { 79 | return nil, fmt.Errorf("get all users from storage err") 80 | } 81 | return users, nil 82 | } 83 | -------------------------------------------------------------------------------- /internal/crm_core/repository/deal.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "crm_system/internal/crm_core/entity" 5 | "crm_system/internal/crm_core/metrics" 6 | ) 7 | 8 | func (r *CRMSystemRepo) GetDeals() (*[]entity.Deal, error) { 9 | var deals *[]entity.Deal 10 | ok, fail := metrics.DatabaseQueryTime("GetDeals") 11 | defer fail() 12 | 13 | if err := r.DB.Find(&deals).Error; err != nil { 14 | return nil, err 15 | } 16 | 17 | ok() 18 | return deals, nil 19 | } 20 | 21 | func (r *CRMSystemRepo) GetDeal(id string) (*entity.Deal, error) { 22 | var deal *entity.Deal 23 | 24 | if err := r.DB.First(&deal, id).Error; err != nil { 25 | return nil, err 26 | } 27 | 28 | return deal, nil 29 | } 30 | 31 | func (r *CRMSystemRepo) CreateDeal(deal *entity.Deal) error { 32 | if err := r.DB.Create(&deal).Error; err != nil { 33 | return err 34 | } 35 | 36 | return nil 37 | } 38 | 39 | func (r *CRMSystemRepo) SaveDeal(newDeal *entity.Deal) error { 40 | if err := r.DB.Save(&newDeal).Error; err != nil { 41 | return err 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func (r *CRMSystemRepo) DeleteDeal(id string, deal *entity.Deal) error { 48 | if err := r.DB.Where("id = ?", id).Delete(deal).Error; err != nil { 49 | return err 50 | } 51 | return nil 52 | } 53 | 54 | func (r *CRMSystemRepo) SearchDeal(query string) (*[]entity.Deal, error) { 55 | var deals *[]entity.Deal 56 | 57 | if err := r.DB.Where("title ILIKE ?", "%"+query+"%").Find(&deals).Error; err != nil { 58 | return nil, err 59 | } 60 | 61 | return deals, nil 62 | } 63 | func (r *CRMSystemRepo) SortDeals(deals *[]entity.Deal, sortBy, sortOrder string) (*[]entity.Deal, error) { 64 | if err := r.DB.Order(sortBy + " " + sortOrder).Find(&deals).Error; err != nil { 65 | return nil, err 66 | } 67 | 68 | return deals, nil 69 | } 70 | 71 | func (r *CRMSystemRepo) FilterDealsByStatus(deals *[]entity.Deal, status string) (*[]entity.Deal, error) { 72 | if err := r.DB.Where("status = ?", status).Find(&deals).Error; err != nil { 73 | return nil, err 74 | } 75 | 76 | return deals, nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/kafka/consumer.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import ( 4 | "crm_system/config/auth" 5 | "crm_system/pkg/auth/logger" 6 | "fmt" 7 | "github.com/IBM/sarama" 8 | "strings" 9 | ) 10 | 11 | type ConsumerCallback interface { 12 | Callback(message <-chan *sarama.ConsumerMessage, error <-chan *sarama.ConsumerError) 13 | } 14 | 15 | type Consumer struct { 16 | logger *logger.Logger 17 | topics []string 18 | master sarama.Consumer 19 | callback ConsumerCallback 20 | } 21 | 22 | func NewConsumer( 23 | logger *logger.Logger, 24 | cfg *auth.Configuration, 25 | callback ConsumerCallback, 26 | ) (*Consumer, error) { 27 | samaraCfg := sarama.NewConfig() 28 | samaraCfg.ClientID = "go-kafka-consumer" 29 | samaraCfg.Consumer.Return.Errors = true 30 | 31 | master, err := sarama.NewConsumer(cfg.Kafka.Brokers, samaraCfg) 32 | if err != nil { 33 | return nil, fmt.Errorf("failed to create NewConsumer err: %w", err) 34 | } 35 | 36 | return &Consumer{ 37 | logger: logger, 38 | topics: cfg.Consumer.Topics, 39 | master: master, 40 | callback: callback, 41 | }, nil 42 | } 43 | 44 | func (c *Consumer) Start() { 45 | consumers := make(chan *sarama.ConsumerMessage, 1) 46 | errors := make(chan *sarama.ConsumerError) 47 | 48 | for _, topic := range c.topics { 49 | if strings.Contains(topic, "__consumer_offsets") { 50 | continue 51 | } 52 | 53 | partitions, _ := c.master.Partitions(topic) 54 | 55 | consumer, err := c.master.ConsumePartition(topic, partitions[0], sarama.OffsetNewest) 56 | if nil != err { 57 | c.logger.Error("Topic %v Partitions: %v, err: %w", topic, partitions, err) 58 | continue 59 | } 60 | 61 | c.logger.Info("Start consuming topic %s", topic) 62 | 63 | go func(topic string, consumer sarama.PartitionConsumer) { 64 | for { 65 | select { 66 | case consumerError := <-consumer.Errors(): 67 | errors <- consumerError 68 | 69 | case msg := <-consumer.Messages(): 70 | consumers <- msg 71 | } 72 | } 73 | }(topic, consumer) 74 | } 75 | 76 | c.callback.Callback(consumers, errors) 77 | } 78 | -------------------------------------------------------------------------------- /internal/crm_core/entity/task.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "gorm.io/gorm" 6 | "time" 7 | ) 8 | 9 | type Task struct { 10 | gorm.Model 11 | Name string `json:"name" gorm:"varchar(255);not null"` 12 | Description string `gorm:"varchar(255);not null" json:"description"` 13 | DueDate time.Time `json:"due_date"` 14 | AssignedTo uuid.UUID `json:"assigned_to"` 15 | AssociatedDealID uint `json:"associated_deal_id"` 16 | State string `json:"state" gorm:"not null"` 17 | Votes []Vote `json:"votes"` 18 | } 19 | type TaskResult struct { 20 | Task Task `json:"task"` 21 | VoteCount int `json:"vote_count"` 22 | UserVoted bool `json:"user_voted"` 23 | } 24 | 25 | type TaskInput struct { 26 | Name string `json:"name"` 27 | Description string `json:"description"` 28 | DueDate time.Time `json:"due_date"` 29 | AssignedTo uuid.UUID `json:"assigned_to"` 30 | AssociatedDealID uint `json:"associated_deal_id"` 31 | State string `json:"state"` 32 | } 33 | 34 | type TaskEditInput struct { 35 | Name string `json:"name"` 36 | Description string `json:"description"` 37 | DueDate time.Time `json:"due_date"` 38 | AssignedTo uuid.UUID `json:"assigned_to"` 39 | AssociatedDealID uint `json:"associated_deal_id"` 40 | State string `json:"state"` 41 | } 42 | 43 | type TaskChanges struct { 44 | gorm.Model 45 | ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primary_key"` 46 | ManagerID uuid.UUID `json:"manager_id" gorm:"not null"` 47 | TaskID uint `json:"task_id" gorm:"not null"` 48 | ChangedField string `json:"changed_field"` 49 | OldValue string `json:"old_value"` 50 | NewValue string `json:"new_value"` 51 | } 52 | 53 | //sample JSON 54 | /* 55 | { 56 | "description": "Sample task description", 57 | "due_date": "2023-10-30T12:00:00Z", 58 | "assigned_to": "f47ac10b-58cc-4372-a567-0e02b2c3d479", 59 | "associated_deal_id": 123456 60 | } 61 | */ 62 | -------------------------------------------------------------------------------- /internal/crm_core/repository/ticket.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "crm_system/internal/crm_core/entity" 5 | "crm_system/internal/crm_core/metrics" 6 | ) 7 | 8 | func (r *CRMSystemRepo) GetTickets() (*[]entity.Ticket, error) { 9 | var tickets *[]entity.Ticket 10 | 11 | ok, fail := metrics.DatabaseQueryTime("GetTickets") 12 | defer fail() 13 | 14 | if err := r.DB.Find(&tickets).Error; err != nil { 15 | return nil, err 16 | } 17 | 18 | ok() 19 | 20 | return tickets, nil 21 | } 22 | func (r *CRMSystemRepo) GetTicket(id string) (*entity.Ticket, error) { 23 | var ticket *entity.Ticket 24 | 25 | if err := r.DB.First(&ticket, id).Error; err != nil { 26 | return nil, err 27 | } 28 | 29 | return ticket, nil 30 | } 31 | func (r *CRMSystemRepo) CreateTicket(ticket *entity.Ticket) error { 32 | if err := r.DB.Create(&ticket).Error; err != nil { 33 | return err 34 | } 35 | 36 | return nil 37 | } 38 | func (r *CRMSystemRepo) SaveTicket(newTicket *entity.Ticket) error { 39 | if err := r.DB.Save(&newTicket).Error; err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | func (r *CRMSystemRepo) DeleteTicket(id string, ticket *entity.Ticket) error { 46 | if err := r.DB.Where("id = ?", id).Delete(ticket).Error; err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | 52 | func (r *CRMSystemRepo) SearchTicket(query string) (*[]entity.Ticket, error) { 53 | var tickets *[]entity.Ticket 54 | 55 | if err := r.DB.Where("issue_description ILIKE ?", "%"+query+"%").Find(&tickets).Error; err != nil { 56 | return nil, err 57 | } 58 | 59 | return tickets, nil 60 | } 61 | func (r *CRMSystemRepo) SortTickets(tickets *[]entity.Ticket, sortBy, sortOrder string) (*[]entity.Ticket, error) { 62 | if err := r.DB.Order(sortBy + " " + sortOrder).Find(&tickets).Error; err != nil { 63 | return nil, err 64 | } 65 | 66 | return tickets, nil 67 | } 68 | 69 | func (r *CRMSystemRepo) FilterTicketsByStatus(tickets *[]entity.Ticket, status string) (*[]entity.Ticket, error) { 70 | if err := r.DB.Where("status = ?", status).Find(&tickets).Error; err != nil { 71 | return nil, err 72 | } 73 | 74 | return tickets, nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/crm_core/repository/contact.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "crm_system/internal/crm_core/entity" 5 | "crm_system/internal/crm_core/metrics" 6 | ) 7 | 8 | func (r *CRMSystemRepo) GetContacts() (*[]entity.Contact, error) { 9 | var contacts *[]entity.Contact 10 | ok, fail := metrics.DatabaseQueryTime("GetContacts") 11 | defer fail() 12 | 13 | if err := r.DB.Find(&contacts).Error; err != nil { 14 | return nil, err 15 | } 16 | 17 | ok() 18 | 19 | return contacts, nil 20 | } 21 | func (r *CRMSystemRepo) GetContact(id string) (*entity.Contact, error) { 22 | var contact *entity.Contact 23 | 24 | if err := r.DB.First(&contact, id).Error; err != nil { 25 | return nil, err 26 | } 27 | 28 | return contact, nil 29 | } 30 | func (r *CRMSystemRepo) CreateContact(contact *entity.Contact) error { 31 | if err := r.DB.Create(&contact).Error; err != nil { 32 | return err 33 | } 34 | 35 | return nil 36 | } 37 | func (r *CRMSystemRepo) SaveContact(newContact *entity.Contact) error { 38 | if err := r.DB.Save(&newContact).Error; err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | func (r *CRMSystemRepo) DeleteContact(id string, contact *entity.Contact) error { 45 | if err := r.DB.Where("id = ?", id).Delete(contact).Error; err != nil { 46 | return err 47 | } 48 | return nil 49 | } 50 | 51 | func (r *CRMSystemRepo) SearchContact(query string) (*[]entity.Contact, error) { 52 | var contacts *[]entity.Contact 53 | 54 | if err := r.DB.Where("first_name ILIKE ?", "%"+query+"%").Find(&contacts).Error; err != nil { 55 | return nil, err 56 | } 57 | 58 | return contacts, nil 59 | } 60 | 61 | func (r *CRMSystemRepo) SortContacts(contacts *[]entity.Contact, sortBy, sortOrder string) (*[]entity.Contact, error) { 62 | if err := r.DB.Order(sortBy + " " + sortOrder).Find(&contacts).Error; err != nil { 63 | return nil, err 64 | } 65 | 66 | return contacts, nil 67 | } 68 | 69 | func (r *CRMSystemRepo) FilterContactsByPhone(contacts *[]entity.Contact, phone string) (*[]entity.Contact, error) { 70 | if err := r.DB.Where("phone = ?", phone).Find(&contacts).Error; err != nil { 71 | return nil, err 72 | } 73 | 74 | return contacts, nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/crm_core/repository/company.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "crm_system/internal/crm_core/entity" 5 | "crm_system/internal/crm_core/metrics" 6 | ) 7 | 8 | func (r *CRMSystemRepo) GetCompanies() (*[]entity.Company, error) { 9 | var companies *[]entity.Company 10 | ok, fail := metrics.DatabaseQueryTime("GetCompanies") 11 | defer fail() 12 | 13 | if err := r.DB.Find(&companies).Error; err != nil { 14 | return nil, err 15 | } 16 | 17 | ok() 18 | 19 | return companies, nil 20 | } 21 | 22 | func (r *CRMSystemRepo) GetCompany(id string) (*entity.Company, error) { 23 | var company *entity.Company 24 | 25 | if err := r.DB.First(&company, id).Error; err != nil { 26 | return nil, err 27 | } 28 | 29 | return company, nil 30 | } 31 | 32 | func (r *CRMSystemRepo) CreateCompany(company *entity.Company) error { 33 | if err := r.DB.Create(&company).Error; err != nil { 34 | return err 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (r *CRMSystemRepo) SaveCompany(newCompany *entity.Company) error { 41 | if err := r.DB.Save(&newCompany).Error; err != nil { 42 | return err 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func (r *CRMSystemRepo) DeleteCompany(id string, company *entity.Company) error { 49 | if err := r.DB.Where("id = ?", id).Delete(company).Error; err != nil { 50 | return err 51 | } 52 | return nil 53 | } 54 | func (r *CRMSystemRepo) SearchCompany(query string) (*[]entity.Company, error) { 55 | var companies *[]entity.Company 56 | 57 | if err := r.DB.Where("name ILIKE ?", "%"+query+"%").Find(&companies).Error; err != nil { 58 | return nil, err 59 | } 60 | 61 | return companies, nil 62 | } 63 | 64 | func (r *CRMSystemRepo) SortCompanies(companies *[]entity.Company, sortBy, sortOrder string) (*[]entity.Company, error) { 65 | if err := r.DB.Order(sortBy + " " + sortOrder).Find(&companies).Error; err != nil { 66 | return nil, err 67 | } 68 | 69 | return companies, nil 70 | } 71 | 72 | func (r *CRMSystemRepo) FilterCompaniesByPhone(companies *[]entity.Company, phone string) (*[]entity.Company, error) { 73 | if err := r.DB.Where("phone = ?", phone).Find(&companies).Error; err != nil { 74 | return nil, err 75 | } 76 | 77 | return companies, nil 78 | } 79 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LOCAL_BIN:=$(CURDIR)/bin 2 | PATH:=$(LOCAL_BIN):$(PATH) 3 | 4 | .PHONY: help 5 | 6 | help: ## Display this help screen 7 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 8 | 9 | compose-up: ### Run docker-compose 10 | docker-compose up --build -d postgres rabbitmq && docker-compose logs -f 11 | .PHONY: compose-up 12 | 13 | compose-up-integration-test: ### Run docker-compose with integration test 14 | docker-compose up --build --abort-on-container-exit --exit-code-from integration 15 | .PHONY: compose-up-integration-test 16 | 17 | compose-down: ### Down docker-compose 18 | docker-compose down --remove-orphans 19 | .PHONY: compose-down 20 | 21 | mock-data: ### run mockgen 22 | go run migrations/crm_mock/crm_mock.go && go run migrations/auth_mock/auth_mock.go 23 | .PHONY: mock 24 | 25 | migrate-up: ### migration up 26 | go run migrations/auth/migrate.go && go run migrations/crm_core/migrate.go 27 | 28 | .PHONY: migrate-up 29 | 30 | migrate-down: ### migration down 31 | go run migrations/auth_down/migrate_down.go && go run migrations/crm_core_down/migrate_down.go 32 | .PHONY: migrate-down 33 | 34 | start-auth: 35 | go run cmd/auth/main.go 36 | .PHONY: start-auth 37 | 38 | start-crm: 39 | go run cmd/crm_core/main.go 40 | .PHONY: start-crm 41 | 42 | build-dockerfile-auth: 43 | docker build -f dockerfile-auth -t auth-service . 44 | ./PHONY: build-dockerfile-auth 45 | 46 | build-dockerfile-crm: 47 | docker build -f dockerfile-crm -t crm-service . 48 | ./PHONY: build-dockerfile-crm 49 | 50 | swag-auth: 51 | cd internal/auth && swag init --parseDependency --parseInternal -g ../../cmd/auth/main.go 52 | ./PHONY: swag-auth 53 | 54 | swag-crm: 55 | cd internal/crm_core && swag init --parseDependency --parseInternal -g ../../cmd/crm_core/main.go 56 | ./PHONY: swag-crm 57 | 58 | gen-proto: 59 | protoc --go_out ./gw --go_opt paths=source_relative --go-grpc_out ./gw --go-grpc_opt paths=source_relative --grpc-gateway_out=./gw --grpc-gateway_opt logtostderr=true --grpc-gateway_opt generate_unbound_methods=true --proto_path=../../ --proto_path=./ auth.proto 60 | ./PHONY: gen-proto 61 | 62 | linter-go: 63 | golangci-lint run 64 | ./PHONY: linter-go -------------------------------------------------------------------------------- /internal/auth/controller/grpc/service.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "crm_system/config/auth" 6 | "crm_system/internal/auth/entity" 7 | "crm_system/internal/auth/repository" 8 | pb "crm_system/pkg/auth/authservice/gw" 9 | "crm_system/pkg/auth/logger" 10 | "crm_system/pkg/auth/utils" 11 | "fmt" 12 | "github.com/spf13/cast" 13 | ) 14 | 15 | type Service struct { 16 | pb.UnimplementedAuthServiceServer 17 | logger *logger.Logger 18 | repo *repository.AuthRepo 19 | config *auth.Configuration 20 | } 21 | 22 | func NewService(logger *logger.Logger, repo *repository.AuthRepo, config *auth.Configuration) *Service { 23 | return &Service{ 24 | logger: logger, 25 | repo: repo, 26 | config: config, 27 | } 28 | } 29 | 30 | func getUser(user *entity.User) *pb.User { 31 | return &pb.User{ 32 | Id: cast.ToString(user.ID), 33 | FirstName: user.FirstName, 34 | LastName: user.LastName, 35 | Age: int64(user.Age), 36 | Phone: user.Phone, 37 | RoleID: int64(user.RoleID), 38 | Email: user.Email, 39 | Provider: user.Provider, 40 | Password: user.Password, 41 | } 42 | } 43 | func getRole(role *entity.Role) *pb.Role { 44 | return &pb.Role{ 45 | ID: int64(role.ID), 46 | Name: role.Name, 47 | } 48 | } 49 | 50 | func getResponse(request *pb.ValidateRequest, role *entity.Role, user *entity.User) (*pb.ValidateResponse, error) { 51 | for _, Role := range request.Roles { 52 | if role.Name == Role || Role == "any" { 53 | response := &pb.ValidateResponse{ 54 | Response: &pb.ResponseJSON{ 55 | User: getUser(user), 56 | Role: getRole(role), 57 | }, 58 | } 59 | return response, nil 60 | } 61 | } 62 | return nil, fmt.Errorf("error happened in getting a Validate Response") 63 | } 64 | 65 | func (s *Service) Validate(ctx context.Context, request *pb.ValidateRequest) (*pb.ValidateResponse, error) { 66 | sub, err := utils.ValidateToken(request.AccessToken, s.config.Jwt.AccessPrivateKey) 67 | 68 | if err != nil { 69 | s.logger.Error("failed to ValidateToken err %v", err) 70 | return nil, err 71 | } 72 | 73 | user, err := s.repo.GetUserByIdWithPreload(fmt.Sprint(sub)) 74 | if err != nil { 75 | return nil, fmt.Errorf("the user belonging to this token no logger exists") 76 | } 77 | 78 | role, _ := s.repo.GetRoleById(user.RoleID) 79 | 80 | return getResponse(request, role, user) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/auth/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/rs/zerolog" 9 | ) 10 | 11 | // Interface -. 12 | type Interface interface { 13 | Debug(message interface{}, args ...interface{}) 14 | Info(message string, args ...interface{}) 15 | Warn(message string, args ...interface{}) 16 | Error(message interface{}, args ...interface{}) 17 | Fatal(message interface{}, args ...interface{}) 18 | } 19 | 20 | // Logger -. 21 | type Logger struct { 22 | logger *zerolog.Logger 23 | } 24 | 25 | var _ Interface = (*Logger)(nil) 26 | 27 | // New -. 28 | func New(level string) *Logger { 29 | var l zerolog.Level 30 | 31 | switch strings.ToLower(level) { 32 | case "error": 33 | l = zerolog.ErrorLevel 34 | case "warn": 35 | l = zerolog.WarnLevel 36 | case "info": 37 | l = zerolog.InfoLevel 38 | case "debug": 39 | l = zerolog.DebugLevel 40 | default: 41 | l = zerolog.InfoLevel 42 | } 43 | 44 | zerolog.SetGlobalLevel(l) 45 | 46 | skipFrameCount := 3 47 | logger := zerolog.New(os.Stdout).With().Timestamp().CallerWithSkipFrameCount(zerolog.CallerSkipFrameCount + skipFrameCount).Logger() 48 | 49 | return &Logger{ 50 | logger: &logger, 51 | } 52 | } 53 | 54 | // Debug -. 55 | func (l *Logger) Debug(message interface{}, args ...interface{}) { 56 | l.msg("debug", message, args...) 57 | } 58 | 59 | func (l *Logger) Info(message string, args ...interface{}) { 60 | l.log(message, args...) 61 | } 62 | 63 | // Warn -. 64 | func (l *Logger) Warn(message string, args ...interface{}) { 65 | l.log(message, args...) 66 | } 67 | 68 | // Error -. 69 | func (l *Logger) Error(message interface{}, args ...interface{}) { 70 | if l.logger.GetLevel() == zerolog.DebugLevel { 71 | l.Debug(message, args...) 72 | } 73 | 74 | l.msg("error", message, args...) 75 | } 76 | 77 | // Fatal -. 78 | func (l *Logger) Fatal(message interface{}, args ...interface{}) { 79 | l.msg("fatal", message, args...) 80 | 81 | os.Exit(1) 82 | } 83 | 84 | func (l *Logger) log(message string, args ...interface{}) { 85 | if len(args) == 0 { 86 | l.logger.Info().Msg(message) 87 | } else { 88 | l.logger.Info().Msgf(message, args...) 89 | } 90 | } 91 | 92 | func (l *Logger) msg(level string, message interface{}, args ...interface{}) { 93 | switch msg := message.(type) { 94 | case error: 95 | l.log(msg.Error(), args...) 96 | case string: 97 | l.log(msg, args...) 98 | default: 99 | l.log(fmt.Sprintf("%s message %v has unknown type %v", level, message, msg), args...) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pkg/crm_core/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/rs/zerolog" 9 | ) 10 | 11 | // Interface -. 12 | type Interface interface { 13 | Debug(message interface{}, args ...interface{}) 14 | Info(message string, args ...interface{}) 15 | Warn(message string, args ...interface{}) 16 | Error(message interface{}, args ...interface{}) 17 | Fatal(message interface{}, args ...interface{}) 18 | } 19 | 20 | // Logger -. 21 | type Logger struct { 22 | logger *zerolog.Logger 23 | } 24 | 25 | var _ Interface = (*Logger)(nil) 26 | 27 | // New -. 28 | func New(level string) *Logger { 29 | var l zerolog.Level 30 | 31 | switch strings.ToLower(level) { 32 | case "error": 33 | l = zerolog.ErrorLevel 34 | case "warn": 35 | l = zerolog.WarnLevel 36 | case "info": 37 | l = zerolog.InfoLevel 38 | case "debug": 39 | l = zerolog.DebugLevel 40 | default: 41 | l = zerolog.InfoLevel 42 | } 43 | 44 | zerolog.SetGlobalLevel(l) 45 | 46 | skipFrameCount := 3 47 | logger := zerolog.New(os.Stdout).With().Timestamp().CallerWithSkipFrameCount(zerolog.CallerSkipFrameCount + skipFrameCount).Logger() 48 | 49 | return &Logger{ 50 | logger: &logger, 51 | } 52 | } 53 | 54 | // Debug -. 55 | func (l *Logger) Debug(message interface{}, args ...interface{}) { 56 | l.msg("debug", message, args...) 57 | } 58 | 59 | func (l *Logger) Info(message string, args ...interface{}) { 60 | l.log(message, args...) 61 | } 62 | 63 | // Warn -. 64 | func (l *Logger) Warn(message string, args ...interface{}) { 65 | l.log(message, args...) 66 | } 67 | 68 | // Error -. 69 | func (l *Logger) Error(message interface{}, args ...interface{}) { 70 | if l.logger.GetLevel() == zerolog.DebugLevel { 71 | l.Debug(message, args...) 72 | } 73 | 74 | l.msg("error", message, args...) 75 | } 76 | 77 | // Fatal -. 78 | func (l *Logger) Fatal(message interface{}, args ...interface{}) { 79 | l.msg("fatal", message, args...) 80 | 81 | os.Exit(1) 82 | } 83 | 84 | func (l *Logger) log(message string, args ...interface{}) { 85 | if len(args) == 0 { 86 | l.logger.Info().Msg(message) 87 | } else { 88 | l.logger.Info().Msgf(message, args...) 89 | } 90 | } 91 | 92 | func (l *Logger) msg(level string, message interface{}, args ...interface{}) { 93 | switch msg := message.(type) { 94 | case error: 95 | l.log(msg.Error(), args...) 96 | case string: 97 | l.log(msg, args...) 98 | default: 99 | l.log(fmt.Sprintf("%s message %v has unknown type %v", level, message, msg), args...) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pkg/auth/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/golang-jwt/jwt" 6 | "golang.org/x/crypto/bcrypt" 7 | "regexp" 8 | "time" 9 | "unicode" 10 | ) 11 | 12 | func GenerateToken(ttl time.Duration, payload interface{}, secretJWTKey string) (string, error) { 13 | token := jwt.New(jwt.SigningMethodHS256) 14 | now := time.Now().UTC() 15 | 16 | claims := token.Claims.(jwt.MapClaims) 17 | claims["sub"] = payload 18 | claims["exp"] = now.Add(ttl).Unix() 19 | claims["iat"] = now.Unix() 20 | claims["nbf"] = now.Unix() 21 | 22 | tokenString, err := token.SignedString([]byte(secretJWTKey)) 23 | if err != nil { 24 | return "", err 25 | } 26 | 27 | return tokenString, nil 28 | } 29 | 30 | func ValidateToken(token string, signedJWTKey string) (interface{}, error) { 31 | tok, err := jwt.Parse(token, func(jwtToken *jwt.Token) (interface{}, error) { 32 | if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok { 33 | return nil, fmt.Errorf("unexpected signing method: %s", jwtToken.Header["alg"]) 34 | } 35 | return []byte(signedJWTKey), nil 36 | }) 37 | if err != nil { 38 | return nil, fmt.Errorf("invalid token: %w", err) 39 | } 40 | 41 | if !tok.Valid { 42 | return nil, fmt.Errorf("token is not valid") 43 | } 44 | 45 | claims, ok := tok.Claims.(jwt.MapClaims) 46 | if !ok { 47 | return nil, fmt.Errorf("invalid token claims") 48 | } 49 | 50 | return claims["sub"], nil 51 | } 52 | 53 | func IsValidEmail(email string) bool { 54 | emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) 55 | return emailRegex.MatchString(email) 56 | } 57 | 58 | func VerifyPassword(hashedPassword string, candidatePassword string) error { 59 | return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(candidatePassword)) 60 | } 61 | 62 | func IsValidPassword(password string) bool { 63 | var ( 64 | upp, low, num bool 65 | tot uint8 66 | ) 67 | for _, char := range password { 68 | switch { 69 | case unicode.IsUpper(char): 70 | upp = true 71 | tot++ 72 | case unicode.IsLower(char): 73 | low = true 74 | tot++ 75 | case unicode.IsNumber(char): 76 | num = true 77 | tot++ 78 | default: 79 | return false 80 | } 81 | } 82 | if !upp || !low || !num || tot < 8 || tot > 64 { 83 | return false 84 | } 85 | return true 86 | } 87 | 88 | func HashPassword(password string) (string, error) { 89 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 90 | 91 | if err != nil { 92 | return "", fmt.Errorf("could not hash password %w", err) 93 | } 94 | return string(hashedPassword), nil 95 | } 96 | -------------------------------------------------------------------------------- /migrations/crm_mock/crm_mock.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crm_system/config/crm_core" 5 | entityRepo "crm_system/internal/crm_core/entity" 6 | repoPkg "crm_system/internal/crm_core/repository" 7 | "crm_system/pkg/crm_core/logger" 8 | "fmt" 9 | "github.com/google/uuid" 10 | "time" 11 | ) 12 | 13 | func main() { 14 | cfg := crm_core.NewConfig() 15 | l := logger.New(cfg.Gin.Mode) 16 | repo := repoPkg.New(cfg, l) 17 | 18 | company := &entityRepo.Company{ 19 | Name: "Sample Company", 20 | Address: "123 Main Street", 21 | Phone: "555-1234", 22 | ManagerID: uuid.New(), 23 | } 24 | if repo.DB.Model(&company).Where("phone = ?", company.Phone).Updates(&company).RowsAffected == 0 { 25 | repo.DB.Create(&company) 26 | } 27 | 28 | contact := &entityRepo.Contact{ 29 | FirstName: "John", 30 | LastName: "Doe", 31 | Email: "john.doe@example.com", 32 | Phone: "555-5678", 33 | CompanyID: 1, // Assuming a valid CompanyID from your Company data 34 | } 35 | if repo.DB.Model(&contact).Where("company_id= ?", contact.CompanyID).Updates(&contact).RowsAffected == 0 { 36 | repo.DB.Create(&contact) 37 | } 38 | 39 | deal := &entityRepo.Deal{ 40 | Title: "Sample Deal", 41 | Value: 100000, 42 | Status: entityRepo.InProgress, 43 | ContactID: 1, 44 | } 45 | if repo.DB.Model(&deal).Where("contact_id= ?", deal.ContactID).Updates(&deal).RowsAffected == 0 { 46 | repo.DB.Create(&deal) 47 | } 48 | 49 | task := &entityRepo.Task{ 50 | Name: "Sample Task", 51 | Description: "This is a sample task description.", 52 | DueDate: time.Now().AddDate(0, 0, 7), 53 | AssignedTo: uuid.New(), 54 | AssociatedDealID: 1, 55 | State: "Open", 56 | } 57 | if repo.DB.Model(&task).Where("associated_deal_id= ?", task.AssociatedDealID).Updates(&task).RowsAffected == 0 { 58 | repo.DB.Create(&task) 59 | } 60 | 61 | vote := &entityRepo.Vote{ 62 | SenderID: uuid.New(), 63 | TaskID: 1, 64 | } 65 | 66 | if repo.DB.Model(&vote).Where("task_id= ?", vote.TaskID).Updates(&vote).RowsAffected == 0 { 67 | repo.DB.Create(&vote) 68 | } 69 | 70 | ticket := &entityRepo.Ticket{ 71 | IssueDescription: "Sample ticket issue description.", 72 | Status: entityRepo.Open, 73 | ContactID: 1, // Assuming a valid ContactID from your Contact data 74 | AssignedTo: uuid.New(), // Generating a random UUID for AssignedTo 75 | } 76 | if repo.DB.Model(&ticket).Where("contact_id= ?", ticket.ContactID).Updates(&ticket).RowsAffected == 0 { 77 | repo.DB.Create(&ticket) 78 | } 79 | 80 | fmt.Println("Mock data inserted successfully✅ crm_core-service") 81 | } 82 | -------------------------------------------------------------------------------- /internal/auth/controller/http/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "crm_system/config/auth" 5 | "crm_system/internal/auth/entity" 6 | "crm_system/internal/auth/repository" 7 | "crm_system/pkg/auth/utils" 8 | "fmt" 9 | "github.com/gin-gonic/gin" 10 | "log" 11 | "net/http" 12 | "strings" 13 | ) 14 | 15 | type Middleware struct { 16 | Repo *repository.AuthRepo 17 | Config *auth.Configuration 18 | } 19 | 20 | func New(repo *repository.AuthRepo, config *auth.Configuration) *Middleware { 21 | return &Middleware{ 22 | Repo: repo, 23 | Config: config, 24 | } 25 | } 26 | 27 | func (m *Middleware) CustomLogger() gin.HandlerFunc { 28 | return func(c *gin.Context) { 29 | log.Println("before") 30 | 31 | c.Next() 32 | 33 | log.Println("after") 34 | } 35 | } 36 | 37 | func validBearer(ctx *gin.Context) string { 38 | var accessToken string 39 | 40 | cookie, err := ctx.Cookie("access_token") 41 | authorizationHeader := ctx.Request.Header.Get("Authorization") 42 | fields := strings.Fields(authorizationHeader) 43 | 44 | if len(fields) != 0 && fields[0] == "Bearer" { 45 | accessToken = fields[1] 46 | } else if err == nil { 47 | accessToken = cookie 48 | } 49 | 50 | return accessToken 51 | } 52 | 53 | func (m *Middleware) DeserializeUser(roles ...interface{}) gin.HandlerFunc { 54 | return func(ctx *gin.Context) { 55 | accessToken := validBearer(ctx) 56 | 57 | if accessToken == "" { 58 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, &entity.CustomResponse{ 59 | Status: -1, 60 | Message: "You are not logged in", 61 | }) 62 | return 63 | } 64 | 65 | sub, err := utils.ValidateToken(accessToken, m.Config.Jwt.AccessPrivateKey) 66 | if err != nil { 67 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, &entity.CustomResponse{ 68 | Status: -2, 69 | Message: err.Error(), 70 | }) 71 | return 72 | } 73 | 74 | user, err := m.Repo.GetUserByIdWithPreload(fmt.Sprint(sub)) 75 | if err != nil { 76 | ctx.AbortWithStatusJSON(http.StatusForbidden, &entity.CustomResponse{ 77 | Status: -3, 78 | Message: "the user belonging to this token no logger exists", 79 | }) 80 | return 81 | } 82 | 83 | role, _ := m.Repo.GetRoleById(user.RoleID) 84 | 85 | for _, Role := range roles { 86 | if role.Name == Role || Role == "any" { 87 | ctx.Set("currentUser", user) 88 | ctx.Set("currentRole", role) 89 | ctx.Next() 90 | return 91 | } 92 | } 93 | 94 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, &entity.CustomResponse{ 95 | Status: -4, 96 | Message: "User must have roles: " + fmt.Sprintf("%v", roles), 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/crm_core/service/company.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crm_system/internal/crm_core/entity" 5 | ) 6 | 7 | func (s *Service) GetCompanies(sortBy, sortOrder, phone string) (*[]entity.Company, error) { 8 | companies, err := s.Repo.GetCompanies() 9 | if err != nil { 10 | return nil, err 11 | } 12 | if phone != "" { 13 | companies, err = s.filterCompaniesByPhone(companies, phone) 14 | if err != nil { 15 | return nil, err 16 | } 17 | } 18 | 19 | if sortBy != "" { 20 | companies, err = s.sortCompanies(companies, sortBy, sortOrder) 21 | if err != nil { 22 | return nil, err 23 | } 24 | } 25 | 26 | return companies, nil 27 | } 28 | 29 | func (s *Service) sortCompanies(companies *[]entity.Company, sortBy, sortOrder string) (*[]entity.Company, error) { 30 | companies, err := s.Repo.SortCompanies(companies, sortBy, sortOrder) 31 | 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return companies, nil 37 | } 38 | 39 | func (s *Service) filterCompaniesByPhone(companies *[]entity.Company, phone string) (*[]entity.Company, error) { 40 | companies, err := s.Repo.FilterCompaniesByPhone(companies, phone) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return companies, nil 45 | } 46 | 47 | func (s *Service) GetCompany(id string) (*entity.Company, error) { 48 | company, err := s.Repo.GetCompany(id) 49 | 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return company, nil 55 | } 56 | 57 | func (s *Service) CreateCompany(company entity.Company) error { 58 | if err := s.Repo.CreateCompany(&company); err != nil { 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (s *Service) UpdateCompany(newCompany entity.NewCompany, id string) error { 66 | company, err := s.Repo.GetCompany(id) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | if newCompany.Name != "" { 72 | company.Name = newCompany.Name 73 | } 74 | 75 | if newCompany.Address != "" { 76 | company.Address = newCompany.Address 77 | } 78 | 79 | if newCompany.Phone != "" { 80 | company.Phone = newCompany.Phone 81 | } 82 | 83 | if err = s.Repo.SaveCompany(company); err != nil { 84 | return err 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (s *Service) DeleteCompany(id string) error { 91 | company, err := s.Repo.GetCompany(id) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | if err = s.Repo.DeleteCompany(id, company); err != nil { 97 | return err 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func (s *Service) SearchCompany(query string) (*[]entity.Company, error) { 104 | companies, err := s.Repo.SearchCompany(query) 105 | if err != nil { 106 | return companies, err 107 | } 108 | 109 | return companies, nil 110 | } 111 | -------------------------------------------------------------------------------- /internal/crm_core/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "crm_system/config/crm_core" 5 | middleware2 "crm_system/internal/crm_core/controller/http/middleware" 6 | "crm_system/internal/crm_core/controller/http/v1" 7 | debugRoute "crm_system/internal/crm_core/controller/http/v1/debug" 8 | _ "crm_system/internal/crm_core/docs" 9 | repoPkg "crm_system/internal/crm_core/repository" 10 | servicePkg "crm_system/internal/crm_core/service" 11 | "crm_system/internal/crm_core/transport" 12 | "crm_system/pkg/crm_core/cache" 13 | "crm_system/pkg/crm_core/httpserver/debug" 14 | "crm_system/pkg/crm_core/httpserver/public" 15 | "crm_system/pkg/crm_core/logger" 16 | "fmt" 17 | "github.com/gin-contrib/cors" 18 | "github.com/gin-gonic/gin" 19 | swaggerFiles "github.com/swaggo/files" 20 | ginSwagger "github.com/swaggo/gin-swagger" 21 | "os" 22 | "os/signal" 23 | "syscall" 24 | "time" 25 | ) 26 | 27 | func Run(cfg *crm_core.Configuration) { 28 | l := logger.New(cfg.Gin.Mode) 29 | repo := repoPkg.New(cfg, l) 30 | 31 | //REDIS implementation 32 | redisClient, err := cache.NewRedisClient() 33 | if err != nil { 34 | return 35 | } 36 | 37 | contactCache := cache.NewContactCache(redisClient, 10*time.Minute) 38 | 39 | validateGrpcTransport := transport.NewValidateGrpcTransport(*cfg) 40 | 41 | service := servicePkg.New(cfg, repo, l) 42 | middleware := middleware2.New(repo, cfg, validateGrpcTransport) 43 | handler := gin.Default() 44 | handlerDebug := gin.Default() 45 | handler.Use(cors.New(cors.Config{ 46 | AllowOrigins: []string{"http://localhost:8082"}, 47 | AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, 48 | AllowHeaders: []string{"Origin", "Authorization", "Content-Type", "Accept-Encoding"}, 49 | ExposeHeaders: []string{"Content-Length"}, 50 | AllowCredentials: true, 51 | })) 52 | handler.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 53 | 54 | v1.NewRouter(handler, service, middleware, contactCache) 55 | debugRoute.NewDebugRouter(handlerDebug) 56 | httpServer := public.New(handler, cfg, public.Port(cfg.HTTP.Port)) 57 | debugServer := debug.New(handlerDebug, cfg, debug.Port(cfg.HTTP.DebugPort)) 58 | 59 | // Waiting signal 60 | interrupt := make(chan os.Signal, 1) 61 | signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) 62 | 63 | select { 64 | case s := <-interrupt: 65 | l.Info("crm_system - Run - signal: " + s.String()) 66 | case err = <-httpServer.Notify(): 67 | l.Error(fmt.Errorf("crm_system - Run - httpServer.Notify: %w", err)) 68 | case err = <-debugServer.Notify(): 69 | l.Error(fmt.Errorf("crm_system - Run - debugServer.Notify: %w", err)) 70 | } 71 | 72 | // Shutdown 73 | err = httpServer.Shutdown() 74 | if err != nil { 75 | l.Error(fmt.Errorf("crm_system - Run - httpServer.Shutdown: %w", err)) 76 | } 77 | err = debugServer.Shutdown() 78 | if err != nil { 79 | l.Error(fmt.Errorf("crm_system - Run - debugServer.Shutdown: %w", err)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/crm_core/repository/interfaces.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "crm_system/internal/crm_core/entity" 5 | ) 6 | 7 | type ( 8 | CompanyRepo interface { 9 | GetCompanies() (*[]entity.Company, error) 10 | GetCompany(id string) (*entity.Company, error) 11 | CreateCompany(company *entity.Company) error 12 | SaveCompany(newCompany *entity.Company) error 13 | DeleteCompany(id string, company *entity.Company) error 14 | SearchCompany(query string) (*[]entity.Company, error) 15 | SortCompanies(companies *[]entity.Company, sortBy, sortOrder string) (*[]entity.Company, error) 16 | FilterCompaniesByPhone(companies *[]entity.Company, phone string) (*[]entity.Company, error) 17 | } 18 | ContactRepo interface { 19 | GetContacts() (*[]entity.Contact, error) 20 | GetContact(id string) (*entity.Contact, error) 21 | CreateContact(contact *entity.Contact) error 22 | SaveContact(newContact *entity.Contact) error 23 | DeleteContact(id string, contact *entity.Contact) error 24 | SearchContact(query string) (*[]entity.Contact, error) 25 | SortContacts(contacts *[]entity.Contact, sortBy, sortOrder string) (*[]entity.Contact, error) 26 | FilterContactsByPhone(contacts *[]entity.Contact, phone string) (*[]entity.Contact, error) 27 | } 28 | 29 | DealRepo interface { 30 | GetDeals() (*[]entity.Deal, error) 31 | GetDeal(id string) (*entity.Deal, error) 32 | CreateDeal(deal *entity.Deal) error 33 | SaveDeal(newDeal *entity.Deal) error 34 | DeleteDeal(id string, deal *entity.Deal) error 35 | SearchDeal(query string) (*[]entity.Deal, error) 36 | SortDeals(deals *[]entity.Deal, sortBy, sortOrder string) (*[]entity.Deal, error) 37 | FilterDealsByStatus(deals *[]entity.Deal, status string) (*[]entity.Deal, error) 38 | } 39 | 40 | TaskRepo interface { 41 | GetTasks() (*[]entity.Task, error) 42 | GetTask(id string) (*entity.Task, error) 43 | GetTasksByDealId(dealId string) ([]entity.Task, error) 44 | CreateTaskChanges(taskChanges *entity.TaskChanges) error 45 | CreateTask(newTask *entity.TaskInput) error 46 | CreateVote(user *entity.User, voteInput *entity.VoteInput) error 47 | GetChangesOfTask(id string) (*[]entity.TaskChanges, error) 48 | SaveTask(newTask *entity.Task) error 49 | DeleteTask(id string, task *entity.Task) error 50 | SearchTask(query string) (*[]entity.Task, error) 51 | FilterTasksByStates(tasks []map[string]interface{}, state string) ([]map[string]interface{}, error) 52 | } 53 | 54 | TicketRepo interface { 55 | GetTickets() (*[]entity.Ticket, error) 56 | GetTicket(id string) (*entity.Ticket, error) 57 | CreateTicket(ticket *entity.Ticket) error 58 | SaveTicket(newTicket *entity.Ticket) error 59 | DeleteTicket(id string, deal *entity.Ticket) error 60 | SearchTicket(query string) (*[]entity.Ticket, error) 61 | SortTickets(tickets *[]entity.Ticket, sortBy, sortOrder string) (*[]entity.Ticket, error) 62 | FilterTicketsByStatus(tickets *[]entity.Ticket, status string) (*[]entity.Ticket, error) 63 | } 64 | ) 65 | -------------------------------------------------------------------------------- /internal/crm_core/service/deal.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crm_system/internal/crm_core/entity" 5 | ) 6 | 7 | func (s *Service) GetDeals(sortBy, sortOrder, status string) (*[]entity.Deal, error) { 8 | deals, err := s.Repo.GetDeals() 9 | if err != nil { 10 | return nil, err 11 | } 12 | 13 | if status != "" { 14 | deals, err = s.filterDealsByStatus(deals, status) 15 | if err != nil { 16 | return nil, err 17 | } 18 | } 19 | 20 | if sortBy != "" { 21 | deals, err = s.sortDeals(deals, sortBy, sortOrder) 22 | if err != nil { 23 | return nil, err 24 | } 25 | } 26 | 27 | return deals, nil 28 | } 29 | func (s *Service) sortDeals(deals *[]entity.Deal, sortBy, sortOrder string) (*[]entity.Deal, error) { 30 | deals, err := s.Repo.SortDeals(deals, sortBy, sortOrder) 31 | 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return deals, nil 37 | } 38 | 39 | func (s *Service) filterDealsByStatus(deals *[]entity.Deal, status string) (*[]entity.Deal, error) { 40 | deals, err := s.Repo.FilterDealsByStatus(deals, status) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return deals, nil 45 | } 46 | 47 | func (s *Service) GetDeal(id string) (*entity.Deal, error) { 48 | deal, err := s.Repo.GetDeal(id) 49 | 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return deal, nil 55 | } 56 | func (s *Service) CreateDeal(deal entity.Deal) error { 57 | if err := s.Repo.CreateDeal(&deal); err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | func (s *Service) UpdateDeal(newDeal entity.Deal, id string) error { 64 | deal, err := s.Repo.GetDeal(id) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if newDeal.Title != "" { 70 | deal.Title = newDeal.Title 71 | } 72 | 73 | if newDeal.Value != 0 { 74 | deal.Value = newDeal.Value 75 | } 76 | 77 | if isValidDealStatus(newDeal.Status) { 78 | deal.Status = newDeal.Status 79 | } 80 | 81 | if newDeal.ContactID != 0 { 82 | deal.ContactID = newDeal.ContactID 83 | } 84 | 85 | if newDeal.RepID != 0 { 86 | deal.RepID = newDeal.RepID 87 | } 88 | 89 | if err = s.Repo.SaveDeal(deal); err != nil { 90 | return err 91 | } 92 | 93 | return nil 94 | } 95 | func isValidDealStatus(status entity.StatusDeal) bool { 96 | switch status { 97 | case entity.Initiated, entity.InProgress, entity.ClosedWon, entity.ClosedLost: 98 | return true 99 | default: 100 | return false 101 | } 102 | } 103 | 104 | func (s *Service) DeleteDeal(id string) error { 105 | deal, err := s.Repo.GetDeal(id) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | if err = s.Repo.DeleteDeal(id, deal); err != nil { 111 | return err 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func (s *Service) SearchDeal(query string) (*[]entity.Deal, error) { 118 | deals, err := s.Repo.SearchDeal(query) 119 | if err != nil { 120 | return deals, err 121 | } 122 | 123 | return deals, nil 124 | } 125 | -------------------------------------------------------------------------------- /internal/crm_core/service/contact.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crm_system/internal/crm_core/entity" 5 | ) 6 | 7 | func (s *Service) GetContacts(sortBy, sortOrder, phone string) (*[]entity.Contact, error) { 8 | contacts, err := s.Repo.GetContacts() 9 | 10 | if phone != "" { 11 | contacts, err = s.filterContactsByPhone(contacts, phone) 12 | if err != nil { 13 | return nil, err 14 | } 15 | } 16 | 17 | if sortBy != "" { 18 | contacts, err = s.sortContacts(contacts, sortBy, sortOrder) 19 | if err != nil { 20 | return nil, err 21 | } 22 | } 23 | 24 | if err != nil { 25 | return nil, err 26 | } 27 | return contacts, nil 28 | } 29 | func (s *Service) sortContacts(contacts *[]entity.Contact, sortBy, sortOrder string) (*[]entity.Contact, error) { 30 | contacts, err := s.Repo.SortContacts(contacts, sortBy, sortOrder) 31 | 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return contacts, nil 37 | } 38 | 39 | func (s *Service) filterContactsByPhone(contacts *[]entity.Contact, phone string) (*[]entity.Contact, error) { 40 | contacts, err := s.Repo.FilterContactsByPhone(contacts, phone) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return contacts, nil 45 | } 46 | 47 | func (s *Service) GetContact(id string) (*entity.Contact, error) { 48 | contact, err := s.Repo.GetContact(id) 49 | 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return contact, nil 55 | } 56 | func (s *Service) CreateContact(contact entity.Contact) error { 57 | if err := s.Repo.CreateContact(&contact); err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | func (s *Service) UpdateContact(newContact entity.Contact, id string) error { 64 | contact, err := s.Repo.GetContact(id) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if newContact.FirstName != "" { 70 | contact.FirstName = newContact.FirstName 71 | } 72 | 73 | if newContact.LastName != "" { 74 | contact.LastName = newContact.LastName 75 | } 76 | 77 | if newContact.Phone != "" { 78 | contact.Phone = newContact.Phone 79 | } 80 | 81 | if newContact.Email != "" { 82 | contact.Email = newContact.Email 83 | } 84 | 85 | if newContact.CompanyID != 0 { 86 | contact.CompanyID = newContact.CompanyID 87 | } else { 88 | contact.CompanyID = 0 89 | } 90 | 91 | if err = s.Repo.SaveContact(contact); err != nil { 92 | return err 93 | } 94 | 95 | return nil 96 | } 97 | 98 | func (s *Service) DeleteContact(id string) error { 99 | contact, err := s.Repo.GetContact(id) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | if err = s.Repo.DeleteContact(id, contact); err != nil { 105 | return err 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func (s *Service) SearchContact(query string) (*[]entity.Contact, error) { 112 | contacts, err := s.Repo.SearchContact(query) 113 | if err != nil { 114 | return contacts, err 115 | } 116 | 117 | return contacts, nil 118 | } 119 | -------------------------------------------------------------------------------- /internal/auth/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "crm_system/config/auth" 5 | "crm_system/internal/auth/controller/consumer" 6 | "crm_system/internal/auth/controller/grpc" 7 | middleware2 "crm_system/internal/auth/controller/http/middleware" 8 | "crm_system/internal/auth/controller/http/v1" 9 | _ "crm_system/internal/auth/docs" 10 | repoPkg "crm_system/internal/auth/repository" 11 | servicePkg "crm_system/internal/auth/service" 12 | storagePkg "crm_system/internal/auth/storage" 13 | "crm_system/internal/kafka" 14 | httpserver2 "crm_system/pkg/auth/httpserver" 15 | "crm_system/pkg/auth/logger" 16 | "fmt" 17 | "github.com/gin-contrib/cors" 18 | "github.com/gin-gonic/gin" 19 | swaggerFiles "github.com/swaggo/files" 20 | ginSwagger "github.com/swaggo/gin-swagger" 21 | "log" 22 | "os" 23 | "os/signal" 24 | "syscall" 25 | ) 26 | 27 | func Run(cfg *auth.Configuration) { 28 | l := logger.New(cfg.Gin.Mode) 29 | repo := repoPkg.New(cfg, l) 30 | userVerificationProducer, err := kafka.NewProducer(cfg) 31 | if err != nil { 32 | l.Fatal("failed NewProducer err: %v", err) 33 | } 34 | 35 | userVerificationConsumerCallback := consumer.NewUserVerificationCallback(l) 36 | 37 | userVerificationConsumer, err := kafka.NewConsumer(l, cfg, userVerificationConsumerCallback) 38 | if err != nil { 39 | l.Fatal("failed NewConsumer err: %v", err) 40 | } 41 | 42 | go userVerificationConsumer.Start() 43 | 44 | storage := storagePkg.NewDataStorage(cfg.Storage.Interval, repo, l) 45 | service := servicePkg.New(cfg, repo, userVerificationProducer, storage) 46 | 47 | go storage.Run() 48 | 49 | middleware := middleware2.New(repo, cfg) 50 | handler := gin.Default() 51 | handler.Use(cors.New(cors.Config{ 52 | AllowOrigins: []string{"http://localhost:8081"}, 53 | AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, 54 | AllowHeaders: []string{"Origin", "Authorization", "Content-Type", "Accept-Encoding"}, 55 | ExposeHeaders: []string{"Content-Length"}, 56 | AllowCredentials: true, 57 | })) 58 | handler.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 59 | 60 | grpcService := grpc.NewService(l, repo, cfg) 61 | grpcServer := grpc.NewServer(cfg.Grpc.Port, grpcService) 62 | err = grpcServer.Start() 63 | if err != nil { 64 | log.Panicf("failed to start grpc-server err: %v", err) 65 | } 66 | 67 | defer grpcServer.Close() 68 | v1.NewRouter(handler, service, middleware) 69 | httpServer := httpserver2.New(handler, cfg, httpserver2.Port(cfg.HTTP.Port)) 70 | 71 | // Waiting signal 72 | interrupt := make(chan os.Signal, 1) 73 | signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) 74 | 75 | select { 76 | case s := <-interrupt: 77 | l.Info("auth - Run - signal: " + s.String()) 78 | case err = <-httpServer.Notify(): 79 | l.Error(fmt.Errorf("auth - Run - httpServer.Notify: %w", err)) 80 | } 81 | 82 | // Shutdown 83 | err = httpServer.Shutdown() 84 | if err != nil { 85 | l.Error(fmt.Errorf("auth - Run - httpServer.Shutdown: %w", err)) 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /internal/crm_core/service/ticket.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crm_system/internal/crm_core/entity" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | func (s *Service) GetTickets(sortBy, sortOrder, status string) (*[]entity.Ticket, error) { 9 | tickets, err := s.Repo.GetTickets() 10 | if status != "" { 11 | tickets, err = s.filterTicketsByStatus(tickets, status) 12 | } 13 | 14 | if sortBy != "" { 15 | tickets, err = s.sortTickets(tickets, sortBy, sortOrder) 16 | if err != nil { 17 | return nil, err 18 | } 19 | } 20 | if err != nil { 21 | return nil, err 22 | } 23 | return tickets, nil 24 | } 25 | 26 | func (s *Service) sortTickets(tickets *[]entity.Ticket, sortBy, sortOrder string) (*[]entity.Ticket, error) { 27 | tickets, err := s.Repo.SortTickets(tickets, sortBy, sortOrder) 28 | 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return tickets, nil 34 | } 35 | 36 | func (s *Service) filterTicketsByStatus(tickets *[]entity.Ticket, status string) (*[]entity.Ticket, error) { 37 | tickets, err := s.Repo.FilterTicketsByStatus(tickets, status) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return tickets, nil 42 | } 43 | 44 | func (s *Service) GetTicket(id string) (*entity.Ticket, error) { 45 | ticket, err := s.Repo.GetTicket(id) 46 | 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return ticket, nil 52 | } 53 | func (s *Service) CreateTicket(ticket entity.Ticket) error { 54 | if err := s.Repo.CreateTicket(&ticket); err != nil { 55 | return err 56 | } 57 | 58 | return nil 59 | } 60 | func (s *Service) UpdateTicket(newTicket entity.Ticket, id string) error { 61 | ticket, err := s.Repo.GetTicket(id) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | if newTicket.IssueDescription != "" { 67 | ticket.IssueDescription = newTicket.IssueDescription 68 | } 69 | 70 | if isValidTicketStatus(newTicket.Status) { 71 | ticket.Status = newTicket.Status 72 | } 73 | 74 | if newTicket.ContactID != 0 { 75 | ticket.ContactID = newTicket.ContactID 76 | } 77 | 78 | if newTicket.AssignedTo != uuid.Nil { 79 | ticket.AssignedTo = newTicket.AssignedTo 80 | } 81 | 82 | if err = s.Repo.SaveTicket(ticket); err != nil { 83 | return err 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func isValidTicketStatus(status entity.StatusTicket) bool { 90 | switch status { 91 | case entity.Open, entity.InProgressTicket, entity.Closed: 92 | return true 93 | default: 94 | return false 95 | } 96 | } 97 | 98 | func (s *Service) DeleteTicket(id string) error { 99 | ticket, err := s.Repo.GetTicket(id) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | if err = s.Repo.DeleteTicket(id, ticket); err != nil { 105 | return err 106 | } 107 | 108 | return nil 109 | } 110 | func (s *Service) SearchTicket(query string) (*[]entity.Ticket, error) { 111 | tickets, err := s.Repo.SearchTicket(query) 112 | if err != nil { 113 | return tickets, err 114 | } 115 | 116 | return tickets, nil 117 | } 118 | -------------------------------------------------------------------------------- /internal/crm_core/service/interfaces.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crm_system/internal/crm_core/entity" 5 | ) 6 | 7 | type ( 8 | CompanyService interface { 9 | GetCompanies(sortBy, sortOrder, phone string) (*[]entity.Company, error) 10 | GetCompany(id string) (*entity.Company, error) 11 | CreateCompany(company entity.Company) error 12 | UpdateCompany(newCompany entity.NewCompany, id string) error 13 | DeleteCompany(id string) error 14 | SearchCompany(query string) (*[]entity.Company, error) 15 | sortCompanies(companies *[]entity.Company, sortBy, sortOrder string) (*[]entity.Company, error) 16 | filterCompaniesByPhone(companies *[]entity.Company, phone string) (*[]entity.Company, error) 17 | } 18 | 19 | TaskService interface { 20 | GetTasks(dealId, sortBy, sortOrder, stateInput string, user *entity.User) ([]map[string]interface{}, error) 21 | GetTask(id string) (*entity.Task, error) 22 | CreateTask(task *entity.TaskInput) error 23 | UpdateTask(newTask entity.TaskEditInput, id string, user *entity.User) error 24 | GetChangesOfTask(id string) (*[]entity.TaskChanges, error) 25 | DeleteTask(id string) error 26 | SearchTask(query string) (*[]entity.Task, error) 27 | sortTasks(columns []map[string]interface{}, sortBy, sortOrder, state string) ([]map[string]interface{}, error) 28 | filterTasksByStates(columns []map[string]interface{}, state string) (map[string]interface{}, error) 29 | Vote(user *entity.User, voteInput *entity.VoteInput) error 30 | } 31 | 32 | ContactService interface { 33 | GetContacts(sortBy, sortOrder, phone string) (*[]entity.Contact, error) 34 | GetContact(id string) (*entity.Contact, error) 35 | CreateContact(contact entity.Contact) error 36 | UpdateContact(newContact entity.Contact, id string) error 37 | DeleteContact(id string) error 38 | SearchContact(query string) (*[]entity.Contact, error) 39 | sortContacts(contacts *[]entity.Contact, sortBy, sortOrder string) (*[]entity.Contact, error) 40 | filterContactsByPhone(contacts *[]entity.Contact, phone string) (*[]entity.Contact, error) 41 | } 42 | 43 | TicketService interface { 44 | GetTickets(sortBy, sortOrder, status string) (*[]entity.Ticket, error) 45 | GetTicket(id string) (*entity.Ticket, error) 46 | CreateTicket(ticket entity.Ticket) error 47 | UpdateTicket(newTicket entity.Ticket, id string) error 48 | DeleteTicket(id string) error 49 | SearchTicket(query string) (*[]entity.Ticket, error) 50 | sortTickets(tickets *[]entity.Ticket, sortBy, sortOrder string) (*[]entity.Ticket, error) 51 | filterTicketsByStatus(tickets *[]entity.Ticket, status string) (*[]entity.Ticket, error) 52 | } 53 | 54 | DealService interface { 55 | GetDeals(sortBy, sortOrder, status string) (*[]entity.Deal, error) 56 | GetDeal(id string) (*entity.Deal, error) 57 | CreateDeal(deal entity.Deal) error 58 | UpdateDeal(newDeal entity.Deal, id string) error 59 | DeleteDeal(id string) error 60 | SearchDeal(query string) (*[]entity.Deal, error) 61 | sortDeals(deals *[]entity.Deal, sortBy, sortOrder string) (*[]entity.Deal, error) 62 | filterDealsByStatus(deals *[]entity.Deal, status string) (*[]entity.Deal, error) 63 | } 64 | ) 65 | -------------------------------------------------------------------------------- /config/crm_core/config.go: -------------------------------------------------------------------------------- 1 | package crm_core 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "log" 6 | "time" 7 | ) 8 | 9 | type ( 10 | // Configuration -. 11 | Configuration struct { 12 | App `yaml:"app"` 13 | HTTP `yaml:"http"` 14 | Log `yaml:"logger"` 15 | Gin `yaml:"gin"` 16 | DB `yaml:"db"` 17 | Transport `yaml:"transport"` 18 | Jwt `yaml:"jwt"` 19 | } 20 | 21 | // App -. 22 | App struct { 23 | Name string `env-required:"true" yaml:"name" env:"APP_NAME"` 24 | Version string `env-required:"true" yaml:"version" env:"APP_VERSION"` 25 | } 26 | 27 | // HTTP -. 28 | HTTP struct { 29 | Port string `env-required:"true" yaml:"port" env:"HTTP_PORT"` 30 | DebugPort string `env-required:"true" yaml:"debugPort" env:"HTTP_DEBUG_PORT"` 31 | DefaultReadTimeout int64 `env-required:"true" yaml:"default_read_timeout" env:"DEFAULT_READ_TIMEOUT"` 32 | DefaultWriteTimeout int64 `env-required:"true" yaml:"default_write_timeout" env:"DEFAULT_WRITE_TIMEOUT"` 33 | DefaultShutdownTimeout int64 `env-required:"true" yaml:"default_shutdown_timeout" env:"DEFAULT_SHUTDOWN_TIMEOUT"` 34 | } 35 | 36 | // Log -. 37 | Log struct { 38 | Level string `env-required:"true" yaml:"log_level" env:"LOG_LEVEL"` 39 | } 40 | 41 | Gin struct { 42 | Mode string `env-required:"true" yaml:"mode" env:"GIN_MODE"` 43 | } 44 | 45 | DB struct { 46 | PoolMax int64 `env-required:"true" yaml:"pool_max" env:"DB_POOL_MAX"` 47 | Host string `env-required:"true" yaml:"host" env:"DB_HOST"` 48 | User string `env-required:"true" yaml:"user" env:"DB_USER"` 49 | Password string `env-required:"true" yaml:"password" env:"DB_PASSWORD"` 50 | Name string `env-required:"true" yaml:"name" env:"DB_NAME"` 51 | Port int64 `env-required:"true" yaml:"port" env:"DB_PORT"` 52 | } 53 | 54 | Transport struct { 55 | Validate ValidateTransport `yaml:"validate"` 56 | ValidateGrpc ValidateGrpcTransport `yaml:"validateGrpc"` 57 | } 58 | 59 | ValidateTransport struct { 60 | Host string `yaml:"host"` 61 | Timeout time.Duration `yaml:"timeout"` 62 | } 63 | 64 | ValidateGrpcTransport struct { 65 | Host string `yaml:"host"` 66 | } 67 | 68 | Jwt struct { 69 | AccessPrivateKey string `mapstructure:"access_private_key"` 70 | AccessPublicKey string `mapstructure:"access_public_key"` 71 | AccessTokenExpiredIn time.Duration `mapstructure:"access_token_expired_in"` 72 | AccessTokenMaxAge int64 `mapstructure:"access_token_max_age"` 73 | 74 | RefreshPrivateKey string `mapstructure:"refresh_private_key"` 75 | RefreshPublicKey string `mapstructure:"refresh_public_key"` 76 | RefreshTokenExpiredIn time.Duration `mapstructure:"refresh_token_expired_in"` 77 | RefreshTokenMaxAge int64 `mapstructure:"refresh_token_max_age"` 78 | } 79 | ) 80 | 81 | func NewConfig() *Configuration { 82 | var config Configuration 83 | viper.SetConfigFile("config/crm_core/config.yml") 84 | viper.AutomaticEnv() 85 | if err := viper.ReadInConfig(); err != nil { 86 | log.Fatal(err) 87 | } 88 | if err := viper.Unmarshal(&config); err != nil { 89 | log.Fatal(err) 90 | } 91 | return &config 92 | } 93 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | zookeeper: 4 | image: confluentinc/cp-zookeeper:5.4.0 5 | hostname: zookeeper 6 | container_name: zookeeper 7 | ports: 8 | - "2181:2181" 9 | environment: 10 | ZOOKEEPER_CLIENT_PORT: 2181 11 | ZOOKEEPER_TICK_TIME: 2000 12 | volumes: 13 | - ./data/zookeeper/data:/data 14 | - ./data/zookeeper/datalog:/datalog 15 | 16 | kafka1: 17 | image: confluentinc/cp-kafka:5.3.0 18 | hostname: kafka1 19 | container_name: kafka1 20 | ports: 21 | - "19092:19092" 22 | environment: 23 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 24 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka1:9092,PLAINTEXT_HOST://localhost:19092 25 | KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" 26 | KAFKA_BROKER_ID: 1 27 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 28 | KAFKA_CONFLUENT_LICENSE_TOPIC_REPLICATION_FACTOR: 1 29 | volumes: 30 | - ./data/kafka1/data:/var/lib/kafka/data 31 | depends_on: 32 | - zookeeper 33 | 34 | kafdrop: 35 | image: obsidiandynamics/kafdrop 36 | restart: "no" 37 | ports: 38 | - "9000:9000" 39 | environment: 40 | KAFKA_BROKERCONNECT: "kafka1:9092" 41 | depends_on: 42 | - kafka1 43 | 44 | prometheus: 45 | image: prom/prometheus:v2.9.2 46 | ports: 47 | - 9091:9090 48 | volumes: 49 | - ./pkg/crm_core/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml 50 | 51 | grafana: 52 | image: grafana/grafana:10.2.2 53 | ports: 54 | - 3000:3000 55 | environment: 56 | - GF_SECURITY_ADMIN_PASSWORD=secret 57 | volumes: 58 | - ./pkg/crm_core/grafana/provisioning:/etc/grafana/provisioning 59 | - ./pkg/crm_core/grafana/dashboards:/var/lib/grafana/dashboards 60 | 61 | auth: 62 | build: 63 | context: . 64 | dockerfile: dockerfile-auth 65 | container_name: auth-service 66 | image: auth-service 67 | depends_on: 68 | - postgres-auth 69 | - redis 70 | - zookeeper 71 | 72 | postgres-auth: 73 | image: postgres:14-alpine 74 | container_name: as-auth-db 75 | restart: always 76 | environment: 77 | POSTGRES_DB: auth_service_crm 78 | POSTGRES_USER: postgres 79 | POSTGRES_PASSWORD: 12345 80 | ports: 81 | - '5432:5432' 82 | volumes: 83 | - postgres-auth:/var/lib/postgresql/data 84 | 85 | crm: 86 | build: 87 | context: . 88 | dockerfile: dockerfile-crm 89 | container_name: crm-service 90 | image: crm-service 91 | depends_on: 92 | - postgres-crm 93 | - redis 94 | 95 | postgres-crm: 96 | image: postgres:14-alpine 97 | container_name: cs-crm-db 98 | restart: always 99 | environment: 100 | POSTGRES_DB: crm_service 101 | POSTGRES_USER: postgres 102 | POSTGRES_PASSWORD: 12345 103 | ports: 104 | - '5433:5432' 105 | volumes: 106 | - postgres-crm:/var/lib/postgresql/data 107 | 108 | 109 | redis: 110 | image: redis 111 | ports: 112 | - '6379:6379' 113 | 114 | volumes: 115 | postgres-auth: 116 | driver: local 117 | postgres-crm: 118 | driver: local -------------------------------------------------------------------------------- /internal/crm_core/entity/user.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type User struct { 9 | gorm.Model 10 | ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primary_key" json:"id"` 11 | FirstName string `gorm:"type:varchar(255);not null" json:"first_name"` 12 | LastName string `gorm:"type:varchar(255);not null" json:"last_name"` 13 | Age uint64 `json:"age"` 14 | Phone string `gorm:"type:varchar(255);" json:"phone"` 15 | RoleID uint `gorm:"not null" json:"role_id"` 16 | Role Role `gorm:"not null" json:"role"` 17 | Email string `gorm:"type:varchar(255);not null;uniqueIndex;not null" json:"email"` 18 | Provider string `gorm:"type:varchar(255);not null" json:"provider"` 19 | Password string `gorm:"type:varchar(255);not null" json:"password"` 20 | } 21 | 22 | func NewUser() UserBuilderI { 23 | return User{}.SetGORMModel(gorm.Model{}).SetID(uuid.UUID{}).SetFirstName("firstName"). 24 | SetLastName("lastName"). 25 | SetEmail("email@gmail.com"). 26 | SetAge(18). 27 | SetPhone("87777771234"). 28 | SetRoleID(2). 29 | SetRole(Role{}). 30 | SetProvider("manager"). 31 | SetPassword("123456789aA"). 32 | Build() 33 | } 34 | 35 | type UserBuilderI interface { 36 | SetGORMModel(val gorm.Model) UserBuilderI 37 | SetID(val uuid.UUID) UserBuilderI 38 | SetFirstName(val string) UserBuilderI 39 | SetLastName(val string) UserBuilderI 40 | SetAge(val uint64) UserBuilderI 41 | SetPhone(val string) UserBuilderI 42 | SetRoleID(val uint) UserBuilderI 43 | SetRole(val Role) UserBuilderI 44 | SetEmail(val string) UserBuilderI 45 | SetProvider(val string) UserBuilderI 46 | SetPassword(val string) UserBuilderI 47 | 48 | Build() User 49 | } 50 | 51 | func (u User) SetGORMModel(val gorm.Model) UserBuilderI { 52 | u.Model = val 53 | return u 54 | } 55 | func (u User) SetID(val uuid.UUID) UserBuilderI { 56 | u.ID = val 57 | return u 58 | } 59 | 60 | func (u User) SetFirstName(val string) UserBuilderI { 61 | u.FirstName = val 62 | return u 63 | } 64 | func (u User) SetLastName(val string) UserBuilderI { 65 | u.LastName = val 66 | return u 67 | } 68 | func (u User) SetAge(val uint64) UserBuilderI { 69 | u.Age = val 70 | return u 71 | } 72 | func (u User) SetPhone(val string) UserBuilderI { 73 | u.Phone = val 74 | return u 75 | } 76 | func (u User) SetRoleID(val uint) UserBuilderI { 77 | u.RoleID = val 78 | return u 79 | } 80 | func (u User) SetRole(val Role) UserBuilderI { 81 | u.Role = val 82 | return u 83 | } 84 | func (u User) SetEmail(val string) UserBuilderI { 85 | u.Email = val 86 | return u 87 | } 88 | func (u User) SetProvider(val string) UserBuilderI { 89 | u.Provider = val 90 | 91 | return u 92 | } 93 | func (u User) SetPassword(val string) UserBuilderI { 94 | u.Password = val 95 | return u 96 | } 97 | func (u User) Build() User { 98 | return User{ 99 | Model: u.Model, 100 | ID: u.ID, 101 | FirstName: u.FirstName, 102 | LastName: u.LastName, 103 | Age: u.Age, 104 | Phone: u.Phone, 105 | RoleID: u.RoleID, 106 | Role: u.Role, 107 | Email: u.Email, 108 | Provider: u.Provider, 109 | Password: u.Password, 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /internal/crm_core/repository/task.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "crm_system/internal/crm_core/entity" 5 | "crm_system/internal/crm_core/metrics" 6 | "errors" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | func (r *CRMSystemRepo) GetTasks() (*[]entity.Task, error) { 11 | var tasks *[]entity.Task 12 | 13 | ok, fail := metrics.DatabaseQueryTime("GetTasks") 14 | defer fail() 15 | 16 | if err := r.DB.Find(&tasks).Error; err != nil { 17 | return nil, err 18 | } 19 | 20 | ok() 21 | 22 | return tasks, nil 23 | } 24 | 25 | func (r *CRMSystemRepo) GetTask(id string) (*entity.Task, error) { 26 | var task *entity.Task 27 | 28 | if err := r.DB.First(&task, id).Error; err != nil { 29 | return nil, err 30 | } 31 | 32 | return task, nil 33 | } 34 | func (r *CRMSystemRepo) GetTasksByDealId(dealId string) ([]entity.Task, error) { 35 | var tasks []entity.Task 36 | if err := r.DB.Preload("Votes").Where("associated_deal_id = ?", dealId).Find(&tasks).Error; err != nil { 37 | return nil, err 38 | } 39 | return tasks, nil 40 | } 41 | func (r *CRMSystemRepo) CreateTaskChanges(taskChanges *entity.TaskChanges) error { 42 | if err := r.DB.Create(&taskChanges).Error; err != nil { 43 | return err 44 | } 45 | return nil 46 | } 47 | 48 | func (r *CRMSystemRepo) CreateTask(newTask *entity.TaskInput) error { 49 | task := &entity.Task{ 50 | Model: gorm.Model{}, 51 | Name: newTask.Name, 52 | Description: newTask.Description, 53 | DueDate: newTask.DueDate, 54 | AssignedTo: newTask.AssignedTo, 55 | AssociatedDealID: newTask.AssociatedDealID, 56 | State: newTask.State, 57 | } 58 | if err := r.DB.Create(&task).Error; err != nil { 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | func (r *CRMSystemRepo) CreateVote(user *entity.User, voteInput *entity.VoteInput) error { 65 | vote := entity.Vote{ 66 | TaskID: voteInput.TaskID, 67 | SenderID: user.ID, 68 | } 69 | 70 | if err := r.DB.Create(&vote).Error; err != nil { 71 | return err 72 | } 73 | return nil 74 | } 75 | func (r *CRMSystemRepo) GetChangesOfTask(id string) (*[]entity.TaskChanges, error) { 76 | var taskChanges []entity.TaskChanges 77 | if err := r.DB.Where("task_id = ?", id).Find(&taskChanges).Error; err != nil { 78 | return nil, errors.New("could not get task history") 79 | } 80 | return &taskChanges, nil 81 | } 82 | 83 | func (r *CRMSystemRepo) SaveTask(newTask *entity.Task) error { 84 | if err := r.DB.Save(&newTask).Error; err != nil { 85 | return err 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (r *CRMSystemRepo) DeleteTask(id string, task *entity.Task) error { 92 | if err := r.DB.Where("id = ?", id).Delete(task).Error; err != nil { 93 | return err 94 | } 95 | return nil 96 | } 97 | func (r *CRMSystemRepo) SearchTask(query string) (*[]entity.Task, error) { 98 | var tasks *[]entity.Task 99 | 100 | if err := r.DB.Where("name ILIKE ?", "%"+query+"%").Find(&tasks).Error; err != nil { 101 | return nil, err 102 | } 103 | 104 | return tasks, nil 105 | } 106 | 107 | func (r *CRMSystemRepo) FilterTasksByStates(tasks []map[string]interface{}, state string) ([]map[string]interface{}, error) { 108 | if err := r.DB.Where("state = ?", state).Find(&tasks).Error; err != nil { 109 | return nil, err 110 | } 111 | 112 | return tasks, nil 113 | } 114 | -------------------------------------------------------------------------------- /config/auth/config.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "log" 6 | "time" 7 | ) 8 | 9 | type ( 10 | // Configuration -. 11 | Configuration struct { 12 | App `yaml:"app"` 13 | HTTP `yaml:"http"` 14 | Grpc `yaml:"Grpc"` 15 | Log `yaml:"logger"` 16 | Gin `yaml:"gin"` 17 | DB `yaml:"db"` 18 | Jwt `yaml:"jwt"` 19 | Kafka `yaml:"kafka"` 20 | Storage `yaml:"Storage"` 21 | } 22 | 23 | // App -. 24 | App struct { 25 | Name string `env-required:"true" yaml:"name" env:"APP_NAME"` 26 | Version string `env-required:"true" yaml:"version" env:"APP_VERSION"` 27 | } 28 | 29 | // HTTP -. 30 | HTTP struct { 31 | Port string `env-required:"true" yaml:"port" env:"HTTP_PORT"` 32 | DefaultReadTimeout int64 `env-required:"true" yaml:"default_read_timeout" env:"DEFAULT_READ_TIMEOUT"` 33 | DefaultWriteTimeout int64 `env-required:"true" yaml:"default_write_timeout" env:"DEFAULT_WRITE_TIMEOUT"` 34 | DefaultShutdownTimeout int64 `env-required:"true" yaml:"default_shutdown_timeout" env:"DEFAULT_SHUTDOWN_TIMEOUT"` 35 | } 36 | 37 | // Log -. 38 | Log struct { 39 | Level string `env-required:"true" yaml:"log_level" env:"LOG_LEVEL"` 40 | } 41 | 42 | Gin struct { 43 | Mode string `env-required:"true" yaml:"mode" env:"GIN_MODE"` 44 | } 45 | 46 | DB struct { 47 | PoolMax int64 `env-required:"true" yaml:"pool_max" env:"DB_POOL_MAX"` 48 | Host string `env-required:"true" yaml:"host" env:"DB_HOST"` 49 | User string `env-required:"true" yaml:"user" env:"DB_USER"` 50 | Password string `env-required:"true" yaml:"password" env:"DB_PASSWORD"` 51 | Name string `env-required:"true" yaml:"name" env:"DB_NAME"` 52 | Port int64 `env-required:"true" yaml:"port" env:"DB_PORT"` 53 | } 54 | 55 | Grpc struct { 56 | Port string `yaml:"Port"` 57 | } 58 | 59 | Jwt struct { 60 | AccessPrivateKey string `mapstructure:"access_private_key"` 61 | AccessPublicKey string `mapstructure:"access_public_key"` 62 | AccessTokenExpiredIn time.Duration `mapstructure:"access_token_expired_in"` 63 | AccessTokenMaxAge int64 `mapstructure:"access_token_max_age"` 64 | 65 | RefreshPrivateKey string `mapstructure:"refresh_private_key"` 66 | RefreshPublicKey string `mapstructure:"refresh_public_key"` 67 | RefreshTokenExpiredIn time.Duration `mapstructure:"refresh_token_expired_in"` 68 | RefreshTokenMaxAge int64 `mapstructure:"refresh_token_max_age"` 69 | } 70 | Kafka struct { 71 | Brokers []string `yaml:"brokers"` 72 | Producer Producer `yaml:"producer"` 73 | Consumer Consumer `yaml:"consumer"` 74 | } 75 | Producer struct { 76 | Topic string `yaml:"topic"` 77 | } 78 | Consumer struct { 79 | Topics []string `yaml:"topics"` 80 | } 81 | 82 | Storage struct { 83 | Interval time.Duration `yaml:"Interval"` 84 | ShutdownTimeout time.Duration `yaml:"ShutdownTimeout"` 85 | } 86 | ) 87 | 88 | func NewConfig() *Configuration { 89 | var config Configuration 90 | viper.SetConfigFile("config/auth/config.yml") 91 | viper.AutomaticEnv() 92 | if err := viper.ReadInConfig(); err != nil { 93 | log.Fatal(err) 94 | } 95 | if err := viper.Unmarshal(&config); err != nil { 96 | log.Fatal(err) 97 | } 98 | return &config 99 | } 100 | -------------------------------------------------------------------------------- /internal/crm_core/controller/http/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "crm_system/config/crm_core" 5 | "crm_system/internal/crm_core/entity" 6 | "crm_system/internal/crm_core/metrics" 7 | "crm_system/internal/crm_core/repository" 8 | "crm_system/internal/crm_core/transport" 9 | pb "crm_system/pkg/auth/authservice/gw" 10 | "github.com/gin-gonic/gin" 11 | "log" 12 | "net/http" 13 | "strconv" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | type Middleware struct { 19 | Repo *repository.CRMSystemRepo 20 | Config *crm_core.Configuration 21 | ValidateGrpcTransport *transport.ValidateGrpcTransport 22 | } 23 | 24 | func New(repo *repository.CRMSystemRepo, config *crm_core.Configuration, validateGrpcTransport *transport.ValidateGrpcTransport) *Middleware { 25 | return &Middleware{ 26 | Repo: repo, 27 | Config: config, 28 | ValidateGrpcTransport: validateGrpcTransport, 29 | } 30 | } 31 | 32 | func (m *Middleware) CustomLogger() gin.HandlerFunc { 33 | return func(c *gin.Context) { 34 | log.Println("before") 35 | 36 | c.Next() 37 | 38 | log.Println("after") 39 | } 40 | } 41 | 42 | func validBearer(ctx *gin.Context) string { 43 | var accessToken string 44 | 45 | cookie, err := ctx.Cookie("access_token") 46 | authorizationHeader := ctx.Request.Header.Get("Authorization") 47 | fields := strings.Fields(authorizationHeader) 48 | 49 | if len(fields) != 0 && fields[0] == "Bearer" { 50 | accessToken = fields[1] 51 | } else if err == nil { 52 | accessToken = cookie 53 | } 54 | 55 | return accessToken 56 | } 57 | 58 | func getUser(resp *pb.ResponseJSON) *entity.User { 59 | userBuilder := entity.NewUser() 60 | user := userBuilder. 61 | SetFirstName(resp.User.FirstName). 62 | SetLastName(resp.User.LastName). 63 | SetAge(uint64(resp.User.Age)). 64 | SetPhone(resp.User.Phone). 65 | SetRoleID(uint(resp.User.RoleID)). 66 | SetEmail(resp.User.Email). 67 | SetProvider(resp.User.Provider). 68 | SetPassword(resp.User.Password). 69 | Build() 70 | return &user 71 | } 72 | 73 | func (m *Middleware) DeserializeUser(roles ...string) gin.HandlerFunc { 74 | return func(ctx *gin.Context) { 75 | accessToken := validBearer(ctx) 76 | 77 | if accessToken == "" { 78 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, &entity.CustomResponse{ 79 | Status: -1, 80 | Message: "You are not logged in", 81 | }) 82 | return 83 | } 84 | 85 | resp, err := m.ValidateGrpcTransport.ValidateTransport(ctx.Request.Context(), accessToken, roles...) 86 | if err != nil { 87 | ctx.AbortWithStatusJSON(http.StatusInternalServerError, &entity.CustomResponse{ 88 | Status: -1, 89 | Message: err.Error(), 90 | }) 91 | return 92 | } 93 | 94 | user := getUser(resp) 95 | 96 | ctx.Set("currentUser", user) 97 | ctx.Set("currentRole", resp.Role) 98 | ctx.Next() 99 | } 100 | } 101 | 102 | func (m *Middleware) MetricsHandler() gin.HandlerFunc { 103 | return func(ctx *gin.Context) { 104 | start := time.Now() 105 | 106 | ctx.Next() 107 | 108 | path := ctx.Request.URL.Path 109 | 110 | statusString := strconv.Itoa(ctx.Writer.Status()) 111 | 112 | metrics.HttpResponseTime.WithLabelValues(path, statusString, ctx.Request.Method).Observe(time.Since(start).Seconds()) 113 | metrics.HttpRequestsTotalCollector.WithLabelValues(path, statusString, ctx.Request.Method).Inc() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /pkg/crm_core/grafana/dashboards/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "editable": true, 16 | "gnetId": null, 17 | "graphTooltip": 0, 18 | "links": [], 19 | "panels": [ 20 | { 21 | "aliasColors": {}, 22 | "bars": false, 23 | "dashLength": 10, 24 | "dashes": false, 25 | "datasource": "Prometheus", 26 | "fill": 1, 27 | "gridPos": { 28 | "h": 9, 29 | "w": 12, 30 | "x": 0, 31 | "y": 0 32 | }, 33 | "id": 2, 34 | "legend": { 35 | "avg": false, 36 | "current": false, 37 | "max": false, 38 | "min": false, 39 | "show": true, 40 | "total": false, 41 | "values": false 42 | }, 43 | "lines": true, 44 | "linewidth": 1, 45 | "nullPointMode": "null", 46 | "percentage": false, 47 | "pointradius": 2, 48 | "points": false, 49 | "renderer": "flot", 50 | "seriesOverrides": [], 51 | "spaceLength": 10, 52 | "stack": false, 53 | "steppedLine": false, 54 | "targets": [ 55 | { 56 | "expr": "go_goroutines{}", 57 | "format": "time_series", 58 | "instant": false, 59 | "intervalFactor": 1, 60 | "refId": "A" 61 | } 62 | ], 63 | "thresholds": [], 64 | "timeFrom": null, 65 | "timeRegions": [], 66 | "timeShift": null, 67 | "title": "Panel Title", 68 | "tooltip": { 69 | "shared": true, 70 | "sort": 0, 71 | "value_type": "individual" 72 | }, 73 | "type": "graph", 74 | "xaxis": { 75 | "buckets": null, 76 | "mode": "time", 77 | "name": null, 78 | "show": true, 79 | "values": [] 80 | }, 81 | "yaxes": [ 82 | { 83 | "format": "short", 84 | "label": null, 85 | "logBase": 1, 86 | "max": null, 87 | "min": null, 88 | "show": true 89 | }, 90 | { 91 | "format": "short", 92 | "label": null, 93 | "logBase": 1, 94 | "max": null, 95 | "min": null, 96 | "show": true 97 | } 98 | ], 99 | "yaxis": { 100 | "align": false, 101 | "alignLevel": null 102 | } 103 | } 104 | ], 105 | "schemaVersion": 18, 106 | "style": "dark", 107 | "tags": [], 108 | "templating": { 109 | "list": [] 110 | }, 111 | "time": { 112 | "from": "now-6h", 113 | "to": "now" 114 | }, 115 | "timepicker": { 116 | "refresh_intervals": [ 117 | "5s", 118 | "10s", 119 | "30s", 120 | "1m", 121 | "5m", 122 | "15m", 123 | "30m", 124 | "1h", 125 | "2h", 126 | "1d" 127 | ], 128 | "time_options": [ 129 | "5m", 130 | "15m", 131 | "1h", 132 | "6h", 133 | "12h", 134 | "24h", 135 | "2d", 136 | "7d", 137 | "30d" 138 | ] 139 | }, 140 | "timezone": "", 141 | "title": "New dashboard Copy", 142 | "uid": "7WpsgjkZk", 143 | "version": 1 144 | } 145 | -------------------------------------------------------------------------------- /internal/auth/entity/user.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type User struct { 9 | gorm.Model 10 | ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primary_key" json:"ID"` 11 | FirstName string `gorm:"type:varchar(255);not null" json:"first_name"` 12 | LastName string `gorm:"type:varchar(255);not null" json:"last_name"` 13 | Age uint64 `json:"age"` 14 | Phone string `gorm:"type:varchar(255);" json:"phone"` 15 | RoleID uint `gorm:"not null" json:"role_id"` 16 | Role Role `gorm:"not null" json:"role"` 17 | Email string `gorm:"type:varchar(255);not null;uniqueIndex;not null" json:"email"` 18 | Provider string `gorm:"type:varchar(255);not null" json:"provider"` 19 | Password string `gorm:"type:varchar(255);not null" json:"-"` 20 | IsConfirmed bool `json:"is_confirmed"` 21 | } 22 | 23 | type UserCode struct { 24 | UserID string `json:"user_id"` 25 | Code string `json:"code"` 26 | } 27 | type InputCode struct { 28 | Code string `json:"code"` 29 | } 30 | 31 | type UserBuilderI interface { 32 | SetGORMModel(val gorm.Model) UserBuilderI 33 | SetID(val uuid.UUID) UserBuilderI 34 | SetFirstName(val string) UserBuilderI 35 | SetLastName(val string) UserBuilderI 36 | SetAge(val uint64) UserBuilderI 37 | SetPhone(val string) UserBuilderI 38 | SetRoleID(val uint) UserBuilderI 39 | SetRole(val Role) UserBuilderI 40 | SetEmail(val string) UserBuilderI 41 | SetProvider(val string) UserBuilderI 42 | SetPassword(val string) UserBuilderI 43 | SetIsConfirmed(val bool) UserBuilderI 44 | Build() User 45 | } 46 | 47 | func NewUser() UserBuilderI { 48 | return User{}.SetGORMModel(gorm.Model{}).SetID(uuid.UUID{}).SetFirstName("firstName"). 49 | SetLastName("lastName"). 50 | SetEmail("email@gmail.com"). 51 | SetAge(18). 52 | SetPhone("87777771234"). 53 | SetRoleID(2). 54 | SetRole(Role{}). 55 | SetProvider("manager"). 56 | SetPassword("123456789aA"). 57 | SetIsConfirmed(false). 58 | Build() 59 | } 60 | 61 | func (u User) SetGORMModel(val gorm.Model) UserBuilderI { 62 | u.Model = val 63 | return u 64 | } 65 | func (u User) SetID(val uuid.UUID) UserBuilderI { 66 | u.ID = val 67 | return u 68 | } 69 | 70 | func (u User) SetFirstName(val string) UserBuilderI { 71 | u.FirstName = val 72 | return u 73 | } 74 | func (u User) SetLastName(val string) UserBuilderI { 75 | u.LastName = val 76 | return u 77 | } 78 | func (u User) SetAge(val uint64) UserBuilderI { 79 | u.Age = val 80 | return u 81 | } 82 | func (u User) SetPhone(val string) UserBuilderI { 83 | u.Phone = val 84 | return u 85 | } 86 | func (u User) SetRoleID(val uint) UserBuilderI { 87 | u.RoleID = val 88 | return u 89 | } 90 | func (u User) SetRole(val Role) UserBuilderI { 91 | u.Role = val 92 | return u 93 | } 94 | func (u User) SetEmail(val string) UserBuilderI { 95 | u.Email = val 96 | return u 97 | } 98 | func (u User) SetProvider(val string) UserBuilderI { 99 | u.Provider = val 100 | 101 | return u 102 | } 103 | func (u User) SetPassword(val string) UserBuilderI { 104 | u.Password = val 105 | return u 106 | } 107 | 108 | func (u User) SetIsConfirmed(val bool) UserBuilderI { 109 | u.IsConfirmed = val 110 | return u 111 | } 112 | 113 | func (u User) Build() User { 114 | return User{ 115 | Model: u.Model, 116 | ID: u.ID, 117 | FirstName: u.FirstName, 118 | LastName: u.LastName, 119 | Age: u.Age, 120 | Phone: u.Phone, 121 | RoleID: u.RoleID, 122 | Role: u.Role, 123 | Email: u.Email, 124 | Provider: u.Provider, 125 | Password: u.Password, 126 | IsConfirmed: u.IsConfirmed, 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /internal/auth/repository/user.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "crm_system/internal/auth/entity" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | func (r *AuthRepo) GetUserByIdWithPreload(id string) (*entity.User, error) { 11 | var user entity.User 12 | if err := r.DB.Where("id = ?", id).First(&user).Error; err != nil { 13 | return nil, errors.New("the user belonging to this token no logger exists") 14 | } 15 | return &user, nil 16 | } 17 | func (r *AuthRepo) GetUser(id string) (*entity.User, error) { 18 | var user *entity.User 19 | if err := r.DB.Where("id = ?", fmt.Sprint(id)).First(&user).Error; err != nil { 20 | return nil, errors.New("the user belonging to this token no logger exists") 21 | } 22 | return user, nil 23 | } 24 | 25 | func (r *AuthRepo) GetUserByEmail(email string) (*entity.User, error) { 26 | var user entity.User 27 | if err := r.DB.Preload("Role").Where("email = ?", strings.ToLower(email)).First(&user).Error; err != nil { 28 | return nil, err 29 | } 30 | return &user, nil 31 | } 32 | 33 | func (r *AuthRepo) GetUsersByRole(roleId uint) (*[]entity.User, error) { 34 | var users []entity.User 35 | if err := r.DB.Preload("Role").Where("role_id = ? ", roleId).Find(&users).Error; err != nil { 36 | return nil, err 37 | } 38 | return &users, nil 39 | } 40 | 41 | func (r *AuthRepo) CreateUser(user *entity.User) error { 42 | err := r.DB.Create(user).Error 43 | if err != nil { 44 | if strings.Contains(err.Error(), "duplicate key value violates unique") { 45 | return errors.New("user with that email already exists") 46 | } 47 | return err 48 | } 49 | return nil 50 | } 51 | 52 | func (r *AuthRepo) SaveUser(user *entity.User) error { 53 | if err := r.DB.Save(&user).Error; err != nil { 54 | return err 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func (r *AuthRepo) GetAllUsers() (*[]entity.User, error) { 61 | var users *[]entity.User 62 | 63 | if err := r.DB.Find(&users).Error; err != nil { 64 | return nil, err 65 | } 66 | 67 | return users, nil 68 | } 69 | 70 | func (r *AuthRepo) DeleteUser(id string, user *entity.User) error { 71 | if err := r.DB.Where("id = ?", id).Delete(user).Error; err != nil { 72 | return err 73 | } 74 | return nil 75 | } 76 | 77 | func (r *AuthRepo) SearchUser(query string) (*[]entity.User, error) { 78 | var users *[]entity.User 79 | 80 | if err := r.DB.Where("first_name ILIKE ?", "%"+query+"%").Find(&users).Error; err != nil { 81 | return nil, err 82 | } 83 | 84 | return users, nil 85 | } 86 | func (r *AuthRepo) CreateUserCode(id string, code string) error { 87 | userCode := entity.UserCode{ 88 | UserID: id, 89 | Code: code, 90 | } 91 | err := r.DB.Create(&userCode).Error 92 | if err != nil { 93 | return err 94 | } 95 | return nil 96 | } 97 | 98 | func (r *AuthRepo) ConfirmUser(code string) error { 99 | var userCode *entity.UserCode 100 | 101 | if err := r.DB.Where("code = ?", fmt.Sprint(code)).First(&userCode).Error; err != nil { 102 | return err 103 | } 104 | 105 | userID := userCode.UserID 106 | 107 | if err := r.DB.Where("code = ?", userCode.Code).Delete(userCode).Error; err != nil { 108 | return err 109 | } 110 | 111 | user, err := r.GetUser(userID) 112 | if err != nil { 113 | return err 114 | } 115 | user.IsConfirmed = true 116 | 117 | err = r.SaveUser(user) 118 | if err != nil { 119 | return err 120 | } 121 | return nil 122 | } 123 | 124 | func (r *AuthRepo) SortUsers(users *[]entity.User, sortBy, sortOrder string) (*[]entity.User, error) { 125 | if err := r.DB.Order(sortBy + " " + sortOrder).Find(&users).Error; err != nil { 126 | return nil, err 127 | } 128 | 129 | return users, nil 130 | } 131 | 132 | func (r *AuthRepo) FilterUsersByAge(users *[]entity.User, age string) (*[]entity.User, error) { 133 | if err := r.DB.Where("age > ?", age).Find(&users).Error; err != nil { 134 | return nil, err 135 | } 136 | 137 | return users, nil 138 | } 139 | -------------------------------------------------------------------------------- /pkg/auth/authservice/gw/auth_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc v4.24.4 5 | // source: auth.proto 6 | 7 | package pb 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | // AuthServiceClient is the client API for AuthService service. 22 | // 23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 24 | type AuthServiceClient interface { 25 | Validate(ctx context.Context, in *ValidateRequest, opts ...grpc.CallOption) (*ValidateResponse, error) 26 | } 27 | 28 | type authServiceClient struct { 29 | cc grpc.ClientConnInterface 30 | } 31 | 32 | func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient { 33 | return &authServiceClient{cc} 34 | } 35 | 36 | func (c *authServiceClient) Validate(ctx context.Context, in *ValidateRequest, opts ...grpc.CallOption) (*ValidateResponse, error) { 37 | out := new(ValidateResponse) 38 | err := c.cc.Invoke(ctx, "/authservice.AuthService/Validate", in, out, opts...) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return out, nil 43 | } 44 | 45 | // AuthServiceServer is the server API for AuthService service. 46 | // All implementations must embed UnimplementedAuthServiceServer 47 | // for forward compatibility 48 | type AuthServiceServer interface { 49 | Validate(context.Context, *ValidateRequest) (*ValidateResponse, error) 50 | mustEmbedUnimplementedAuthServiceServer() 51 | } 52 | 53 | // UnimplementedAuthServiceServer must be embedded to have forward compatible implementations. 54 | type UnimplementedAuthServiceServer struct { 55 | } 56 | 57 | func (UnimplementedAuthServiceServer) Validate(context.Context, *ValidateRequest) (*ValidateResponse, error) { 58 | return nil, status.Errorf(codes.Unimplemented, "method Validate not implemented") 59 | } 60 | func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {} 61 | 62 | // UnsafeAuthServiceServer may be embedded to opt out of forward compatibility for this service. 63 | // Use of this interface is not recommended, as added methods to AuthServiceServer will 64 | // result in compilation errors. 65 | type UnsafeAuthServiceServer interface { 66 | mustEmbedUnimplementedAuthServiceServer() 67 | } 68 | 69 | func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) { 70 | s.RegisterService(&AuthService_ServiceDesc, srv) 71 | } 72 | 73 | func _AuthService_Validate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 74 | in := new(ValidateRequest) 75 | if err := dec(in); err != nil { 76 | return nil, err 77 | } 78 | if interceptor == nil { 79 | return srv.(AuthServiceServer).Validate(ctx, in) 80 | } 81 | info := &grpc.UnaryServerInfo{ 82 | Server: srv, 83 | FullMethod: "/authservice.AuthService/Validate", 84 | } 85 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 86 | return srv.(AuthServiceServer).Validate(ctx, req.(*ValidateRequest)) 87 | } 88 | return interceptor(ctx, in, info, handler) 89 | } 90 | 91 | // AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service. 92 | // It's only intended for direct use with grpc.RegisterService, 93 | // and not to be introspected or modified (even as a copy) 94 | var AuthService_ServiceDesc = grpc.ServiceDesc{ 95 | ServiceName: "authservice.AuthService", 96 | HandlerType: (*AuthServiceServer)(nil), 97 | Methods: []grpc.MethodDesc{ 98 | { 99 | MethodName: "Validate", 100 | Handler: _AuthService_Validate_Handler, 101 | }, 102 | }, 103 | Streams: []grpc.StreamDesc{}, 104 | Metadata: "auth.proto", 105 | } 106 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module crm_system 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/IBM/sarama v1.42.1 7 | github.com/gin-contrib/cors v1.5.0 8 | github.com/gin-contrib/pprof v1.4.0 9 | github.com/gin-gonic/gin v1.9.1 10 | github.com/golang-jwt/jwt v3.2.2+incompatible 11 | github.com/google/uuid v1.3.1 12 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 13 | github.com/prometheus/client_golang v1.17.0 14 | github.com/redis/go-redis/v9 v9.2.1 15 | github.com/rs/zerolog v1.31.0 16 | github.com/spf13/cast v1.5.1 17 | github.com/spf13/viper v1.17.0 18 | github.com/swaggo/files v1.0.1 19 | github.com/swaggo/gin-swagger v1.6.0 20 | github.com/swaggo/swag v1.16.2 21 | golang.org/x/crypto v0.16.0 22 | google.golang.org/grpc v1.59.0 23 | google.golang.org/protobuf v1.31.0 24 | gorm.io/driver/postgres v1.5.3 25 | gorm.io/gorm v1.25.5 26 | ) 27 | 28 | require ( 29 | github.com/KyleBanks/depth v1.2.1 // indirect 30 | github.com/beorn7/perks v1.0.1 // indirect 31 | github.com/bytedance/sonic v1.10.1 // indirect 32 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 33 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 34 | github.com/chenzhuoyu/iasm v0.9.0 // indirect 35 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 36 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 37 | github.com/eapache/go-resiliency v1.4.0 // indirect 38 | github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect 39 | github.com/eapache/queue v1.1.0 // indirect 40 | github.com/fsnotify/fsnotify v1.6.0 // indirect 41 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 42 | github.com/gin-contrib/sse v0.1.0 // indirect 43 | github.com/go-openapi/jsonpointer v0.20.0 // indirect 44 | github.com/go-openapi/jsonreference v0.20.2 // indirect 45 | github.com/go-openapi/spec v0.20.11 // indirect 46 | github.com/go-openapi/swag v0.22.4 // indirect 47 | github.com/go-playground/locales v0.14.1 // indirect 48 | github.com/go-playground/universal-translator v0.18.1 // indirect 49 | github.com/go-playground/validator/v10 v10.15.5 // indirect 50 | github.com/goccy/go-json v0.10.2 // indirect 51 | github.com/golang/protobuf v1.5.3 // indirect 52 | github.com/golang/snappy v0.0.4 // indirect 53 | github.com/hashicorp/errwrap v1.0.0 // indirect 54 | github.com/hashicorp/go-multierror v1.1.1 // indirect 55 | github.com/hashicorp/go-uuid v1.0.3 // indirect 56 | github.com/hashicorp/hcl v1.0.0 // indirect 57 | github.com/jackc/pgpassfile v1.0.0 // indirect 58 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 59 | github.com/jackc/pgx/v5 v5.4.3 // indirect 60 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 61 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 62 | github.com/jcmturner/gofork v1.7.6 // indirect 63 | github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect 64 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 65 | github.com/jinzhu/inflection v1.0.0 // indirect 66 | github.com/jinzhu/now v1.1.5 // indirect 67 | github.com/josharian/intern v1.0.0 // indirect 68 | github.com/json-iterator/go v1.1.12 // indirect 69 | github.com/klauspost/compress v1.17.0 // indirect 70 | github.com/klauspost/cpuid/v2 v2.2.5 // indirect 71 | github.com/leodido/go-urn v1.2.4 // indirect 72 | github.com/magiconair/properties v1.8.7 // indirect 73 | github.com/mailru/easyjson v0.7.7 // indirect 74 | github.com/mattn/go-colorable v0.1.13 // indirect 75 | github.com/mattn/go-isatty v0.0.19 // indirect 76 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 77 | github.com/mitchellh/mapstructure v1.5.0 // indirect 78 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 79 | github.com/modern-go/reflect2 v1.0.2 // indirect 80 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 81 | github.com/pierrec/lz4/v4 v4.1.18 // indirect 82 | github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect 83 | github.com/prometheus/common v0.44.0 // indirect 84 | github.com/prometheus/procfs v0.11.1 // indirect 85 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 86 | github.com/sagikazarmark/locafero v0.3.0 // indirect 87 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 88 | github.com/sourcegraph/conc v0.3.0 // indirect 89 | github.com/spf13/afero v1.10.0 // indirect 90 | github.com/spf13/pflag v1.0.5 // indirect 91 | github.com/subosito/gotenv v1.6.0 // indirect 92 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 93 | github.com/ugorji/go/codec v1.2.11 // indirect 94 | go.uber.org/atomic v1.9.0 // indirect 95 | go.uber.org/multierr v1.9.0 // indirect 96 | golang.org/x/arch v0.5.0 // indirect 97 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 98 | golang.org/x/net v0.19.0 // indirect 99 | golang.org/x/sys v0.15.0 // indirect 100 | golang.org/x/text v0.14.0 // indirect 101 | golang.org/x/tools v0.16.0 // indirect 102 | google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect 103 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect 104 | gopkg.in/ini.v1 v1.67.0 // indirect 105 | gopkg.in/yaml.v3 v3.0.1 // indirect 106 | ) 107 | -------------------------------------------------------------------------------- /internal/crm_core/controller/http/v1/deal.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "crm_system/internal/crm_core/controller/http/middleware" 5 | "crm_system/internal/crm_core/entity" 6 | "crm_system/internal/crm_core/service" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | ) 10 | 11 | type dealRoutes struct { 12 | s *service.Service 13 | } 14 | 15 | func newDealRoutes(handler *gin.RouterGroup, s *service.Service, MW *middleware.Middleware) { 16 | r := &dealRoutes{s: s} 17 | 18 | dealHandler := handler.Group("/deal") 19 | { 20 | //middleware for users 21 | dealHandler.Use(MW.MetricsHandler()) 22 | 23 | dealHandler.GET("/", MW.DeserializeUser("any"), r.getDeals) 24 | dealHandler.GET("/:id", MW.DeserializeUser("any"), r.getDeal) 25 | dealHandler.POST("/", MW.DeserializeUser("any"), r.createDeal) 26 | dealHandler.PUT("/:id", MW.DeserializeUser("any"), r.updateDeal) 27 | dealHandler.DELETE("/:id", MW.DeserializeUser("any"), r.deleteDeal) 28 | dealHandler.GET("/search", MW.DeserializeUser("any"), r.searchDeal) 29 | } 30 | } 31 | 32 | // getDeals godoc 33 | // @Summary Получить список соглашений 34 | // @Description Получить список соглашений 35 | // @Tags deal 36 | // @Accept json 37 | // @Produce json 38 | // @Security BearerAuth 39 | // @Param sortBy query string false "sortBy" 40 | // @Param sortOrder query string false "sortOrder" 41 | // @Param status query string false "filter by status" 42 | // @Success 200 {object} entity.CustomResponseWithData 43 | // @Failure 404 {object} entity.CustomResponse 44 | // @Router /v1/deal/ [get] 45 | func (dr *dealRoutes) getDeals(ctx *gin.Context) { 46 | sortBy := ctx.Query("sortBy") 47 | sortOrder := ctx.Query("sortOrder") 48 | status := ctx.Query("status") 49 | deals, err := dr.s.GetDeals(sortBy, sortOrder, status) 50 | 51 | if err != nil { 52 | ctx.JSON(http.StatusNotFound, &entity.CustomResponse{ 53 | Status: -1, 54 | Message: err.Error(), 55 | }) 56 | return 57 | } 58 | 59 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 60 | Status: 0, 61 | Message: "OK", 62 | Data: deals, 63 | }) 64 | } 65 | 66 | // getDeal godoc 67 | // @Summary Получить соглашение по id 68 | // @Description Получить соглашение по id 69 | // @Tags deal 70 | // @Accept json 71 | // @Produce json 72 | // @Security BearerAuth 73 | // @Param id path string true "id deal" 74 | // @Success 200 {object} entity.CustomResponseWithData 75 | // @Failure 400 {object} entity.CustomResponse 76 | // @Router /v1/deal/{id} [get] 77 | func (dr *dealRoutes) getDeal(ctx *gin.Context) { 78 | id := ctx.Param("id") 79 | 80 | deal, err := dr.s.GetDeal(id) 81 | 82 | if err != nil { 83 | ctx.JSON(http.StatusNotFound, &entity.CustomResponse{ 84 | Status: -1, 85 | Message: err.Error(), 86 | }) 87 | return 88 | } 89 | 90 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 91 | Status: 0, 92 | Message: "OK", 93 | Data: deal, 94 | }) 95 | } 96 | 97 | // createDeal godoc 98 | // @Summary Создать Соглашение 99 | // @Description Создать Соглашение 100 | // @Tags deal 101 | // @Accept json 102 | // @Produce json 103 | // @Security BearerAuth 104 | // @Param dealInput body entity.Deal true "Create deal" 105 | // @Success 201 {object} entity.CustomResponse 106 | // @Failure 400 {object} entity.CustomResponse 107 | // @Router /v1/deal/ [post] 108 | func (dr *dealRoutes) createDeal(ctx *gin.Context) { 109 | var deal entity.Deal 110 | 111 | if err := ctx.ShouldBindJSON(&deal); err != nil { 112 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 113 | Status: -1, 114 | Message: err.Error(), 115 | }) 116 | return 117 | } 118 | 119 | if err := dr.s.CreateDeal(deal); err != nil { 120 | ctx.JSON(http.StatusInternalServerError, &entity.CustomResponse{ 121 | Status: -2, 122 | Message: err.Error(), 123 | }) 124 | return 125 | } 126 | 127 | ctx.JSON(http.StatusCreated, &entity.CustomResponse{ 128 | Status: 0, 129 | Message: "OK", 130 | }) 131 | } 132 | 133 | // updateDeal godoc 134 | // @Summary Редактировать соглашение по id 135 | // @Description Редактировать соглашение по id 136 | // @Tags deal 137 | // @Accept json 138 | // @Produce json 139 | // @Security BearerAuth 140 | // @Param id path string true "id deal" 141 | // @Param newDeal body entity.Deal true "Update deal" 142 | // @Success 200 {object} entity.CustomResponse 143 | // @Failure 400 {object} entity.CustomResponse 144 | // @Router /v1/deal/{id} [put] 145 | func (dr *dealRoutes) updateDeal(ctx *gin.Context) { 146 | id := ctx.Param("id") 147 | 148 | var newDeal entity.Deal 149 | 150 | if err := ctx.ShouldBindJSON(&newDeal); err != nil { 151 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 152 | Status: -1, 153 | Message: err.Error(), 154 | }) 155 | return 156 | } 157 | 158 | if err := dr.s.UpdateDeal(newDeal, id); err != nil { 159 | ctx.JSON(http.StatusInternalServerError, &entity.CustomResponse{ 160 | Status: -2, 161 | Message: err.Error(), 162 | }) 163 | return 164 | } 165 | 166 | ctx.JSON(http.StatusOK, &entity.CustomResponse{ 167 | Status: 0, 168 | Message: "OK", 169 | }) 170 | } 171 | 172 | // deleteDeal godoc 173 | // @Summary Удалить соглашение по id 174 | // @Description Удалить соглашение по id 175 | // @Tags deal 176 | // @Accept json 177 | // @Produce json 178 | // @Security BearerAuth 179 | // @Param id path string true "id deal" 180 | // @Success 200 {object} entity.CustomResponse 181 | // @Failure 400 {object} entity.CustomResponse 182 | // @Router /v1/deal/{id} [delete] 183 | func (dr *dealRoutes) deleteDeal(ctx *gin.Context) { 184 | id := ctx.Param("id") 185 | 186 | if err := dr.s.DeleteDeal(id); err != nil { 187 | ctx.JSON(http.StatusNoContent, &entity.CustomResponse{ 188 | Status: -1, 189 | Message: err.Error(), 190 | }) 191 | return 192 | } 193 | 194 | ctx.JSON(http.StatusOK, &entity.CustomResponse{ 195 | Status: 0, 196 | Message: "OK", 197 | }) 198 | } 199 | 200 | // searchDeal godoc 201 | // @Summary Поиск соглашений по имени 202 | // @Description Поиск соглашений по имени 203 | // @Tags deal 204 | // @Accept json 205 | // @Produce json 206 | // @Security BearerAuth 207 | // @Param searchQuery query string true "query" 208 | // @Success 200 {object} entity.CustomResponseWithData 209 | // @Failure 400 {object} entity.CustomResponse 210 | // @Router /v1/deal/search [get] 211 | func (dr *dealRoutes) searchDeal(ctx *gin.Context) { 212 | query := ctx.Query("searchQuery") 213 | deals, err := dr.s.SearchDeal(query) 214 | if err != nil { 215 | ctx.JSON(http.StatusNotFound, &entity.CustomResponseWithData{ 216 | Status: -1, 217 | Message: "Not found", 218 | Data: deals, 219 | }) 220 | return 221 | } 222 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 223 | Status: 0, 224 | Message: "OK", 225 | Data: deals, 226 | }) 227 | } 228 | -------------------------------------------------------------------------------- /internal/crm_core/controller/http/v1/ticket.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "crm_system/internal/crm_core/controller/http/middleware" 5 | "crm_system/internal/crm_core/entity" 6 | "crm_system/internal/crm_core/service" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | ) 10 | 11 | type ticketRoutes struct { 12 | s *service.Service 13 | } 14 | 15 | func newTicketRoutes(handler *gin.RouterGroup, s *service.Service, MW *middleware.Middleware) { 16 | r := &ticketRoutes{s: s} 17 | 18 | ticketHandler := handler.Group("/ticket") 19 | { 20 | //middleware for users 21 | ticketHandler.Use(MW.MetricsHandler()) 22 | ticketHandler.Use(MW.DeserializeUser("admin", "manager")) 23 | 24 | ticketHandler.GET("/", r.getTickets) 25 | ticketHandler.GET("/:id", r.getTicket) 26 | ticketHandler.POST("/", r.createTicket) 27 | ticketHandler.PUT("/:id", r.updateTicket) 28 | ticketHandler.DELETE("/:id", r.deleteTicket) 29 | ticketHandler.GET("/search", r.searchTicket) 30 | } 31 | } 32 | 33 | // getTickets godoc 34 | // @Summary Получить список билетов 35 | // @Description Получить список билетов 36 | // @Tags ticket 37 | // @Accept json 38 | // @Produce json 39 | // @Security BearerAuth 40 | // @Param sortBy query string false "sortBy" 41 | // @Param sortOrder query string false "sortOrder" 42 | // @Param status query string false "filter by status" 43 | // @Success 200 {object} entity.CustomResponseWithData 44 | // @Failure 404 {object} entity.CustomResponse 45 | // @Router /v1/ticket/ [get] 46 | func (tr *ticketRoutes) getTickets(ctx *gin.Context) { 47 | sortBy := ctx.Query("sortBy") 48 | sortOrder := ctx.Query("sortOrder") 49 | status := ctx.Query("status") 50 | tickets, err := tr.s.GetTickets(sortBy, sortOrder, status) 51 | 52 | if err != nil { 53 | ctx.JSON(http.StatusNotFound, &entity.CustomResponse{ 54 | Status: -1, 55 | Message: err.Error(), 56 | }) 57 | return 58 | } 59 | 60 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 61 | Status: 0, 62 | Message: "OK", 63 | Data: tickets, 64 | }) 65 | } 66 | 67 | // getTicket godoc 68 | // @Summary Получить билет по id 69 | // @Description Получить билет по id 70 | // @Tags ticket 71 | // @Accept json 72 | // @Produce json 73 | // @Security BearerAuth 74 | // @Param id path string true "id ticket" 75 | // @Success 200 {object} entity.CustomResponseWithData 76 | // @Failure 400 {object} entity.CustomResponse 77 | // @Router /v1/ticket/{id} [get] 78 | func (tr *ticketRoutes) getTicket(ctx *gin.Context) { 79 | id := ctx.Param("id") 80 | 81 | ticket, err := tr.s.GetTicket(id) 82 | 83 | if err != nil { 84 | ctx.JSON(http.StatusNotFound, &entity.CustomResponse{ 85 | Status: -1, 86 | Message: err.Error(), 87 | }) 88 | return 89 | } 90 | 91 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 92 | Status: 0, 93 | Message: "OK", 94 | Data: ticket, 95 | }) 96 | } 97 | 98 | // createTicket godoc 99 | // @Summary Создать Билет 100 | // @Description Создать Билет 101 | // @Tags ticket 102 | // @Accept json 103 | // @Produce json 104 | // @Security BearerAuth 105 | // @Param ticketInput body entity.Ticket true "Create ticket" 106 | // @Success 201 {object} entity.CustomResponse 107 | // @Failure 400 {object} entity.CustomResponse 108 | // @Router /v1/ticket/ [post] 109 | func (tr *ticketRoutes) createTicket(ctx *gin.Context) { 110 | var ticket entity.Ticket 111 | 112 | if err := ctx.ShouldBindJSON(&ticket); err != nil { 113 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 114 | Status: -1, 115 | Message: err.Error(), 116 | }) 117 | return 118 | } 119 | 120 | if err := tr.s.CreateTicket(ticket); err != nil { 121 | ctx.JSON(http.StatusInternalServerError, &entity.CustomResponse{ 122 | Status: -2, 123 | Message: err.Error(), 124 | }) 125 | return 126 | } 127 | 128 | ctx.JSON(http.StatusCreated, &entity.CustomResponse{ 129 | Status: 0, 130 | Message: "OK", 131 | }) 132 | } 133 | 134 | // updateTicket godoc 135 | // @Summary Редактировать билет по id 136 | // @Description Редактировать билет по id 137 | // @Tags ticket 138 | // @Accept json 139 | // @Produce json 140 | // @Security BearerAuth 141 | // @Param id path string true "id ticket" 142 | // @Param inputTicket body entity.Ticket true "Update Ticket" 143 | // @Success 200 {object} entity.CustomResponse 144 | // @Failure 400 {object} entity.CustomResponse 145 | // @Router /v1/ticket/{id} [put] 146 | func (tr *ticketRoutes) updateTicket(ctx *gin.Context) { 147 | id := ctx.Param("id") 148 | 149 | var newTicket entity.Ticket 150 | 151 | if err := ctx.ShouldBindJSON(&newTicket); err != nil { 152 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 153 | Status: -1, 154 | Message: err.Error(), 155 | }) 156 | return 157 | } 158 | 159 | if err := tr.s.UpdateTicket(newTicket, id); err != nil { 160 | ctx.JSON(http.StatusInternalServerError, &entity.CustomResponse{ 161 | Status: -2, 162 | Message: err.Error(), 163 | }) 164 | return 165 | } 166 | 167 | ctx.JSON(http.StatusOK, &entity.CustomResponse{ 168 | Status: 0, 169 | Message: "OK", 170 | }) 171 | } 172 | 173 | // deleteTicket godoc 174 | // @Summary Удалить билет по id 175 | // @Description Удалить билет по id 176 | // @Tags ticket 177 | // @Accept json 178 | // @Produce json 179 | // @Security BearerAuth 180 | // @Param id path string true "id ticket" 181 | // @Success 200 {object} entity.CustomResponse 182 | // @Failure 400 {object} entity.CustomResponse 183 | // @Router /v1/ticket/{id} [delete] 184 | func (tr *ticketRoutes) deleteTicket(ctx *gin.Context) { 185 | id := ctx.Param("id") 186 | 187 | if err := tr.s.DeleteTicket(id); err != nil { 188 | ctx.JSON(http.StatusNoContent, &entity.CustomResponse{ 189 | Status: -1, 190 | Message: err.Error(), 191 | }) 192 | return 193 | } 194 | 195 | ctx.JSON(http.StatusOK, &entity.CustomResponse{ 196 | Status: 0, 197 | Message: "OK", 198 | }) 199 | } 200 | 201 | // searchTicket godoc 202 | // @Summary Поиск билета по имени 203 | // @Description Поиск билета по имени 204 | // @Tags ticket 205 | // @Accept json 206 | // @Produce json 207 | // @Security BearerAuth 208 | // @Param searchQuery query string true "query" 209 | // @Success 200 {object} entity.CustomResponseWithData 210 | // @Failure 400 {object} entity.CustomResponse 211 | // @Router /v1/ticket/search [get] 212 | func (tr *ticketRoutes) searchTicket(ctx *gin.Context) { 213 | query := ctx.Query("searchQuery") 214 | tickets, err := tr.s.SearchTicket(query) 215 | if err != nil { 216 | ctx.JSON(http.StatusNotFound, &entity.CustomResponseWithData{ 217 | Status: -1, 218 | Message: "Not found", 219 | Data: tickets, 220 | }) 221 | return 222 | } 223 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 224 | Status: 0, 225 | Message: "OK", 226 | Data: tickets, 227 | }) 228 | } 229 | -------------------------------------------------------------------------------- /internal/crm_core/controller/http/v1/company.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | _ "crm_system/internal/auth/entity" 5 | "crm_system/internal/crm_core/controller/http/middleware" 6 | "crm_system/internal/crm_core/entity" 7 | "crm_system/internal/crm_core/service" 8 | "github.com/gin-gonic/gin" 9 | "net/http" 10 | ) 11 | 12 | type companyRoutes struct { 13 | s *service.Service 14 | } 15 | 16 | func newCompanyRoutes(handler *gin.RouterGroup, s *service.Service, MW *middleware.Middleware) { 17 | r := &companyRoutes{s: s} 18 | 19 | companyHandler := handler.Group("/company") 20 | { 21 | //middleware for users 22 | companyHandler.Use(MW.DeserializeUser("admin", "manager")) 23 | companyHandler.Use(MW.MetricsHandler()) 24 | 25 | companyHandler.GET("/", r.getCompanies) 26 | companyHandler.GET("/:id", r.getCompany) 27 | companyHandler.POST("/", r.createCompany) 28 | companyHandler.PUT("/:id", r.updateCompany) 29 | companyHandler.DELETE("/:id", r.deleteCompany) 30 | companyHandler.GET("/search", r.searchCompany) 31 | } 32 | } 33 | 34 | // getCompanies godoc 35 | // @Summary Получить список компаний 36 | // @Description Получить список компаний 37 | // @Tags company 38 | // @Accept json 39 | // @Produce json 40 | // @Security BearerAuth 41 | // @Param sortBy query string false "sortBy" 42 | // @Param sortOrder query string false "sortOrder" 43 | // @Param phone query string false "filter by phone" 44 | // @Success 200 {object} entity.CustomResponseWithData 45 | // @Failure 404 {object} entity.CustomResponse 46 | // @Router /v1/company/ [get] 47 | func (cr *companyRoutes) getCompanies(ctx *gin.Context) { 48 | sortBy := ctx.Query("sortBy") 49 | sortOrder := ctx.Query("sortOrder") 50 | phone := ctx.Query("phone") 51 | 52 | companies, err := cr.s.GetCompanies(sortBy, sortOrder, phone) 53 | 54 | if err != nil { 55 | ctx.JSON(http.StatusNotFound, &entity.CustomResponse{ 56 | Status: -1, 57 | Message: err.Error(), 58 | }) 59 | return 60 | } 61 | 62 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 63 | Status: 0, 64 | Message: "OK", 65 | Data: companies, 66 | }) 67 | } 68 | 69 | // getCompany godoc 70 | // @Summary Получить компанию по id 71 | // @Description Получить компанию по id 72 | // @Tags company 73 | // @Accept json 74 | // @Produce json 75 | // @Security BearerAuth 76 | // @Param id path string true "id company" 77 | // @Success 200 {object} entity.CustomResponseWithData 78 | // @Failure 400 {object} entity.CustomResponse 79 | // @Router /v1/company/{id} [get] 80 | func (cr *companyRoutes) getCompany(ctx *gin.Context) { 81 | id := ctx.Param("id") 82 | 83 | company, err := cr.s.GetCompany(id) 84 | 85 | if err != nil { 86 | ctx.JSON(http.StatusNotFound, &entity.CustomResponse{ 87 | Status: -1, 88 | Message: err.Error(), 89 | }) 90 | return 91 | } 92 | 93 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 94 | Status: 0, 95 | Message: "OK", 96 | Data: company, 97 | }) 98 | } 99 | 100 | // createCompany godoc 101 | // @Summary Создать Компанию 102 | // @Description Создать Компанию 103 | // @Tags company 104 | // @Accept json 105 | // @Produce json 106 | // @Security BearerAuth 107 | // @Param companyInput body entity.Company true "Create Company" 108 | // @Success 201 {object} entity.CustomResponse 109 | // @Failure 400 {object} entity.CustomResponse 110 | // @Router /v1/company/ [post] 111 | func (cr *companyRoutes) createCompany(ctx *gin.Context) { 112 | var company entity.Company 113 | 114 | if err := ctx.ShouldBindJSON(&company); err != nil { 115 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 116 | Status: -1, 117 | Message: err.Error(), 118 | }) 119 | return 120 | } 121 | 122 | if err := cr.s.CreateCompany(company); err != nil { 123 | ctx.JSON(http.StatusInternalServerError, &entity.CustomResponse{ 124 | Status: -2, 125 | Message: err.Error(), 126 | }) 127 | return 128 | } 129 | 130 | ctx.JSON(http.StatusCreated, &entity.CustomResponse{ 131 | Status: 0, 132 | Message: "OK", 133 | }) 134 | } 135 | 136 | // updateCompany godoc 137 | // @Summary Редактировать компанию по id 138 | // @Description Редактировать компанию по id 139 | // @Tags company 140 | // @Accept json 141 | // @Produce json 142 | // @Security BearerAuth 143 | // @Param id path string true "id company" 144 | // @Param inputCompany body entity.NewCompany true "Update Company" 145 | // @Success 200 {object} entity.CustomResponse 146 | // @Failure 400 {object} entity.CustomResponse 147 | // @Router /v1/company/{id} [put] 148 | func (cr *companyRoutes) updateCompany(ctx *gin.Context) { 149 | id := ctx.Param("id") 150 | 151 | var newCompany entity.NewCompany 152 | 153 | if err := ctx.ShouldBindJSON(&newCompany); err != nil { 154 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 155 | Status: -1, 156 | Message: err.Error(), 157 | }) 158 | return 159 | } 160 | 161 | if err := cr.s.UpdateCompany(newCompany, id); err != nil { 162 | ctx.JSON(http.StatusInternalServerError, &entity.CustomResponse{ 163 | Status: -2, 164 | Message: err.Error(), 165 | }) 166 | return 167 | } 168 | 169 | ctx.JSON(http.StatusOK, &entity.CustomResponse{ 170 | Status: 0, 171 | Message: "OK", 172 | }) 173 | } 174 | 175 | // deleteCompany godoc 176 | // @Summary Удалить компанию по id 177 | // @Description Удалить компанию по id 178 | // @Tags company 179 | // @Accept json 180 | // @Produce json 181 | // @Security BearerAuth 182 | // @Param id path string true "id company" 183 | // @Success 200 {object} entity.CustomResponse 184 | // @Failure 400 {object} entity.CustomResponse 185 | // @Router /v1/company/{id} [delete] 186 | func (cr *companyRoutes) deleteCompany(ctx *gin.Context) { 187 | id := ctx.Param("id") 188 | 189 | if err := cr.s.DeleteCompany(id); err != nil { 190 | ctx.JSON(http.StatusNoContent, &entity.CustomResponse{ 191 | Status: -1, 192 | Message: err.Error(), 193 | }) 194 | return 195 | } 196 | 197 | ctx.JSON(http.StatusOK, &entity.CustomResponse{ 198 | Status: 0, 199 | Message: "OK", 200 | }) 201 | } 202 | 203 | // searchCompany godoc 204 | // @Summary Поиск компании по имени 205 | // @Description Поиск компании по имени 206 | // @Tags company 207 | // @Accept json 208 | // @Produce json 209 | // @Security BearerAuth 210 | // @Param searchQuery query string true "query" 211 | // @Success 200 {object} entity.CustomResponseWithData 212 | // @Failure 400 {object} entity.CustomResponse 213 | // @Router /v1/company/search [get] 214 | func (cr *companyRoutes) searchCompany(ctx *gin.Context) { 215 | query := ctx.Query("searchQuery") 216 | companies, err := cr.s.SearchCompany(query) 217 | if err != nil { 218 | ctx.JSON(http.StatusNotFound, &entity.CustomResponseWithData{ 219 | Status: -1, 220 | Message: "Not found", 221 | Data: companies, 222 | }) 223 | return 224 | } 225 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 226 | Status: 0, 227 | Message: "OK", 228 | Data: companies, 229 | }) 230 | } 231 | -------------------------------------------------------------------------------- /internal/crm_core/controller/http/v1/contact.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "crm_system/internal/crm_core/controller/http/middleware" 5 | "crm_system/internal/crm_core/entity" 6 | "crm_system/internal/crm_core/service" 7 | "crm_system/pkg/crm_core/cache" 8 | "crm_system/pkg/crm_core/logger" 9 | "github.com/gin-gonic/gin" 10 | "log" 11 | "net/http" 12 | ) 13 | 14 | type contactRoutes struct { 15 | s *service.Service 16 | l *logger.Logger 17 | contactCache cache.Contact 18 | } 19 | 20 | func newContactRoutes(handler *gin.RouterGroup, s *service.Service, MW *middleware.Middleware, cc cache.Contact) { 21 | r := &contactRoutes{s: s, contactCache: cc} 22 | 23 | contactHandler := handler.Group("/contact") 24 | { 25 | //middleware for users 26 | contactHandler.Use(MW.MetricsHandler()) 27 | contactHandler.Use(MW.DeserializeUser("manager", "admin")) 28 | 29 | contactHandler.GET("/", r.getContacts) 30 | contactHandler.GET("/:id", r.getContact) 31 | contactHandler.POST("/", r.createContact) 32 | contactHandler.PUT("/:id", r.updateContact) 33 | contactHandler.DELETE("/:id", r.deleteContact) 34 | contactHandler.GET("/search", r.searchContact) 35 | } 36 | } 37 | 38 | // getContacts godoc 39 | // @Summary Получить список контактов 40 | // @Description Получить список контактов 41 | // @Tags contact 42 | // @Accept json 43 | // @Produce json 44 | // @Security BearerAuth 45 | // @Param sortBy query string false "sortBy" 46 | // @Param sortOrder query string false "sortOrder" 47 | // @Param phone query string false "filter by phone" 48 | // @Success 200 {object} entity.CustomResponseWithData 49 | // @Failure 404 {object} entity.CustomResponse 50 | // @Router /v1/contact/ [get] 51 | func (cr *contactRoutes) getContacts(ctx *gin.Context) { 52 | sortBy := ctx.Query("sortBy") 53 | sortOrder := ctx.Query("sortOrder") 54 | phone := ctx.Query("phone") 55 | 56 | contacts, err := cr.s.GetContacts(sortBy, sortOrder, phone) 57 | 58 | if err != nil { 59 | ctx.JSON(http.StatusNotFound, &entity.CustomResponse{ 60 | Status: -1, 61 | Message: err.Error(), 62 | }) 63 | return 64 | } 65 | 66 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 67 | Status: 0, 68 | Message: "OK", 69 | Data: contacts, 70 | }) 71 | } 72 | 73 | // getContact godoc 74 | // @Summary Получить контакт по id 75 | // @Description Получить контакт по id 76 | // @Tags contact 77 | // @Accept json 78 | // @Produce json 79 | // @Security BearerAuth 80 | // @Param id path string true "id contact" 81 | // @Success 200 {object} entity.CustomResponseWithData 82 | // @Failure 400 {object} entity.CustomResponse 83 | // @Router /v1/contact/{id} [get] 84 | func (cr *contactRoutes) getContact(ctx *gin.Context) { 85 | id := ctx.Param("id") 86 | 87 | contact, err := cr.contactCache.Get(ctx, id) 88 | if err != nil { 89 | return 90 | } 91 | 92 | if contact == nil { 93 | contact, err = cr.s.GetContact(id) 94 | if err != nil { 95 | ctx.JSON(http.StatusNotFound, &entity.CustomResponse{ 96 | Status: -2, 97 | Message: err.Error(), 98 | }) 99 | return 100 | } 101 | 102 | err = cr.contactCache.Set(ctx, id, contact) 103 | if err != nil { 104 | log.Printf("could not cache contact with id %s: %v", id, err) 105 | } 106 | } 107 | 108 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 109 | Status: -1, 110 | Message: "OK", 111 | Data: contact, 112 | }) 113 | } 114 | 115 | // createContact godoc 116 | // @Summary Создать Контакт 117 | // @Description Создать Контакт 118 | // @Tags contact 119 | // @Accept json 120 | // @Produce json 121 | // @Security BearerAuth 122 | // @Param contactInput body entity.Contact true "Create Contact" 123 | // @Success 201 {object} entity.CustomResponse 124 | // @Failure 400 {object} entity.CustomResponse 125 | // @Router /v1/contact/ [post] 126 | func (cr *contactRoutes) createContact(ctx *gin.Context) { 127 | var contact entity.Contact 128 | 129 | if err := ctx.ShouldBindJSON(&contact); err != nil { 130 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 131 | Status: -1, 132 | Message: err.Error(), 133 | }) 134 | return 135 | } 136 | 137 | if err := cr.s.CreateContact(contact); err != nil { 138 | ctx.JSON(http.StatusInternalServerError, &entity.CustomResponse{ 139 | Status: -2, 140 | Message: err.Error(), 141 | }) 142 | return 143 | } 144 | 145 | ctx.JSON(http.StatusCreated, &entity.CustomResponse{ 146 | Status: 0, 147 | Message: "OK", 148 | }) 149 | } 150 | 151 | // updateContact godoc 152 | // @Summary Редактировать контакт по id 153 | // @Description Редактировать контакт по id 154 | // @Tags contact 155 | // @Accept json 156 | // @Produce json 157 | // @Security BearerAuth 158 | // @Param id path string true "id contact" 159 | // @Param inputContact body entity.Contact true "Update Contact" 160 | // @Success 200 {object} entity.CustomResponse 161 | // @Failure 400 {object} entity.CustomResponse 162 | // @Router /v1/contact/{id} [put] 163 | func (cr *contactRoutes) updateContact(ctx *gin.Context) { 164 | id := ctx.Param("id") 165 | 166 | var newContact entity.Contact 167 | 168 | if err := ctx.ShouldBindJSON(&newContact); err != nil { 169 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 170 | Status: -1, 171 | Message: err.Error(), 172 | }) 173 | return 174 | } 175 | 176 | if err := cr.s.UpdateContact(newContact, id); err != nil { 177 | ctx.JSON(http.StatusInternalServerError, &entity.CustomResponse{ 178 | Status: -2, 179 | Message: err.Error(), 180 | }) 181 | return 182 | } 183 | 184 | err := cr.contactCache.Set(ctx, id, &newContact) 185 | if err != nil { 186 | cr.l.Debug("could not cache contact with id %s: %v", id, err) 187 | } 188 | 189 | ctx.JSON(http.StatusOK, &entity.CustomResponse{ 190 | Status: 0, 191 | Message: "OK", 192 | }) 193 | } 194 | 195 | // deleteContact godoc 196 | // @Summary Удалить контакт по id 197 | // @Description Удалить контакт по id 198 | // @Tags contact 199 | // @Accept json 200 | // @Produce json 201 | // @Security BearerAuth 202 | // @Param id path string true "id contact" 203 | // @Success 200 {object} entity.CustomResponse 204 | // @Failure 204 {object} entity.CustomResponse 205 | // @Router /v1/contact/{id} [delete] 206 | func (cr *contactRoutes) deleteContact(ctx *gin.Context) { 207 | id := ctx.Param("id") 208 | 209 | if err := cr.s.DeleteContact(id); err != nil { 210 | ctx.JSON(http.StatusNoContent, &entity.CustomResponse{ 211 | Status: -1, 212 | Message: err.Error(), 213 | }) 214 | return 215 | } 216 | 217 | ctx.JSON(http.StatusOK, &entity.CustomResponse{ 218 | Status: 0, 219 | Message: "OK", 220 | }) 221 | } 222 | 223 | // searchContact godoc 224 | // @Summary Поиск контакта по имени 225 | // @Description Поиск контакта по имени 226 | // @Tags contact 227 | // @Accept json 228 | // @Produce json 229 | // @Security BearerAuth 230 | // @Param searchQuery query string true "query" 231 | // @Success 200 {object} entity.CustomResponseWithData 232 | // @Failure 400 {object} entity.CustomResponse 233 | // @Router /v1/contact/search [get] 234 | func (cr *contactRoutes) searchContact(ctx *gin.Context) { 235 | query := ctx.Query("searchQuery") 236 | contacts, err := cr.s.SearchContact(query) 237 | if err != nil { 238 | ctx.JSON(http.StatusNotFound, &entity.CustomResponseWithData{ 239 | Status: -1, 240 | Message: "Not found", 241 | Data: contacts, 242 | }) 243 | return 244 | } 245 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 246 | Status: 0, 247 | Message: "OK", 248 | Data: contacts, 249 | }) 250 | } 251 | -------------------------------------------------------------------------------- /internal/auth/service/user.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crm_system/internal/auth/controller/consumer/dto" 5 | entity2 "crm_system/internal/auth/entity" 6 | "crm_system/pkg/auth/utils" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "github.com/gin-gonic/gin" 11 | "math/rand" 12 | "strings" 13 | ) 14 | 15 | func getUser(payload *entity2.SignUpInput, hashedPassword, provider string, roleId uint) entity2.User { 16 | userBuilder := entity2.NewUser() 17 | user := userBuilder. 18 | SetFirstName(payload.FirstName). 19 | SetLastName(payload.LastName). 20 | SetEmail(strings.ToLower(payload.Email)). 21 | SetPassword(hashedPassword). 22 | SetRoleID(roleId). 23 | SetProvider(provider). 24 | Build() 25 | return user 26 | } 27 | 28 | func (s *Service) SignUp(payload *entity2.SignUpInput, roleId uint, provider string) (string, error) { 29 | if !utils.IsValidEmail(payload.Email) { 30 | return "", errors.New("email validation error") 31 | } 32 | 33 | if !utils.IsValidPassword(payload.Password) { 34 | return "", errors.New("passwords should contain:\nUppercase letters: A-Z\nLowercase letters: a-z\nNumbers: 0-9") 35 | } 36 | 37 | if payload.Password != payload.PasswordConfirm { 38 | return "", errors.New("passwords do not match") 39 | } 40 | 41 | hashedPassword, err := utils.HashPassword(payload.Password) 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | user := getUser(payload, hashedPassword, provider, roleId) 47 | 48 | if err = s.Repo.CreateUser(&user); err != nil { 49 | return "", err 50 | } 51 | 52 | userResponse, err := s.Repo.GetUserByEmail(user.Email) 53 | if err != nil { 54 | return "", err 55 | } 56 | 57 | return userResponse.ID.String(), nil 58 | } 59 | 60 | func (s *Service) SignIn(payload *entity2.SignInInput) (*entity2.SignInResult, error) { 61 | user, err := s.Repo.GetUserByEmail(payload.Email) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | if err = utils.VerifyPassword(user.Password, payload.Password); err != nil { 67 | return nil, err 68 | } 69 | 70 | accessToken, err := utils.GenerateToken(s.Config.Jwt.AccessTokenExpiredIn, user.ID, s.Config.Jwt.AccessPrivateKey) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | refreshToken, err := utils.GenerateToken(s.Config.Jwt.RefreshTokenExpiredIn, user.ID, s.Config.Jwt.RefreshPrivateKey) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | data := entity2.SignInResult{ 81 | Role: user.Role.Name, 82 | AccessToken: accessToken, 83 | RefreshToken: refreshToken, 84 | AccessTokenAge: int(s.Config.Jwt.AccessTokenMaxAge * 60), 85 | RefreshTokenAge: int(s.Config.Jwt.RefreshTokenMaxAge * 60), 86 | } 87 | return &data, nil 88 | } 89 | 90 | func (s *Service) GetUsers(sortBy, sortOrder, age string) (*[]entity2.User, error) { 91 | users, err := s.Storage.GetAllUsers() 92 | if err != nil { 93 | return nil, err 94 | } 95 | if age != "" { 96 | users, err = s.filterUsersByAge(users, age) 97 | if err != nil { 98 | return nil, err 99 | } 100 | } 101 | 102 | if sortBy != "" { 103 | users, err = s.sortUsers(users, sortBy, sortOrder) 104 | if err != nil { 105 | return nil, err 106 | } 107 | } 108 | 109 | return users, nil 110 | } 111 | 112 | func (s *Service) sortUsers(users *[]entity2.User, sortBy, sortOrder string) (*[]entity2.User, error) { 113 | users, err := s.Repo.SortUsers(users, sortBy, sortOrder) 114 | 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | return users, nil 120 | } 121 | func (s *Service) filterUsersByAge(users *[]entity2.User, age string) (*[]entity2.User, error) { 122 | users, err := s.Repo.FilterUsersByAge(users, age) 123 | 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | return users, nil 129 | } 130 | 131 | func (s *Service) GetUser(id string) (*entity2.User, error) { 132 | user, err := s.Repo.GetUser(id) 133 | 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | return user, nil 139 | } 140 | func (s *Service) CreateUser(user *entity2.User) error { 141 | if err := s.Repo.CreateUser(user); err != nil { 142 | return err 143 | } 144 | 145 | return nil 146 | } 147 | func (s *Service) UpdateUser(newUser *entity2.User, id string) error { 148 | user, err := s.Repo.GetUser(id) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | if newUser.FirstName != "" { 154 | user.FirstName = newUser.FirstName 155 | } 156 | 157 | if newUser.LastName != "" { 158 | user.LastName = newUser.LastName 159 | } 160 | 161 | if newUser.Age != 0 { 162 | user.Age = newUser.Age 163 | } 164 | 165 | if newUser.Phone != "" { 166 | user.Phone = newUser.Phone 167 | } 168 | 169 | if newUser.RoleID != 0 { 170 | user.RoleID = newUser.RoleID 171 | } 172 | 173 | if newUser.Email != "" { 174 | user.Email = newUser.Email 175 | } 176 | 177 | if newUser.Provider != "" { 178 | user.Provider = newUser.Provider 179 | } 180 | 181 | if newUser.Password != "" { 182 | user.Password = newUser.Password 183 | } 184 | 185 | if err = s.Repo.SaveUser(user); err != nil { 186 | return err 187 | } 188 | 189 | return nil 190 | } 191 | 192 | func (s *Service) DeleteUser(id string) error { 193 | user, err := s.Repo.GetUser(id) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | if err = s.Repo.DeleteUser(id, user); err != nil { 199 | return err 200 | } 201 | 202 | return nil 203 | } 204 | 205 | func (s *Service) GetMe(ctx *gin.Context) (interface{}, error) { 206 | user, exist := ctx.MustGet("currentUser").(*entity2.User) 207 | if !exist { 208 | return nil, fmt.Errorf("the current user did not authorize or does not exist") 209 | } 210 | 211 | return user, nil 212 | } 213 | 214 | func (s *Service) UpdateMe(ctx *gin.Context, newUser *entity2.User) error { 215 | user := ctx.MustGet("currentUser").(*entity2.User) 216 | 217 | if newUser.FirstName != "" { 218 | user.FirstName = newUser.FirstName 219 | } 220 | 221 | if newUser.LastName != "" { 222 | user.LastName = newUser.LastName 223 | } 224 | 225 | if newUser.Age != 0 { 226 | user.Age = newUser.Age 227 | } 228 | 229 | if newUser.Phone != "" { 230 | user.Phone = newUser.Phone 231 | } 232 | 233 | if newUser.RoleID != 0 { 234 | user.RoleID = newUser.RoleID 235 | } 236 | 237 | if newUser.Email != "" { 238 | user.Email = newUser.Email 239 | } 240 | 241 | if newUser.Provider != "" { 242 | user.Provider = newUser.Provider 243 | } 244 | 245 | if newUser.Password != "" { 246 | user.Password = newUser.Password 247 | } 248 | 249 | if err := s.Repo.SaveUser(user); err != nil { 250 | return err 251 | } 252 | 253 | return nil 254 | } 255 | 256 | func (s *Service) SearchUser(query string) (*[]entity2.User, error) { 257 | users, err := s.Repo.SearchUser(query) 258 | if err != nil { 259 | return users, err 260 | } 261 | 262 | return users, nil 263 | } 264 | 265 | func (s *Service) CreateUserCode(id string) error { 266 | randNum1 := rand.Intn(999-100) + 100 267 | randNum2 := rand.Intn(999-100) + 100 268 | 269 | userCode := dto.UserCode{Code: fmt.Sprintf("%d%d", randNum1, randNum2)} 270 | b, err := json.Marshal(&userCode) 271 | if err != nil { 272 | return fmt.Errorf("failed to marshall UserCode err: %w", err) 273 | } 274 | 275 | s.userVerificationProducer.ProduceMessage(b) 276 | err = s.Repo.CreateUserCode(id, userCode.Code) 277 | if err != nil { 278 | return err 279 | } 280 | 281 | return nil 282 | 283 | } 284 | 285 | func (s *Service) ConfirmUser(code string) error { 286 | err := s.Repo.ConfirmUser(code) 287 | if err != nil { 288 | return err 289 | } 290 | 291 | return nil 292 | 293 | } 294 | -------------------------------------------------------------------------------- /pkg/auth/authservice/gw/auth.pb.gw.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. 2 | // source: auth.proto 3 | 4 | /* 5 | Package pb is a reverse proxy. 6 | 7 | It translates gRPC into RESTful JSON APIs. 8 | */ 9 | package pb 10 | 11 | import ( 12 | "context" 13 | "io" 14 | "net/http" 15 | 16 | "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" 17 | "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" 18 | "google.golang.org/grpc" 19 | "google.golang.org/grpc/codes" 20 | "google.golang.org/grpc/grpclog" 21 | "google.golang.org/grpc/metadata" 22 | "google.golang.org/grpc/status" 23 | "google.golang.org/protobuf/proto" 24 | ) 25 | 26 | // Suppress "imported and not used" errors 27 | var _ codes.Code 28 | var _ io.Reader 29 | var _ status.Status 30 | var _ = runtime.String 31 | var _ = utilities.NewDoubleArray 32 | var _ = metadata.Join 33 | 34 | func request_AuthService_Validate_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { 35 | var protoReq ValidateRequest 36 | var metadata runtime.ServerMetadata 37 | 38 | newReader, berr := utilities.IOReaderFactory(req.Body) 39 | if berr != nil { 40 | return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) 41 | } 42 | if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { 43 | return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) 44 | } 45 | 46 | msg, err := client.Validate(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) 47 | return msg, metadata, err 48 | 49 | } 50 | 51 | func local_request_AuthService_Validate_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { 52 | var protoReq ValidateRequest 53 | var metadata runtime.ServerMetadata 54 | 55 | newReader, berr := utilities.IOReaderFactory(req.Body) 56 | if berr != nil { 57 | return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) 58 | } 59 | if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { 60 | return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) 61 | } 62 | 63 | msg, err := server.Validate(ctx, &protoReq) 64 | return msg, metadata, err 65 | 66 | } 67 | 68 | // RegisterAuthServiceHandlerServer registers the http handlers for service AuthService to "mux". 69 | // UnaryRPC :call AuthServiceServer directly. 70 | // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. 71 | // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAuthServiceHandlerFromEndpoint instead. 72 | func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AuthServiceServer) error { 73 | 74 | mux.Handle("POST", pattern_AuthService_Validate_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { 75 | ctx, cancel := context.WithCancel(req.Context()) 76 | defer cancel() 77 | var stream runtime.ServerTransportStream 78 | ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) 79 | inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) 80 | var err error 81 | var annotatedContext context.Context 82 | annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/authservice.AuthService/Validate", runtime.WithHTTPPathPattern("/authservice.AuthService/Validate")) 83 | if err != nil { 84 | runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) 85 | return 86 | } 87 | resp, md, err := local_request_AuthService_Validate_0(annotatedContext, inboundMarshaler, server, req, pathParams) 88 | md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) 89 | annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) 90 | if err != nil { 91 | runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) 92 | return 93 | } 94 | 95 | forward_AuthService_Validate_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) 96 | 97 | }) 98 | 99 | return nil 100 | } 101 | 102 | // RegisterAuthServiceHandlerFromEndpoint is same as RegisterAuthServiceHandler but 103 | // automatically dials to "endpoint" and closes the connection when "ctx" gets done. 104 | func RegisterAuthServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { 105 | conn, err := grpc.DialContext(ctx, endpoint, opts...) 106 | if err != nil { 107 | return err 108 | } 109 | defer func() { 110 | if err != nil { 111 | if cerr := conn.Close(); cerr != nil { 112 | grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) 113 | } 114 | return 115 | } 116 | go func() { 117 | <-ctx.Done() 118 | if cerr := conn.Close(); cerr != nil { 119 | grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) 120 | } 121 | }() 122 | }() 123 | 124 | return RegisterAuthServiceHandler(ctx, mux, conn) 125 | } 126 | 127 | // RegisterAuthServiceHandler registers the http handlers for service AuthService to "mux". 128 | // The handlers forward requests to the grpc endpoint over "conn". 129 | func RegisterAuthServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { 130 | return RegisterAuthServiceHandlerClient(ctx, mux, NewAuthServiceClient(conn)) 131 | } 132 | 133 | // RegisterAuthServiceHandlerClient registers the http handlers for service AuthService 134 | // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AuthServiceClient". 135 | // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AuthServiceClient" 136 | // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in 137 | // "AuthServiceClient" to call the correct interceptors. 138 | func RegisterAuthServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AuthServiceClient) error { 139 | 140 | mux.Handle("POST", pattern_AuthService_Validate_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { 141 | ctx, cancel := context.WithCancel(req.Context()) 142 | defer cancel() 143 | inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) 144 | var err error 145 | var annotatedContext context.Context 146 | annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/authservice.AuthService/Validate", runtime.WithHTTPPathPattern("/authservice.AuthService/Validate")) 147 | if err != nil { 148 | runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) 149 | return 150 | } 151 | resp, md, err := request_AuthService_Validate_0(annotatedContext, inboundMarshaler, client, req, pathParams) 152 | annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) 153 | if err != nil { 154 | runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) 155 | return 156 | } 157 | 158 | forward_AuthService_Validate_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) 159 | 160 | }) 161 | 162 | return nil 163 | } 164 | 165 | var ( 166 | pattern_AuthService_Validate_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"authservice.AuthService", "Validate"}, "")) 167 | ) 168 | 169 | var ( 170 | forward_AuthService_Validate_0 = runtime.ForwardResponseMessage 171 | ) 172 | -------------------------------------------------------------------------------- /internal/crm_core/service/task.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crm_system/internal/crm_core/entity" 5 | "github.com/google/uuid" 6 | "sort" 7 | "strconv" 8 | ) 9 | 10 | func (s *Service) GetTasks(dealId, sortBy, sortOrder, stateInput string, user *entity.User) ([]map[string]interface{}, error) { 11 | tasks, err := s.Repo.GetTasksByDealId(dealId) 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | dashboardTodos := map[string][]entity.TaskResult{ 17 | "task": nil, 18 | "planning": nil, 19 | "doing": nil, 20 | "done": nil, 21 | } 22 | 23 | stateTypes := []string{"task", "planning", "doing", "done"} 24 | identifier := map[string]string{ 25 | "task": "📄 Задача", 26 | "planning": "📝 Планируется", 27 | "doing": "⚡ В процессе", 28 | "done": "✅ Сделано", 29 | } 30 | 31 | dashboardTodos = appendTasks(tasks, user, dashboardTodos) 32 | var columns []map[string]interface{} 33 | for _, state := range stateTypes { 34 | column := map[string]interface{}{ 35 | "id": state, 36 | "name": identifier[state], 37 | "tasks": dashboardTodos[state], 38 | } 39 | columns = append(columns, column) 40 | } 41 | 42 | if sortBy != "" { 43 | columns, err = s.sortTasks(columns, sortBy, sortOrder, stateInput) 44 | if err != nil { 45 | return nil, err 46 | } 47 | } 48 | 49 | if stateInput != "" && sortBy == "" && sortOrder == "" { 50 | column, err := s.filterTasksByStates(columns, stateInput) 51 | if err != nil { 52 | return nil, err 53 | } 54 | columns = nil 55 | columns = append(columns, column) 56 | } 57 | 58 | return columns, nil 59 | } 60 | 61 | type CompareFunc[T any] func(i, j T) bool 62 | 63 | func SortByField[T any](items []T, less CompareFunc[T]) { 64 | sort.Slice(items, func(i, j int) bool { 65 | return less(items[i], items[j]) 66 | }) 67 | } 68 | 69 | func TaskComparison(i, j entity.TaskResult, sortBy string) bool { 70 | switch sortBy { 71 | case "name": 72 | return i.Task.Name < j.Task.Name 73 | case "description": 74 | return i.Task.Description < j.Task.Description 75 | default: 76 | return i.Task.DueDate.Before(j.Task.DueDate) 77 | } 78 | } 79 | 80 | func (s *Service) sortTasks(columns []map[string]interface{}, sortBy, sortOrder, state string) ([]map[string]interface{}, error) { 81 | var tasks []entity.TaskResult 82 | for _, column := range columns { 83 | if column["id"] == state { 84 | tasks = column["tasks"].([]entity.TaskResult) 85 | break 86 | } 87 | } 88 | 89 | less := func(i, j entity.TaskResult) bool { 90 | return TaskComparison(i, j, sortBy) 91 | } 92 | 93 | SortByField(tasks, less) 94 | 95 | if sortOrder == "desc" { 96 | sort.SliceStable(tasks, func(i, j int) bool { 97 | return !less(tasks[i], tasks[j]) 98 | }) 99 | } 100 | 101 | columns = getColumns(columns, sortBy, tasks) 102 | 103 | return columns, nil 104 | } 105 | 106 | func getColumns(columns []map[string]interface{}, sortBy string, tasks []entity.TaskResult) []map[string]interface{} { 107 | for i, column := range columns { 108 | if column["id"] == sortBy { 109 | columns[i]["tasks"] = tasks 110 | break 111 | } 112 | } 113 | return columns 114 | } 115 | 116 | func (s *Service) filterTasksByStates(columns []map[string]interface{}, state string) (map[string]interface{}, error) { 117 | var response map[string]interface{} 118 | for _, column := range columns { 119 | if column["id"] == state { 120 | response = column 121 | break 122 | } 123 | } 124 | return response, nil 125 | } 126 | 127 | func appendTasks(tasks []entity.Task, user *entity.User, dashboardTodos map[string][]entity.TaskResult) map[string][]entity.TaskResult { 128 | for i, task := range tasks { 129 | var taskResult entity.TaskResult 130 | taskResult.Task = tasks[i] 131 | taskResult.VoteCount = len(tasks[i].Votes) 132 | 133 | if taskResult.Task.AssignedTo == user.ID { 134 | taskResult.UserVoted = true 135 | } else { 136 | for _, vote := range tasks[i].Votes { 137 | if vote.SenderID == user.ID { 138 | taskResult.UserVoted = true 139 | break 140 | } 141 | } 142 | } 143 | 144 | dashboardTodos[task.State] = append(dashboardTodos[task.State], taskResult) 145 | } 146 | 147 | for _, task := range tasks { 148 | sort.Slice(dashboardTodos[task.State], func(i, j int) bool { 149 | return dashboardTodos[task.State][i].VoteCount > dashboardTodos[task.State][j].VoteCount 150 | }) 151 | } 152 | return dashboardTodos 153 | } 154 | 155 | func (s *Service) GetTask(id string) (*entity.Task, error) { 156 | task, err := s.Repo.GetTask(id) 157 | 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | return task, nil 163 | } 164 | 165 | func (s *Service) CreateTask(task *entity.TaskInput) error { 166 | if err := s.Repo.CreateTask(task); err != nil { 167 | return err 168 | } 169 | 170 | return nil 171 | } 172 | 173 | func (s *Service) Vote(user *entity.User, voteInput *entity.VoteInput) error { 174 | if err := s.Repo.CreateVote(user, voteInput); err != nil { 175 | return err 176 | } 177 | return nil 178 | } 179 | 180 | func (s *Service) GetChangesOfTask(id string) (*[]entity.TaskChanges, error) { 181 | todoChanges, err := s.Repo.GetChangesOfTask(id) 182 | if err != nil { 183 | return nil, err 184 | } 185 | return todoChanges, nil 186 | } 187 | 188 | func (s *Service) UpdateTask(newTask entity.TaskEditInput, id string, user *entity.User) error { 189 | task, err := s.Repo.GetTask(id) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | var taskChanges entity.TaskChanges 195 | taskChanges.ManagerID = user.ID 196 | 197 | if newTask.Name != "" { 198 | taskChanges.TaskID = task.ID 199 | taskChanges.ChangedField = "name" 200 | taskChanges.OldValue = task.Name 201 | taskChanges.NewValue = newTask.Name 202 | task.Name = newTask.Name 203 | } 204 | if newTask.Description != "" { 205 | taskChanges.TaskID = task.ID 206 | taskChanges.ChangedField = "description" 207 | taskChanges.OldValue = task.Description 208 | taskChanges.NewValue = newTask.Description 209 | task.Description = newTask.Description 210 | } 211 | 212 | if !newTask.DueDate.IsZero() { 213 | taskChanges.TaskID = task.ID 214 | taskChanges.ChangedField = "due_date" 215 | taskChanges.OldValue = task.DueDate.String() 216 | taskChanges.NewValue = newTask.DueDate.String() 217 | task.DueDate = newTask.DueDate 218 | } 219 | 220 | if newTask.AssignedTo != uuid.Nil { 221 | taskChanges.TaskID = task.ID 222 | taskChanges.ChangedField = "assigned_to" 223 | taskChanges.OldValue = task.AssignedTo.String() 224 | taskChanges.NewValue = newTask.AssignedTo.String() 225 | task.AssignedTo = newTask.AssignedTo 226 | } 227 | 228 | if newTask.AssociatedDealID != 0 { 229 | taskChanges.TaskID = task.ID 230 | taskChanges.ChangedField = "associated_deal_id" 231 | taskChanges.OldValue = strconv.Itoa(int(task.AssociatedDealID)) 232 | taskChanges.NewValue = strconv.Itoa(int(newTask.AssociatedDealID)) 233 | task.AssociatedDealID = newTask.AssociatedDealID 234 | } 235 | 236 | if newTask.State != "" { 237 | taskChanges.TaskID = task.ID 238 | taskChanges.ChangedField = "state" 239 | taskChanges.OldValue = task.State 240 | taskChanges.NewValue = newTask.State 241 | task.State = newTask.State 242 | } 243 | 244 | if err = s.Repo.CreateTaskChanges(&taskChanges); err != nil { 245 | return err 246 | } 247 | 248 | if err = s.Repo.SaveTask(task); err != nil { 249 | return err 250 | } 251 | 252 | return nil 253 | } 254 | 255 | func (s *Service) DeleteTask(id string) error { 256 | company, err := s.Repo.GetTask(id) 257 | if err != nil { 258 | return err 259 | } 260 | 261 | if err = s.Repo.DeleteTask(id, company); err != nil { 262 | return err 263 | } 264 | 265 | return nil 266 | } 267 | func (s *Service) SearchTask(query string) (*[]entity.Task, error) { 268 | tasks, err := s.Repo.SearchTask(query) 269 | if err != nil { 270 | return tasks, err 271 | } 272 | 273 | return tasks, nil 274 | } 275 | -------------------------------------------------------------------------------- /internal/crm_core/controller/http/v1/task.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "crm_system/internal/crm_core/controller/http/middleware" 5 | "crm_system/internal/crm_core/entity" 6 | "crm_system/internal/crm_core/service" 7 | "fmt" 8 | "github.com/gin-gonic/gin" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | type taskRoutes struct { 14 | s *service.Service 15 | } 16 | 17 | func newTaskRoutes(handler *gin.RouterGroup, s *service.Service, MW *middleware.Middleware) { 18 | r := &taskRoutes{s: s} 19 | 20 | taskHandler := handler.Group("/task") 21 | { 22 | //middleware for users 23 | taskHandler.Use(MW.MetricsHandler()) 24 | taskHandler.GET("/deal/:dealId", MW.DeserializeUser("any"), r.getTasks) 25 | taskHandler.GET("/:id", MW.DeserializeUser("any"), r.getTask) 26 | taskHandler.POST("/", MW.DeserializeUser("manager", "admin"), r.createTask) 27 | taskHandler.POST("/vote", MW.DeserializeUser("any"), r.vote) 28 | taskHandler.PUT("/:id", MW.DeserializeUser("manager", "admin"), r.updateTask) 29 | taskHandler.DELETE("/:id", MW.DeserializeUser("manager", "admin"), r.deleteTask) 30 | taskHandler.GET("/changes/:id", MW.DeserializeUser("manager", "admin"), r.getChangesOfTask) 31 | taskHandler.GET("/search", r.searchTask) 32 | } 33 | } 34 | 35 | // getTasks godoc 36 | // @Summary Получить список заданий 37 | // @Description Получить список заданий 38 | // @Tags task 39 | // @Accept json 40 | // @Produce json 41 | // @Security BearerAuth 42 | // @Param sortBy query string false "sortBy" 43 | // @Param sortOrder query string false "sortOrder" 44 | // @Param state query string false "filter by state" 45 | // @Param dealId path string true "dealId" 46 | // @Success 200 {object} entity.CustomResponseWithData 47 | // @Failure 404 {object} entity.CustomResponse 48 | // @Router /v1/task/deal/{dealId} [get] 49 | func (tr *taskRoutes) getTasks(ctx *gin.Context) { 50 | dealId := ctx.Param("dealId") 51 | sortBy := ctx.Query("sortBy") 52 | sortOrder := ctx.Query("sortOrder") 53 | state := ctx.Query("state") 54 | 55 | user, exists := ctx.MustGet("currentUser").(*entity.User) 56 | if !exists { 57 | ctx.JSON(http.StatusNotFound, &entity.CustomResponse{ 58 | Status: -1, 59 | Message: fmt.Errorf("the current user did not authorize or does not exist").Error(), 60 | }) 61 | return 62 | } 63 | 64 | tasks, err := tr.s.GetTasks(dealId, sortBy, sortOrder, state, user) 65 | 66 | if err != nil { 67 | ctx.JSON(http.StatusNotFound, &entity.CustomResponse{ 68 | Status: -2, 69 | Message: err.Error(), 70 | }) 71 | return 72 | } 73 | 74 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 75 | Status: 0, 76 | Message: "OK", 77 | Data: tasks, 78 | }) 79 | } 80 | 81 | // getTask godoc 82 | // @Summary Получить задание по id 83 | // @Description Получить задание по id 84 | // @Tags task 85 | // @Accept json 86 | // @Produce json 87 | // @Security BearerAuth 88 | // @Param id path string true "id task" 89 | // @Success 200 {object} entity.CustomResponseWithData 90 | // @Failure 400 {object} entity.CustomResponse 91 | // @Router /v1/task/{id} [get] 92 | func (tr *taskRoutes) getTask(ctx *gin.Context) { 93 | id := ctx.Param("id") 94 | 95 | task, err := tr.s.GetTask(id) 96 | 97 | if err != nil { 98 | ctx.JSON(http.StatusNotFound, &entity.CustomResponse{ 99 | Status: -1, 100 | Message: err.Error(), 101 | }) 102 | return 103 | } 104 | 105 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 106 | Status: 0, 107 | Message: "OK", 108 | Data: task, 109 | }) 110 | } 111 | 112 | // createTask godoc 113 | // @Summary Создать Задание 114 | // @Description Создать Задание 115 | // @Tags task 116 | // @Accept json 117 | // @Produce json 118 | // @Security BearerAuth 119 | // @Param taskInput body entity.TaskInput true "Create task" 120 | // @Success 201 {object} entity.CustomResponse 121 | // @Failure 400 {object} entity.CustomResponse 122 | // @Router /v1/task/ [post] 123 | func (tr *taskRoutes) createTask(ctx *gin.Context) { 124 | var task entity.TaskInput 125 | 126 | if err := ctx.ShouldBindJSON(&task); err != nil { 127 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 128 | Status: -1, 129 | Message: err.Error(), 130 | }) 131 | return 132 | } 133 | 134 | if time.Now().After(task.DueDate) { 135 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 136 | Status: -1, 137 | Message: "Due date must be in the future", 138 | }) 139 | return 140 | } 141 | 142 | if err := tr.s.CreateTask(&task); err != nil { 143 | ctx.JSON(http.StatusInternalServerError, &entity.CustomResponse{ 144 | Status: -2, 145 | Message: err.Error(), 146 | }) 147 | return 148 | } 149 | 150 | ctx.JSON(http.StatusCreated, &entity.CustomResponse{ 151 | Status: 0, 152 | Message: "OK", 153 | }) 154 | } 155 | 156 | // vote godoc 157 | // @Summary Проголосовать за задание 158 | // @Description Проголосовать за задание 159 | // @Tags task 160 | // @Accept json 161 | // @Produce json 162 | // @Security BearerAuth 163 | // @Param voteInput body entity.VoteInput true "Create Vote" 164 | // @Success 201 {object} entity.CustomResponse 165 | // @Failure 400 {object} entity.CustomResponse 166 | // @Router /v1/task/vote [post] 167 | func (tr *taskRoutes) vote(ctx *gin.Context) { 168 | user := ctx.MustGet("currentUser").(*entity.User) 169 | var voteInput *entity.VoteInput 170 | if err := ctx.ShouldBindJSON(&voteInput); err != nil { 171 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 172 | Status: -1, 173 | Message: err.Error(), 174 | }) 175 | return 176 | } 177 | 178 | err := tr.s.Vote(user, voteInput) 179 | if err != nil { 180 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 181 | Status: -2, 182 | Message: err.Error(), 183 | }) 184 | return 185 | } 186 | ctx.JSON(http.StatusOK, &entity.CustomResponse{ 187 | Status: 0, 188 | Message: "OK", 189 | }) 190 | 191 | } 192 | 193 | // updateTask godoc 194 | // @Summary Редактировать задание по id 195 | // @Description Редактировать задание по id 196 | // @Tags task 197 | // @Accept json 198 | // @Produce json 199 | // @Security BearerAuth 200 | // @Param id path string true "id task" 201 | // @Param inputTask body entity.TaskEditInput true "Update task" 202 | // @Success 200 {object} entity.CustomResponse 203 | // @Failure 400 {object} entity.CustomResponse 204 | // @Router /v1/task/{id} [put] 205 | func (tr *taskRoutes) updateTask(ctx *gin.Context) { 206 | id := ctx.Param("id") 207 | user := ctx.MustGet("currentUser").(*entity.User) 208 | 209 | var task entity.TaskEditInput 210 | 211 | if err := ctx.ShouldBindJSON(&task); err != nil { 212 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 213 | Status: -1, 214 | Message: err.Error(), 215 | }) 216 | return 217 | } 218 | 219 | if err := tr.s.UpdateTask(task, id, user); err != nil { 220 | ctx.JSON(http.StatusInternalServerError, &entity.CustomResponse{ 221 | Status: -2, 222 | Message: err.Error(), 223 | }) 224 | return 225 | } 226 | 227 | ctx.JSON(http.StatusOK, &entity.CustomResponse{ 228 | Status: 0, 229 | Message: "OK", 230 | }) 231 | } 232 | 233 | // deleteTask godoc 234 | // @Summary Удалить задание по id 235 | // @Description Удалить задание по id 236 | // @Tags task 237 | // @Accept json 238 | // @Produce json 239 | // @Security BearerAuth 240 | // @Param id path string true "id task" 241 | // @Success 200 {object} entity.CustomResponse 242 | // @Failure 400 {object} entity.CustomResponse 243 | // @Router /v1/task/{id} [delete] 244 | func (tr *taskRoutes) deleteTask(ctx *gin.Context) { 245 | id := ctx.Param("id") 246 | 247 | if err := tr.s.DeleteTask(id); err != nil { 248 | ctx.JSON(http.StatusNoContent, &entity.CustomResponse{ 249 | Status: -1, 250 | Message: err.Error(), 251 | }) 252 | return 253 | } 254 | 255 | ctx.JSON(http.StatusOK, &entity.CustomResponse{ 256 | Status: 0, 257 | Message: "OK", 258 | }) 259 | } 260 | 261 | // getChangesOfTask godoc 262 | // @Summary Получить историю изменений по заданию 263 | // @Description Получить историю изменений по заданию 264 | // @Tags task 265 | // @Accept json 266 | // @Produce json 267 | // @Security BearerAuth 268 | // @Param id path string true "id task" 269 | // @Success 200 {object} entity.CustomResponseWithData 270 | // @Failure 400 {object} entity.CustomResponse 271 | // @Router /v1/task/changes/{id} [get] 272 | func (tr *taskRoutes) getChangesOfTask(ctx *gin.Context) { 273 | id := ctx.Param("id") 274 | todoChanges, err := tr.s.GetChangesOfTask(id) 275 | if err != nil { 276 | ctx.JSON(http.StatusBadRequest, &entity.CustomResponse{ 277 | Status: -1, 278 | Message: err.Error(), 279 | }) 280 | return 281 | } 282 | 283 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 284 | Status: 0, 285 | Message: "OK", 286 | Data: todoChanges, 287 | }) 288 | 289 | } 290 | 291 | // searchTask godoc 292 | // @Summary Поиск задание по имени 293 | // @Description Поиск задание по имени 294 | // @Tags task 295 | // @Accept json 296 | // @Produce json 297 | // @Security BearerAuth 298 | // @Param searchQuery query string true "query" 299 | // @Success 200 {object} entity.CustomResponseWithData 300 | // @Failure 400 {object} entity.CustomResponse 301 | // @Router /v1/task/search [get] 302 | func (tr *taskRoutes) searchTask(ctx *gin.Context) { 303 | query := ctx.Query("searchQuery") 304 | tasks, err := tr.s.SearchTask(query) 305 | if err != nil { 306 | ctx.JSON(http.StatusNotFound, &entity.CustomResponseWithData{ 307 | Status: -1, 308 | Message: "Not found", 309 | Data: tasks, 310 | }) 311 | return 312 | } 313 | ctx.JSON(http.StatusOK, &entity.CustomResponseWithData{ 314 | Status: 0, 315 | Message: "OK", 316 | Data: tasks, 317 | }) 318 | } 319 | -------------------------------------------------------------------------------- /internal/auth/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | entity.CustomResponse: 3 | properties: 4 | message: 5 | type: string 6 | status: 7 | type: integer 8 | type: object 9 | entity.CustomResponseWithData: 10 | properties: 11 | data: {} 12 | message: 13 | type: string 14 | status: 15 | type: integer 16 | type: object 17 | entity.InputCode: 18 | properties: 19 | code: 20 | type: string 21 | type: object 22 | entity.Role: 23 | properties: 24 | id: 25 | type: integer 26 | name: 27 | type: string 28 | type: object 29 | entity.SignInInput: 30 | properties: 31 | email: 32 | type: string 33 | password: 34 | type: string 35 | required: 36 | - email 37 | - password 38 | type: object 39 | entity.SignUpInput: 40 | properties: 41 | email: 42 | type: string 43 | name: 44 | type: string 45 | password: 46 | minLength: 8 47 | type: string 48 | passwordConfirm: 49 | type: string 50 | surname: 51 | type: string 52 | required: 53 | - email 54 | - name 55 | - password 56 | - passwordConfirm 57 | - surname 58 | type: object 59 | entity.User: 60 | properties: 61 | ID: 62 | type: string 63 | age: 64 | type: integer 65 | createdAt: 66 | type: string 67 | deletedAt: 68 | $ref: '#/definitions/gorm.DeletedAt' 69 | email: 70 | type: string 71 | first_name: 72 | type: string 73 | id: 74 | type: integer 75 | is_confirmed: 76 | type: boolean 77 | last_name: 78 | type: string 79 | phone: 80 | type: string 81 | provider: 82 | type: string 83 | role: 84 | $ref: '#/definitions/entity.Role' 85 | role_id: 86 | type: integer 87 | updatedAt: 88 | type: string 89 | type: object 90 | gorm.DeletedAt: 91 | properties: 92 | time: 93 | type: string 94 | valid: 95 | description: Valid is true if Time is not NULL 96 | type: boolean 97 | type: object 98 | host: localhost:8081 99 | info: 100 | contact: {} 101 | description: auth_service 102 | license: 103 | name: Apache 2.0 104 | url: http://www.apache.org/licenses/LICENSE-2.0.html 105 | termsOfService: http://swagger.io/terms/ 106 | title: auth-service 107 | version: "1.0" 108 | paths: 109 | /v1/admin/user/: 110 | get: 111 | consumes: 112 | - application/json 113 | description: Получить список пользователей 114 | parameters: 115 | - description: sortBy 116 | in: query 117 | name: sortBy 118 | type: string 119 | - description: sortOrder 120 | in: query 121 | name: sortOrder 122 | type: string 123 | - description: filter by age 124 | in: query 125 | name: age 126 | type: string 127 | produces: 128 | - application/json 129 | responses: 130 | "200": 131 | description: OK 132 | schema: 133 | $ref: '#/definitions/entity.CustomResponseWithData' 134 | "400": 135 | description: Bad Request 136 | schema: 137 | $ref: '#/definitions/entity.CustomResponse' 138 | security: 139 | - BearerAuth: [] 140 | summary: Получить список пользователей 141 | tags: 142 | - admin 143 | post: 144 | consumes: 145 | - application/json 146 | description: Создать Пользователя 147 | parameters: 148 | - description: Create User 149 | in: body 150 | name: body 151 | required: true 152 | schema: 153 | $ref: '#/definitions/entity.User' 154 | produces: 155 | - application/json 156 | responses: 157 | "200": 158 | description: OK 159 | schema: 160 | $ref: '#/definitions/entity.CustomResponse' 161 | "400": 162 | description: Bad Request 163 | schema: 164 | $ref: '#/definitions/entity.CustomResponse' 165 | security: 166 | - BearerAuth: [] 167 | summary: Создать Пользователя 168 | tags: 169 | - admin 170 | /v1/admin/user/{id}: 171 | delete: 172 | consumes: 173 | - application/json 174 | description: Удалить пользователя по id 175 | parameters: 176 | - description: id user 177 | in: path 178 | name: id 179 | required: true 180 | type: string 181 | produces: 182 | - application/json 183 | responses: 184 | "200": 185 | description: OK 186 | schema: 187 | $ref: '#/definitions/entity.CustomResponse' 188 | "400": 189 | description: Bad Request 190 | schema: 191 | $ref: '#/definitions/entity.CustomResponse' 192 | security: 193 | - BearerAuth: [] 194 | summary: Удалить пользователя по id 195 | tags: 196 | - admin 197 | get: 198 | consumes: 199 | - application/json 200 | description: Получить пользователя по id 201 | parameters: 202 | - description: id user 203 | in: path 204 | name: id 205 | required: true 206 | type: string 207 | produces: 208 | - application/json 209 | responses: 210 | "200": 211 | description: OK 212 | schema: 213 | $ref: '#/definitions/entity.CustomResponseWithData' 214 | "400": 215 | description: Bad Request 216 | schema: 217 | $ref: '#/definitions/entity.CustomResponse' 218 | security: 219 | - BearerAuth: [] 220 | summary: Получить пользователя по id 221 | tags: 222 | - admin 223 | put: 224 | consumes: 225 | - application/json 226 | description: Редактировать пользователя по id 227 | parameters: 228 | - description: id user 229 | in: path 230 | name: id 231 | required: true 232 | type: string 233 | - description: Update User 234 | in: body 235 | name: inputUser 236 | required: true 237 | schema: 238 | $ref: '#/definitions/entity.User' 239 | produces: 240 | - application/json 241 | responses: 242 | "200": 243 | description: OK 244 | schema: 245 | $ref: '#/definitions/entity.CustomResponse' 246 | "400": 247 | description: Bad Request 248 | schema: 249 | $ref: '#/definitions/entity.CustomResponse' 250 | security: 251 | - BearerAuth: [] 252 | summary: Редактировать пользователя по id 253 | tags: 254 | - admin 255 | /v1/admin/user/search: 256 | get: 257 | consumes: 258 | - application/json 259 | description: Поиск пользователя по имени 260 | parameters: 261 | - description: query 262 | in: query 263 | name: searchQuery 264 | required: true 265 | type: string 266 | produces: 267 | - application/json 268 | responses: 269 | "200": 270 | description: OK 271 | schema: 272 | $ref: '#/definitions/entity.CustomResponseWithData' 273 | "400": 274 | description: Bad Request 275 | schema: 276 | $ref: '#/definitions/entity.CustomResponse' 277 | security: 278 | - BearerAuth: [] 279 | summary: Поиск пользователя по имени 280 | tags: 281 | - admin 282 | /v1/user/confirm/: 283 | post: 284 | consumes: 285 | - application/json 286 | description: Подтвердить пользователя по коду 287 | parameters: 288 | - description: Код 289 | in: body 290 | name: inputCode 291 | required: true 292 | schema: 293 | $ref: '#/definitions/entity.InputCode' 294 | produces: 295 | - application/json 296 | responses: 297 | "200": 298 | description: OK 299 | schema: 300 | $ref: '#/definitions/entity.CustomResponse' 301 | "400": 302 | description: Bad Request 303 | schema: 304 | $ref: '#/definitions/entity.CustomResponse' 305 | summary: Подтвердить пользователя по коду 306 | tags: 307 | - auth 308 | /v1/user/login: 309 | post: 310 | consumes: 311 | - application/json 312 | description: Авторизовать пользователя с помощью signIn 313 | parameters: 314 | - description: Authorize user 315 | in: body 316 | name: signIn 317 | required: true 318 | schema: 319 | $ref: '#/definitions/entity.SignInInput' 320 | produces: 321 | - application/json 322 | responses: 323 | "200": 324 | description: OK 325 | schema: 326 | $ref: '#/definitions/entity.CustomResponseWithData' 327 | "400": 328 | description: Bad Request 329 | schema: 330 | $ref: '#/definitions/entity.CustomResponse' 331 | summary: Авторизовать пользователя с помощью signUp 332 | tags: 333 | - auth 334 | /v1/user/me/: 335 | get: 336 | consumes: 337 | - application/json 338 | description: Получить информацию о себе 339 | produces: 340 | - application/json 341 | responses: 342 | "200": 343 | description: OK 344 | schema: 345 | $ref: '#/definitions/entity.CustomResponseWithData' 346 | "400": 347 | description: Bad Request 348 | schema: 349 | $ref: '#/definitions/entity.CustomResponse' 350 | security: 351 | - BearerAuth: [] 352 | summary: Получить информацию о себе 353 | tags: 354 | - cabinet 355 | put: 356 | consumes: 357 | - application/json 358 | description: Поменять информацию о себе 359 | parameters: 360 | - description: Поменять информацию о себе 361 | in: body 362 | name: body 363 | required: true 364 | schema: 365 | $ref: '#/definitions/entity.User' 366 | produces: 367 | - application/json 368 | responses: 369 | "200": 370 | description: OK 371 | schema: 372 | $ref: '#/definitions/entity.CustomResponse' 373 | "400": 374 | description: Bad Request 375 | schema: 376 | $ref: '#/definitions/entity.CustomResponse' 377 | security: 378 | - BearerAuth: [] 379 | summary: Поменять информацию о себе 380 | tags: 381 | - cabinet 382 | /v1/user/register: 383 | post: 384 | consumes: 385 | - application/json 386 | description: Зарегистрировать пользователя с помощью signUp 387 | parameters: 388 | - description: Register user 389 | in: body 390 | name: signUp 391 | required: true 392 | schema: 393 | $ref: '#/definitions/entity.SignUpInput' 394 | produces: 395 | - application/json 396 | responses: 397 | "200": 398 | description: OK 399 | schema: 400 | $ref: '#/definitions/entity.CustomResponse' 401 | "400": 402 | description: Bad Request 403 | schema: 404 | $ref: '#/definitions/entity.CustomResponse' 405 | summary: Зарегистрировать пользователя с помощью signUp 406 | tags: 407 | - auth 408 | schemes: 409 | - https 410 | - http 411 | securityDefinitions: 412 | BearerAuth: 413 | in: header 414 | name: Authorization 415 | type: apiKey 416 | swagger: "2.0" 417 | --------------------------------------------------------------------------------