├── .dockerignore ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .golangci.yml ├── Makefile ├── README.md ├── cmd └── api │ └── main.go ├── config ├── config-docker.yml ├── config-local.yml └── config.go ├── docker-compose.delve.yml ├── docker-compose.dev.yml ├── docker-compose.local.yml ├── docker ├── Dockerfile ├── Dockerfile.DelveHotReload ├── Dockerfile.HotReload └── monitoring │ ├── alerts.yml │ ├── prometheus-local.yml │ └── prometheus.yml ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── go.mod ├── go.sum ├── internal ├── auth │ ├── aws_repository.go │ ├── delivery.go │ ├── delivery │ │ └── http │ │ │ ├── handlers.go │ │ │ ├── handlers_test.go │ │ │ └── routes.go │ ├── mock │ │ ├── aws_repository_mock.go │ │ ├── pg_repository_mock.go │ │ ├── redis_repository_mock.go │ │ └── usecase_mock.go │ ├── pg_repository.go │ ├── redis_repository.go │ ├── repository │ │ ├── aws_repository.go │ │ ├── pg_repository.go │ │ ├── pg_repository_test.go │ │ ├── redis_repository.go │ │ ├── redis_repository_test.go │ │ └── sql_queries.go │ ├── usecase.go │ └── usecase │ │ ├── usecase.go │ │ └── usecase_test.go ├── comments │ ├── delivery.go │ ├── delivery │ │ └── http │ │ │ ├── handlers_test.go │ │ │ ├── hanldlers.go │ │ │ └── routes.go │ ├── mock │ │ ├── pg_repository_mock.go │ │ └── usecase_mock.go │ ├── pg_repository.go │ ├── repository │ │ ├── pg_repository.go │ │ ├── pg_repository_test.go │ │ └── sql_queries.go │ ├── usecase.go │ └── usecase │ │ ├── usecase.go │ │ └── usecase_test.go ├── middleware │ ├── auth.go │ ├── csrf.go │ ├── debug.go │ ├── metrics.go │ ├── middlewares.go │ ├── request_logger.go │ └── sanitize.go ├── models │ ├── aws.go │ ├── comment.go │ ├── news.go │ ├── session.go │ └── user.go ├── news │ ├── delivery.go │ ├── delivery │ │ └── http │ │ │ ├── handlers.go │ │ │ ├── handlers_test.go │ │ │ └── routes.go │ ├── mock │ │ ├── pg_repository_mock.go │ │ ├── redis_repository_mock.go │ │ └── usecase_mock.go │ ├── pg_repository.go │ ├── redis_repository.go │ ├── repository │ │ ├── pg_repository.go │ │ ├── pg_repository_test.go │ │ ├── redis_repository.go │ │ ├── redis_repository_test.go │ │ └── sql_queries.go │ ├── usecase.go │ └── usecase │ │ ├── usecase.go │ │ └── usecase_test.go ├── server │ ├── handlers.go │ └── server.go └── session │ ├── mock │ ├── redis_repository_mock.go │ └── usecase_mock.go │ ├── redis_repository.go │ ├── repository │ ├── redis_repository.go │ └── redis_repository_test.go │ ├── usecase.go │ └── usecase │ ├── usecase.go │ └── usecase_test.go ├── migrations ├── 01_create_initial_tables.down.sql └── 01_create_initial_tables.up.sql ├── pkg ├── converter │ └── converter.go ├── csrf │ └── csrf.go ├── db │ ├── aws │ │ └── aws.go │ ├── postgres │ │ └── db_conn.go │ └── redis │ │ └── conn.go ├── httpErrors │ └── http_errors.go ├── logger │ └── zap_logger.go ├── metric │ └── metric.go ├── sanitize │ └── sanitize.go └── utils │ ├── auth.go │ ├── http.go │ ├── images.go │ ├── jwt.go │ ├── pagination.go │ └── validator.go └── ssl ├── ca.crt ├── ca.key ├── instructions.sh ├── server.crt ├── server.csr ├── server.key └── server.pem /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .git 3 | .golangci.yml -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | golangci-main: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out code into the Go module directory 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | - name: golangci-lint 19 | uses: reviewdog/action-golangci-lint@v1 20 | # with: 21 | # golangci_lint_flags: "--config=../.golangci.yml" 22 | # workdir: . 23 | test: 24 | name: tests 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Install Go 28 | uses: actions/setup-go@v2 29 | with: 30 | go-version: '1.16.x' 31 | - name: Checkout code 32 | uses: actions/checkout@v2 33 | - name: Test 34 | run: go test ./... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | media 17 | 18 | # Config files 19 | config/apple_config_files 20 | /config/configs.env 21 | /config/config_file.go 22 | 23 | # Enviroment files 24 | envs/ 25 | main 26 | .idea 27 | pgdata 28 | vendor 29 | 30 | # Avatars storage directory 31 | avatars/ -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | skip-dirs: 4 | - vendor/ 5 | 6 | skip-files: 7 | - "_easyjson.go" 8 | - ".pb.go" 9 | - ".svc.go" 10 | 11 | modules-download-mode: readonly 12 | 13 | linters-settings: 14 | golint: 15 | min-confidence: 0.3 16 | gocyclo: 17 | min-complexity: 20 18 | dupl: 19 | threshold: 200 20 | lll: 21 | line-length: 120 22 | funlen: 23 | statements: 100 24 | lines: 160 25 | 26 | 27 | output: 28 | format: tab -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: migrate migrate_down migrate_up migrate_version docker prod docker_delve local swaggo test 2 | 3 | # ============================================================================== 4 | # Go migrate postgresql 5 | 6 | force: 7 | migrate -database postgres://postgres:postgres@localhost:5432/auth_db?sslmode=disable -path migrations force 1 8 | 9 | version: 10 | migrate -database postgres://postgres:postgres@localhost:5432/auth_db?sslmode=disable -path migrations version 11 | 12 | migrate_up: 13 | migrate -database postgres://postgres:postgres@localhost:5432/auth_db?sslmode=disable -path migrations up 1 14 | 15 | migrate_down: 16 | migrate -database postgres://postgres:postgres@localhost:5432/auth_db?sslmode=disable -path migrations down 1 17 | 18 | 19 | # ============================================================================== 20 | # Docker compose commands 21 | 22 | develop: 23 | echo "Starting docker environment" 24 | docker-compose -f docker-compose.dev.yml up --build 25 | 26 | docker_delve: 27 | echo "Starting docker debug environment" 28 | docker-compose -f docker-compose.delve.yml up --build 29 | 30 | prod: 31 | echo "Starting docker prod environment" 32 | docker-compose -f docker-compose.prod.yml up --build 33 | 34 | local: 35 | echo "Starting local environment" 36 | docker-compose -f docker-compose.local.yml up --build 37 | 38 | 39 | # ============================================================================== 40 | # Tools commands 41 | 42 | run-linter: 43 | echo "Starting linters" 44 | golangci-lint run ./... 45 | 46 | swaggo: 47 | echo "Starting swagger generating" 48 | swag init -g **/**/*.go 49 | 50 | 51 | # ============================================================================== 52 | # Main 53 | 54 | run: 55 | go run ./cmd/api/main.go 56 | 57 | build: 58 | go build ./cmd/api/main.go 59 | 60 | test: 61 | go test -cover ./... 62 | 63 | 64 | # ============================================================================== 65 | # Modules support 66 | 67 | deps-reset: 68 | git checkout -- go.mod 69 | go mod tidy 70 | go mod vendor 71 | 72 | tidy: 73 | go mod tidy 74 | go mod vendor 75 | 76 | deps-upgrade: 77 | # go get $(go list -f '{{if not (or .Main .Indirect)}}{{.Path}}{{end}}' -m all) 78 | go get -u -t -d -v ./... 79 | go mod tidy 80 | go mod vendor 81 | 82 | deps-cleancache: 83 | go clean -modcache 84 | 85 | 86 | # ============================================================================== 87 | # Docker support 88 | 89 | FILES := $(shell docker ps -aq) 90 | 91 | down-local: 92 | docker stop $(FILES) 93 | docker rm $(FILES) 94 | 95 | clean: 96 | docker system prune -f 97 | 98 | logs-local: 99 | docker logs -f $(FILES) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Golang [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) REST API example 🚀 2 | 3 | #### 👨‍💻 Full list what has been used: 4 | * [echo](https://github.com/labstack/echo) - Web framework 5 | * [sqlx](https://github.com/jmoiron/sqlx) - Extensions to database/sql. 6 | * [pgx](https://github.com/jackc/pgx) - PostgreSQL driver and toolkit for Go 7 | * [viper](https://github.com/spf13/viper) - Go configuration with fangs 8 | * [go-redis](https://github.com/go-redis/redis) - Type-safe Redis client for Golang 9 | * [zap](https://github.com/uber-go/zap) - Logger 10 | * [validator](https://github.com/go-playground/validator) - Go Struct and Field validation 11 | * [jwt-go](https://github.com/dgrijalva/jwt-go) - JSON Web Tokens (JWT) 12 | * [uuid](https://github.com/google/uuid) - UUID 13 | * [migrate](https://github.com/golang-migrate/migrate) - Database migrations. CLI and Golang library. 14 | * [minio-go](https://github.com/minio/minio-go) - AWS S3 MinIO Client SDK for Go 15 | * [bluemonday](https://github.com/microcosm-cc/bluemonday) - HTML sanitizer 16 | * [swag](https://github.com/swaggo/swag) - Swagger 17 | * [testify](https://github.com/stretchr/testify) - Testing toolkit 18 | * [gomock](https://github.com/golang/mock) - Mocking framework 19 | * [CompileDaemon](https://github.com/githubnemo/CompileDaemon) - Compile daemon for Go 20 | * [Docker](https://www.docker.com/) - Docker 21 | 22 | #### Recomendation for local development most comfortable usage: 23 | make local // run all containers 24 | make run // it's easier way to attach debugger or rebuild/rerun project 25 | 26 | #### 🙌👨‍💻🚀 Docker-compose files: 27 | docker-compose.local.yml - run postgresql, redis, aws, prometheus, grafana containrs 28 | docker-compose.dev.yml - run docker development environment 29 | docker-compose.delve.yml run development environment with delve debug 30 | 31 | ### Docker development usage: 32 | make docker 33 | 34 | ### Local development usage: 35 | make local 36 | make run 37 | 38 | ### SWAGGER UI: 39 | 40 | https://localhost:5000/swagger/index.html 41 | 42 | ### Jaeger UI: 43 | 44 | http://localhost:16686 45 | 46 | ### Prometheus UI: 47 | 48 | http://localhost:9090 49 | 50 | ### Grafana UI: 51 | 52 | http://localhost:3000 -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/opentracing/opentracing-go" 8 | jaegerlog "github.com/uber/jaeger-client-go/log" 9 | "github.com/uber/jaeger-lib/metrics" 10 | 11 | "github.com/AleksK1NG/api-mc/config" 12 | "github.com/AleksK1NG/api-mc/internal/server" 13 | "github.com/AleksK1NG/api-mc/pkg/db/aws" 14 | "github.com/AleksK1NG/api-mc/pkg/db/postgres" 15 | "github.com/AleksK1NG/api-mc/pkg/db/redis" 16 | "github.com/AleksK1NG/api-mc/pkg/logger" 17 | "github.com/AleksK1NG/api-mc/pkg/utils" 18 | 19 | "github.com/uber/jaeger-client-go" 20 | jaegercfg "github.com/uber/jaeger-client-go/config" 21 | ) 22 | 23 | // @title Go Example REST API 24 | // @version 1.0 25 | // @description Example Golang REST API 26 | // @contact.name Alexander Bryksin 27 | // @contact.url https://github.com/AleksK1NG 28 | // @contact.email alexander.bryksin@yandex.ru 29 | // @BasePath /api/v1 30 | func main() { 31 | log.Println("Starting api server") 32 | 33 | configPath := utils.GetConfigPath(os.Getenv("config")) 34 | 35 | cfgFile, err := config.LoadConfig(configPath) 36 | if err != nil { 37 | log.Fatalf("LoadConfig: %v", err) 38 | } 39 | 40 | cfg, err := config.ParseConfig(cfgFile) 41 | if err != nil { 42 | log.Fatalf("ParseConfig: %v", err) 43 | } 44 | 45 | appLogger := logger.NewApiLogger(cfg) 46 | 47 | appLogger.InitLogger() 48 | appLogger.Infof("AppVersion: %s, LogLevel: %s, Mode: %s, SSL: %v", cfg.Server.AppVersion, cfg.Logger.Level, cfg.Server.Mode, cfg.Server.SSL) 49 | 50 | psqlDB, err := postgres.NewPsqlDB(cfg) 51 | if err != nil { 52 | appLogger.Fatalf("Postgresql init: %s", err) 53 | } else { 54 | appLogger.Infof("Postgres connected, Status: %#v", psqlDB.Stats()) 55 | } 56 | defer psqlDB.Close() 57 | 58 | redisClient := redis.NewRedisClient(cfg) 59 | defer redisClient.Close() 60 | appLogger.Info("Redis connected") 61 | 62 | awsClient, err := aws.NewAWSClient(cfg.AWS.Endpoint, cfg.AWS.MinioAccessKey, cfg.AWS.MinioSecretKey, cfg.AWS.UseSSL) 63 | if err != nil { 64 | appLogger.Errorf("AWS Client init: %s", err) 65 | } 66 | appLogger.Info("AWS S3 connected") 67 | 68 | jaegerCfgInstance := jaegercfg.Configuration{ 69 | ServiceName: cfg.Jaeger.ServiceName, 70 | Sampler: &jaegercfg.SamplerConfig{ 71 | Type: jaeger.SamplerTypeConst, 72 | Param: 1, 73 | }, 74 | Reporter: &jaegercfg.ReporterConfig{ 75 | LogSpans: cfg.Jaeger.LogSpans, 76 | LocalAgentHostPort: cfg.Jaeger.Host, 77 | }, 78 | } 79 | 80 | tracer, closer, err := jaegerCfgInstance.NewTracer( 81 | jaegercfg.Logger(jaegerlog.StdLogger), 82 | jaegercfg.Metrics(metrics.NullFactory), 83 | ) 84 | if err != nil { 85 | log.Fatal("cannot create tracer", err) 86 | } 87 | appLogger.Info("Jaeger connected") 88 | 89 | opentracing.SetGlobalTracer(tracer) 90 | defer closer.Close() 91 | appLogger.Info("Opentracing connected") 92 | 93 | s := server.NewServer(cfg, psqlDB, redisClient, awsClient, appLogger) 94 | if err = s.Run(); err != nil { 95 | log.Fatal(err) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /config/config-docker.yml: -------------------------------------------------------------------------------- 1 | server: 2 | AppVersion: 1.0.0 3 | Port: :5000 4 | PprofPort: :5555 5 | Mode: Development 6 | JwtSecretKey: secretkey 7 | CookieName: jwt-token 8 | ReadTimeout: 10 9 | WriteTimeout: 10 10 | SSL: true 11 | CtxDefaultTimeout: 12 12 | CSRF: true 13 | Debug: false 14 | 15 | logger: 16 | Development: true 17 | DisableCaller: false 18 | DisableStacktrace: false 19 | Encoding: console 20 | Level: info 21 | 22 | postgres: 23 | PostgresqlHost: postgesql 24 | PostgresqlPort: 5432 25 | PostgresqlUser: postgres 26 | PostgresqlPassword: postgres 27 | PostgresqlDbname: auth_db 28 | PostgresqlSslmode: false 29 | PgDriver: pgx 30 | 31 | redis: 32 | RedisAddr: redis:6379 33 | RedisPassword: 34 | RedisDb: 0 35 | RedisDefaultdb: 0 36 | MinIdleConns: 200 37 | PoolSize: 12000 38 | PoolTimeout: 240 39 | Password: "" 40 | DB: 0 41 | 42 | cookie: 43 | Name: jwt-token 44 | MaxAge: 86400 45 | Secure: false 46 | HttpOnly: true 47 | 48 | session: 49 | Name: session-id 50 | Prefix: api-session 51 | Expire: 3600 52 | 53 | metrics: 54 | url: 0.0.0.0:7070 55 | service: api 56 | 57 | mongodb: 58 | MongoURI: uristring 59 | 60 | 61 | aws: 62 | Endpoint: 127.0.0.1:9000 63 | MinioAccessKey: minio 64 | MinioSecretKey: minio123 65 | UseSSL: false 66 | MinioEndpoint: http://127.0.0.1:9000 67 | 68 | 69 | jaeger: 70 | Host: localhost:6831 71 | ServiceName: REST_API 72 | LogSpans: true 73 | 74 | #aws: 75 | # Endpoint: play.min.io 76 | # MinioAccessKey: Q3AM3UQ867SPQQA43P2F 77 | # MinioSecretKey: zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG 78 | # UseSSL: false 79 | # MinioEndpoint: http://127.0.0.1:9000 80 | 81 | -------------------------------------------------------------------------------- /config/config-local.yml: -------------------------------------------------------------------------------- 1 | server: 2 | AppVersion: 1.0.0 3 | Port: :5000 4 | PprofPort: :5555 5 | Mode: Development 6 | JwtSecretKey: secretkey 7 | CookieName: jwt-token 8 | ReadTimeout: 5 9 | WriteTimeout: 5 10 | SSL: true 11 | CtxDefaultTimeout: 12 12 | CSRF: true 13 | Debug: false 14 | 15 | logger: 16 | Development: true 17 | DisableCaller: false 18 | DisableStacktrace: false 19 | Encoding: json 20 | Level: info 21 | 22 | postgres: 23 | PostgresqlHost: localhost 24 | PostgresqlPort: 5432 25 | PostgresqlUser: postgres 26 | PostgresqlPassword: postgres 27 | PostgresqlDbname: auth_db 28 | PostgresqlSslmode: false 29 | PgDriver: pgx 30 | 31 | redis: 32 | RedisAddr: localhost:6379 33 | RedisPassword: 34 | RedisDb: 0 35 | RedisDefaultdb: 0 36 | MinIdleConns: 200 37 | PoolSize: 12000 38 | PoolTimeout: 240 39 | Password: "" 40 | DB: 0 41 | 42 | cookie: 43 | Name: jwt-token 44 | MaxAge: 86400 45 | Secure: false 46 | HttpOnly: true 47 | 48 | session: 49 | Name: session-id 50 | Prefix: api-session 51 | Expire: 3600 52 | 53 | metrics: 54 | Url: 0.0.0.0:7070 55 | ServiceName: api 56 | 57 | 58 | mongodb: 59 | MongoURI: uristring 60 | 61 | aws: 62 | Endpoint: 127.0.0.1:9000 63 | MinioAccessKey: minio 64 | MinioSecretKey: minio123 65 | UseSSL: false 66 | MinioEndpoint: http://127.0.0.1:9000 67 | 68 | jaeger: 69 | Host: localhost:6831 70 | ServiceName: REST_API 71 | LogSpans: false 72 | 73 | #aws: 74 | # Endpoint: play.min.io 75 | # MinioAccessKey: Q3AM3UQ867SPQQA43P2F 76 | # MinioSecretKey: zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "time" 7 | 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | // App config struct 12 | type Config struct { 13 | Server ServerConfig 14 | Postgres PostgresConfig 15 | Redis RedisConfig 16 | MongoDB MongoDB 17 | Cookie Cookie 18 | Store Store 19 | Session Session 20 | Metrics Metrics 21 | Logger Logger 22 | AWS AWS 23 | Jaeger Jaeger 24 | } 25 | 26 | // Server config struct 27 | type ServerConfig struct { 28 | AppVersion string 29 | Port string 30 | PprofPort string 31 | Mode string 32 | JwtSecretKey string 33 | CookieName string 34 | ReadTimeout time.Duration 35 | WriteTimeout time.Duration 36 | SSL bool 37 | CtxDefaultTimeout time.Duration 38 | CSRF bool 39 | Debug bool 40 | } 41 | 42 | // Logger config 43 | type Logger struct { 44 | Development bool 45 | DisableCaller bool 46 | DisableStacktrace bool 47 | Encoding string 48 | Level string 49 | } 50 | 51 | // Postgresql config 52 | type PostgresConfig struct { 53 | PostgresqlHost string 54 | PostgresqlPort string 55 | PostgresqlUser string 56 | PostgresqlPassword string 57 | PostgresqlDbname string 58 | PostgresqlSSLMode bool 59 | PgDriver string 60 | } 61 | 62 | // Redis config 63 | type RedisConfig struct { 64 | RedisAddr string 65 | RedisPassword string 66 | RedisDB string 67 | RedisDefaultdb string 68 | MinIdleConns int 69 | PoolSize int 70 | PoolTimeout int 71 | Password string 72 | DB int 73 | } 74 | 75 | // MongoDB config 76 | type MongoDB struct { 77 | MongoURI string 78 | } 79 | 80 | // Cookie config 81 | type Cookie struct { 82 | Name string 83 | MaxAge int 84 | Secure bool 85 | HTTPOnly bool 86 | } 87 | 88 | // Session config 89 | type Session struct { 90 | Prefix string 91 | Name string 92 | Expire int 93 | } 94 | 95 | // Metrics config 96 | type Metrics struct { 97 | URL string 98 | ServiceName string 99 | } 100 | 101 | // Store config 102 | type Store struct { 103 | ImagesFolder string 104 | } 105 | 106 | // AWS S3 107 | type AWS struct { 108 | Endpoint string 109 | MinioAccessKey string 110 | MinioSecretKey string 111 | UseSSL bool 112 | MinioEndpoint string 113 | } 114 | 115 | // AWS S3 116 | type Jaeger struct { 117 | Host string 118 | ServiceName string 119 | LogSpans bool 120 | } 121 | 122 | // Load config file from given path 123 | func LoadConfig(filename string) (*viper.Viper, error) { 124 | v := viper.New() 125 | 126 | v.SetConfigName(filename) 127 | v.AddConfigPath(".") 128 | v.AutomaticEnv() 129 | if err := v.ReadInConfig(); err != nil { 130 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 131 | return nil, errors.New("config file not found") 132 | } 133 | return nil, err 134 | } 135 | 136 | return v, nil 137 | } 138 | 139 | // Parse config file 140 | func ParseConfig(v *viper.Viper) (*Config, error) { 141 | var c Config 142 | 143 | err := v.Unmarshal(&c) 144 | if err != nil { 145 | log.Printf("unable to decode into struct, %v", err) 146 | return nil, err 147 | } 148 | 149 | return &c, nil 150 | } 151 | -------------------------------------------------------------------------------- /docker-compose.delve.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | web: 5 | container_name: api 6 | build: 7 | context: ./ 8 | dockerfile: docker/Dockerfile.DelveHotReload 9 | ports: 10 | - "5000:5000" 11 | - "5555:5555" 12 | - "7070:7070" 13 | - "40000:40000" 14 | environment: 15 | - PORT=5000 16 | security_opt: 17 | - "seccomp:unconfined" 18 | cap_add: 19 | - SYS_PTRACE 20 | depends_on: 21 | - postgesql 22 | - redis 23 | restart: always 24 | volumes: 25 | - ./:/app 26 | networks: 27 | - web_api 28 | 29 | redis: 30 | image: redis:6-alpine 31 | container_name: api_redis 32 | ports: 33 | - "6379:6379" 34 | restart: always 35 | networks: 36 | - web_api 37 | 38 | postgesql: 39 | image: postgres:12-alpine 40 | container_name: api_postgesql 41 | ports: 42 | - "5432:5432" 43 | restart: always 44 | environment: 45 | - POSTGRES_USER=postgres 46 | - POSTGRES_PASSWORD=postgres 47 | - POSTGRES_DB=auth_db 48 | volumes: 49 | - ./pgdata:/var/lib/postgresql/data 50 | networks: 51 | - web_api 52 | 53 | prometheus: 54 | container_name: prometheus_container 55 | image: prom/prometheus 56 | volumes: 57 | - ./docker/monitoring/prometheus-local.yml:/etc/prometheus/prometheus.yml:Z 58 | command: 59 | - '--config.file=/etc/prometheus/prometheus.yml' 60 | - '--storage.tsdb.path=/prometheus' 61 | - '--storage.tsdb.retention=20d' 62 | - '--web.console.libraries=/usr/share/prometheus/console_libraries' 63 | - '--web.console.templates=/usr/share/prometheus/consoles' 64 | ports: 65 | - '9090:9090' 66 | networks: 67 | - web_api 68 | 69 | node_exporter: 70 | container_name: node_exporter_container 71 | image: prom/node-exporter 72 | ports: 73 | - '9101:9100' 74 | networks: 75 | - web_api 76 | 77 | grafana: 78 | container_name: grafana_container 79 | image: grafana/grafana 80 | ports: 81 | - '3000:3000' 82 | networks: 83 | - web_api 84 | 85 | jaeger: 86 | container_name: jaeger_container 87 | image: jaegertracing/all-in-one:1.21 88 | environment: 89 | - COLLECTOR_ZIPKIN_HTTP_PORT=9411 90 | ports: 91 | - 5775:5775/udp 92 | - 6831:6831/udp 93 | - 6832:6832/udp 94 | - 5778:5778 95 | - 16686:16686 96 | - 14268:14268 97 | - 14250:14250 98 | - 9411:9411 99 | networks: 100 | - web_api 101 | 102 | networks: 103 | web_api: 104 | driver: bridge -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | web: 5 | container_name: api 6 | build: 7 | context: ./ 8 | dockerfile: docker/Dockerfile 9 | ports: 10 | - "5000:5000" 11 | - "5555:5555" 12 | - "7070:7070" 13 | environment: 14 | - PORT=5000 15 | depends_on: 16 | - postgesql 17 | - redis 18 | restart: always 19 | volumes: 20 | - ./:/app 21 | networks: 22 | - web_api 23 | 24 | redis: 25 | image: redis:6-alpine 26 | container_name: api_redis 27 | ports: 28 | - "6379:6379" 29 | restart: always 30 | networks: 31 | - web_api 32 | 33 | postgesql: 34 | image: postgres:12-alpine 35 | container_name: api_postgesql 36 | ports: 37 | - "5432:5432" 38 | restart: always 39 | environment: 40 | - POSTGRES_USER=postgres 41 | - POSTGRES_PASSWORD=postgres 42 | - POSTGRES_DB=auth_db 43 | volumes: 44 | - ./pgdata:/var/lib/postgresql/data 45 | networks: 46 | - web_api 47 | 48 | prometheus: 49 | container_name: prometheus_container 50 | image: prom/prometheus 51 | volumes: 52 | - ./docker/monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:Z 53 | command: 54 | - '--config.file=/etc/prometheus/prometheus.yml' 55 | - '--storage.tsdb.path=/prometheus' 56 | - '--storage.tsdb.retention=20d' 57 | - '--web.console.libraries=/usr/share/prometheus/console_libraries' 58 | - '--web.console.templates=/usr/share/prometheus/consoles' 59 | ports: 60 | - '9090:9090' 61 | networks: 62 | - web_api 63 | 64 | node_exporter: 65 | container_name: node_exporter_container 66 | image: prom/node-exporter 67 | ports: 68 | - '9101:9100' 69 | networks: 70 | - web_api 71 | 72 | grafana: 73 | container_name: grafana_container 74 | image: grafana/grafana 75 | ports: 76 | - '3000:3000' 77 | networks: 78 | - web_api 79 | 80 | minio: 81 | image: minio/minio:latest 82 | ports: 83 | - '9000:9000' 84 | container_name: myminio 85 | environment: 86 | MINIO_ACCESS_KEY: minio 87 | MINIO_SECRET_KEY: minio123 88 | command: server /data 89 | networks: 90 | - web_api 91 | 92 | mc: 93 | image: minio/mc:latest 94 | depends_on: 95 | - minio 96 | entrypoint: > 97 | /bin/sh -c " 98 | /usr/bin/mc config host rm local; 99 | /usr/bin/mc config host add --quiet --api s3v4 local http://myminio:9000 minio minio123; 100 | /usr/bin/mc rb --force local/somebucketname1/; 101 | /usr/bin/mc mb --quiet local/somebucketname1/; 102 | /usr/bin/mc policy set public local/somebucketname1; 103 | " 104 | networks: 105 | - web_api 106 | 107 | jaeger: 108 | container_name: jaeger_container 109 | image: jaegertracing/all-in-one:1.21 110 | environment: 111 | - COLLECTOR_ZIPKIN_HTTP_PORT=9411 112 | ports: 113 | - 5775:5775/udp 114 | - 6831:6831/udp 115 | - 6832:6832/udp 116 | - 5778:5778 117 | - 16686:16686 118 | - 14268:14268 119 | - 14250:14250 120 | - 9411:9411 121 | networks: 122 | - web_api 123 | 124 | 125 | networks: 126 | web_api: 127 | driver: bridge -------------------------------------------------------------------------------- /docker-compose.local.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | redis: 5 | image: redis:6.0.9-alpine 6 | container_name: api_redis 7 | ports: 8 | - "6379:6379" 9 | restart: always 10 | networks: 11 | - web_api 12 | 13 | postgesql: 14 | image: postgres:12-alpine 15 | container_name: api_postgesql 16 | ports: 17 | - "5432:5432" 18 | restart: always 19 | environment: 20 | - POSTGRES_USER=postgres 21 | - POSTGRES_PASSWORD=postgres 22 | - POSTGRES_DB=auth_db 23 | volumes: 24 | - ./pgdata:/var/lib/postgresql/data 25 | networks: 26 | - web_api 27 | 28 | prometheus: 29 | container_name: prometheus_container 30 | image: prom/prometheus 31 | restart: always 32 | volumes: 33 | - ./docker/monitoring/prometheus-local.yml:/etc/prometheus/prometheus.yml:Z 34 | command: 35 | - '--config.file=/etc/prometheus/prometheus.yml' 36 | - '--storage.tsdb.path=/prometheus' 37 | - '--storage.tsdb.retention=20d' 38 | - '--web.console.libraries=/usr/share/prometheus/console_libraries' 39 | - '--web.console.templates=/usr/share/prometheus/consoles' 40 | ports: 41 | - '9090:9090' 42 | networks: 43 | - web_api 44 | 45 | node_exporter: 46 | container_name: node_exporter_container 47 | restart: always 48 | image: prom/node-exporter 49 | ports: 50 | - '9101:9100' 51 | networks: 52 | - web_api 53 | 54 | grafana: 55 | container_name: grafana_container 56 | restart: always 57 | image: grafana/grafana 58 | ports: 59 | - '3000:3000' 60 | networks: 61 | - web_api 62 | 63 | minio: 64 | image: minio/minio:latest 65 | ports: 66 | - '9000:9000' 67 | container_name: myminio 68 | environment: 69 | MINIO_ACCESS_KEY: minio 70 | MINIO_SECRET_KEY: minio123 71 | command: server /data 72 | networks: 73 | - web_api 74 | 75 | mc: 76 | image: minio/mc:latest 77 | depends_on: 78 | - minio 79 | entrypoint: > 80 | /bin/sh -c " 81 | /usr/bin/mc config host rm local; 82 | /usr/bin/mc config host add --quiet --api s3v4 local http://myminio:9000 minio minio123; 83 | /usr/bin/mc rb --force local/somebucketname1/; 84 | /usr/bin/mc mb --quiet local/somebucketname1/; 85 | /usr/bin/mc policy set public local/somebucketname1; 86 | " 87 | networks: 88 | - web_api 89 | 90 | jaeger: 91 | container_name: jaeger_container 92 | restart: always 93 | image: jaegertracing/all-in-one:1.21 94 | environment: 95 | - COLLECTOR_ZIPKIN_HTTP_PORT=9411 96 | ports: 97 | - 5775:5775/udp 98 | - 6831:6831/udp 99 | - 6832:6832/udp 100 | - 5778:5778 101 | - 16686:16686 102 | - 14268:14268 103 | - 14250:14250 104 | - 9411:9411 105 | networks: 106 | - web_api 107 | 108 | networks: 109 | web_api: 110 | driver: bridge 111 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Initial stage: download modules 2 | FROM golang:1.16-alpine as builder 3 | 4 | ENV config=docker 5 | 6 | WORKDIR /app 7 | 8 | COPY ./ /app 9 | 10 | RUN go mod download 11 | 12 | 13 | # Intermediate stage: Build the binary 14 | FROM golang:1.16-alpine as runner 15 | 16 | COPY --from=builder ./app ./app 17 | 18 | RUN go get github.com/githubnemo/CompileDaemon 19 | 20 | WORKDIR /app 21 | ENV config=docker 22 | 23 | EXPOSE 5000 24 | EXPOSE 5555 25 | EXPOSE 7070 26 | 27 | ENTRYPOINT CompileDaemon --build="go build cmd/api/main.go" --command=./main 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /docker/Dockerfile.DelveHotReload: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 2 | 3 | RUN go get github.com/githubnemo/CompileDaemon && \ 4 | go get github.com/go-delve/delve/cmd/dlv 5 | WORKDIR /app 6 | 7 | ENV config=docker 8 | 9 | COPY .. /app 10 | 11 | RUN go mod download 12 | 13 | 14 | EXPOSE 5000 40000 15 | 16 | ENTRYPOINT CompileDaemon --build="go build cmd/api/main.go" --command="dlv debug --headless --listen=:40000 --api-version=2 --accept-multiclient cmd/api/main.go" -------------------------------------------------------------------------------- /docker/Dockerfile.HotReload: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 2 | 3 | #ENV TZ=Europe/Moscow 4 | #RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 5 | 6 | ENV config=docker 7 | 8 | WORKDIR /app 9 | 10 | COPY ./ /app 11 | 12 | RUN go mod download 13 | 14 | RUN go get github.com/githubnemo/CompileDaemon 15 | 16 | EXPOSE 5000 17 | 18 | ENTRYPOINT CompileDaemon --build="go build cmd/api/main.go" --command=./main -------------------------------------------------------------------------------- /docker/monitoring/alerts.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: default 3 | rules: 4 | - alert: InternalServerError 5 | expr: increase(hits{status="500"}[1m]) > 0 6 | for: 1s 7 | labels: 8 | severity: critical 9 | annotations: 10 | summary: "path {{ $labels.path }} returned status 500" 11 | description: "{{ $labels.path }} of job {{ $labels.job }} returned status {{ $labels.status }}" -------------------------------------------------------------------------------- /docker/monitoring/prometheus-local.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 10s 3 | evaluation_interval: 10s 4 | 5 | scrape_configs: 6 | - job_name: 'prometheus' 7 | static_configs: 8 | - targets: ['localhost:9090'] 9 | 10 | - job_name: 'system' 11 | static_configs: 12 | - targets: ['node_exporter:9100'] 13 | 14 | - job_name: 'api' 15 | static_configs: 16 | - targets: ['host.docker.internal:7070'] 17 | 18 | # https://docs.docker.com/config/daemon/prometheus/ -------------------------------------------------------------------------------- /docker/monitoring/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 10s 3 | evaluation_interval: 10s 4 | 5 | scrape_configs: 6 | - job_name: 'prometheus' 7 | static_configs: 8 | - targets: ['localhost:9090'] 9 | 10 | - job_name: 'system' 11 | static_configs: 12 | - targets: ['node_exporter:9100'] 13 | 14 | - job_name: 'api' 15 | static_configs: 16 | - targets: ['api:7070'] -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AleksK1NG/api-mc 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.0 7 | github.com/HdrHistogram/hdrhistogram-go v1.0.1 // indirect 8 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 9 | github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect 10 | github.com/alicebob/miniredis v2.5.0+incompatible 11 | github.com/cockroachdb/apd v1.1.0 // indirect 12 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 13 | github.com/go-openapi/spec v0.20.3 // indirect 14 | github.com/go-playground/validator/v10 v10.4.1 15 | github.com/go-redis/redis/v8 v8.5.0 16 | github.com/gofrs/uuid v3.3.0+incompatible // indirect 17 | github.com/golang/mock v1.4.4 18 | github.com/gomodule/redigo v2.0.0+incompatible // indirect 19 | github.com/google/uuid v1.2.0 20 | github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect 21 | github.com/jackc/pgx v3.6.2+incompatible 22 | github.com/jmoiron/sqlx v1.3.1 23 | github.com/labstack/echo v3.3.10+incompatible 24 | github.com/labstack/echo/v4 v4.2.0 25 | github.com/leodido/go-urn v1.2.1 // indirect 26 | github.com/magiconair/properties v1.8.4 // indirect 27 | github.com/mailru/easyjson v0.7.7 // indirect 28 | github.com/mattn/go-colorable v0.1.8 // indirect 29 | github.com/microcosm-cc/bluemonday v1.0.4 30 | github.com/minio/md5-simd v1.1.1 // indirect 31 | github.com/minio/minio-go/v7 v7.0.8 32 | github.com/mitchellh/mapstructure v1.4.1 // indirect 33 | github.com/opentracing/opentracing-go v1.2.0 34 | github.com/pelletier/go-toml v1.8.1 // indirect 35 | github.com/pkg/errors v0.9.1 36 | github.com/prometheus/client_golang v1.9.0 37 | github.com/prometheus/procfs v0.6.0 // indirect 38 | github.com/shopspring/decimal v1.2.0 // indirect 39 | github.com/spf13/afero v1.5.1 // indirect 40 | github.com/spf13/cast v1.3.1 // indirect 41 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 42 | github.com/spf13/pflag v1.0.5 // indirect 43 | github.com/spf13/viper v1.7.1 44 | github.com/stretchr/objx v0.3.0 // indirect 45 | github.com/stretchr/testify v1.7.0 46 | github.com/swaggo/echo-swagger v1.1.0 47 | github.com/swaggo/swag v1.7.0 48 | github.com/uber/jaeger-client-go v2.25.0+incompatible 49 | github.com/uber/jaeger-lib v2.4.0+incompatible 50 | github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da // indirect 51 | go.uber.org/multierr v1.6.0 // indirect 52 | go.uber.org/zap v1.16.0 53 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad 54 | golang.org/x/tools v0.1.0 // indirect 55 | google.golang.org/protobuf v1.25.0 // indirect 56 | gopkg.in/ini.v1 v1.62.0 // indirect 57 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 58 | ) 59 | -------------------------------------------------------------------------------- /internal/auth/aws_repository.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source aws_repository.go -destination mock/aws_repository_mock.go -package mock 2 | package auth 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/minio/minio-go/v7" 8 | 9 | "github.com/AleksK1NG/api-mc/internal/models" 10 | ) 11 | 12 | // Minio AWS S3 interface 13 | type AWSRepository interface { 14 | PutObject(ctx context.Context, input models.UploadInput) (*minio.UploadInfo, error) 15 | GetObject(ctx context.Context, bucket string, fileName string) (*minio.Object, error) 16 | RemoveObject(ctx context.Context, bucket string, fileName string) error 17 | } 18 | -------------------------------------------------------------------------------- /internal/auth/delivery.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "github.com/labstack/echo/v4" 4 | 5 | // Auth HTTP Handlers interface 6 | type Handlers interface { 7 | Register() echo.HandlerFunc 8 | Login() echo.HandlerFunc 9 | Logout() echo.HandlerFunc 10 | Update() echo.HandlerFunc 11 | Delete() echo.HandlerFunc 12 | GetUserByID() echo.HandlerFunc 13 | FindByName() echo.HandlerFunc 14 | GetUsers() echo.HandlerFunc 15 | GetMe() echo.HandlerFunc 16 | UploadAvatar() echo.HandlerFunc 17 | GetCSRFToken() echo.HandlerFunc 18 | } 19 | -------------------------------------------------------------------------------- /internal/auth/delivery/http/handlers_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/golang/mock/gomock" 10 | "github.com/google/uuid" 11 | "github.com/labstack/echo/v4" 12 | "github.com/opentracing/opentracing-go" 13 | "github.com/stretchr/testify/require" 14 | 15 | "github.com/AleksK1NG/api-mc/config" 16 | "github.com/AleksK1NG/api-mc/internal/auth/mock" 17 | "github.com/AleksK1NG/api-mc/internal/models" 18 | mockSess "github.com/AleksK1NG/api-mc/internal/session/mock" 19 | "github.com/AleksK1NG/api-mc/pkg/converter" 20 | "github.com/AleksK1NG/api-mc/pkg/logger" 21 | "github.com/AleksK1NG/api-mc/pkg/utils" 22 | ) 23 | 24 | func TestAuthHandlers_Register(t *testing.T) { 25 | t.Parallel() 26 | 27 | ctrl := gomock.NewController(t) 28 | defer ctrl.Finish() 29 | 30 | mockAuthUC := mock.NewMockUseCase(ctrl) 31 | mockSessUC := mockSess.NewMockUCSession(ctrl) 32 | 33 | cfg := &config.Config{ 34 | Session: config.Session{ 35 | Expire: 10, 36 | }, 37 | Logger: config.Logger{ 38 | Development: true, 39 | }, 40 | } 41 | 42 | apiLogger := logger.NewApiLogger(cfg) 43 | authHandlers := NewAuthHandlers(cfg, mockAuthUC, mockSessUC, apiLogger) 44 | 45 | gender := "male" 46 | user := &models.User{ 47 | FirstName: "FirstName", 48 | LastName: "LastName", 49 | Email: "email@gmail.com", 50 | Password: "123456", 51 | Gender: &gender, 52 | } 53 | 54 | buf, err := converter.AnyToBytesBuffer(user) 55 | require.NoError(t, err) 56 | require.NotNil(t, buf) 57 | require.Nil(t, err) 58 | 59 | e := echo.New() 60 | req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", strings.NewReader(buf.String())) 61 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 62 | rec := httptest.NewRecorder() 63 | 64 | c := e.NewContext(req, rec) 65 | ctx := utils.GetRequestCtx(c) 66 | span, ctxWithTrace := opentracing.StartSpanFromContext(ctx, "auth.Register") 67 | defer span.Finish() 68 | 69 | handlerFunc := authHandlers.Register() 70 | 71 | userUID := uuid.New() 72 | userWithToken := &models.UserWithToken{ 73 | User: &models.User{ 74 | UserID: userUID, 75 | }, 76 | } 77 | sess := &models.Session{ 78 | UserID: userUID, 79 | } 80 | session := "session" 81 | 82 | mockAuthUC.EXPECT().Register(ctxWithTrace, gomock.Eq(user)).Return(userWithToken, nil) 83 | mockSessUC.EXPECT().CreateSession(ctxWithTrace, gomock.Eq(sess), 10).Return(session, nil) 84 | 85 | err = handlerFunc(c) 86 | require.NoError(t, err) 87 | require.Nil(t, err) 88 | } 89 | 90 | func TestAuthHandlers_Login(t *testing.T) { 91 | t.Parallel() 92 | 93 | ctrl := gomock.NewController(t) 94 | defer ctrl.Finish() 95 | 96 | mockAuthUC := mock.NewMockUseCase(ctrl) 97 | mockSessUC := mockSess.NewMockUCSession(ctrl) 98 | 99 | cfg := &config.Config{ 100 | Session: config.Session{ 101 | Expire: 10, 102 | }, 103 | Logger: config.Logger{ 104 | Development: true, 105 | }, 106 | } 107 | 108 | apiLogger := logger.NewApiLogger(cfg) 109 | authHandlers := NewAuthHandlers(cfg, mockAuthUC, mockSessUC, apiLogger) 110 | 111 | type Login struct { 112 | Email string `json:"email" db:"email" validate:"omitempty,lte=60,email"` 113 | Password string `json:"password,omitempty" db:"password" validate:"required,gte=6"` 114 | } 115 | 116 | login := &Login{ 117 | Email: "email@mail.com", 118 | Password: "123456", 119 | } 120 | 121 | user := &models.User{ 122 | Email: login.Email, 123 | Password: login.Password, 124 | } 125 | 126 | buf, err := converter.AnyToBytesBuffer(user) 127 | require.NoError(t, err) 128 | require.NotNil(t, buf) 129 | require.Nil(t, err) 130 | 131 | e := echo.New() 132 | req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader(buf.String())) 133 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 134 | rec := httptest.NewRecorder() 135 | 136 | c := e.NewContext(req, rec) 137 | ctx := utils.GetRequestCtx(c) 138 | span, ctxWithTrace := opentracing.StartSpanFromContext(ctx, "auth.Login") 139 | defer span.Finish() 140 | 141 | handlerFunc := authHandlers.Login() 142 | 143 | userUID := uuid.New() 144 | userWithToken := &models.UserWithToken{ 145 | User: &models.User{ 146 | UserID: userUID, 147 | }, 148 | } 149 | sess := &models.Session{ 150 | UserID: userUID, 151 | } 152 | session := "session" 153 | 154 | mockAuthUC.EXPECT().Login(ctxWithTrace, gomock.Eq(user)).Return(userWithToken, nil) 155 | mockSessUC.EXPECT().CreateSession(ctxWithTrace, gomock.Eq(sess), 10).Return(session, nil) 156 | 157 | err = handlerFunc(c) 158 | require.NoError(t, err) 159 | require.Nil(t, err) 160 | } 161 | 162 | func TestAuthHandlers_Logout(t *testing.T) { 163 | t.Parallel() 164 | 165 | ctrl := gomock.NewController(t) 166 | defer ctrl.Finish() 167 | 168 | mockAuthUC := mock.NewMockUseCase(ctrl) 169 | mockSessUC := mockSess.NewMockUCSession(ctrl) 170 | 171 | cfg := &config.Config{ 172 | Session: config.Session{ 173 | Expire: 10, 174 | }, 175 | Logger: config.Logger{ 176 | Development: true, 177 | }, 178 | } 179 | 180 | apiLogger := logger.NewApiLogger(cfg) 181 | authHandlers := NewAuthHandlers(cfg, mockAuthUC, mockSessUC, apiLogger) 182 | sessionKey := "session-id" 183 | cookieValue := "cookieValue" 184 | 185 | e := echo.New() 186 | req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/logout", nil) 187 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 188 | req.AddCookie(&http.Cookie{Name: sessionKey, Value: cookieValue}) 189 | 190 | rec := httptest.NewRecorder() 191 | 192 | c := e.NewContext(req, rec) 193 | ctx := utils.GetRequestCtx(c) 194 | span, ctxWithTrace := opentracing.StartSpanFromContext(ctx, "auth.Logout") 195 | defer span.Finish() 196 | 197 | logout := authHandlers.Logout() 198 | 199 | cookie, err := req.Cookie(sessionKey) 200 | require.NoError(t, err) 201 | require.NotNil(t, cookie) 202 | require.NotEqual(t, cookie.Value, "") 203 | require.Equal(t, cookie.Value, cookieValue) 204 | 205 | mockSessUC.EXPECT().DeleteByID(ctxWithTrace, gomock.Eq(cookie.Value)).Return(nil) 206 | 207 | err = logout(c) 208 | require.NoError(t, err) 209 | require.Nil(t, err) 210 | } 211 | -------------------------------------------------------------------------------- /internal/auth/delivery/http/routes.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | 6 | "github.com/AleksK1NG/api-mc/internal/auth" 7 | "github.com/AleksK1NG/api-mc/internal/middleware" 8 | ) 9 | 10 | // Map auth routes 11 | func MapAuthRoutes(authGroup *echo.Group, h auth.Handlers, mw *middleware.MiddlewareManager) { 12 | authGroup.POST("/register", h.Register()) 13 | authGroup.POST("/login", h.Login()) 14 | authGroup.POST("/logout", h.Logout()) 15 | authGroup.GET("/find", h.FindByName()) 16 | authGroup.GET("/all", h.GetUsers()) 17 | authGroup.GET("/:user_id", h.GetUserByID()) 18 | // authGroup.Use(middleware.AuthJWTMiddleware(authUC, cfg)) 19 | authGroup.Use(mw.AuthSessionMiddleware) 20 | authGroup.GET("/me", h.GetMe()) 21 | authGroup.GET("/token", h.GetCSRFToken()) 22 | authGroup.POST("/:user_id/avatar", h.UploadAvatar(), mw.CSRF) 23 | authGroup.PUT("/:user_id", h.Update(), mw.OwnerOrAdminMiddleware(), mw.CSRF) 24 | authGroup.DELETE("/:user_id", h.Delete(), mw.CSRF, mw.RoleBasedAuthMiddleware([]string{"admin"})) 25 | } 26 | -------------------------------------------------------------------------------- /internal/auth/mock/aws_repository_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: aws_repository.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | models "github.com/AleksK1NG/api-mc/internal/models" 10 | gomock "github.com/golang/mock/gomock" 11 | minio "github.com/minio/minio-go/v7" 12 | reflect "reflect" 13 | ) 14 | 15 | // MockAWSRepository is a mock of AWSRepository interface 16 | type MockAWSRepository struct { 17 | ctrl *gomock.Controller 18 | recorder *MockAWSRepositoryMockRecorder 19 | } 20 | 21 | // MockAWSRepositoryMockRecorder is the mock recorder for MockAWSRepository 22 | type MockAWSRepositoryMockRecorder struct { 23 | mock *MockAWSRepository 24 | } 25 | 26 | // NewMockAWSRepository creates a new mock instance 27 | func NewMockAWSRepository(ctrl *gomock.Controller) *MockAWSRepository { 28 | mock := &MockAWSRepository{ctrl: ctrl} 29 | mock.recorder = &MockAWSRepositoryMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use 34 | func (m *MockAWSRepository) EXPECT() *MockAWSRepositoryMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // PutObject mocks base method 39 | func (m *MockAWSRepository) PutObject(ctx context.Context, input models.UploadInput) (*minio.UploadInfo, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "PutObject", ctx, input) 42 | ret0, _ := ret[0].(*minio.UploadInfo) 43 | ret1, _ := ret[1].(error) 44 | return ret0, ret1 45 | } 46 | 47 | // PutObject indicates an expected call of PutObject 48 | func (mr *MockAWSRepositoryMockRecorder) PutObject(ctx, input interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObject", reflect.TypeOf((*MockAWSRepository)(nil).PutObject), ctx, input) 51 | } 52 | 53 | // GetObject mocks base method 54 | func (m *MockAWSRepository) GetObject(ctx context.Context, bucket, fileName string) (*minio.Object, error) { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "GetObject", ctx, bucket, fileName) 57 | ret0, _ := ret[0].(*minio.Object) 58 | ret1, _ := ret[1].(error) 59 | return ret0, ret1 60 | } 61 | 62 | // GetObject indicates an expected call of GetObject 63 | func (mr *MockAWSRepositoryMockRecorder) GetObject(ctx, bucket, fileName interface{}) *gomock.Call { 64 | mr.mock.ctrl.T.Helper() 65 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*MockAWSRepository)(nil).GetObject), ctx, bucket, fileName) 66 | } 67 | 68 | // RemoveObject mocks base method 69 | func (m *MockAWSRepository) RemoveObject(ctx context.Context, bucket, fileName string) error { 70 | m.ctrl.T.Helper() 71 | ret := m.ctrl.Call(m, "RemoveObject", ctx, bucket, fileName) 72 | ret0, _ := ret[0].(error) 73 | return ret0 74 | } 75 | 76 | // RemoveObject indicates an expected call of RemoveObject 77 | func (mr *MockAWSRepositoryMockRecorder) RemoveObject(ctx, bucket, fileName interface{}) *gomock.Call { 78 | mr.mock.ctrl.T.Helper() 79 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObject", reflect.TypeOf((*MockAWSRepository)(nil).RemoveObject), ctx, bucket, fileName) 80 | } 81 | -------------------------------------------------------------------------------- /internal/auth/mock/pg_repository_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: pg_repository.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | models "github.com/AleksK1NG/api-mc/internal/models" 10 | utils "github.com/AleksK1NG/api-mc/pkg/utils" 11 | gomock "github.com/golang/mock/gomock" 12 | uuid "github.com/google/uuid" 13 | reflect "reflect" 14 | ) 15 | 16 | // MockRepository is a mock of Repository interface 17 | type MockRepository struct { 18 | ctrl *gomock.Controller 19 | recorder *MockRepositoryMockRecorder 20 | } 21 | 22 | // MockRepositoryMockRecorder is the mock recorder for MockRepository 23 | type MockRepositoryMockRecorder struct { 24 | mock *MockRepository 25 | } 26 | 27 | // NewMockRepository creates a new mock instance 28 | func NewMockRepository(ctrl *gomock.Controller) *MockRepository { 29 | mock := &MockRepository{ctrl: ctrl} 30 | mock.recorder = &MockRepositoryMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use 35 | func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // Register mocks base method 40 | func (m *MockRepository) Register(ctx context.Context, user *models.User) (*models.User, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "Register", ctx, user) 43 | ret0, _ := ret[0].(*models.User) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // Register indicates an expected call of Register 49 | func (mr *MockRepositoryMockRecorder) Register(ctx, user interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockRepository)(nil).Register), ctx, user) 52 | } 53 | 54 | // Update mocks base method 55 | func (m *MockRepository) Update(ctx context.Context, user *models.User) (*models.User, error) { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "Update", ctx, user) 58 | ret0, _ := ret[0].(*models.User) 59 | ret1, _ := ret[1].(error) 60 | return ret0, ret1 61 | } 62 | 63 | // Update indicates an expected call of Update 64 | func (mr *MockRepositoryMockRecorder) Update(ctx, user interface{}) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), ctx, user) 67 | } 68 | 69 | // Delete mocks base method 70 | func (m *MockRepository) Delete(ctx context.Context, userID uuid.UUID) error { 71 | m.ctrl.T.Helper() 72 | ret := m.ctrl.Call(m, "Delete", ctx, userID) 73 | ret0, _ := ret[0].(error) 74 | return ret0 75 | } 76 | 77 | // Delete indicates an expected call of Delete 78 | func (mr *MockRepositoryMockRecorder) Delete(ctx, userID interface{}) *gomock.Call { 79 | mr.mock.ctrl.T.Helper() 80 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), ctx, userID) 81 | } 82 | 83 | // GetByID mocks base method 84 | func (m *MockRepository) GetByID(ctx context.Context, userID uuid.UUID) (*models.User, error) { 85 | m.ctrl.T.Helper() 86 | ret := m.ctrl.Call(m, "GetByID", ctx, userID) 87 | ret0, _ := ret[0].(*models.User) 88 | ret1, _ := ret[1].(error) 89 | return ret0, ret1 90 | } 91 | 92 | // GetByID indicates an expected call of GetByID 93 | func (mr *MockRepositoryMockRecorder) GetByID(ctx, userID interface{}) *gomock.Call { 94 | mr.mock.ctrl.T.Helper() 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByID", reflect.TypeOf((*MockRepository)(nil).GetByID), ctx, userID) 96 | } 97 | 98 | // FindByName mocks base method 99 | func (m *MockRepository) FindByName(ctx context.Context, name string, query *utils.PaginationQuery) (*models.UsersList, error) { 100 | m.ctrl.T.Helper() 101 | ret := m.ctrl.Call(m, "FindByName", ctx, name, query) 102 | ret0, _ := ret[0].(*models.UsersList) 103 | ret1, _ := ret[1].(error) 104 | return ret0, ret1 105 | } 106 | 107 | // FindByName indicates an expected call of FindByName 108 | func (mr *MockRepositoryMockRecorder) FindByName(ctx, name, query interface{}) *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByName", reflect.TypeOf((*MockRepository)(nil).FindByName), ctx, name, query) 111 | } 112 | 113 | // FindByEmail mocks base method 114 | func (m *MockRepository) FindByEmail(ctx context.Context, user *models.User) (*models.User, error) { 115 | m.ctrl.T.Helper() 116 | ret := m.ctrl.Call(m, "FindByEmail", ctx, user) 117 | ret0, _ := ret[0].(*models.User) 118 | ret1, _ := ret[1].(error) 119 | return ret0, ret1 120 | } 121 | 122 | // FindByEmail indicates an expected call of FindByEmail 123 | func (mr *MockRepositoryMockRecorder) FindByEmail(ctx, user interface{}) *gomock.Call { 124 | mr.mock.ctrl.T.Helper() 125 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByEmail", reflect.TypeOf((*MockRepository)(nil).FindByEmail), ctx, user) 126 | } 127 | 128 | // GetUsers mocks base method 129 | func (m *MockRepository) GetUsers(ctx context.Context, pq *utils.PaginationQuery) (*models.UsersList, error) { 130 | m.ctrl.T.Helper() 131 | ret := m.ctrl.Call(m, "GetUsers", ctx, pq) 132 | ret0, _ := ret[0].(*models.UsersList) 133 | ret1, _ := ret[1].(error) 134 | return ret0, ret1 135 | } 136 | 137 | // GetUsers indicates an expected call of GetUsers 138 | func (mr *MockRepositoryMockRecorder) GetUsers(ctx, pq interface{}) *gomock.Call { 139 | mr.mock.ctrl.T.Helper() 140 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsers", reflect.TypeOf((*MockRepository)(nil).GetUsers), ctx, pq) 141 | } 142 | -------------------------------------------------------------------------------- /internal/auth/mock/redis_repository_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: redis_repository.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | models "github.com/AleksK1NG/api-mc/internal/models" 10 | gomock "github.com/golang/mock/gomock" 11 | reflect "reflect" 12 | ) 13 | 14 | // MockRedisRepository is a mock of RedisRepository interface 15 | type MockRedisRepository struct { 16 | ctrl *gomock.Controller 17 | recorder *MockRedisRepositoryMockRecorder 18 | } 19 | 20 | // MockRedisRepositoryMockRecorder is the mock recorder for MockRedisRepository 21 | type MockRedisRepositoryMockRecorder struct { 22 | mock *MockRedisRepository 23 | } 24 | 25 | // NewMockRedisRepository creates a new mock instance 26 | func NewMockRedisRepository(ctrl *gomock.Controller) *MockRedisRepository { 27 | mock := &MockRedisRepository{ctrl: ctrl} 28 | mock.recorder = &MockRedisRepositoryMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockRedisRepository) EXPECT() *MockRedisRepositoryMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // GetByIDCtx mocks base method 38 | func (m *MockRedisRepository) GetByIDCtx(ctx context.Context, key string) (*models.User, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "GetByIDCtx", ctx, key) 41 | ret0, _ := ret[0].(*models.User) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // GetByIDCtx indicates an expected call of GetByIDCtx 47 | func (mr *MockRedisRepositoryMockRecorder) GetByIDCtx(ctx, key interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByIDCtx", reflect.TypeOf((*MockRedisRepository)(nil).GetByIDCtx), ctx, key) 50 | } 51 | 52 | // SetUserCtx mocks base method 53 | func (m *MockRedisRepository) SetUserCtx(ctx context.Context, key string, seconds int, user *models.User) error { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "SetUserCtx", ctx, key, seconds, user) 56 | ret0, _ := ret[0].(error) 57 | return ret0 58 | } 59 | 60 | // SetUserCtx indicates an expected call of SetUserCtx 61 | func (mr *MockRedisRepositoryMockRecorder) SetUserCtx(ctx, key, seconds, user interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserCtx", reflect.TypeOf((*MockRedisRepository)(nil).SetUserCtx), ctx, key, seconds, user) 64 | } 65 | 66 | // DeleteUserCtx mocks base method 67 | func (m *MockRedisRepository) DeleteUserCtx(ctx context.Context, key string) error { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "DeleteUserCtx", ctx, key) 70 | ret0, _ := ret[0].(error) 71 | return ret0 72 | } 73 | 74 | // DeleteUserCtx indicates an expected call of DeleteUserCtx 75 | func (mr *MockRedisRepositoryMockRecorder) DeleteUserCtx(ctx, key interface{}) *gomock.Call { 76 | mr.mock.ctrl.T.Helper() 77 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserCtx", reflect.TypeOf((*MockRedisRepository)(nil).DeleteUserCtx), ctx, key) 78 | } 79 | -------------------------------------------------------------------------------- /internal/auth/pg_repository.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source pg_repository.go -destination mock/pg_repository_mock.go -package mock 2 | package auth 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/google/uuid" 8 | 9 | "github.com/AleksK1NG/api-mc/internal/models" 10 | "github.com/AleksK1NG/api-mc/pkg/utils" 11 | ) 12 | 13 | // Auth repository interface 14 | type Repository interface { 15 | Register(ctx context.Context, user *models.User) (*models.User, error) 16 | Update(ctx context.Context, user *models.User) (*models.User, error) 17 | Delete(ctx context.Context, userID uuid.UUID) error 18 | GetByID(ctx context.Context, userID uuid.UUID) (*models.User, error) 19 | FindByName(ctx context.Context, name string, query *utils.PaginationQuery) (*models.UsersList, error) 20 | FindByEmail(ctx context.Context, user *models.User) (*models.User, error) 21 | GetUsers(ctx context.Context, pq *utils.PaginationQuery) (*models.UsersList, error) 22 | } 23 | -------------------------------------------------------------------------------- /internal/auth/redis_repository.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source redis_repository.go -destination mock/redis_repository_mock.go -package mock 2 | package auth 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/AleksK1NG/api-mc/internal/models" 8 | ) 9 | 10 | // Auth Redis repository interface 11 | type RedisRepository interface { 12 | GetByIDCtx(ctx context.Context, key string) (*models.User, error) 13 | SetUserCtx(ctx context.Context, key string, seconds int, user *models.User) error 14 | DeleteUserCtx(ctx context.Context, key string) error 15 | } 16 | -------------------------------------------------------------------------------- /internal/auth/repository/aws_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/google/uuid" 8 | "github.com/minio/minio-go/v7" 9 | "github.com/opentracing/opentracing-go" 10 | "github.com/pkg/errors" 11 | 12 | "github.com/AleksK1NG/api-mc/internal/auth" 13 | "github.com/AleksK1NG/api-mc/internal/models" 14 | ) 15 | 16 | // Auth AWS S3 repository 17 | type authAWSRepository struct { 18 | client *minio.Client 19 | } 20 | 21 | // Auth AWS S3 repository constructor 22 | func NewAuthAWSRepository(awsClient *minio.Client) auth.AWSRepository { 23 | return &authAWSRepository{client: awsClient} 24 | } 25 | 26 | // Upload file to AWS 27 | func (aws *authAWSRepository) PutObject(ctx context.Context, input models.UploadInput) (*minio.UploadInfo, error) { 28 | span, ctx := opentracing.StartSpanFromContext(ctx, "authAWSRepository.PutObject") 29 | defer span.Finish() 30 | 31 | options := minio.PutObjectOptions{ 32 | ContentType: input.ContentType, 33 | UserMetadata: map[string]string{"x-amz-acl": "public-read"}, 34 | } 35 | 36 | uploadInfo, err := aws.client.PutObject(ctx, input.BucketName, aws.generateFileName(input.Name), input.File, input.Size, options) 37 | if err != nil { 38 | return nil, errors.Wrap(err, "authAWSRepository.FileUpload.PutObject") 39 | } 40 | 41 | return &uploadInfo, err 42 | } 43 | 44 | // Download file from AWS 45 | func (aws *authAWSRepository) GetObject(ctx context.Context, bucket string, fileName string) (*minio.Object, error) { 46 | span, ctx := opentracing.StartSpanFromContext(ctx, "authAWSRepository.GetObject") 47 | defer span.Finish() 48 | 49 | object, err := aws.client.GetObject(ctx, bucket, fileName, minio.GetObjectOptions{}) 50 | if err != nil { 51 | return nil, errors.Wrap(err, "authAWSRepository.FileDownload.GetObject") 52 | } 53 | return object, nil 54 | } 55 | 56 | // Delete file from AWS 57 | func (aws *authAWSRepository) RemoveObject(ctx context.Context, bucket string, fileName string) error { 58 | span, ctx := opentracing.StartSpanFromContext(ctx, "authAWSRepository.RemoveObject") 59 | defer span.Finish() 60 | 61 | if err := aws.client.RemoveObject(ctx, bucket, fileName, minio.RemoveObjectOptions{}); err != nil { 62 | return errors.Wrap(err, "authAWSRepository.RemoveObject") 63 | } 64 | return nil 65 | } 66 | 67 | func (aws *authAWSRepository) generateFileName(fileName string) string { 68 | uid := uuid.New().String() 69 | return fmt.Sprintf("%s-%s", uid, fileName) 70 | } 71 | -------------------------------------------------------------------------------- /internal/auth/repository/redis_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/go-redis/redis/v8" 9 | "github.com/opentracing/opentracing-go" 10 | "github.com/pkg/errors" 11 | 12 | "github.com/AleksK1NG/api-mc/internal/auth" 13 | "github.com/AleksK1NG/api-mc/internal/models" 14 | ) 15 | 16 | // Auth redis repository 17 | type authRedisRepo struct { 18 | redisClient *redis.Client 19 | } 20 | 21 | // Auth redis repository constructor 22 | func NewAuthRedisRepo(redisClient *redis.Client) auth.RedisRepository { 23 | return &authRedisRepo{redisClient: redisClient} 24 | } 25 | 26 | // Get user by id 27 | func (a *authRedisRepo) GetByIDCtx(ctx context.Context, key string) (*models.User, error) { 28 | span, ctx := opentracing.StartSpanFromContext(ctx, "authRedisRepo.GetByIDCtx") 29 | defer span.Finish() 30 | 31 | userBytes, err := a.redisClient.Get(ctx, key).Bytes() 32 | if err != nil { 33 | return nil, errors.Wrap(err, "authRedisRepo.GetByIDCtx.redisClient.Get") 34 | } 35 | user := &models.User{} 36 | if err = json.Unmarshal(userBytes, user); err != nil { 37 | return nil, errors.Wrap(err, "authRedisRepo.GetByIDCtx.json.Unmarshal") 38 | } 39 | return user, nil 40 | } 41 | 42 | // Cache user with duration in seconds 43 | func (a *authRedisRepo) SetUserCtx(ctx context.Context, key string, seconds int, user *models.User) error { 44 | span, ctx := opentracing.StartSpanFromContext(ctx, "authRedisRepo.SetUserCtx") 45 | defer span.Finish() 46 | 47 | userBytes, err := json.Marshal(user) 48 | if err != nil { 49 | return errors.Wrap(err, "authRedisRepo.SetUserCtx.json.Unmarshal") 50 | } 51 | if err = a.redisClient.Set(ctx, key, userBytes, time.Second*time.Duration(seconds)).Err(); err != nil { 52 | return errors.Wrap(err, "authRedisRepo.SetUserCtx.redisClient.Set") 53 | } 54 | return nil 55 | } 56 | 57 | // Delete user by key 58 | func (a *authRedisRepo) DeleteUserCtx(ctx context.Context, key string) error { 59 | span, ctx := opentracing.StartSpanFromContext(ctx, "authRedisRepo.DeleteUserCtx") 60 | defer span.Finish() 61 | 62 | if err := a.redisClient.Del(ctx, key).Err(); err != nil { 63 | return errors.Wrap(err, "authRedisRepo.DeleteUserCtx.redisClient.Del") 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/auth/repository/redis_repository_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "testing" 7 | 8 | "github.com/alicebob/miniredis" 9 | "github.com/go-redis/redis/v8" 10 | "github.com/google/uuid" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/AleksK1NG/api-mc/internal/auth" 14 | "github.com/AleksK1NG/api-mc/internal/models" 15 | ) 16 | 17 | func SetupRedis() auth.RedisRepository { 18 | mr, err := miniredis.Run() 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | client := redis.NewClient(&redis.Options{ 23 | Addr: mr.Addr(), 24 | }) 25 | 26 | authRedisRepo := NewAuthRedisRepo(client) 27 | return authRedisRepo 28 | } 29 | 30 | func TestAuthRedisRepo_GetByIDCtx(t *testing.T) { 31 | t.Parallel() 32 | 33 | authRedisRepo := SetupRedis() 34 | 35 | t.Run("GetByIDCtx", func(t *testing.T) { 36 | key := uuid.New().String() 37 | userID := uuid.New() 38 | u := &models.User{ 39 | UserID: userID, 40 | FirstName: "Alex", 41 | LastName: "Bryksin", 42 | } 43 | 44 | err := authRedisRepo.SetUserCtx(context.Background(), key, 10, u) 45 | require.NoError(t, err) 46 | require.Nil(t, err) 47 | 48 | user, err := authRedisRepo.GetByIDCtx(context.Background(), key) 49 | require.NoError(t, err) 50 | require.NotNil(t, user) 51 | }) 52 | } 53 | 54 | func TestAuthRedisRepo_SetUserCtx(t *testing.T) { 55 | t.Parallel() 56 | 57 | authRedisRepo := SetupRedis() 58 | 59 | t.Run("SetUserCtx", func(t *testing.T) { 60 | key := uuid.New().String() 61 | userID := uuid.New() 62 | u := &models.User{ 63 | UserID: userID, 64 | FirstName: "Alex", 65 | LastName: "Bryksin", 66 | } 67 | 68 | err := authRedisRepo.SetUserCtx(context.Background(), key, 10, u) 69 | require.NoError(t, err) 70 | require.Nil(t, err) 71 | }) 72 | } 73 | 74 | func TestAuthRedisRepo_DeleteUserCtx(t *testing.T) { 75 | t.Parallel() 76 | 77 | authRedisRepo := SetupRedis() 78 | 79 | t.Run("DeleteUserCtx", func(t *testing.T) { 80 | key := uuid.New().String() 81 | 82 | err := authRedisRepo.DeleteUserCtx(context.Background(), key) 83 | require.NoError(t, err) 84 | require.Nil(t, err) 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /internal/auth/repository/sql_queries.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | const ( 4 | createUserQuery = `INSERT INTO users (first_name, last_name, email, password, role, about, avatar, phone_number, address, 5 | city, gender, postcode, birthday, created_at, updated_at, login_date) 6 | VALUES ($1, $2, $3, $4, COALESCE(NULLIF($5, ''), 'user'), $6, $7, $8, $9, $10, $11, $12, $13, now(), now(), now()) 7 | RETURNING *` 8 | 9 | updateUserQuery = `UPDATE users 10 | SET first_name = COALESCE(NULLIF($1, ''), first_name), 11 | last_name = COALESCE(NULLIF($2, ''), last_name), 12 | email = COALESCE(NULLIF($3, ''), email), 13 | role = COALESCE(NULLIF($4, ''), role), 14 | about = COALESCE(NULLIF($5, ''), about), 15 | avatar = COALESCE(NULLIF($6, ''), avatar), 16 | phone_number = COALESCE(NULLIF($7, ''), phone_number), 17 | address = COALESCE(NULLIF($8, ''), address), 18 | city = COALESCE(NULLIF($9, ''), city), 19 | gender = COALESCE(NULLIF($10, ''), gender), 20 | postcode = COALESCE(NULLIF($11, 0), postcode), 21 | birthday = COALESCE(NULLIF($12, '')::date, birthday), 22 | updated_at = now() 23 | WHERE user_id = $13 24 | RETURNING * 25 | ` 26 | 27 | deleteUserQuery = `DELETE FROM users WHERE user_id = $1` 28 | 29 | getUserQuery = `SELECT user_id, first_name, last_name, email, role, about, avatar, phone_number, 30 | address, city, gender, postcode, birthday, created_at, updated_at, login_date 31 | FROM users 32 | WHERE user_id = $1` 33 | 34 | getTotalCount = `SELECT COUNT(user_id) FROM users 35 | WHERE first_name ILIKE '%' || $1 || '%' or last_name ILIKE '%' || $1 || '%'` 36 | 37 | findUsers = `SELECT user_id, first_name, last_name, email, role, about, avatar, phone_number, address, 38 | city, gender, postcode, birthday, created_at, updated_at, login_date 39 | FROM users 40 | WHERE first_name ILIKE '%' || $1 || '%' or last_name ILIKE '%' || $1 || '%' 41 | ORDER BY first_name, last_name 42 | OFFSET $2 LIMIT $3 43 | ` 44 | 45 | getTotal = `SELECT COUNT(user_id) FROM users` 46 | 47 | getUsers = `SELECT user_id, first_name, last_name, email, role, about, avatar, phone_number, 48 | address, city, gender, postcode, birthday, created_at, updated_at, login_date 49 | FROM users 50 | ORDER BY COALESCE(NULLIF($1, ''), first_name) OFFSET $2 LIMIT $3` 51 | 52 | findUserByEmail = `SELECT user_id, first_name, last_name, email, role, about, avatar, phone_number, 53 | address, city, gender, postcode, birthday, created_at, updated_at, login_date, password 54 | FROM users 55 | WHERE email = $1` 56 | ) 57 | -------------------------------------------------------------------------------- /internal/auth/usecase.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source usecase.go -destination mock/usecase_mock.go -package mock 2 | package auth 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/google/uuid" 8 | 9 | "github.com/AleksK1NG/api-mc/internal/models" 10 | "github.com/AleksK1NG/api-mc/pkg/utils" 11 | ) 12 | 13 | // Auth repository interface 14 | type UseCase interface { 15 | Register(ctx context.Context, user *models.User) (*models.UserWithToken, error) 16 | Login(ctx context.Context, user *models.User) (*models.UserWithToken, error) 17 | Update(ctx context.Context, user *models.User) (*models.User, error) 18 | Delete(ctx context.Context, userID uuid.UUID) error 19 | GetByID(ctx context.Context, userID uuid.UUID) (*models.User, error) 20 | FindByName(ctx context.Context, name string, query *utils.PaginationQuery) (*models.UsersList, error) 21 | GetUsers(ctx context.Context, pq *utils.PaginationQuery) (*models.UsersList, error) 22 | UploadAvatar(ctx context.Context, userID uuid.UUID, file models.UploadInput) (*models.User, error) 23 | } 24 | -------------------------------------------------------------------------------- /internal/comments/delivery.go: -------------------------------------------------------------------------------- 1 | package comments 2 | 3 | import "github.com/labstack/echo/v4" 4 | 5 | // Comments HTTP Handlers interface 6 | type Handlers interface { 7 | Create() echo.HandlerFunc 8 | Update() echo.HandlerFunc 9 | Delete() echo.HandlerFunc 10 | GetByID() echo.HandlerFunc 11 | GetAllByNewsID() echo.HandlerFunc 12 | } 13 | -------------------------------------------------------------------------------- /internal/comments/delivery/http/handlers_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/golang/mock/gomock" 12 | "github.com/google/uuid" 13 | "github.com/labstack/echo/v4" 14 | "github.com/stretchr/testify/require" 15 | 16 | "github.com/AleksK1NG/api-mc/internal/comments/mock" 17 | "github.com/AleksK1NG/api-mc/internal/comments/usecase" 18 | "github.com/AleksK1NG/api-mc/internal/models" 19 | "github.com/AleksK1NG/api-mc/pkg/converter" 20 | "github.com/AleksK1NG/api-mc/pkg/logger" 21 | "github.com/AleksK1NG/api-mc/pkg/utils" 22 | ) 23 | 24 | func TestCommentsHandlers_Create(t *testing.T) { 25 | t.Parallel() 26 | 27 | ctrl := gomock.NewController(t) 28 | defer ctrl.Finish() 29 | 30 | apiLogger := logger.NewApiLogger(nil) 31 | mockCommUC := mock.NewMockUseCase(ctrl) 32 | commUC := usecase.NewCommentsUseCase(nil, mockCommUC, apiLogger) 33 | 34 | commHandlers := NewCommentsHandlers(nil, commUC, apiLogger) 35 | handlerFunc := commHandlers.Create() 36 | 37 | userID := uuid.New() 38 | newsUID := uuid.New() 39 | comment := &models.Comment{ 40 | AuthorID: userID, 41 | Message: "message Key: 'Comment.Message' Error:Field validation for 'Message' failed on the 'gte' tag", 42 | NewsID: newsUID, 43 | } 44 | 45 | buf, err := converter.AnyToBytesBuffer(comment) 46 | require.NoError(t, err) 47 | require.NotNil(t, buf) 48 | require.Nil(t, err) 49 | 50 | req := httptest.NewRequest(http.MethodPost, "/api/v1/comments", strings.NewReader(buf.String())) 51 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 52 | res := httptest.NewRecorder() 53 | u := &models.User{ 54 | UserID: userID, 55 | } 56 | ctxWithValue := context.WithValue(context.Background(), utils.UserCtxKey{}, u) 57 | req = req.WithContext(ctxWithValue) 58 | 59 | e := echo.New() 60 | ctx := e.NewContext(req, res) 61 | 62 | mockComm := &models.Comment{ 63 | AuthorID: userID, 64 | NewsID: comment.NewsID, 65 | Message: "message", 66 | } 67 | 68 | fmt.Printf("COMMENT: %#v\n", comment) 69 | fmt.Printf("MOCK COMMENT: %#v\n", mockComm) 70 | 71 | mockCommUC.EXPECT().Create(gomock.Any(), gomock.Any()).Return(mockComm, nil) 72 | 73 | err = handlerFunc(ctx) 74 | require.NoError(t, err) 75 | } 76 | 77 | func TestCommentsHandlers_GetByID(t *testing.T) { 78 | t.Parallel() 79 | 80 | ctrl := gomock.NewController(t) 81 | defer ctrl.Finish() 82 | 83 | apiLogger := logger.NewApiLogger(nil) 84 | mockCommUC := mock.NewMockUseCase(ctrl) 85 | commUC := usecase.NewCommentsUseCase(nil, mockCommUC, apiLogger) 86 | 87 | commHandlers := NewCommentsHandlers(nil, commUC, apiLogger) 88 | handlerFunc := commHandlers.GetByID() 89 | 90 | r := httptest.NewRequest(http.MethodGet, "/api/v1/comments/5c9a9d67-ad38-499c-9858-086bfdeaf7d2", nil) 91 | w := httptest.NewRecorder() 92 | e := echo.New() 93 | c := e.NewContext(r, w) 94 | c.SetParamNames("comment_id") 95 | c.SetParamValues("5c9a9d67-ad38-499c-9858-086bfdeaf7d2") 96 | 97 | comm := &models.CommentBase{} 98 | 99 | mockCommUC.EXPECT().GetByID(gomock.Any(), gomock.Any()).Return(comm, nil) 100 | 101 | err := handlerFunc(c) 102 | require.NoError(t, err) 103 | } 104 | 105 | func TestCommentsHandlers_Delete(t *testing.T) { 106 | t.Parallel() 107 | 108 | ctrl := gomock.NewController(t) 109 | defer ctrl.Finish() 110 | 111 | apiLogger := logger.NewApiLogger(nil) 112 | mockCommUC := mock.NewMockUseCase(ctrl) 113 | commUC := usecase.NewCommentsUseCase(nil, mockCommUC, apiLogger) 114 | 115 | commHandlers := NewCommentsHandlers(nil, commUC, apiLogger) 116 | handlerFunc := commHandlers.Delete() 117 | 118 | userID := uuid.New() 119 | commID := uuid.New() 120 | comm := &models.CommentBase{ 121 | CommentID: commID, 122 | AuthorID: userID, 123 | } 124 | 125 | r := httptest.NewRequest(http.MethodDelete, "/api/v1/comments/5c9a9d67-ad38-499c-9858-086bfdeaf7d2", nil) 126 | w := httptest.NewRecorder() 127 | u := &models.User{ 128 | UserID: userID, 129 | } 130 | ctxWithValue := context.WithValue(context.Background(), utils.UserCtxKey{}, u) 131 | r = r.WithContext(ctxWithValue) 132 | e := echo.New() 133 | c := e.NewContext(r, w) 134 | c.SetParamNames("comment_id") 135 | c.SetParamValues(commID.String()) 136 | 137 | mockCommUC.EXPECT().GetByID(gomock.Any(), commID).Return(comm, nil) 138 | mockCommUC.EXPECT().Delete(gomock.Any(), commID).Return(nil) 139 | 140 | err := handlerFunc(c) 141 | require.NoError(t, err) 142 | } 143 | -------------------------------------------------------------------------------- /internal/comments/delivery/http/routes.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | 6 | "github.com/AleksK1NG/api-mc/internal/comments" 7 | "github.com/AleksK1NG/api-mc/internal/middleware" 8 | ) 9 | 10 | // Map comments routes 11 | func MapCommentsRoutes(commGroup *echo.Group, h comments.Handlers, mw *middleware.MiddlewareManager) { 12 | commGroup.POST("", h.Create(), mw.AuthSessionMiddleware, mw.CSRF) 13 | commGroup.DELETE("/:comment_id", h.Delete(), mw.AuthSessionMiddleware, mw.CSRF) 14 | commGroup.PUT("/:comment_id", h.Update(), mw.AuthSessionMiddleware, mw.CSRF) 15 | commGroup.GET("/:comment_id", h.GetByID()) 16 | commGroup.GET("/byNewsId/:news_id", h.GetAllByNewsID()) 17 | } 18 | -------------------------------------------------------------------------------- /internal/comments/mock/pg_repository_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: pg_repository.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | models "github.com/AleksK1NG/api-mc/internal/models" 10 | utils "github.com/AleksK1NG/api-mc/pkg/utils" 11 | gomock "github.com/golang/mock/gomock" 12 | uuid "github.com/google/uuid" 13 | reflect "reflect" 14 | ) 15 | 16 | // MockRepository is a mock of Repository interface 17 | type MockRepository struct { 18 | ctrl *gomock.Controller 19 | recorder *MockRepositoryMockRecorder 20 | } 21 | 22 | // MockRepositoryMockRecorder is the mock recorder for MockRepository 23 | type MockRepositoryMockRecorder struct { 24 | mock *MockRepository 25 | } 26 | 27 | // NewMockRepository creates a new mock instance 28 | func NewMockRepository(ctrl *gomock.Controller) *MockRepository { 29 | mock := &MockRepository{ctrl: ctrl} 30 | mock.recorder = &MockRepositoryMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use 35 | func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // Create mocks base method 40 | func (m *MockRepository) Create(ctx context.Context, comment *models.Comment) (*models.Comment, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "Create", ctx, comment) 43 | ret0, _ := ret[0].(*models.Comment) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // Create indicates an expected call of Create 49 | func (mr *MockRepositoryMockRecorder) Create(ctx, comment interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), ctx, comment) 52 | } 53 | 54 | // Update mocks base method 55 | func (m *MockRepository) Update(ctx context.Context, comment *models.Comment) (*models.Comment, error) { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "Update", ctx, comment) 58 | ret0, _ := ret[0].(*models.Comment) 59 | ret1, _ := ret[1].(error) 60 | return ret0, ret1 61 | } 62 | 63 | // Update indicates an expected call of Update 64 | func (mr *MockRepositoryMockRecorder) Update(ctx, comment interface{}) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), ctx, comment) 67 | } 68 | 69 | // Delete mocks base method 70 | func (m *MockRepository) Delete(ctx context.Context, commentID uuid.UUID) error { 71 | m.ctrl.T.Helper() 72 | ret := m.ctrl.Call(m, "Delete", ctx, commentID) 73 | ret0, _ := ret[0].(error) 74 | return ret0 75 | } 76 | 77 | // Delete indicates an expected call of Delete 78 | func (mr *MockRepositoryMockRecorder) Delete(ctx, commentID interface{}) *gomock.Call { 79 | mr.mock.ctrl.T.Helper() 80 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), ctx, commentID) 81 | } 82 | 83 | // GetByID mocks base method 84 | func (m *MockRepository) GetByID(ctx context.Context, commentID uuid.UUID) (*models.CommentBase, error) { 85 | m.ctrl.T.Helper() 86 | ret := m.ctrl.Call(m, "GetByID", ctx, commentID) 87 | ret0, _ := ret[0].(*models.CommentBase) 88 | ret1, _ := ret[1].(error) 89 | return ret0, ret1 90 | } 91 | 92 | // GetByID indicates an expected call of GetByID 93 | func (mr *MockRepositoryMockRecorder) GetByID(ctx, commentID interface{}) *gomock.Call { 94 | mr.mock.ctrl.T.Helper() 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByID", reflect.TypeOf((*MockRepository)(nil).GetByID), ctx, commentID) 96 | } 97 | 98 | // GetAllByNewsID mocks base method 99 | func (m *MockRepository) GetAllByNewsID(ctx context.Context, newsID uuid.UUID, query *utils.PaginationQuery) (*models.CommentsList, error) { 100 | m.ctrl.T.Helper() 101 | ret := m.ctrl.Call(m, "GetAllByNewsID", ctx, newsID, query) 102 | ret0, _ := ret[0].(*models.CommentsList) 103 | ret1, _ := ret[1].(error) 104 | return ret0, ret1 105 | } 106 | 107 | // GetAllByNewsID indicates an expected call of GetAllByNewsID 108 | func (mr *MockRepositoryMockRecorder) GetAllByNewsID(ctx, newsID, query interface{}) *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByNewsID", reflect.TypeOf((*MockRepository)(nil).GetAllByNewsID), ctx, newsID, query) 111 | } 112 | -------------------------------------------------------------------------------- /internal/comments/mock/usecase_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: usecase.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | models "github.com/AleksK1NG/api-mc/internal/models" 10 | utils "github.com/AleksK1NG/api-mc/pkg/utils" 11 | gomock "github.com/golang/mock/gomock" 12 | uuid "github.com/google/uuid" 13 | reflect "reflect" 14 | ) 15 | 16 | // MockUseCase is a mock of UseCase interface 17 | type MockUseCase struct { 18 | ctrl *gomock.Controller 19 | recorder *MockUseCaseMockRecorder 20 | } 21 | 22 | // MockUseCaseMockRecorder is the mock recorder for MockUseCase 23 | type MockUseCaseMockRecorder struct { 24 | mock *MockUseCase 25 | } 26 | 27 | // NewMockUseCase creates a new mock instance 28 | func NewMockUseCase(ctrl *gomock.Controller) *MockUseCase { 29 | mock := &MockUseCase{ctrl: ctrl} 30 | mock.recorder = &MockUseCaseMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use 35 | func (m *MockUseCase) EXPECT() *MockUseCaseMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // Create mocks base method 40 | func (m *MockUseCase) Create(ctx context.Context, comment *models.Comment) (*models.Comment, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "Create", ctx, comment) 43 | ret0, _ := ret[0].(*models.Comment) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // Create indicates an expected call of Create 49 | func (mr *MockUseCaseMockRecorder) Create(ctx, comment interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUseCase)(nil).Create), ctx, comment) 52 | } 53 | 54 | // Update mocks base method 55 | func (m *MockUseCase) Update(ctx context.Context, comment *models.Comment) (*models.Comment, error) { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "Update", ctx, comment) 58 | ret0, _ := ret[0].(*models.Comment) 59 | ret1, _ := ret[1].(error) 60 | return ret0, ret1 61 | } 62 | 63 | // Update indicates an expected call of Update 64 | func (mr *MockUseCaseMockRecorder) Update(ctx, comment interface{}) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUseCase)(nil).Update), ctx, comment) 67 | } 68 | 69 | // Delete mocks base method 70 | func (m *MockUseCase) Delete(ctx context.Context, commentID uuid.UUID) error { 71 | m.ctrl.T.Helper() 72 | ret := m.ctrl.Call(m, "Delete", ctx, commentID) 73 | ret0, _ := ret[0].(error) 74 | return ret0 75 | } 76 | 77 | // Delete indicates an expected call of Delete 78 | func (mr *MockUseCaseMockRecorder) Delete(ctx, commentID interface{}) *gomock.Call { 79 | mr.mock.ctrl.T.Helper() 80 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUseCase)(nil).Delete), ctx, commentID) 81 | } 82 | 83 | // GetByID mocks base method 84 | func (m *MockUseCase) GetByID(ctx context.Context, commentID uuid.UUID) (*models.CommentBase, error) { 85 | m.ctrl.T.Helper() 86 | ret := m.ctrl.Call(m, "GetByID", ctx, commentID) 87 | ret0, _ := ret[0].(*models.CommentBase) 88 | ret1, _ := ret[1].(error) 89 | return ret0, ret1 90 | } 91 | 92 | // GetByID indicates an expected call of GetByID 93 | func (mr *MockUseCaseMockRecorder) GetByID(ctx, commentID interface{}) *gomock.Call { 94 | mr.mock.ctrl.T.Helper() 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByID", reflect.TypeOf((*MockUseCase)(nil).GetByID), ctx, commentID) 96 | } 97 | 98 | // GetAllByNewsID mocks base method 99 | func (m *MockUseCase) GetAllByNewsID(ctx context.Context, newsID uuid.UUID, query *utils.PaginationQuery) (*models.CommentsList, error) { 100 | m.ctrl.T.Helper() 101 | ret := m.ctrl.Call(m, "GetAllByNewsID", ctx, newsID, query) 102 | ret0, _ := ret[0].(*models.CommentsList) 103 | ret1, _ := ret[1].(error) 104 | return ret0, ret1 105 | } 106 | 107 | // GetAllByNewsID indicates an expected call of GetAllByNewsID 108 | func (mr *MockUseCaseMockRecorder) GetAllByNewsID(ctx, newsID, query interface{}) *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByNewsID", reflect.TypeOf((*MockUseCase)(nil).GetAllByNewsID), ctx, newsID, query) 111 | } 112 | -------------------------------------------------------------------------------- /internal/comments/pg_repository.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source pg_repository.go -destination mock/pg_repository_mock.go -package mock 2 | package comments 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/google/uuid" 8 | 9 | "github.com/AleksK1NG/api-mc/internal/models" 10 | "github.com/AleksK1NG/api-mc/pkg/utils" 11 | ) 12 | 13 | // Comments repository interface 14 | type Repository interface { 15 | Create(ctx context.Context, comment *models.Comment) (*models.Comment, error) 16 | Update(ctx context.Context, comment *models.Comment) (*models.Comment, error) 17 | Delete(ctx context.Context, commentID uuid.UUID) error 18 | GetByID(ctx context.Context, commentID uuid.UUID) (*models.CommentBase, error) 19 | GetAllByNewsID(ctx context.Context, newsID uuid.UUID, query *utils.PaginationQuery) (*models.CommentsList, error) 20 | } 21 | -------------------------------------------------------------------------------- /internal/comments/repository/pg_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/google/uuid" 8 | "github.com/jmoiron/sqlx" 9 | "github.com/opentracing/opentracing-go" 10 | "github.com/pkg/errors" 11 | 12 | "github.com/AleksK1NG/api-mc/internal/comments" 13 | "github.com/AleksK1NG/api-mc/internal/models" 14 | "github.com/AleksK1NG/api-mc/pkg/utils" 15 | ) 16 | 17 | // Comments Repository 18 | type commentsRepo struct { 19 | db *sqlx.DB 20 | } 21 | 22 | // Comments Repository constructor 23 | func NewCommentsRepository(db *sqlx.DB) comments.Repository { 24 | return &commentsRepo{db: db} 25 | } 26 | 27 | // Create comment 28 | func (r *commentsRepo) Create(ctx context.Context, comment *models.Comment) (*models.Comment, error) { 29 | span, ctx := opentracing.StartSpanFromContext(ctx, "commentsRepo.Create") 30 | defer span.Finish() 31 | 32 | c := &models.Comment{} 33 | if err := r.db.QueryRowxContext( 34 | ctx, 35 | createComment, 36 | &comment.AuthorID, 37 | &comment.NewsID, 38 | &comment.Message, 39 | ).StructScan(c); err != nil { 40 | return nil, errors.Wrap(err, "commentsRepo.Create.StructScan") 41 | } 42 | 43 | return c, nil 44 | } 45 | 46 | // Update comment 47 | func (r *commentsRepo) Update(ctx context.Context, comment *models.Comment) (*models.Comment, error) { 48 | span, ctx := opentracing.StartSpanFromContext(ctx, "commentsRepo.Update") 49 | defer span.Finish() 50 | 51 | comm := &models.Comment{} 52 | if err := r.db.QueryRowxContext(ctx, updateComment, comment.Message, comment.CommentID).StructScan(comm); err != nil { 53 | return nil, errors.Wrap(err, "commentsRepo.Update.QueryRowxContext") 54 | } 55 | 56 | return comm, nil 57 | } 58 | 59 | // Delete comment 60 | func (r *commentsRepo) Delete(ctx context.Context, commentID uuid.UUID) error { 61 | span, ctx := opentracing.StartSpanFromContext(ctx, "commentsRepo.Delete") 62 | defer span.Finish() 63 | 64 | result, err := r.db.ExecContext(ctx, deleteComment, commentID) 65 | if err != nil { 66 | return errors.Wrap(err, "commentsRepo.Delete.ExecContext") 67 | } 68 | rowsAffected, err := result.RowsAffected() 69 | if err != nil { 70 | return errors.Wrap(err, "commentsRepo.Delete.RowsAffected") 71 | } 72 | 73 | if rowsAffected == 0 { 74 | return errors.Wrap(sql.ErrNoRows, "commentsRepo.Delete.rowsAffected") 75 | } 76 | 77 | return nil 78 | } 79 | 80 | // GetByID comment 81 | func (r *commentsRepo) GetByID(ctx context.Context, commentID uuid.UUID) (*models.CommentBase, error) { 82 | span, ctx := opentracing.StartSpanFromContext(ctx, "commentsRepo.GetByID") 83 | defer span.Finish() 84 | 85 | comment := &models.CommentBase{} 86 | if err := r.db.GetContext(ctx, comment, getCommentByID, commentID); err != nil { 87 | return nil, errors.Wrap(err, "commentsRepo.GetByID.GetContext") 88 | } 89 | return comment, nil 90 | } 91 | 92 | // GetAllByNewsID comments 93 | func (r *commentsRepo) GetAllByNewsID(ctx context.Context, newsID uuid.UUID, query *utils.PaginationQuery) (*models.CommentsList, error) { 94 | span, ctx := opentracing.StartSpanFromContext(ctx, "commentsRepo.GetAllByNewsID") 95 | defer span.Finish() 96 | 97 | var totalCount int 98 | if err := r.db.QueryRowContext(ctx, getTotalCountByNewsID, newsID).Scan(&totalCount); err != nil { 99 | return nil, errors.Wrap(err, "commentsRepo.GetAllByNewsID.QueryRowContext") 100 | } 101 | if totalCount == 0 { 102 | return &models.CommentsList{ 103 | TotalCount: totalCount, 104 | TotalPages: utils.GetTotalPages(totalCount, query.GetSize()), 105 | Page: query.GetPage(), 106 | Size: query.GetSize(), 107 | HasMore: utils.GetHasMore(query.GetPage(), totalCount, query.GetSize()), 108 | Comments: make([]*models.CommentBase, 0), 109 | }, nil 110 | } 111 | 112 | rows, err := r.db.QueryxContext(ctx, getCommentsByNewsID, newsID, query.GetOffset(), query.GetLimit()) 113 | if err != nil { 114 | return nil, errors.Wrap(err, "commentsRepo.GetAllByNewsID.QueryxContext") 115 | } 116 | defer rows.Close() 117 | 118 | commentsList := make([]*models.CommentBase, 0, query.GetSize()) 119 | for rows.Next() { 120 | comment := &models.CommentBase{} 121 | if err = rows.StructScan(comment); err != nil { 122 | return nil, errors.Wrap(err, "commentsRepo.GetAllByNewsID.StructScan") 123 | } 124 | commentsList = append(commentsList, comment) 125 | } 126 | 127 | if err = rows.Err(); err != nil { 128 | return nil, errors.Wrap(err, "commentsRepo.GetAllByNewsID.rows.Err") 129 | } 130 | 131 | return &models.CommentsList{ 132 | TotalCount: totalCount, 133 | TotalPages: utils.GetTotalPages(totalCount, query.GetSize()), 134 | Page: query.GetPage(), 135 | Size: query.GetSize(), 136 | HasMore: utils.GetHasMore(query.GetPage(), totalCount, query.GetSize()), 137 | Comments: commentsList, 138 | }, nil 139 | } 140 | -------------------------------------------------------------------------------- /internal/comments/repository/pg_repository_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/DATA-DOG/go-sqlmock" 8 | "github.com/google/uuid" 9 | "github.com/jmoiron/sqlx" 10 | "github.com/pkg/errors" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/AleksK1NG/api-mc/internal/models" 14 | ) 15 | 16 | func TestCommentsRepo_Create(t *testing.T) { 17 | t.Parallel() 18 | 19 | db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 20 | require.NoError(t, err) 21 | defer db.Close() 22 | 23 | sqlxDB := sqlx.NewDb(db, "sqlmock") 24 | defer sqlxDB.Close() 25 | 26 | commRepo := NewCommentsRepository(sqlxDB) 27 | 28 | t.Run("Create", func(t *testing.T) { 29 | authorUID := uuid.New() 30 | newsUID := uuid.New() 31 | message := "message" 32 | 33 | rows := sqlmock.NewRows([]string{"author_id", "news_id", "message"}).AddRow(authorUID, newsUID, message) 34 | 35 | comment := &models.Comment{ 36 | AuthorID: authorUID, 37 | NewsID: newsUID, 38 | Message: message, 39 | } 40 | 41 | mock.ExpectQuery(createComment).WithArgs(comment.AuthorID, &comment.NewsID, comment.Message).WillReturnRows(rows) 42 | 43 | createdComment, err := commRepo.Create(context.Background(), comment) 44 | 45 | require.NoError(t, err) 46 | require.NotNil(t, createdComment) 47 | require.Equal(t, createdComment, comment) 48 | }) 49 | 50 | t.Run("Create ERR", func(t *testing.T) { 51 | newsUID := uuid.New() 52 | message := "message" 53 | createErr := errors.New("Create comment error") 54 | 55 | comment := &models.Comment{ 56 | NewsID: newsUID, 57 | Message: message, 58 | } 59 | 60 | mock.ExpectQuery(createComment).WithArgs(comment.AuthorID, &comment.NewsID, comment.Message).WillReturnError(createErr) 61 | 62 | createdComment, err := commRepo.Create(context.Background(), comment) 63 | 64 | require.Nil(t, createdComment) 65 | require.NotNil(t, err) 66 | }) 67 | } 68 | 69 | func TestCommentsRepo_Update(t *testing.T) { 70 | t.Parallel() 71 | 72 | db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 73 | require.NoError(t, err) 74 | defer db.Close() 75 | 76 | sqlxDB := sqlx.NewDb(db, "sqlmock") 77 | defer sqlxDB.Close() 78 | 79 | commRepo := NewCommentsRepository(sqlxDB) 80 | 81 | t.Run("Update", func(t *testing.T) { 82 | commUID := uuid.New() 83 | newsUID := uuid.New() 84 | message := "message" 85 | 86 | rows := sqlmock.NewRows([]string{"comment_id", "news_id", "message"}).AddRow(commUID, newsUID, message) 87 | 88 | comment := &models.Comment{ 89 | CommentID: commUID, 90 | Message: message, 91 | } 92 | 93 | mock.ExpectQuery(updateComment).WithArgs(comment.Message, comment.CommentID).WillReturnRows(rows) 94 | 95 | createdComment, err := commRepo.Update(context.Background(), comment) 96 | 97 | require.NoError(t, err) 98 | require.NotNil(t, createdComment) 99 | require.Equal(t, createdComment.Message, comment.Message) 100 | }) 101 | 102 | t.Run("Update ERR", func(t *testing.T) { 103 | commUID := uuid.New() 104 | message := "message" 105 | updateErr := errors.New("Create comment error") 106 | 107 | comment := &models.Comment{ 108 | CommentID: commUID, 109 | Message: message, 110 | } 111 | 112 | mock.ExpectQuery(updateComment).WithArgs(comment.Message, comment.CommentID).WillReturnError(updateErr) 113 | 114 | createdComment, err := commRepo.Update(context.Background(), comment) 115 | 116 | require.NotNil(t, err) 117 | require.Nil(t, createdComment) 118 | }) 119 | } 120 | 121 | func TestCommentsRepo_Delete(t *testing.T) { 122 | t.Parallel() 123 | 124 | db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 125 | require.NoError(t, err) 126 | defer db.Close() 127 | 128 | sqlxDB := sqlx.NewDb(db, "sqlmock") 129 | defer sqlxDB.Close() 130 | 131 | commRepo := NewCommentsRepository(sqlxDB) 132 | 133 | t.Run("Delete", func(t *testing.T) { 134 | commUID := uuid.New() 135 | mock.ExpectExec(deleteComment).WithArgs(commUID).WillReturnResult(sqlmock.NewResult(1, 1)) 136 | err := commRepo.Delete(context.Background(), commUID) 137 | 138 | require.NoError(t, err) 139 | }) 140 | 141 | t.Run("Delete Err", func(t *testing.T) { 142 | commUID := uuid.New() 143 | 144 | mock.ExpectExec(deleteComment).WithArgs(commUID).WillReturnResult(sqlmock.NewResult(1, 0)) 145 | 146 | err := commRepo.Delete(context.Background(), commUID) 147 | require.NotNil(t, err) 148 | }) 149 | } 150 | -------------------------------------------------------------------------------- /internal/comments/repository/sql_queries.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | const ( 4 | createComment = `INSERT INTO comments (author_id, news_id, message) VALUES ($1, $2, $3) RETURNING *` 5 | 6 | updateComment = `UPDATE comments SET message = $1, updated_at = CURRENT_TIMESTAMP WHERE comment_id = $2 RETURNING *` 7 | 8 | deleteComment = `DELETE FROM comments WHERE comment_id = $1` 9 | 10 | getCommentByID = `SELECT concat(u.first_name, ' ', u.last_name) as author, u.avatar as avatar_url, c.message, c.likes, c.updated_at, c.author_id, c.comment_id 11 | FROM comments c 12 | LEFT JOIN users u on c.author_id = u.user_id 13 | WHERE c.comment_id = $1` 14 | 15 | getTotalCountByNewsID = `SELECT COUNT(comment_id) FROM comments WHERE news_id = $1` 16 | 17 | getCommentsByNewsID = `SELECT concat(u.first_name, ' ', u.last_name) as author, u.avatar as avatar_url, c.message, c.likes, c.updated_at, c.author_id, c.comment_id 18 | FROM comments c 19 | LEFT JOIN users u on c.author_id = u.user_id 20 | WHERE c.news_id = $1 21 | ORDER BY updated_at OFFSET $2 LIMIT $3` 22 | ) 23 | -------------------------------------------------------------------------------- /internal/comments/usecase.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source usecase.go -destination mock/usecase_mock.go -package mock 2 | package comments 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/google/uuid" 8 | 9 | "github.com/AleksK1NG/api-mc/internal/models" 10 | "github.com/AleksK1NG/api-mc/pkg/utils" 11 | ) 12 | 13 | // Comments use case 14 | type UseCase interface { 15 | Create(ctx context.Context, comment *models.Comment) (*models.Comment, error) 16 | Update(ctx context.Context, comment *models.Comment) (*models.Comment, error) 17 | Delete(ctx context.Context, commentID uuid.UUID) error 18 | GetByID(ctx context.Context, commentID uuid.UUID) (*models.CommentBase, error) 19 | GetAllByNewsID(ctx context.Context, newsID uuid.UUID, query *utils.PaginationQuery) (*models.CommentsList, error) 20 | } 21 | -------------------------------------------------------------------------------- /internal/comments/usecase/usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/google/uuid" 8 | "github.com/opentracing/opentracing-go" 9 | "github.com/pkg/errors" 10 | 11 | "github.com/AleksK1NG/api-mc/config" 12 | "github.com/AleksK1NG/api-mc/internal/comments" 13 | "github.com/AleksK1NG/api-mc/internal/models" 14 | "github.com/AleksK1NG/api-mc/pkg/httpErrors" 15 | "github.com/AleksK1NG/api-mc/pkg/logger" 16 | "github.com/AleksK1NG/api-mc/pkg/utils" 17 | ) 18 | 19 | // Comments UseCase 20 | type commentsUC struct { 21 | cfg *config.Config 22 | commRepo comments.Repository 23 | logger logger.Logger 24 | } 25 | 26 | // Comments UseCase constructor 27 | func NewCommentsUseCase(cfg *config.Config, commRepo comments.Repository, logger logger.Logger) comments.UseCase { 28 | return &commentsUC{cfg: cfg, commRepo: commRepo, logger: logger} 29 | } 30 | 31 | // Create comment 32 | func (u *commentsUC) Create(ctx context.Context, comment *models.Comment) (*models.Comment, error) { 33 | span, ctx := opentracing.StartSpanFromContext(ctx, "commentsUC.Create") 34 | defer span.Finish() 35 | return u.commRepo.Create(ctx, comment) 36 | } 37 | 38 | // Update comment 39 | func (u *commentsUC) Update(ctx context.Context, comment *models.Comment) (*models.Comment, error) { 40 | span, ctx := opentracing.StartSpanFromContext(ctx, "commentsUC.Update") 41 | defer span.Finish() 42 | 43 | comm, err := u.commRepo.GetByID(ctx, comment.CommentID) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | if err = utils.ValidateIsOwner(ctx, comm.AuthorID.String(), u.logger); err != nil { 49 | return nil, httpErrors.NewRestError(http.StatusForbidden, "Forbidden", errors.Wrap(err, "commentsUC.Update.ValidateIsOwner")) 50 | } 51 | 52 | updatedComment, err := u.commRepo.Update(ctx, comment) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return updatedComment, nil 58 | } 59 | 60 | // Delete comment 61 | func (u *commentsUC) Delete(ctx context.Context, commentID uuid.UUID) error { 62 | span, ctx := opentracing.StartSpanFromContext(ctx, "commentsUC.Delete") 63 | defer span.Finish() 64 | 65 | comm, err := u.commRepo.GetByID(ctx, commentID) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | if err = utils.ValidateIsOwner(ctx, comm.AuthorID.String(), u.logger); err != nil { 71 | return httpErrors.NewRestError(http.StatusForbidden, "Forbidden", errors.Wrap(err, "commentsUC.Delete.ValidateIsOwner")) 72 | } 73 | 74 | if err = u.commRepo.Delete(ctx, commentID); err != nil { 75 | return err 76 | } 77 | 78 | return nil 79 | } 80 | 81 | // GetByID comment 82 | func (u *commentsUC) GetByID(ctx context.Context, commentID uuid.UUID) (*models.CommentBase, error) { 83 | span, ctx := opentracing.StartSpanFromContext(ctx, "commentsUC.GetByID") 84 | defer span.Finish() 85 | 86 | return u.commRepo.GetByID(ctx, commentID) 87 | } 88 | 89 | // GetAllByNewsID comments 90 | func (u *commentsUC) GetAllByNewsID(ctx context.Context, newsID uuid.UUID, query *utils.PaginationQuery) (*models.CommentsList, error) { 91 | span, ctx := opentracing.StartSpanFromContext(ctx, "commentsUC.GetAllByNewsID") 92 | defer span.Finish() 93 | 94 | return u.commRepo.GetAllByNewsID(ctx, newsID, query) 95 | } 96 | -------------------------------------------------------------------------------- /internal/comments/usecase/usecase_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golang/mock/gomock" 8 | "github.com/google/uuid" 9 | "github.com/opentracing/opentracing-go" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/AleksK1NG/api-mc/internal/comments/mock" 13 | "github.com/AleksK1NG/api-mc/internal/models" 14 | "github.com/AleksK1NG/api-mc/pkg/logger" 15 | "github.com/AleksK1NG/api-mc/pkg/utils" 16 | ) 17 | 18 | func TestCommentsUC_Create(t *testing.T) { 19 | t.Parallel() 20 | 21 | ctrl := gomock.NewController(t) 22 | defer ctrl.Finish() 23 | 24 | apiLogger := logger.NewApiLogger(nil) 25 | mockCommRepo := mock.NewMockRepository(ctrl) 26 | commUC := NewCommentsUseCase(nil, mockCommRepo, apiLogger) 27 | 28 | comm := &models.Comment{} 29 | 30 | span, ctx := opentracing.StartSpanFromContext(context.Background(), "commentsUC.Create") 31 | defer span.Finish() 32 | 33 | mockCommRepo.EXPECT().Create(ctx, gomock.Eq(comm)).Return(comm, nil) 34 | 35 | createdComment, err := commUC.Create(context.Background(), comm) 36 | require.NoError(t, err) 37 | require.NotNil(t, createdComment) 38 | } 39 | 40 | func TestCommentsUC_Update(t *testing.T) { 41 | t.Parallel() 42 | 43 | ctrl := gomock.NewController(t) 44 | defer ctrl.Finish() 45 | 46 | apiLogger := logger.NewApiLogger(nil) 47 | mockCommRepo := mock.NewMockRepository(ctrl) 48 | commUC := NewCommentsUseCase(nil, mockCommRepo, apiLogger) 49 | 50 | authorUID := uuid.New() 51 | 52 | comm := &models.Comment{ 53 | CommentID: uuid.New(), 54 | AuthorID: authorUID, 55 | } 56 | 57 | baseComm := &models.CommentBase{ 58 | AuthorID: authorUID, 59 | } 60 | 61 | user := &models.User{ 62 | UserID: authorUID, 63 | } 64 | 65 | ctx := context.WithValue(context.Background(), utils.UserCtxKey{}, user) 66 | span, ctxWithTrace := opentracing.StartSpanFromContext(ctx, "commentsUC.Update") 67 | defer span.Finish() 68 | 69 | mockCommRepo.EXPECT().GetByID(ctxWithTrace, gomock.Eq(comm.CommentID)).Return(baseComm, nil) 70 | mockCommRepo.EXPECT().Update(ctxWithTrace, gomock.Eq(comm)).Return(comm, nil) 71 | 72 | updatedComment, err := commUC.Update(ctx, comm) 73 | require.NoError(t, err) 74 | require.NotNil(t, updatedComment) 75 | } 76 | 77 | func TestCommentsUC_Delete(t *testing.T) { 78 | t.Parallel() 79 | 80 | ctrl := gomock.NewController(t) 81 | defer ctrl.Finish() 82 | 83 | apiLogger := logger.NewApiLogger(nil) 84 | mockCommRepo := mock.NewMockRepository(ctrl) 85 | commUC := NewCommentsUseCase(nil, mockCommRepo, apiLogger) 86 | 87 | authorUID := uuid.New() 88 | 89 | comm := &models.Comment{ 90 | CommentID: uuid.New(), 91 | AuthorID: authorUID, 92 | } 93 | 94 | baseComm := &models.CommentBase{ 95 | AuthorID: authorUID, 96 | } 97 | 98 | user := &models.User{ 99 | UserID: authorUID, 100 | } 101 | 102 | ctx := context.WithValue(context.Background(), utils.UserCtxKey{}, user) 103 | span, ctxWithTrace := opentracing.StartSpanFromContext(ctx, "commentsUC.Delete") 104 | defer span.Finish() 105 | 106 | mockCommRepo.EXPECT().GetByID(ctxWithTrace, gomock.Eq(comm.CommentID)).Return(baseComm, nil) 107 | mockCommRepo.EXPECT().Delete(ctxWithTrace, gomock.Eq(comm.CommentID)).Return(nil) 108 | 109 | err := commUC.Delete(ctx, comm.CommentID) 110 | require.NoError(t, err) 111 | require.Nil(t, err) 112 | } 113 | 114 | func TestCommentsUC_GetByID(t *testing.T) { 115 | t.Parallel() 116 | 117 | ctrl := gomock.NewController(t) 118 | defer ctrl.Finish() 119 | 120 | apiLogger := logger.NewApiLogger(nil) 121 | mockCommRepo := mock.NewMockRepository(ctrl) 122 | commUC := NewCommentsUseCase(nil, mockCommRepo, apiLogger) 123 | 124 | comm := &models.Comment{ 125 | CommentID: uuid.New(), 126 | } 127 | 128 | baseComm := &models.CommentBase{} 129 | 130 | ctx := context.Background() 131 | span, ctxWithTrace := opentracing.StartSpanFromContext(ctx, "commentsUC.GetByID") 132 | defer span.Finish() 133 | 134 | mockCommRepo.EXPECT().GetByID(ctxWithTrace, gomock.Eq(comm.CommentID)).Return(baseComm, nil) 135 | 136 | commentBase, err := commUC.GetByID(ctx, comm.CommentID) 137 | require.NoError(t, err) 138 | require.Nil(t, err) 139 | require.NotNil(t, commentBase) 140 | } 141 | 142 | func TestCommentsUC_GetAllByNewsID(t *testing.T) { 143 | t.Parallel() 144 | 145 | ctrl := gomock.NewController(t) 146 | defer ctrl.Finish() 147 | 148 | apiLogger := logger.NewApiLogger(nil) 149 | mockCommRepo := mock.NewMockRepository(ctrl) 150 | commUC := NewCommentsUseCase(nil, mockCommRepo, apiLogger) 151 | 152 | newsUID := uuid.New() 153 | 154 | comm := &models.Comment{ 155 | CommentID: uuid.New(), 156 | NewsID: newsUID, 157 | } 158 | 159 | commentsList := &models.CommentsList{} 160 | 161 | ctx := context.Background() 162 | span, ctxWithTrace := opentracing.StartSpanFromContext(ctx, "commentsUC.GetAllByNewsID") 163 | defer span.Finish() 164 | 165 | query := &utils.PaginationQuery{ 166 | Size: 10, 167 | Page: 1, 168 | OrderBy: "", 169 | } 170 | 171 | mockCommRepo.EXPECT().GetAllByNewsID(ctxWithTrace, gomock.Eq(comm.NewsID), query).Return(commentsList, nil) 172 | 173 | commList, err := commUC.GetAllByNewsID(ctx, comm.NewsID, query) 174 | require.NoError(t, err) 175 | require.Nil(t, err) 176 | require.NotNil(t, commList) 177 | } 178 | -------------------------------------------------------------------------------- /internal/middleware/csrf.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo/v4" 7 | 8 | "github.com/AleksK1NG/api-mc/pkg/csrf" 9 | "github.com/AleksK1NG/api-mc/pkg/httpErrors" 10 | "github.com/AleksK1NG/api-mc/pkg/utils" 11 | ) 12 | 13 | // CSRF Middleware 14 | func (mw *MiddlewareManager) CSRF(next echo.HandlerFunc) echo.HandlerFunc { 15 | return func(ctx echo.Context) error { 16 | if !mw.cfg.Server.CSRF { 17 | return next(ctx) 18 | } 19 | 20 | token := ctx.Request().Header.Get(csrf.CSRFHeader) 21 | if token == "" { 22 | mw.logger.Errorf("CSRF Middleware get CSRF header, Token: %s, Error: %s, RequestId: %s", 23 | token, 24 | "empty CSRF token", 25 | utils.GetRequestID(ctx), 26 | ) 27 | return ctx.JSON(http.StatusForbidden, httpErrors.NewRestError(http.StatusForbidden, "Invalid CSRF Token", "no CSRF Token")) 28 | } 29 | 30 | sid, ok := ctx.Get("sid").(string) 31 | if !csrf.ValidateToken(token, sid, mw.logger) || !ok { 32 | mw.logger.Errorf("CSRF Middleware csrf.ValidateToken Token: %s, Error: %s, RequestId: %s", 33 | token, 34 | "empty token", 35 | utils.GetRequestID(ctx), 36 | ) 37 | return ctx.JSON(http.StatusForbidden, httpErrors.NewRestError(http.StatusForbidden, "Invalid CSRF Token", "no CSRF Token")) 38 | } 39 | 40 | return next(ctx) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/middleware/debug.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httputil" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | // Debug dump request middleware 12 | func (mw *MiddlewareManager) DebugMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 13 | return func(c echo.Context) error { 14 | if mw.cfg.Server.Debug { 15 | dump, err := httputil.DumpRequest(c.Request(), true) 16 | if err != nil { 17 | return c.NoContent(http.StatusInternalServerError) 18 | } 19 | mw.logger.Info(fmt.Sprintf("\nRequest dump begin :--------------\n\n%s\n\nRequest dump end :--------------", dump)) 20 | } 21 | return next(c) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/middleware/metrics.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/labstack/echo/v4" 7 | 8 | "github.com/AleksK1NG/api-mc/pkg/metric" 9 | ) 10 | 11 | // Prometheus metrics middleware 12 | func (mw *MiddlewareManager) MetricsMiddleware(metrics metric.Metrics) echo.MiddlewareFunc { 13 | return func(next echo.HandlerFunc) echo.HandlerFunc { 14 | return func(c echo.Context) error { 15 | start := time.Now() 16 | err := next(c) 17 | var status int 18 | if err != nil { 19 | status = err.(*echo.HTTPError).Code 20 | } else { 21 | status = c.Response().Status 22 | } 23 | metrics.ObserveResponseTime(status, c.Request().Method, c.Path(), time.Since(start).Seconds()) 24 | metrics.IncHits(status, c.Request().Method, c.Path()) 25 | return err 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/middleware/middlewares.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/AleksK1NG/api-mc/config" 5 | "github.com/AleksK1NG/api-mc/internal/auth" 6 | "github.com/AleksK1NG/api-mc/internal/session" 7 | "github.com/AleksK1NG/api-mc/pkg/logger" 8 | ) 9 | 10 | // Middleware manager 11 | type MiddlewareManager struct { 12 | sessUC session.UCSession 13 | authUC auth.UseCase 14 | cfg *config.Config 15 | origins []string 16 | logger logger.Logger 17 | } 18 | 19 | // Middleware manager constructor 20 | func NewMiddlewareManager(sessUC session.UCSession, authUC auth.UseCase, cfg *config.Config, origins []string, logger logger.Logger) *MiddlewareManager { 21 | return &MiddlewareManager{sessUC: sessUC, authUC: authUC, cfg: cfg, origins: origins, logger: logger} 22 | } 23 | -------------------------------------------------------------------------------- /internal/middleware/request_logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/labstack/echo/v4" 7 | 8 | "github.com/AleksK1NG/api-mc/pkg/utils" 9 | ) 10 | 11 | // Request logger middleware 12 | func (mw *MiddlewareManager) RequestLoggerMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 13 | return func(ctx echo.Context) error { 14 | start := time.Now() 15 | err := next(ctx) 16 | 17 | req := ctx.Request() 18 | res := ctx.Response() 19 | status := res.Status 20 | size := res.Size 21 | s := time.Since(start).String() 22 | requestID := utils.GetRequestID(ctx) 23 | 24 | mw.logger.Infof("RequestID: %s, Method: %s, URI: %s, Status: %v, Size: %v, Time: %s", 25 | requestID, req.Method, req.URL, status, size, s, 26 | ) 27 | return err 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/middleware/sanitize.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | 7 | "github.com/labstack/echo" 8 | 9 | "github.com/AleksK1NG/api-mc/pkg/sanitize" 10 | ) 11 | 12 | // Sanitize and read request body to ctx for next use in easy json 13 | func (mw *MiddlewareManager) Sanitize(next echo.HandlerFunc) echo.HandlerFunc { 14 | return func(ctx echo.Context) error { 15 | body, err := ioutil.ReadAll(ctx.Request().Body) 16 | if err != nil { 17 | return ctx.NoContent(http.StatusBadRequest) 18 | } 19 | defer ctx.Request().Body.Close() 20 | 21 | sanBody, err := sanitize.SanitizeJSON(body) 22 | if err != nil { 23 | return ctx.NoContent(http.StatusBadRequest) 24 | } 25 | 26 | ctx.Set("body", sanBody) 27 | return next(ctx) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/models/aws.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "io" 4 | 5 | // AWS Upload Input 6 | type UploadInput struct { 7 | File io.Reader 8 | Name string 9 | Size int64 10 | ContentType string 11 | BucketName string 12 | } 13 | -------------------------------------------------------------------------------- /internal/models/comment.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | // Comment model 10 | type Comment struct { 11 | CommentID uuid.UUID `json:"comment_id" db:"comment_id" validate:"omitempty,uuid"` 12 | AuthorID uuid.UUID `json:"author_id" db:"author_id" validate:"required"` 13 | NewsID uuid.UUID `json:"news_id" db:"news_id" validate:"required"` 14 | Message string `json:"message" db:"message" validate:"required,gte=10"` 15 | Likes int64 `json:"likes" db:"likes" validate:"omitempty"` 16 | CreatedAt time.Time `json:"created_at" db:"created_at"` 17 | UpdatedAt time.Time `json:"updated_at" db:"updated_at"` 18 | } 19 | 20 | // Base Comment response 21 | type CommentBase struct { 22 | CommentID uuid.UUID `json:"comment_id" db:"comment_id" validate:"omitempty,uuid"` 23 | AuthorID uuid.UUID `json:"author_id" db:"author_id" validate:"required"` 24 | Author string `json:"author" db:"author" validate:"required"` 25 | AvatarURL *string `json:"avatar_url" db:"avatar_url"` 26 | Message string `json:"message" db:"message" validate:"required,gte=10"` 27 | Likes int64 `json:"likes" db:"likes" validate:"omitempty"` 28 | UpdatedAt time.Time `json:"updated_at" db:"updated_at"` 29 | } 30 | 31 | // All News response 32 | type CommentsList struct { 33 | TotalCount int `json:"total_count"` 34 | TotalPages int `json:"total_pages"` 35 | Page int `json:"page"` 36 | Size int `json:"size"` 37 | HasMore bool `json:"has_more"` 38 | Comments []*CommentBase `json:"comments"` 39 | } 40 | -------------------------------------------------------------------------------- /internal/models/news.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | // News base model 10 | type News struct { 11 | NewsID uuid.UUID `json:"news_id" db:"news_id" validate:"omitempty,uuid"` 12 | AuthorID uuid.UUID `json:"author_id,omitempty" db:"author_id" validate:"required"` 13 | Title string `json:"title" db:"title" validate:"required,gte=10"` 14 | Content string `json:"content" db:"content" validate:"required,gte=20"` 15 | ImageURL *string `json:"image_url,omitempty" db:"image_url" validate:"omitempty,lte=512,url"` 16 | Category *string `json:"category,omitempty" db:"category" validate:"omitempty,lte=10"` 17 | CreatedAt time.Time `json:"created_at,omitempty" db:"created_at"` 18 | UpdatedAt time.Time `json:"updated_at,omitempty" db:"updated_at"` 19 | } 20 | 21 | // All News response 22 | type NewsList struct { 23 | TotalCount int `json:"total_count"` 24 | TotalPages int `json:"total_pages"` 25 | Page int `json:"page"` 26 | Size int `json:"size"` 27 | HasMore bool `json:"has_more"` 28 | News []*News `json:"news"` 29 | } 30 | 31 | // News base 32 | type NewsBase struct { 33 | NewsID uuid.UUID `json:"news_id" db:"news_id" validate:"omitempty,uuid"` 34 | AuthorID uuid.UUID `json:"author_id" db:"author_id" validate:"omitempty,uuid"` 35 | Title string `json:"title" db:"title" validate:"required,gte=10"` 36 | Content string `json:"content" db:"content" validate:"required,gte=20"` 37 | ImageURL *string `json:"image_url,omitempty" db:"image_url" validate:"omitempty,lte=512,url"` 38 | Category *string `json:"category,omitempty" db:"category" validate:"omitempty,lte=10"` 39 | Author string `json:"author" db:"author"` 40 | UpdatedAt time.Time `json:"updated_at,omitempty" db:"updated_at"` 41 | } 42 | -------------------------------------------------------------------------------- /internal/models/session.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/google/uuid" 4 | 5 | // Session model 6 | type Session struct { 7 | SessionID string `json:"session_id" redis:"session_id"` 8 | UserID uuid.UUID `json:"user_id" redis:"user_id"` 9 | } 10 | -------------------------------------------------------------------------------- /internal/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | "golang.org/x/crypto/bcrypt" 9 | ) 10 | 11 | // User full model 12 | type User struct { 13 | UserID uuid.UUID `json:"user_id" db:"user_id" redis:"user_id" validate:"omitempty"` 14 | FirstName string `json:"first_name" db:"first_name" redis:"first_name" validate:"required,lte=30"` 15 | LastName string `json:"last_name" db:"last_name" redis:"last_name" validate:"required,lte=30"` 16 | Email string `json:"email,omitempty" db:"email" redis:"email" validate:"omitempty,lte=60,email"` 17 | Password string `json:"password,omitempty" db:"password" redis:"password" validate:"omitempty,required,gte=6"` 18 | Role *string `json:"role,omitempty" db:"role" redis:"role" validate:"omitempty,lte=10"` 19 | About *string `json:"about,omitempty" db:"about" redis:"about" validate:"omitempty,lte=1024"` 20 | Avatar *string `json:"avatar,omitempty" db:"avatar" redis:"avatar" validate:"omitempty,lte=512,url"` 21 | PhoneNumber *string `json:"phone_number,omitempty" db:"phone_number" redis:"phone_number" validate:"omitempty,lte=20"` 22 | Address *string `json:"address,omitempty" db:"address" redis:"address" validate:"omitempty,lte=250"` 23 | City *string `json:"city,omitempty" db:"city" redis:"city" validate:"omitempty,lte=24"` 24 | Country *string `json:"country,omitempty" db:"country" redis:"country" validate:"omitempty,lte=24"` 25 | Gender *string `json:"gender,omitempty" db:"gender" redis:"gender" validate:"omitempty,lte=10"` 26 | Postcode *int `json:"postcode,omitempty" db:"postcode" redis:"postcode" validate:"omitempty"` 27 | Birthday *time.Time `json:"birthday,omitempty" db:"birthday" redis:"birthday" validate:"omitempty,lte=10"` 28 | CreatedAt time.Time `json:"created_at,omitempty" db:"created_at" redis:"created_at"` 29 | UpdatedAt time.Time `json:"updated_at,omitempty" db:"updated_at" redis:"updated_at"` 30 | LoginDate time.Time `json:"login_date" db:"login_date" redis:"login_date"` 31 | } 32 | 33 | // Hash user password with bcrypt 34 | func (u *User) HashPassword() error { 35 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) 36 | if err != nil { 37 | return err 38 | } 39 | u.Password = string(hashedPassword) 40 | return nil 41 | } 42 | 43 | // Compare user password and payload 44 | func (u *User) ComparePasswords(password string) error { 45 | if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)); err != nil { 46 | return err 47 | } 48 | return nil 49 | } 50 | 51 | // Sanitize user password 52 | func (u *User) SanitizePassword() { 53 | u.Password = "" 54 | } 55 | 56 | // Prepare user for register 57 | func (u *User) PrepareCreate() error { 58 | u.Email = strings.ToLower(strings.TrimSpace(u.Email)) 59 | u.Password = strings.TrimSpace(u.Password) 60 | 61 | if err := u.HashPassword(); err != nil { 62 | return err 63 | } 64 | 65 | if u.PhoneNumber != nil { 66 | *u.PhoneNumber = strings.TrimSpace(*u.PhoneNumber) 67 | } 68 | if u.Role != nil { 69 | *u.Role = strings.ToLower(strings.TrimSpace(*u.Role)) 70 | } 71 | return nil 72 | } 73 | 74 | // Prepare user for register 75 | func (u *User) PrepareUpdate() error { 76 | u.Email = strings.ToLower(strings.TrimSpace(u.Email)) 77 | 78 | if u.PhoneNumber != nil { 79 | *u.PhoneNumber = strings.TrimSpace(*u.PhoneNumber) 80 | } 81 | if u.Role != nil { 82 | *u.Role = strings.ToLower(strings.TrimSpace(*u.Role)) 83 | } 84 | return nil 85 | } 86 | 87 | // All Users response 88 | type UsersList struct { 89 | TotalCount int `json:"total_count"` 90 | TotalPages int `json:"total_pages"` 91 | Page int `json:"page"` 92 | Size int `json:"size"` 93 | HasMore bool `json:"has_more"` 94 | Users []*User `json:"users"` 95 | } 96 | 97 | // Find user query 98 | type UserWithToken struct { 99 | User *User `json:"user"` 100 | Token string `json:"token"` 101 | } 102 | -------------------------------------------------------------------------------- /internal/news/delivery.go: -------------------------------------------------------------------------------- 1 | package news 2 | 3 | import "github.com/labstack/echo/v4" 4 | 5 | // News HTTP Handlers interface 6 | type Handlers interface { 7 | Create() echo.HandlerFunc 8 | Update() echo.HandlerFunc 9 | GetByID() echo.HandlerFunc 10 | Delete() echo.HandlerFunc 11 | GetNews() echo.HandlerFunc 12 | SearchByTitle() echo.HandlerFunc 13 | } 14 | -------------------------------------------------------------------------------- /internal/news/delivery/http/handlers_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/golang/mock/gomock" 11 | "github.com/google/uuid" 12 | "github.com/labstack/echo/v4" 13 | "github.com/opentracing/opentracing-go" 14 | "github.com/stretchr/testify/require" 15 | 16 | "github.com/AleksK1NG/api-mc/internal/models" 17 | "github.com/AleksK1NG/api-mc/internal/news/mock" 18 | "github.com/AleksK1NG/api-mc/pkg/converter" 19 | "github.com/AleksK1NG/api-mc/pkg/logger" 20 | "github.com/AleksK1NG/api-mc/pkg/utils" 21 | ) 22 | 23 | func TestNewsHandlers_Create(t *testing.T) { 24 | t.Parallel() 25 | 26 | ctrl := gomock.NewController(t) 27 | defer ctrl.Finish() 28 | 29 | apiLogger := logger.NewApiLogger(nil) 30 | mockNewsUC := mock.NewMockUseCase(ctrl) 31 | newsHandlers := NewNewsHandlers(nil, mockNewsUC, apiLogger) 32 | 33 | handlerFunc := newsHandlers.Create() 34 | 35 | userID := uuid.New() 36 | 37 | news := &models.News{ 38 | AuthorID: userID, 39 | Title: "TestNewsHandlers_Create title", 40 | Content: "TestNewsHandlers_Create title content some text content", 41 | } 42 | 43 | buf, err := converter.AnyToBytesBuffer(news) 44 | require.NoError(t, err) 45 | require.NotNil(t, buf) 46 | require.Nil(t, err) 47 | 48 | req := httptest.NewRequest(http.MethodPost, "/api/v1/news/create", strings.NewReader(buf.String())) 49 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 50 | res := httptest.NewRecorder() 51 | u := &models.User{ 52 | UserID: userID, 53 | } 54 | ctxWithValue := context.WithValue(context.Background(), utils.UserCtxKey{}, u) 55 | req = req.WithContext(ctxWithValue) 56 | e := echo.New() 57 | ctx := e.NewContext(req, res) 58 | ctxWithReqID := utils.GetRequestCtx(ctx) 59 | span, ctxWithTrace := opentracing.StartSpanFromContext(ctxWithReqID, "newsHandlers.Create") 60 | defer span.Finish() 61 | 62 | mockNews := &models.News{ 63 | AuthorID: userID, 64 | Title: "TestNewsHandlers_Create title", 65 | Content: "TestNewsHandlers_Create title content asdasdsadsadadsad", 66 | } 67 | 68 | mockNewsUC.EXPECT().Create(ctxWithTrace, gomock.Any()).Return(mockNews, nil) 69 | 70 | err = handlerFunc(ctx) 71 | require.NoError(t, err) 72 | } 73 | 74 | func TestNewsHandlers_Update(t *testing.T) { 75 | t.Parallel() 76 | 77 | ctrl := gomock.NewController(t) 78 | defer ctrl.Finish() 79 | 80 | apiLogger := logger.NewApiLogger(nil) 81 | mockNewsUC := mock.NewMockUseCase(ctrl) 82 | newsHandlers := NewNewsHandlers(nil, mockNewsUC, apiLogger) 83 | 84 | handlerFunc := newsHandlers.Update() 85 | 86 | userID := uuid.New() 87 | 88 | news := &models.News{ 89 | AuthorID: userID, 90 | Title: "TestNewsHandlers_Create title", 91 | Content: "TestNewsHandlers_Create title content asdasdsadsadadsad", 92 | } 93 | 94 | buf, err := converter.AnyToBytesBuffer(news) 95 | require.NoError(t, err) 96 | require.NotNil(t, buf) 97 | require.Nil(t, err) 98 | 99 | req := httptest.NewRequest(http.MethodPut, "/api/v1/news/f8a3cc26-fbe1-4713-98be-a2927201356e", strings.NewReader(buf.String())) 100 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 101 | res := httptest.NewRecorder() 102 | u := &models.User{ 103 | UserID: userID, 104 | } 105 | ctxWithValue := context.WithValue(context.Background(), utils.UserCtxKey{}, u) 106 | req = req.WithContext(ctxWithValue) 107 | e := echo.New() 108 | ctx := e.NewContext(req, res) 109 | ctx.SetParamNames("news_id") 110 | ctx.SetParamValues("f8a3cc26-fbe1-4713-98be-a2927201356e") 111 | ctxWithReqID := utils.GetRequestCtx(ctx) 112 | span, ctxWithTrace := opentracing.StartSpanFromContext(ctxWithReqID, "newsHandlers.Update") 113 | defer span.Finish() 114 | 115 | mockNews := &models.News{ 116 | AuthorID: userID, 117 | Title: "TestNewsHandlers_Create title", 118 | Content: "TestNewsHandlers_Create title content asdasdsadsadadsad", 119 | } 120 | 121 | mockNewsUC.EXPECT().Update(ctxWithTrace, gomock.Any()).Return(mockNews, nil) 122 | 123 | err = handlerFunc(ctx) 124 | require.NoError(t, err) 125 | } 126 | 127 | func TestNewsHandlers_GetByID(t *testing.T) { 128 | t.Parallel() 129 | 130 | ctrl := gomock.NewController(t) 131 | defer ctrl.Finish() 132 | 133 | apiLogger := logger.NewApiLogger(nil) 134 | mockNewsUC := mock.NewMockUseCase(ctrl) 135 | newsHandlers := NewNewsHandlers(nil, mockNewsUC, apiLogger) 136 | 137 | handlerFunc := newsHandlers.GetByID() 138 | 139 | userID := uuid.New() 140 | newsID := uuid.New() 141 | req := httptest.NewRequest(http.MethodGet, "/api/v1/news/f8a3cc26-fbe1-4713-98be-a2927201356e", nil) 142 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 143 | res := httptest.NewRecorder() 144 | u := &models.User{ 145 | UserID: userID, 146 | } 147 | ctxWithValue := context.WithValue(context.Background(), utils.UserCtxKey{}, u) 148 | req = req.WithContext(ctxWithValue) 149 | e := echo.New() 150 | ctx := e.NewContext(req, res) 151 | ctx.SetParamNames("news_id") 152 | ctx.SetParamValues(newsID.String()) 153 | ctxWithReqID := utils.GetRequestCtx(ctx) 154 | span, ctxWithTrace := opentracing.StartSpanFromContext(ctxWithReqID, "newsHandlers.GetByID") 155 | defer span.Finish() 156 | 157 | mockNews := &models.NewsBase{ 158 | NewsID: newsID, 159 | AuthorID: userID, 160 | Title: "TestNewsHandlers_Create title", 161 | Content: "TestNewsHandlers_Create title content asdasdsadsadadsad", 162 | } 163 | 164 | mockNewsUC.EXPECT().GetNewsByID(ctxWithTrace, newsID).Return(mockNews, nil) 165 | 166 | err := handlerFunc(ctx) 167 | require.NoError(t, err) 168 | } 169 | -------------------------------------------------------------------------------- /internal/news/delivery/http/routes.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | 6 | "github.com/AleksK1NG/api-mc/internal/middleware" 7 | "github.com/AleksK1NG/api-mc/internal/news" 8 | ) 9 | 10 | // Map news routes 11 | func MapNewsRoutes(newsGroup *echo.Group, h news.Handlers, mw *middleware.MiddlewareManager) { 12 | newsGroup.POST("/create", h.Create(), mw.AuthSessionMiddleware, mw.CSRF) 13 | newsGroup.PUT("/:news_id", h.Update(), mw.AuthSessionMiddleware, mw.CSRF) 14 | newsGroup.DELETE("/:news_id", h.Delete(), mw.AuthSessionMiddleware, mw.CSRF) 15 | newsGroup.GET("/:news_id", h.GetByID()) 16 | newsGroup.GET("/search", h.SearchByTitle()) 17 | newsGroup.GET("", h.GetNews()) 18 | } 19 | -------------------------------------------------------------------------------- /internal/news/mock/pg_repository_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: pg_repository.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | models "github.com/AleksK1NG/api-mc/internal/models" 10 | utils "github.com/AleksK1NG/api-mc/pkg/utils" 11 | gomock "github.com/golang/mock/gomock" 12 | uuid "github.com/google/uuid" 13 | reflect "reflect" 14 | ) 15 | 16 | // MockRepository is a mock of Repository interface 17 | type MockRepository struct { 18 | ctrl *gomock.Controller 19 | recorder *MockRepositoryMockRecorder 20 | } 21 | 22 | // MockRepositoryMockRecorder is the mock recorder for MockRepository 23 | type MockRepositoryMockRecorder struct { 24 | mock *MockRepository 25 | } 26 | 27 | // NewMockRepository creates a new mock instance 28 | func NewMockRepository(ctrl *gomock.Controller) *MockRepository { 29 | mock := &MockRepository{ctrl: ctrl} 30 | mock.recorder = &MockRepositoryMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use 35 | func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // Create mocks base method 40 | func (m *MockRepository) Create(ctx context.Context, news *models.News) (*models.News, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "Create", ctx, news) 43 | ret0, _ := ret[0].(*models.News) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // Create indicates an expected call of Create 49 | func (mr *MockRepositoryMockRecorder) Create(ctx, news interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), ctx, news) 52 | } 53 | 54 | // Update mocks base method 55 | func (m *MockRepository) Update(ctx context.Context, news *models.News) (*models.News, error) { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "Update", ctx, news) 58 | ret0, _ := ret[0].(*models.News) 59 | ret1, _ := ret[1].(error) 60 | return ret0, ret1 61 | } 62 | 63 | // Update indicates an expected call of Update 64 | func (mr *MockRepositoryMockRecorder) Update(ctx, news interface{}) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), ctx, news) 67 | } 68 | 69 | // GetNewsByID mocks base method 70 | func (m *MockRepository) GetNewsByID(ctx context.Context, newsID uuid.UUID) (*models.NewsBase, error) { 71 | m.ctrl.T.Helper() 72 | ret := m.ctrl.Call(m, "GetNewsByID", ctx, newsID) 73 | ret0, _ := ret[0].(*models.NewsBase) 74 | ret1, _ := ret[1].(error) 75 | return ret0, ret1 76 | } 77 | 78 | // GetNewsByID indicates an expected call of GetNewsByID 79 | func (mr *MockRepositoryMockRecorder) GetNewsByID(ctx, newsID interface{}) *gomock.Call { 80 | mr.mock.ctrl.T.Helper() 81 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNewsByID", reflect.TypeOf((*MockRepository)(nil).GetNewsByID), ctx, newsID) 82 | } 83 | 84 | // Delete mocks base method 85 | func (m *MockRepository) Delete(ctx context.Context, newsID uuid.UUID) error { 86 | m.ctrl.T.Helper() 87 | ret := m.ctrl.Call(m, "Delete", ctx, newsID) 88 | ret0, _ := ret[0].(error) 89 | return ret0 90 | } 91 | 92 | // Delete indicates an expected call of Delete 93 | func (mr *MockRepositoryMockRecorder) Delete(ctx, newsID interface{}) *gomock.Call { 94 | mr.mock.ctrl.T.Helper() 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), ctx, newsID) 96 | } 97 | 98 | // GetNews mocks base method 99 | func (m *MockRepository) GetNews(ctx context.Context, pq *utils.PaginationQuery) (*models.NewsList, error) { 100 | m.ctrl.T.Helper() 101 | ret := m.ctrl.Call(m, "GetNews", ctx, pq) 102 | ret0, _ := ret[0].(*models.NewsList) 103 | ret1, _ := ret[1].(error) 104 | return ret0, ret1 105 | } 106 | 107 | // GetNews indicates an expected call of GetNews 108 | func (mr *MockRepositoryMockRecorder) GetNews(ctx, pq interface{}) *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNews", reflect.TypeOf((*MockRepository)(nil).GetNews), ctx, pq) 111 | } 112 | 113 | // SearchByTitle mocks base method 114 | func (m *MockRepository) SearchByTitle(ctx context.Context, title string, query *utils.PaginationQuery) (*models.NewsList, error) { 115 | m.ctrl.T.Helper() 116 | ret := m.ctrl.Call(m, "SearchByTitle", ctx, title, query) 117 | ret0, _ := ret[0].(*models.NewsList) 118 | ret1, _ := ret[1].(error) 119 | return ret0, ret1 120 | } 121 | 122 | // SearchByTitle indicates an expected call of SearchByTitle 123 | func (mr *MockRepositoryMockRecorder) SearchByTitle(ctx, title, query interface{}) *gomock.Call { 124 | mr.mock.ctrl.T.Helper() 125 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchByTitle", reflect.TypeOf((*MockRepository)(nil).SearchByTitle), ctx, title, query) 126 | } 127 | -------------------------------------------------------------------------------- /internal/news/mock/redis_repository_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: redis_repository.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | models "github.com/AleksK1NG/api-mc/internal/models" 10 | gomock "github.com/golang/mock/gomock" 11 | reflect "reflect" 12 | ) 13 | 14 | // MockRedisRepository is a mock of RedisRepository interface 15 | type MockRedisRepository struct { 16 | ctrl *gomock.Controller 17 | recorder *MockRedisRepositoryMockRecorder 18 | } 19 | 20 | // MockRedisRepositoryMockRecorder is the mock recorder for MockRedisRepository 21 | type MockRedisRepositoryMockRecorder struct { 22 | mock *MockRedisRepository 23 | } 24 | 25 | // NewMockRedisRepository creates a new mock instance 26 | func NewMockRedisRepository(ctrl *gomock.Controller) *MockRedisRepository { 27 | mock := &MockRedisRepository{ctrl: ctrl} 28 | mock.recorder = &MockRedisRepositoryMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockRedisRepository) EXPECT() *MockRedisRepositoryMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // GetNewsByIDCtx mocks base method 38 | func (m *MockRedisRepository) GetNewsByIDCtx(ctx context.Context, key string) (*models.NewsBase, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "GetNewsByIDCtx", ctx, key) 41 | ret0, _ := ret[0].(*models.NewsBase) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // GetNewsByIDCtx indicates an expected call of GetNewsByIDCtx 47 | func (mr *MockRedisRepositoryMockRecorder) GetNewsByIDCtx(ctx, key interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNewsByIDCtx", reflect.TypeOf((*MockRedisRepository)(nil).GetNewsByIDCtx), ctx, key) 50 | } 51 | 52 | // SetNewsCtx mocks base method 53 | func (m *MockRedisRepository) SetNewsCtx(ctx context.Context, key string, seconds int, news *models.NewsBase) error { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "SetNewsCtx", ctx, key, seconds, news) 56 | ret0, _ := ret[0].(error) 57 | return ret0 58 | } 59 | 60 | // SetNewsCtx indicates an expected call of SetNewsCtx 61 | func (mr *MockRedisRepositoryMockRecorder) SetNewsCtx(ctx, key, seconds, news interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNewsCtx", reflect.TypeOf((*MockRedisRepository)(nil).SetNewsCtx), ctx, key, seconds, news) 64 | } 65 | 66 | // DeleteNewsCtx mocks base method 67 | func (m *MockRedisRepository) DeleteNewsCtx(ctx context.Context, key string) error { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "DeleteNewsCtx", ctx, key) 70 | ret0, _ := ret[0].(error) 71 | return ret0 72 | } 73 | 74 | // DeleteNewsCtx indicates an expected call of DeleteNewsCtx 75 | func (mr *MockRedisRepositoryMockRecorder) DeleteNewsCtx(ctx, key interface{}) *gomock.Call { 76 | mr.mock.ctrl.T.Helper() 77 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNewsCtx", reflect.TypeOf((*MockRedisRepository)(nil).DeleteNewsCtx), ctx, key) 78 | } 79 | -------------------------------------------------------------------------------- /internal/news/mock/usecase_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: usecase.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | models "github.com/AleksK1NG/api-mc/internal/models" 10 | utils "github.com/AleksK1NG/api-mc/pkg/utils" 11 | gomock "github.com/golang/mock/gomock" 12 | uuid "github.com/google/uuid" 13 | reflect "reflect" 14 | ) 15 | 16 | // MockUseCase is a mock of UseCase interface 17 | type MockUseCase struct { 18 | ctrl *gomock.Controller 19 | recorder *MockUseCaseMockRecorder 20 | } 21 | 22 | // MockUseCaseMockRecorder is the mock recorder for MockUseCase 23 | type MockUseCaseMockRecorder struct { 24 | mock *MockUseCase 25 | } 26 | 27 | // NewMockUseCase creates a new mock instance 28 | func NewMockUseCase(ctrl *gomock.Controller) *MockUseCase { 29 | mock := &MockUseCase{ctrl: ctrl} 30 | mock.recorder = &MockUseCaseMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use 35 | func (m *MockUseCase) EXPECT() *MockUseCaseMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // Create mocks base method 40 | func (m *MockUseCase) Create(ctx context.Context, news *models.News) (*models.News, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "Create", ctx, news) 43 | ret0, _ := ret[0].(*models.News) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // Create indicates an expected call of Create 49 | func (mr *MockUseCaseMockRecorder) Create(ctx, news interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUseCase)(nil).Create), ctx, news) 52 | } 53 | 54 | // Update mocks base method 55 | func (m *MockUseCase) Update(ctx context.Context, news *models.News) (*models.News, error) { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "Update", ctx, news) 58 | ret0, _ := ret[0].(*models.News) 59 | ret1, _ := ret[1].(error) 60 | return ret0, ret1 61 | } 62 | 63 | // Update indicates an expected call of Update 64 | func (mr *MockUseCaseMockRecorder) Update(ctx, news interface{}) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUseCase)(nil).Update), ctx, news) 67 | } 68 | 69 | // GetNewsByID mocks base method 70 | func (m *MockUseCase) GetNewsByID(ctx context.Context, newsID uuid.UUID) (*models.NewsBase, error) { 71 | m.ctrl.T.Helper() 72 | ret := m.ctrl.Call(m, "GetNewsByID", ctx, newsID) 73 | ret0, _ := ret[0].(*models.NewsBase) 74 | ret1, _ := ret[1].(error) 75 | return ret0, ret1 76 | } 77 | 78 | // GetNewsByID indicates an expected call of GetNewsByID 79 | func (mr *MockUseCaseMockRecorder) GetNewsByID(ctx, newsID interface{}) *gomock.Call { 80 | mr.mock.ctrl.T.Helper() 81 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNewsByID", reflect.TypeOf((*MockUseCase)(nil).GetNewsByID), ctx, newsID) 82 | } 83 | 84 | // Delete mocks base method 85 | func (m *MockUseCase) Delete(ctx context.Context, newsID uuid.UUID) error { 86 | m.ctrl.T.Helper() 87 | ret := m.ctrl.Call(m, "Delete", ctx, newsID) 88 | ret0, _ := ret[0].(error) 89 | return ret0 90 | } 91 | 92 | // Delete indicates an expected call of Delete 93 | func (mr *MockUseCaseMockRecorder) Delete(ctx, newsID interface{}) *gomock.Call { 94 | mr.mock.ctrl.T.Helper() 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUseCase)(nil).Delete), ctx, newsID) 96 | } 97 | 98 | // GetNews mocks base method 99 | func (m *MockUseCase) GetNews(ctx context.Context, pq *utils.PaginationQuery) (*models.NewsList, error) { 100 | m.ctrl.T.Helper() 101 | ret := m.ctrl.Call(m, "GetNews", ctx, pq) 102 | ret0, _ := ret[0].(*models.NewsList) 103 | ret1, _ := ret[1].(error) 104 | return ret0, ret1 105 | } 106 | 107 | // GetNews indicates an expected call of GetNews 108 | func (mr *MockUseCaseMockRecorder) GetNews(ctx, pq interface{}) *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNews", reflect.TypeOf((*MockUseCase)(nil).GetNews), ctx, pq) 111 | } 112 | 113 | // SearchByTitle mocks base method 114 | func (m *MockUseCase) SearchByTitle(ctx context.Context, title string, query *utils.PaginationQuery) (*models.NewsList, error) { 115 | m.ctrl.T.Helper() 116 | ret := m.ctrl.Call(m, "SearchByTitle", ctx, title, query) 117 | ret0, _ := ret[0].(*models.NewsList) 118 | ret1, _ := ret[1].(error) 119 | return ret0, ret1 120 | } 121 | 122 | // SearchByTitle indicates an expected call of SearchByTitle 123 | func (mr *MockUseCaseMockRecorder) SearchByTitle(ctx, title, query interface{}) *gomock.Call { 124 | mr.mock.ctrl.T.Helper() 125 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchByTitle", reflect.TypeOf((*MockUseCase)(nil).SearchByTitle), ctx, title, query) 126 | } 127 | -------------------------------------------------------------------------------- /internal/news/pg_repository.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source pg_repository.go -destination mock/pg_repository_mock.go -package mock 2 | package news 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/google/uuid" 8 | 9 | "github.com/AleksK1NG/api-mc/internal/models" 10 | "github.com/AleksK1NG/api-mc/pkg/utils" 11 | ) 12 | 13 | // News Repository 14 | type Repository interface { 15 | Create(ctx context.Context, news *models.News) (*models.News, error) 16 | Update(ctx context.Context, news *models.News) (*models.News, error) 17 | GetNewsByID(ctx context.Context, newsID uuid.UUID) (*models.NewsBase, error) 18 | Delete(ctx context.Context, newsID uuid.UUID) error 19 | GetNews(ctx context.Context, pq *utils.PaginationQuery) (*models.NewsList, error) 20 | SearchByTitle(ctx context.Context, title string, query *utils.PaginationQuery) (*models.NewsList, error) 21 | } 22 | -------------------------------------------------------------------------------- /internal/news/redis_repository.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source redis_repository.go -destination mock/redis_repository_mock.go -package mock 2 | package news 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/AleksK1NG/api-mc/internal/models" 8 | ) 9 | 10 | // News redis repository 11 | type RedisRepository interface { 12 | GetNewsByIDCtx(ctx context.Context, key string) (*models.NewsBase, error) 13 | SetNewsCtx(ctx context.Context, key string, seconds int, news *models.NewsBase) error 14 | DeleteNewsCtx(ctx context.Context, key string) error 15 | } 16 | -------------------------------------------------------------------------------- /internal/news/repository/pg_repository_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/DATA-DOG/go-sqlmock" 8 | "github.com/google/uuid" 9 | "github.com/jmoiron/sqlx" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/AleksK1NG/api-mc/internal/models" 13 | ) 14 | 15 | func TestNewsRepo_Create(t *testing.T) { 16 | t.Parallel() 17 | 18 | db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 19 | require.NoError(t, err) 20 | defer db.Close() 21 | 22 | sqlxDB := sqlx.NewDb(db, "sqlmock") 23 | defer sqlxDB.Close() 24 | 25 | newsRepo := NewNewsRepository(sqlxDB) 26 | 27 | t.Run("Create", func(t *testing.T) { 28 | authorUID := uuid.New() 29 | title := "title" 30 | content := "content" 31 | 32 | rows := sqlmock.NewRows([]string{"author_id", "title", "content"}).AddRow(authorUID, title, content) 33 | 34 | news := &models.News{ 35 | AuthorID: authorUID, 36 | Title: title, 37 | Content: content, 38 | } 39 | 40 | mock.ExpectQuery(createNews).WithArgs(news.AuthorID, news.Title, news.Content, news.Category).WillReturnRows(rows) 41 | 42 | createdNews, err := newsRepo.Create(context.Background(), news) 43 | 44 | require.NoError(t, err) 45 | require.NotNil(t, createdNews) 46 | require.Equal(t, news.Title, createdNews.Title) 47 | }) 48 | } 49 | 50 | func TestNewsRepo_Update(t *testing.T) { 51 | t.Parallel() 52 | 53 | db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 54 | require.NoError(t, err) 55 | defer db.Close() 56 | 57 | sqlxDB := sqlx.NewDb(db, "sqlmock") 58 | defer sqlxDB.Close() 59 | 60 | newsRepo := NewNewsRepository(sqlxDB) 61 | 62 | t.Run("Update", func(t *testing.T) { 63 | newsUID := uuid.New() 64 | title := "title" 65 | content := "content" 66 | 67 | rows := sqlmock.NewRows([]string{"news_id", "title", "content"}).AddRow(newsUID, title, content) 68 | 69 | news := &models.News{ 70 | NewsID: newsUID, 71 | Title: title, 72 | Content: content, 73 | } 74 | 75 | mock.ExpectQuery(updateNews).WithArgs(news.Title, 76 | news.Content, 77 | news.ImageURL, 78 | news.Category, 79 | news.NewsID, 80 | ).WillReturnRows(rows) 81 | 82 | updatedNews, err := newsRepo.Update(context.Background(), news) 83 | 84 | require.NoError(t, err) 85 | require.NotNil(t, updateNews) 86 | require.Equal(t, updatedNews, news) 87 | }) 88 | } 89 | 90 | func TestNewsRepo_Delete(t *testing.T) { 91 | t.Parallel() 92 | 93 | db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 94 | require.NoError(t, err) 95 | defer db.Close() 96 | 97 | sqlxDB := sqlx.NewDb(db, "sqlmock") 98 | defer sqlxDB.Close() 99 | 100 | newsRepo := NewNewsRepository(sqlxDB) 101 | 102 | t.Run("Delete", func(t *testing.T) { 103 | newsUID := uuid.New() 104 | mock.ExpectExec(deleteNews).WithArgs(newsUID).WillReturnResult(sqlmock.NewResult(1, 1)) 105 | 106 | err := newsRepo.Delete(context.Background(), newsUID) 107 | 108 | require.NoError(t, err) 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /internal/news/repository/redis_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/go-redis/redis/v8" 9 | "github.com/opentracing/opentracing-go" 10 | "github.com/pkg/errors" 11 | 12 | "github.com/AleksK1NG/api-mc/internal/models" 13 | "github.com/AleksK1NG/api-mc/internal/news" 14 | ) 15 | 16 | // News redis repository 17 | type newsRedisRepo struct { 18 | redisClient *redis.Client 19 | } 20 | 21 | // News redis repository constructor 22 | func NewNewsRedisRepo(redisClient *redis.Client) news.RedisRepository { 23 | return &newsRedisRepo{redisClient: redisClient} 24 | } 25 | 26 | // Get new by id 27 | func (n *newsRedisRepo) GetNewsByIDCtx(ctx context.Context, key string) (*models.NewsBase, error) { 28 | span, ctx := opentracing.StartSpanFromContext(ctx, "newsRedisRepo.GetNewsByIDCtx") 29 | defer span.Finish() 30 | 31 | newsBytes, err := n.redisClient.Get(ctx, key).Bytes() 32 | if err != nil { 33 | return nil, errors.Wrap(err, "newsRedisRepo.GetNewsByIDCtx.redisClient.Get") 34 | } 35 | newsBase := &models.NewsBase{} 36 | if err = json.Unmarshal(newsBytes, newsBase); err != nil { 37 | return nil, errors.Wrap(err, "newsRedisRepo.GetNewsByIDCtx.json.Unmarshal") 38 | } 39 | 40 | return newsBase, nil 41 | } 42 | 43 | // Cache news item 44 | func (n *newsRedisRepo) SetNewsCtx(ctx context.Context, key string, seconds int, news *models.NewsBase) error { 45 | span, ctx := opentracing.StartSpanFromContext(ctx, "newsRedisRepo.SetNewsCtx") 46 | defer span.Finish() 47 | 48 | newsBytes, err := json.Marshal(news) 49 | if err != nil { 50 | return errors.Wrap(err, "newsRedisRepo.SetNewsCtx.json.Marshal") 51 | } 52 | if err = n.redisClient.Set(ctx, key, newsBytes, time.Second*time.Duration(seconds)).Err(); err != nil { 53 | return errors.Wrap(err, "newsRedisRepo.SetNewsCtx.redisClient.Set") 54 | } 55 | return nil 56 | } 57 | 58 | // Delete new item from cache 59 | func (n *newsRedisRepo) DeleteNewsCtx(ctx context.Context, key string) error { 60 | span, ctx := opentracing.StartSpanFromContext(ctx, "newsRedisRepo.DeleteNewsCtx") 61 | defer span.Finish() 62 | 63 | if err := n.redisClient.Del(ctx, key).Err(); err != nil { 64 | return errors.Wrap(err, "newsRedisRepo.DeleteNewsCtx.redisClient.Del") 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /internal/news/repository/redis_repository_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "testing" 7 | 8 | "github.com/alicebob/miniredis" 9 | "github.com/go-redis/redis/v8" 10 | "github.com/google/uuid" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/AleksK1NG/api-mc/internal/models" 14 | "github.com/AleksK1NG/api-mc/internal/news" 15 | ) 16 | 17 | func SetupRedis() news.RedisRepository { 18 | mr, err := miniredis.Run() 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | client := redis.NewClient(&redis.Options{ 23 | Addr: mr.Addr(), 24 | }) 25 | 26 | newsRedisRepo := NewNewsRedisRepo(client) 27 | return newsRedisRepo 28 | } 29 | 30 | func TestNewsRedisRepo_SetNewsCtx(t *testing.T) { 31 | t.Parallel() 32 | 33 | newsRedisRepo := SetupRedis() 34 | 35 | t.Run("SetNewsCtx", func(t *testing.T) { 36 | newsUID := uuid.New() 37 | key := "key" 38 | n := &models.NewsBase{ 39 | NewsID: newsUID, 40 | Title: "Title", 41 | Content: "Content", 42 | } 43 | 44 | err := newsRedisRepo.SetNewsCtx(context.Background(), key, 10, n) 45 | require.NoError(t, err) 46 | require.Nil(t, err) 47 | }) 48 | } 49 | 50 | func TestNewsRedisRepo_GetNewsByIDCtx(t *testing.T) { 51 | t.Parallel() 52 | 53 | newsRedisRepo := SetupRedis() 54 | 55 | t.Run("GetNewsByIDCtx", func(t *testing.T) { 56 | newsUID := uuid.New() 57 | key := "key" 58 | n := &models.NewsBase{ 59 | NewsID: newsUID, 60 | Title: "Title", 61 | Content: "Content", 62 | } 63 | 64 | newsBase, err := newsRedisRepo.GetNewsByIDCtx(context.Background(), key) 65 | require.Nil(t, newsBase) 66 | require.NotNil(t, err) 67 | 68 | err = newsRedisRepo.SetNewsCtx(context.Background(), key, 10, n) 69 | require.NoError(t, err) 70 | require.Nil(t, err) 71 | 72 | newsBase, err = newsRedisRepo.GetNewsByIDCtx(context.Background(), key) 73 | require.NoError(t, err) 74 | require.Nil(t, err) 75 | require.NotNil(t, newsBase) 76 | }) 77 | } 78 | 79 | func TestNewsRedisRepo_DeleteNewsCtx(t *testing.T) { 80 | t.Parallel() 81 | 82 | newsRedisRepo := SetupRedis() 83 | 84 | t.Run("SetNewsCtx", func(t *testing.T) { 85 | key := "key" 86 | 87 | err := newsRedisRepo.DeleteNewsCtx(context.Background(), key) 88 | require.NoError(t, err) 89 | require.Nil(t, err) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /internal/news/repository/sql_queries.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | const ( 4 | createNews = `INSERT INTO news (author_id, title, content, image_url, category, created_at) 5 | VALUES ($1, $2, $3, NULLIF($4, ''), NULLIF($4, ''), now()) 6 | RETURNING *` 7 | 8 | updateNews = `UPDATE news 9 | SET title = COALESCE(NULLIF($1, ''), title), 10 | content = COALESCE(NULLIF($2, ''), content), 11 | image_url = COALESCE(NULLIF($3, ''), image_url), 12 | category = COALESCE(NULLIF($4, ''), category), 13 | updated_at = now() 14 | WHERE news_id = $5 15 | RETURNING *` 16 | 17 | getNewsByID = `SELECT n.news_id, 18 | n.title, 19 | n.content, 20 | n.updated_at, 21 | n.image_url, 22 | n.category, 23 | CONCAT(u.first_name, ' ', u.last_name) as author, 24 | u.user_id as author_id 25 | FROM news n 26 | LEFT JOIN users u on u.user_id = n.author_id 27 | WHERE news_id = $1` 28 | 29 | deleteNews = `DELETE FROM news WHERE news_id = $1` 30 | 31 | getTotalCount = `SELECT COUNT(news_id) FROM news` 32 | 33 | getNews = `SELECT news_id, author_id, title, content, image_url, category, updated_at, created_at 34 | FROM news 35 | ORDER BY created_at, updated_at OFFSET $1 LIMIT $2` 36 | 37 | findByTitleCount = `SELECT COUNT(*) 38 | FROM news 39 | WHERE title ILIKE '%' || $1 || '%'` 40 | 41 | findByTitle = `SELECT news_id, author_id, title, content, image_url, category, updated_at, created_at 42 | FROM news 43 | WHERE title ILIKE '%' || $1 || '%' 44 | ORDER BY title, created_at, updated_at 45 | OFFSET $2 LIMIT $3` 46 | ) 47 | -------------------------------------------------------------------------------- /internal/news/usecase.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source usecase.go -destination mock/usecase_mock.go -package mock 2 | package news 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/google/uuid" 8 | 9 | "github.com/AleksK1NG/api-mc/internal/models" 10 | "github.com/AleksK1NG/api-mc/pkg/utils" 11 | ) 12 | 13 | // News use case 14 | type UseCase interface { 15 | Create(ctx context.Context, news *models.News) (*models.News, error) 16 | Update(ctx context.Context, news *models.News) (*models.News, error) 17 | GetNewsByID(ctx context.Context, newsID uuid.UUID) (*models.NewsBase, error) 18 | Delete(ctx context.Context, newsID uuid.UUID) error 19 | GetNews(ctx context.Context, pq *utils.PaginationQuery) (*models.NewsList, error) 20 | SearchByTitle(ctx context.Context, title string, query *utils.PaginationQuery) (*models.NewsList, error) 21 | } 22 | -------------------------------------------------------------------------------- /internal/news/usecase/usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/google/uuid" 9 | "github.com/opentracing/opentracing-go" 10 | "github.com/pkg/errors" 11 | 12 | "github.com/AleksK1NG/api-mc/config" 13 | "github.com/AleksK1NG/api-mc/internal/models" 14 | "github.com/AleksK1NG/api-mc/internal/news" 15 | "github.com/AleksK1NG/api-mc/pkg/httpErrors" 16 | "github.com/AleksK1NG/api-mc/pkg/logger" 17 | "github.com/AleksK1NG/api-mc/pkg/utils" 18 | ) 19 | 20 | const ( 21 | basePrefix = "api-news:" 22 | cacheDuration = 3600 23 | ) 24 | 25 | // News UseCase 26 | type newsUC struct { 27 | cfg *config.Config 28 | newsRepo news.Repository 29 | redisRepo news.RedisRepository 30 | logger logger.Logger 31 | } 32 | 33 | // News UseCase constructor 34 | func NewNewsUseCase(cfg *config.Config, newsRepo news.Repository, redisRepo news.RedisRepository, logger logger.Logger) news.UseCase { 35 | return &newsUC{cfg: cfg, newsRepo: newsRepo, redisRepo: redisRepo, logger: logger} 36 | } 37 | 38 | // Create news 39 | func (u *newsUC) Create(ctx context.Context, news *models.News) (*models.News, error) { 40 | span, ctx := opentracing.StartSpanFromContext(ctx, "newsUC.Create") 41 | defer span.Finish() 42 | 43 | user, err := utils.GetUserFromCtx(ctx) 44 | if err != nil { 45 | return nil, httpErrors.NewUnauthorizedError(errors.WithMessage(err, "newsUC.Create.GetUserFromCtx")) 46 | } 47 | 48 | news.AuthorID = user.UserID 49 | 50 | if err = utils.ValidateStruct(ctx, news); err != nil { 51 | return nil, httpErrors.NewBadRequestError(errors.WithMessage(err, "newsUC.Create.ValidateStruct")) 52 | } 53 | 54 | n, err := u.newsRepo.Create(ctx, news) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return n, err 60 | } 61 | 62 | // Update news item 63 | func (u *newsUC) Update(ctx context.Context, news *models.News) (*models.News, error) { 64 | span, ctx := opentracing.StartSpanFromContext(ctx, "newsUC.Update") 65 | defer span.Finish() 66 | 67 | newsByID, err := u.newsRepo.GetNewsByID(ctx, news.NewsID) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | if err = utils.ValidateIsOwner(ctx, newsByID.AuthorID.String(), u.logger); err != nil { 73 | return nil, httpErrors.NewRestError(http.StatusForbidden, "Forbidden", errors.Wrap(err, "newsUC.Update.ValidateIsOwner")) 74 | } 75 | 76 | updatedUser, err := u.newsRepo.Update(ctx, news) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | if err = u.redisRepo.DeleteNewsCtx(ctx, u.getKeyWithPrefix(news.NewsID.String())); err != nil { 82 | u.logger.Errorf("newsUC.Update.DeleteNewsCtx: %v", err) 83 | } 84 | 85 | return updatedUser, nil 86 | } 87 | 88 | // Get news by id 89 | func (u *newsUC) GetNewsByID(ctx context.Context, newsID uuid.UUID) (*models.NewsBase, error) { 90 | span, ctx := opentracing.StartSpanFromContext(ctx, "newsUC.GetNewsByID") 91 | defer span.Finish() 92 | 93 | newsBase, err := u.redisRepo.GetNewsByIDCtx(ctx, u.getKeyWithPrefix(newsID.String())) 94 | if err != nil { 95 | u.logger.Errorf("newsUC.GetNewsByID.GetNewsByIDCtx: %v", err) 96 | } 97 | if newsBase != nil { 98 | return newsBase, nil 99 | } 100 | 101 | n, err := u.newsRepo.GetNewsByID(ctx, newsID) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | if err = u.redisRepo.SetNewsCtx(ctx, u.getKeyWithPrefix(newsID.String()), cacheDuration, n); err != nil { 107 | u.logger.Errorf("newsUC.GetNewsByID.SetNewsCtx: %s", err) 108 | } 109 | 110 | return n, nil 111 | } 112 | 113 | // Delete news 114 | func (u *newsUC) Delete(ctx context.Context, newsID uuid.UUID) error { 115 | span, ctx := opentracing.StartSpanFromContext(ctx, "newsUC.Delete") 116 | defer span.Finish() 117 | 118 | newsByID, err := u.newsRepo.GetNewsByID(ctx, newsID) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | if err = utils.ValidateIsOwner(ctx, newsByID.AuthorID.String(), u.logger); err != nil { 124 | return httpErrors.NewRestError(http.StatusForbidden, "Forbidden", errors.Wrap(err, "newsUC.Delete.ValidateIsOwner")) 125 | } 126 | 127 | if err = u.newsRepo.Delete(ctx, newsID); err != nil { 128 | return err 129 | } 130 | 131 | if err = u.redisRepo.DeleteNewsCtx(ctx, u.getKeyWithPrefix(newsID.String())); err != nil { 132 | u.logger.Errorf("newsUC.Delete.DeleteNewsCtx: %v", err) 133 | } 134 | 135 | return nil 136 | } 137 | 138 | // Get news 139 | func (u *newsUC) GetNews(ctx context.Context, pq *utils.PaginationQuery) (*models.NewsList, error) { 140 | span, ctx := opentracing.StartSpanFromContext(ctx, "newsUC.GetNews") 141 | defer span.Finish() 142 | 143 | return u.newsRepo.GetNews(ctx, pq) 144 | } 145 | 146 | // Find nes by title 147 | func (u *newsUC) SearchByTitle(ctx context.Context, title string, query *utils.PaginationQuery) (*models.NewsList, error) { 148 | span, ctx := opentracing.StartSpanFromContext(ctx, "newsUC.SearchByTitle") 149 | defer span.Finish() 150 | 151 | return u.newsRepo.SearchByTitle(ctx, title, query) 152 | } 153 | 154 | func (u *newsUC) getKeyWithPrefix(newsID string) string { 155 | return fmt.Sprintf("%s: %s", basePrefix, newsID) 156 | } 157 | -------------------------------------------------------------------------------- /internal/server/handlers.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/AleksK1NG/api-mc/docs" 8 | "github.com/AleksK1NG/api-mc/pkg/csrf" 9 | 10 | "github.com/labstack/echo/v4" 11 | "github.com/labstack/echo/v4/middleware" 12 | echoSwagger "github.com/swaggo/echo-swagger" 13 | 14 | // _ "github.com/AleksK1NG/api-mc/docs" 15 | authHttp "github.com/AleksK1NG/api-mc/internal/auth/delivery/http" 16 | authRepository "github.com/AleksK1NG/api-mc/internal/auth/repository" 17 | authUseCase "github.com/AleksK1NG/api-mc/internal/auth/usecase" 18 | commentsHttp "github.com/AleksK1NG/api-mc/internal/comments/delivery/http" 19 | commentsRepository "github.com/AleksK1NG/api-mc/internal/comments/repository" 20 | commentsUseCase "github.com/AleksK1NG/api-mc/internal/comments/usecase" 21 | apiMiddlewares "github.com/AleksK1NG/api-mc/internal/middleware" 22 | newsHttp "github.com/AleksK1NG/api-mc/internal/news/delivery/http" 23 | newsRepository "github.com/AleksK1NG/api-mc/internal/news/repository" 24 | newsUseCase "github.com/AleksK1NG/api-mc/internal/news/usecase" 25 | sessionRepository "github.com/AleksK1NG/api-mc/internal/session/repository" 26 | "github.com/AleksK1NG/api-mc/internal/session/usecase" 27 | "github.com/AleksK1NG/api-mc/pkg/metric" 28 | "github.com/AleksK1NG/api-mc/pkg/utils" 29 | ) 30 | 31 | // Map Server Handlers 32 | func (s *Server) MapHandlers(e *echo.Echo) error { 33 | metrics, err := metric.CreateMetrics(s.cfg.Metrics.URL, s.cfg.Metrics.ServiceName) 34 | if err != nil { 35 | s.logger.Errorf("CreateMetrics Error: %s", err) 36 | } 37 | s.logger.Info( 38 | "Metrics available URL: %s, ServiceName: %s", 39 | s.cfg.Metrics.URL, 40 | s.cfg.Metrics.ServiceName, 41 | ) 42 | 43 | // Init repositories 44 | aRepo := authRepository.NewAuthRepository(s.db) 45 | nRepo := newsRepository.NewNewsRepository(s.db) 46 | cRepo := commentsRepository.NewCommentsRepository(s.db) 47 | sRepo := sessionRepository.NewSessionRepository(s.redisClient, s.cfg) 48 | aAWSRepo := authRepository.NewAuthAWSRepository(s.awsClient) 49 | authRedisRepo := authRepository.NewAuthRedisRepo(s.redisClient) 50 | newsRedisRepo := newsRepository.NewNewsRedisRepo(s.redisClient) 51 | 52 | // Init useCases 53 | authUC := authUseCase.NewAuthUseCase(s.cfg, aRepo, authRedisRepo, aAWSRepo, s.logger) 54 | newsUC := newsUseCase.NewNewsUseCase(s.cfg, nRepo, newsRedisRepo, s.logger) 55 | commUC := commentsUseCase.NewCommentsUseCase(s.cfg, cRepo, s.logger) 56 | sessUC := usecase.NewSessionUseCase(sRepo, s.cfg) 57 | 58 | // Init handlers 59 | authHandlers := authHttp.NewAuthHandlers(s.cfg, authUC, sessUC, s.logger) 60 | newsHandlers := newsHttp.NewNewsHandlers(s.cfg, newsUC, s.logger) 61 | commHandlers := commentsHttp.NewCommentsHandlers(s.cfg, commUC, s.logger) 62 | 63 | mw := apiMiddlewares.NewMiddlewareManager(sessUC, authUC, s.cfg, []string{"*"}, s.logger) 64 | 65 | e.Use(mw.RequestLoggerMiddleware) 66 | 67 | docs.SwaggerInfo.Title = "Go example REST API" 68 | e.GET("/swagger/*", echoSwagger.WrapHandler) 69 | 70 | if s.cfg.Server.SSL { 71 | e.Pre(middleware.HTTPSRedirect()) 72 | } 73 | 74 | e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 75 | AllowOrigins: []string{"*"}, 76 | AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderXRequestID, csrf.CSRFHeader}, 77 | })) 78 | e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ 79 | StackSize: 1 << 10, // 1 KB 80 | DisablePrintStack: true, 81 | DisableStackAll: true, 82 | })) 83 | e.Use(middleware.RequestID()) 84 | e.Use(mw.MetricsMiddleware(metrics)) 85 | 86 | e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ 87 | Level: 5, 88 | Skipper: func(c echo.Context) bool { 89 | return strings.Contains(c.Request().URL.Path, "swagger") 90 | }, 91 | })) 92 | e.Use(middleware.Secure()) 93 | e.Use(middleware.BodyLimit("2M")) 94 | if s.cfg.Server.Debug { 95 | e.Use(mw.DebugMiddleware) 96 | } 97 | 98 | v1 := e.Group("/api/v1") 99 | 100 | health := v1.Group("/health") 101 | authGroup := v1.Group("/auth") 102 | newsGroup := v1.Group("/news") 103 | commGroup := v1.Group("/comments") 104 | 105 | authHttp.MapAuthRoutes(authGroup, authHandlers, mw) 106 | newsHttp.MapNewsRoutes(newsGroup, newsHandlers, mw) 107 | commentsHttp.MapCommentsRoutes(commGroup, commHandlers, mw) 108 | 109 | health.GET("", func(c echo.Context) error { 110 | s.logger.Infof("Health check RequestID: %s", utils.GetRequestID(c)) 111 | return c.JSON(http.StatusOK, map[string]string{"status": "OK"}) 112 | }) 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | _ "net/http/pprof" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/go-redis/redis/v8" 13 | "github.com/jmoiron/sqlx" 14 | "github.com/labstack/echo/v4" 15 | "github.com/minio/minio-go/v7" 16 | 17 | "github.com/AleksK1NG/api-mc/config" 18 | _ "github.com/AleksK1NG/api-mc/docs" 19 | "github.com/AleksK1NG/api-mc/pkg/logger" 20 | ) 21 | 22 | const ( 23 | certFile = "ssl/Server.crt" 24 | keyFile = "ssl/Server.pem" 25 | maxHeaderBytes = 1 << 20 26 | ctxTimeout = 5 27 | ) 28 | 29 | // Server struct 30 | type Server struct { 31 | echo *echo.Echo 32 | cfg *config.Config 33 | db *sqlx.DB 34 | redisClient *redis.Client 35 | awsClient *minio.Client 36 | logger logger.Logger 37 | } 38 | 39 | // NewServer New Server constructor 40 | func NewServer(cfg *config.Config, db *sqlx.DB, redisClient *redis.Client, awsS3Client *minio.Client, logger logger.Logger) *Server { 41 | return &Server{echo: echo.New(), cfg: cfg, db: db, redisClient: redisClient, awsClient: awsS3Client, logger: logger} 42 | } 43 | 44 | func (s *Server) Run() error { 45 | if s.cfg.Server.SSL { 46 | if err := s.MapHandlers(s.echo); err != nil { 47 | return err 48 | } 49 | 50 | s.echo.Server.ReadTimeout = time.Second * s.cfg.Server.ReadTimeout 51 | s.echo.Server.WriteTimeout = time.Second * s.cfg.Server.WriteTimeout 52 | 53 | go func() { 54 | s.logger.Infof("Server is listening on PORT: %s", s.cfg.Server.Port) 55 | s.echo.Server.ReadTimeout = time.Second * s.cfg.Server.ReadTimeout 56 | s.echo.Server.WriteTimeout = time.Second * s.cfg.Server.WriteTimeout 57 | s.echo.Server.MaxHeaderBytes = maxHeaderBytes 58 | if err := s.echo.StartTLS(s.cfg.Server.Port, certFile, keyFile); err != nil { 59 | s.logger.Fatalf("Error starting TLS Server: ", err) 60 | } 61 | }() 62 | 63 | go func() { 64 | s.logger.Infof("Starting Debug Server on PORT: %s", s.cfg.Server.PprofPort) 65 | if err := http.ListenAndServe(s.cfg.Server.PprofPort, http.DefaultServeMux); err != nil { 66 | s.logger.Errorf("Error PPROF ListenAndServe: %s", err) 67 | } 68 | }() 69 | 70 | quit := make(chan os.Signal, 1) 71 | signal.Notify(quit, os.Interrupt, syscall.SIGTERM) 72 | 73 | <-quit 74 | 75 | ctx, shutdown := context.WithTimeout(context.Background(), ctxTimeout*time.Second) 76 | defer shutdown() 77 | 78 | s.logger.Info("Server Exited Properly") 79 | return s.echo.Server.Shutdown(ctx) 80 | } 81 | 82 | server := &http.Server{ 83 | Addr: s.cfg.Server.Port, 84 | ReadTimeout: time.Second * s.cfg.Server.ReadTimeout, 85 | WriteTimeout: time.Second * s.cfg.Server.WriteTimeout, 86 | MaxHeaderBytes: maxHeaderBytes, 87 | } 88 | 89 | go func() { 90 | s.logger.Infof("Server is listening on PORT: %s", s.cfg.Server.Port) 91 | if err := s.echo.StartServer(server); err != nil { 92 | s.logger.Fatalf("Error starting Server: ", err) 93 | } 94 | }() 95 | 96 | go func() { 97 | s.logger.Infof("Starting Debug Server on PORT: %s", s.cfg.Server.PprofPort) 98 | if err := http.ListenAndServe(s.cfg.Server.PprofPort, http.DefaultServeMux); err != nil { 99 | s.logger.Errorf("Error PPROF ListenAndServe: %s", err) 100 | } 101 | }() 102 | 103 | if err := s.MapHandlers(s.echo); err != nil { 104 | return err 105 | } 106 | 107 | quit := make(chan os.Signal, 1) 108 | signal.Notify(quit, os.Interrupt, syscall.SIGTERM) 109 | 110 | <-quit 111 | 112 | ctx, shutdown := context.WithTimeout(context.Background(), ctxTimeout*time.Second) 113 | defer shutdown() 114 | 115 | s.logger.Info("Server Exited Properly") 116 | return s.echo.Server.Shutdown(ctx) 117 | } 118 | -------------------------------------------------------------------------------- /internal/session/mock/redis_repository_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: redis_repository.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | models "github.com/AleksK1NG/api-mc/internal/models" 10 | gomock "github.com/golang/mock/gomock" 11 | reflect "reflect" 12 | ) 13 | 14 | // MockSessRepository is a mock of SessRepository interface 15 | type MockSessRepository struct { 16 | ctrl *gomock.Controller 17 | recorder *MockSessRepositoryMockRecorder 18 | } 19 | 20 | // MockSessRepositoryMockRecorder is the mock recorder for MockSessRepository 21 | type MockSessRepositoryMockRecorder struct { 22 | mock *MockSessRepository 23 | } 24 | 25 | // NewMockSessRepository creates a new mock instance 26 | func NewMockSessRepository(ctrl *gomock.Controller) *MockSessRepository { 27 | mock := &MockSessRepository{ctrl: ctrl} 28 | mock.recorder = &MockSessRepositoryMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockSessRepository) EXPECT() *MockSessRepositoryMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // CreateSession mocks base method 38 | func (m *MockSessRepository) CreateSession(ctx context.Context, session *models.Session, expire int) (string, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "CreateSession", ctx, session, expire) 41 | ret0, _ := ret[0].(string) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // CreateSession indicates an expected call of CreateSession 47 | func (mr *MockSessRepositoryMockRecorder) CreateSession(ctx, session, expire interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockSessRepository)(nil).CreateSession), ctx, session, expire) 50 | } 51 | 52 | // GetSessionByID mocks base method 53 | func (m *MockSessRepository) GetSessionByID(ctx context.Context, sessionID string) (*models.Session, error) { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "GetSessionByID", ctx, sessionID) 56 | ret0, _ := ret[0].(*models.Session) 57 | ret1, _ := ret[1].(error) 58 | return ret0, ret1 59 | } 60 | 61 | // GetSessionByID indicates an expected call of GetSessionByID 62 | func (mr *MockSessRepositoryMockRecorder) GetSessionByID(ctx, sessionID interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessionByID", reflect.TypeOf((*MockSessRepository)(nil).GetSessionByID), ctx, sessionID) 65 | } 66 | 67 | // DeleteByID mocks base method 68 | func (m *MockSessRepository) DeleteByID(ctx context.Context, sessionID string) error { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "DeleteByID", ctx, sessionID) 71 | ret0, _ := ret[0].(error) 72 | return ret0 73 | } 74 | 75 | // DeleteByID indicates an expected call of DeleteByID 76 | func (mr *MockSessRepositoryMockRecorder) DeleteByID(ctx, sessionID interface{}) *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByID", reflect.TypeOf((*MockSessRepository)(nil).DeleteByID), ctx, sessionID) 79 | } 80 | -------------------------------------------------------------------------------- /internal/session/mock/usecase_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: usecase.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | models "github.com/AleksK1NG/api-mc/internal/models" 10 | gomock "github.com/golang/mock/gomock" 11 | reflect "reflect" 12 | ) 13 | 14 | // MockUCSession is a mock of UCSession interface 15 | type MockUCSession struct { 16 | ctrl *gomock.Controller 17 | recorder *MockUCSessionMockRecorder 18 | } 19 | 20 | // MockUCSessionMockRecorder is the mock recorder for MockUCSession 21 | type MockUCSessionMockRecorder struct { 22 | mock *MockUCSession 23 | } 24 | 25 | // NewMockUCSession creates a new mock instance 26 | func NewMockUCSession(ctrl *gomock.Controller) *MockUCSession { 27 | mock := &MockUCSession{ctrl: ctrl} 28 | mock.recorder = &MockUCSessionMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockUCSession) EXPECT() *MockUCSessionMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // CreateSession mocks base method 38 | func (m *MockUCSession) CreateSession(ctx context.Context, session *models.Session, expire int) (string, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "CreateSession", ctx, session, expire) 41 | ret0, _ := ret[0].(string) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // CreateSession indicates an expected call of CreateSession 47 | func (mr *MockUCSessionMockRecorder) CreateSession(ctx, session, expire interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockUCSession)(nil).CreateSession), ctx, session, expire) 50 | } 51 | 52 | // GetSessionByID mocks base method 53 | func (m *MockUCSession) GetSessionByID(ctx context.Context, sessionID string) (*models.Session, error) { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "GetSessionByID", ctx, sessionID) 56 | ret0, _ := ret[0].(*models.Session) 57 | ret1, _ := ret[1].(error) 58 | return ret0, ret1 59 | } 60 | 61 | // GetSessionByID indicates an expected call of GetSessionByID 62 | func (mr *MockUCSessionMockRecorder) GetSessionByID(ctx, sessionID interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessionByID", reflect.TypeOf((*MockUCSession)(nil).GetSessionByID), ctx, sessionID) 65 | } 66 | 67 | // DeleteByID mocks base method 68 | func (m *MockUCSession) DeleteByID(ctx context.Context, sessionID string) error { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "DeleteByID", ctx, sessionID) 71 | ret0, _ := ret[0].(error) 72 | return ret0 73 | } 74 | 75 | // DeleteByID indicates an expected call of DeleteByID 76 | func (mr *MockUCSessionMockRecorder) DeleteByID(ctx, sessionID interface{}) *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByID", reflect.TypeOf((*MockUCSession)(nil).DeleteByID), ctx, sessionID) 79 | } 80 | -------------------------------------------------------------------------------- /internal/session/redis_repository.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source redis_repository.go -destination mock/redis_repository_mock.go -package mock 2 | package session 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/AleksK1NG/api-mc/internal/models" 8 | ) 9 | 10 | // Session repository 11 | type SessRepository interface { 12 | CreateSession(ctx context.Context, session *models.Session, expire int) (string, error) 13 | GetSessionByID(ctx context.Context, sessionID string) (*models.Session, error) 14 | DeleteByID(ctx context.Context, sessionID string) error 15 | } 16 | -------------------------------------------------------------------------------- /internal/session/repository/redis_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/go-redis/redis/v8" 10 | "github.com/google/uuid" 11 | "github.com/opentracing/opentracing-go" 12 | "github.com/pkg/errors" 13 | 14 | "github.com/AleksK1NG/api-mc/config" 15 | "github.com/AleksK1NG/api-mc/internal/models" 16 | "github.com/AleksK1NG/api-mc/internal/session" 17 | ) 18 | 19 | const ( 20 | basePrefix = "api-session:" 21 | ) 22 | 23 | // Session repository 24 | type sessionRepo struct { 25 | redisClient *redis.Client 26 | basePrefix string 27 | cfg *config.Config 28 | } 29 | 30 | // Session repository constructor 31 | func NewSessionRepository(redisClient *redis.Client, cfg *config.Config) session.SessRepository { 32 | return &sessionRepo{redisClient: redisClient, basePrefix: basePrefix, cfg: cfg} 33 | } 34 | 35 | // Create session in redis 36 | func (s *sessionRepo) CreateSession(ctx context.Context, sess *models.Session, expire int) (string, error) { 37 | span, ctx := opentracing.StartSpanFromContext(ctx, "sessionRepo.CreateSession") 38 | defer span.Finish() 39 | 40 | sess.SessionID = uuid.New().String() 41 | sessionKey := s.createKey(sess.SessionID) 42 | 43 | sessBytes, err := json.Marshal(&sess) 44 | if err != nil { 45 | return "", errors.WithMessage(err, "sessionRepo.CreateSession.json.Marshal") 46 | } 47 | if err = s.redisClient.Set(ctx, sessionKey, sessBytes, time.Second*time.Duration(expire)).Err(); err != nil { 48 | return "", errors.Wrap(err, "sessionRepo.CreateSession.redisClient.Set") 49 | } 50 | return sessionKey, nil 51 | } 52 | 53 | // Get session by id 54 | func (s *sessionRepo) GetSessionByID(ctx context.Context, sessionID string) (*models.Session, error) { 55 | span, ctx := opentracing.StartSpanFromContext(ctx, "sessionRepo.GetSessionByID") 56 | defer span.Finish() 57 | 58 | sessBytes, err := s.redisClient.Get(ctx, sessionID).Bytes() 59 | if err != nil { 60 | return nil, errors.Wrap(err, "sessionRep.GetSessionByID.redisClient.Get") 61 | } 62 | 63 | sess := &models.Session{} 64 | if err = json.Unmarshal(sessBytes, &sess); err != nil { 65 | return nil, errors.Wrap(err, "sessionRepo.GetSessionByID.json.Unmarshal") 66 | } 67 | return sess, nil 68 | } 69 | 70 | // Delete session by id 71 | func (s *sessionRepo) DeleteByID(ctx context.Context, sessionID string) error { 72 | span, ctx := opentracing.StartSpanFromContext(ctx, "sessionRepo.DeleteByID") 73 | defer span.Finish() 74 | 75 | if err := s.redisClient.Del(ctx, sessionID).Err(); err != nil { 76 | return errors.Wrap(err, "sessionRepo.DeleteByID") 77 | } 78 | return nil 79 | } 80 | 81 | func (s *sessionRepo) createKey(sessionID string) string { 82 | return fmt.Sprintf("%s: %s", s.basePrefix, sessionID) 83 | } 84 | -------------------------------------------------------------------------------- /internal/session/repository/redis_repository_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "testing" 7 | 8 | "github.com/alicebob/miniredis" 9 | "github.com/go-redis/redis/v8" 10 | "github.com/google/uuid" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/AleksK1NG/api-mc/internal/models" 14 | "github.com/AleksK1NG/api-mc/internal/session" 15 | ) 16 | 17 | func SetupRedis() session.SessRepository { 18 | mr, err := miniredis.Run() 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | client := redis.NewClient(&redis.Options{ 23 | Addr: mr.Addr(), 24 | }) 25 | 26 | sessRepository := NewSessionRepository(client, nil) 27 | return sessRepository 28 | } 29 | 30 | func TestSessionRepo_CreateSession(t *testing.T) { 31 | t.Parallel() 32 | 33 | sessRepository := SetupRedis() 34 | 35 | t.Run("CreateSession", func(t *testing.T) { 36 | sessUUID := uuid.New() 37 | sess := &models.Session{ 38 | SessionID: sessUUID.String(), 39 | UserID: sessUUID, 40 | } 41 | s, err := sessRepository.CreateSession(context.Background(), sess, 10) 42 | require.NoError(t, err) 43 | require.NotEqual(t, s, "") 44 | }) 45 | } 46 | 47 | func TestSessionRepo_GetSessionByID(t *testing.T) { 48 | t.Parallel() 49 | 50 | sessRepository := SetupRedis() 51 | 52 | t.Run("GetSessionByID", func(t *testing.T) { 53 | sessUUID := uuid.New() 54 | sess := &models.Session{ 55 | SessionID: sessUUID.String(), 56 | UserID: sessUUID, 57 | } 58 | createdSess, err := sessRepository.CreateSession(context.Background(), sess, 10) 59 | require.NoError(t, err) 60 | require.NotEqual(t, createdSess, "") 61 | 62 | s, err := sessRepository.GetSessionByID(context.Background(), createdSess) 63 | require.NoError(t, err) 64 | require.NotEqual(t, s, "") 65 | }) 66 | } 67 | 68 | func TestSessionRepo_DeleteByID(t *testing.T) { 69 | t.Parallel() 70 | 71 | sessRepository := SetupRedis() 72 | 73 | t.Run("DeleteByID", func(t *testing.T) { 74 | sessUUID := uuid.New() 75 | err := sessRepository.DeleteByID(context.Background(), sessUUID.String()) 76 | require.NoError(t, err) 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /internal/session/usecase.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source usecase.go -destination mock/usecase_mock.go -package mock 2 | package session 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/AleksK1NG/api-mc/internal/models" 8 | ) 9 | 10 | // Session use case 11 | type UCSession interface { 12 | CreateSession(ctx context.Context, session *models.Session, expire int) (string, error) 13 | GetSessionByID(ctx context.Context, sessionID string) (*models.Session, error) 14 | DeleteByID(ctx context.Context, sessionID string) error 15 | } 16 | -------------------------------------------------------------------------------- /internal/session/usecase/usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/opentracing/opentracing-go" 7 | 8 | "github.com/AleksK1NG/api-mc/config" 9 | "github.com/AleksK1NG/api-mc/internal/models" 10 | "github.com/AleksK1NG/api-mc/internal/session" 11 | ) 12 | 13 | // Session use case 14 | type sessionUC struct { 15 | sessionRepo session.SessRepository 16 | cfg *config.Config 17 | } 18 | 19 | // New session use case constructor 20 | func NewSessionUseCase(sessionRepo session.SessRepository, cfg *config.Config) session.UCSession { 21 | return &sessionUC{sessionRepo: sessionRepo, cfg: cfg} 22 | } 23 | 24 | // Create new session 25 | func (u *sessionUC) CreateSession(ctx context.Context, session *models.Session, expire int) (string, error) { 26 | span, ctx := opentracing.StartSpanFromContext(ctx, "sessionUC.CreateSession") 27 | defer span.Finish() 28 | 29 | return u.sessionRepo.CreateSession(ctx, session, expire) 30 | } 31 | 32 | // Delete session by id 33 | func (u *sessionUC) DeleteByID(ctx context.Context, sessionID string) error { 34 | span, ctx := opentracing.StartSpanFromContext(ctx, "sessionUC.DeleteByID") 35 | defer span.Finish() 36 | 37 | return u.sessionRepo.DeleteByID(ctx, sessionID) 38 | } 39 | 40 | // get session by id 41 | func (u *sessionUC) GetSessionByID(ctx context.Context, sessionID string) (*models.Session, error) { 42 | span, ctx := opentracing.StartSpanFromContext(ctx, "sessionUC.GetSessionByID") 43 | defer span.Finish() 44 | 45 | return u.sessionRepo.GetSessionByID(ctx, sessionID) 46 | } 47 | -------------------------------------------------------------------------------- /internal/session/usecase/usecase_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golang/mock/gomock" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/AleksK1NG/api-mc/internal/models" 11 | "github.com/AleksK1NG/api-mc/internal/session/mock" 12 | ) 13 | 14 | func TestSessionUC_CreateSession(t *testing.T) { 15 | t.Parallel() 16 | 17 | ctrl := gomock.NewController(t) 18 | defer ctrl.Finish() 19 | 20 | mockSessRepo := mock.NewMockSessRepository(ctrl) 21 | sessUC := NewSessionUseCase(mockSessRepo, nil) 22 | 23 | ctx := context.Background() 24 | sess := &models.Session{} 25 | sid := "session id" 26 | 27 | mockSessRepo.EXPECT().CreateSession(gomock.Any(), gomock.Eq(sess), 10).Return(sid, nil) 28 | 29 | createdSess, err := sessUC.CreateSession(ctx, sess, 10) 30 | require.NoError(t, err) 31 | require.Nil(t, err) 32 | require.NotEqual(t, createdSess, "") 33 | } 34 | 35 | func TestSessionUC_GetSessionByID(t *testing.T) { 36 | t.Parallel() 37 | 38 | ctrl := gomock.NewController(t) 39 | defer ctrl.Finish() 40 | 41 | mockSessRepo := mock.NewMockSessRepository(ctrl) 42 | sessUC := NewSessionUseCase(mockSessRepo, nil) 43 | 44 | ctx := context.Background() 45 | sess := &models.Session{} 46 | sid := "session id" 47 | 48 | mockSessRepo.EXPECT().GetSessionByID(gomock.Any(), gomock.Eq(sid)).Return(sess, nil) 49 | 50 | session, err := sessUC.GetSessionByID(ctx, sid) 51 | require.NoError(t, err) 52 | require.Nil(t, err) 53 | require.NotNil(t, session) 54 | } 55 | 56 | func TestSessionUC_DeleteByID(t *testing.T) { 57 | t.Parallel() 58 | 59 | ctrl := gomock.NewController(t) 60 | defer ctrl.Finish() 61 | 62 | mockSessRepo := mock.NewMockSessRepository(ctrl) 63 | sessUC := NewSessionUseCase(mockSessRepo, nil) 64 | 65 | ctx := context.Background() 66 | sid := "session id" 67 | 68 | mockSessRepo.EXPECT().DeleteByID(gomock.Any(), gomock.Eq(sid)).Return(nil) 69 | 70 | err := sessUC.DeleteByID(ctx, sid) 71 | require.NoError(t, err) 72 | require.Nil(t, err) 73 | } 74 | -------------------------------------------------------------------------------- /migrations/01_create_initial_tables.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users CASCADE; 2 | DROP TABLE IF EXISTS news CASCADE; 3 | DROP TABLE IF EXISTS comments CASCADE; 4 | 5 | 6 | -- DROP EXTENSION IF EXISTS postgis_topology; 7 | -- DROP EXTENSION IF EXISTS postgis; 8 | -------------------------------------------------------------------------------- /migrations/01_create_initial_tables.up.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users CASCADE; 2 | DROP TABLE IF EXISTS news CASCADE; 3 | 4 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 5 | CREATE EXTENSION IF NOT EXISTS CITEXT; 6 | -- CREATE EXTENSION IF NOT EXISTS postgis; 7 | -- CREATE EXTENSION IF NOT EXISTS postgis_topology; 8 | 9 | 10 | CREATE TABLE users 11 | ( 12 | user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 13 | first_name VARCHAR(32) NOT NULL CHECK ( first_name <> '' ), 14 | last_name VARCHAR(32) NOT NULL CHECK ( last_name <> '' ), 15 | email VARCHAR(64) UNIQUE NOT NULL CHECK ( email <> '' ), 16 | password VARCHAR(250) NOT NULL CHECK ( octet_length(password) <> 0 ), 17 | role VARCHAR(10) NOT NULL DEFAULT 'user', 18 | about VARCHAR(1024) DEFAULT '', 19 | avatar VARCHAR(512), 20 | phone_number VARCHAR(20), 21 | address VARCHAR(250), 22 | city VARCHAR(30), 23 | country VARCHAR(30), 24 | gender VARCHAR(20) NOT NULL DEFAULT 'male', 25 | postcode INTEGER, 26 | birthday DATE DEFAULT NULL, 27 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 28 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, 29 | login_date TIMESTAMP(0) WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP 30 | ); 31 | 32 | 33 | 34 | CREATE TABLE news 35 | ( 36 | news_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 37 | author_id UUID NOT NULL REFERENCES users (user_id), 38 | title VARCHAR(250) NOT NULL CHECK ( title <> '' ), 39 | content TEXT NOT NULL CHECK ( content <> '' ), 40 | image_url VARCHAR(1024) check ( image_url <> '' ), 41 | category VARCHAR(250), 42 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 43 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 44 | ); 45 | 46 | CREATE TABLE comments 47 | ( 48 | comment_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 49 | author_id UUID NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, 50 | news_id UUID NOT NULL REFERENCES news (news_id) ON DELETE CASCADE, 51 | message VARCHAR(1024) NOT NULL CHECK ( message <> '' ), 52 | likes BIGINT DEFAULT 0, 53 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, 54 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 55 | ); 56 | 57 | CREATE INDEX IF NOT EXISTS news_title_id_idx ON news (title); 58 | -------------------------------------------------------------------------------- /pkg/converter/converter.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // Convert bytes to buffer helper 9 | func AnyToBytesBuffer(i interface{}) (*bytes.Buffer, error) { 10 | buf := new(bytes.Buffer) 11 | err := json.NewEncoder(buf).Encode(i) 12 | if err != nil { 13 | return buf, err 14 | } 15 | return buf, nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/csrf/csrf.go: -------------------------------------------------------------------------------- 1 | package csrf 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/base64" 6 | "io" 7 | 8 | "github.com/AleksK1NG/api-mc/pkg/logger" 9 | ) 10 | 11 | const ( 12 | CSRFHeader = "X-CSRF-Token" 13 | // 32 bytes 14 | csrfSalt = "KbWaoi5xtDC3GEfBa9ovQdzOzXsuVU9I" 15 | ) 16 | 17 | // Create CSRF token 18 | func MakeToken(sid string, logger logger.Logger) string { 19 | hash := sha256.New() 20 | _, err := io.WriteString(hash, csrfSalt+sid) 21 | if err != nil { 22 | logger.Errorf("Make CSRF Token", err) 23 | } 24 | token := base64.RawStdEncoding.EncodeToString(hash.Sum(nil)) 25 | return token 26 | } 27 | 28 | // Validate CSRF token 29 | func ValidateToken(token string, sid string, logger logger.Logger) bool { 30 | trueToken := MakeToken(sid, logger) 31 | return token == trueToken 32 | } 33 | -------------------------------------------------------------------------------- /pkg/db/aws/aws.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "github.com/minio/minio-go/v7" 5 | "github.com/minio/minio-go/v7/pkg/credentials" 6 | ) 7 | 8 | // Minio AWS S3 Client constructor 9 | func NewAWSClient(endpoint string, accessKeyID string, secretAccessKey string, useSSL bool) (*minio.Client, error) { 10 | // Initialize minio client object. 11 | minioClient, err := minio.New(endpoint, &minio.Options{ 12 | Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), 13 | Secure: useSSL, 14 | }) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return minioClient, nil 20 | } 21 | -------------------------------------------------------------------------------- /pkg/db/postgres/db_conn.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | _ "github.com/jackc/pgx/stdlib" // pgx driver 8 | "github.com/jmoiron/sqlx" 9 | 10 | "github.com/AleksK1NG/api-mc/config" 11 | ) 12 | 13 | const ( 14 | maxOpenConns = 60 15 | connMaxLifetime = 120 16 | maxIdleConns = 30 17 | connMaxIdleTime = 20 18 | ) 19 | 20 | // Return new Postgresql db instance 21 | func NewPsqlDB(c *config.Config) (*sqlx.DB, error) { 22 | dataSourceName := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", 23 | c.Postgres.PostgresqlHost, 24 | c.Postgres.PostgresqlPort, 25 | c.Postgres.PostgresqlUser, 26 | c.Postgres.PostgresqlDbname, 27 | c.Postgres.PostgresqlPassword, 28 | ) 29 | 30 | db, err := sqlx.Connect(c.Postgres.PgDriver, dataSourceName) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | db.SetMaxOpenConns(maxOpenConns) 36 | db.SetConnMaxLifetime(connMaxLifetime * time.Second) 37 | db.SetMaxIdleConns(maxIdleConns) 38 | db.SetConnMaxIdleTime(connMaxIdleTime * time.Second) 39 | if err = db.Ping(); err != nil { 40 | return nil, err 41 | } 42 | 43 | return db, nil 44 | } 45 | -------------------------------------------------------------------------------- /pkg/db/redis/conn.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-redis/redis/v8" 7 | 8 | "github.com/AleksK1NG/api-mc/config" 9 | ) 10 | 11 | // Returns new redis client 12 | func NewRedisClient(cfg *config.Config) *redis.Client { 13 | redisHost := cfg.Redis.RedisAddr 14 | 15 | if redisHost == "" { 16 | redisHost = ":6379" 17 | } 18 | 19 | client := redis.NewClient(&redis.Options{ 20 | Addr: redisHost, 21 | MinIdleConns: cfg.Redis.MinIdleConns, 22 | PoolSize: cfg.Redis.PoolSize, 23 | PoolTimeout: time.Duration(cfg.Redis.PoolTimeout) * time.Second, 24 | Password: cfg.Redis.Password, // no password set 25 | DB: cfg.Redis.DB, // use default DB 26 | }) 27 | 28 | return client 29 | } 30 | -------------------------------------------------------------------------------- /pkg/logger/zap_logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | 9 | "github.com/AleksK1NG/api-mc/config" 10 | ) 11 | 12 | // Logger methods interface 13 | type Logger interface { 14 | InitLogger() 15 | Debug(args ...interface{}) 16 | Debugf(template string, args ...interface{}) 17 | Info(args ...interface{}) 18 | Infof(template string, args ...interface{}) 19 | Warn(args ...interface{}) 20 | Warnf(template string, args ...interface{}) 21 | Error(args ...interface{}) 22 | Errorf(template string, args ...interface{}) 23 | DPanic(args ...interface{}) 24 | DPanicf(template string, args ...interface{}) 25 | Fatal(args ...interface{}) 26 | Fatalf(template string, args ...interface{}) 27 | } 28 | 29 | // Logger 30 | type apiLogger struct { 31 | cfg *config.Config 32 | sugarLogger *zap.SugaredLogger 33 | } 34 | 35 | // App Logger constructor 36 | func NewApiLogger(cfg *config.Config) *apiLogger { 37 | return &apiLogger{cfg: cfg} 38 | } 39 | 40 | // For mapping config logger to app logger levels 41 | var loggerLevelMap = map[string]zapcore.Level{ 42 | "debug": zapcore.DebugLevel, 43 | "info": zapcore.InfoLevel, 44 | "warn": zapcore.WarnLevel, 45 | "error": zapcore.ErrorLevel, 46 | "dpanic": zapcore.DPanicLevel, 47 | "panic": zapcore.PanicLevel, 48 | "fatal": zapcore.FatalLevel, 49 | } 50 | 51 | func (l *apiLogger) getLoggerLevel(cfg *config.Config) zapcore.Level { 52 | level, exist := loggerLevelMap[cfg.Logger.Level] 53 | if !exist { 54 | return zapcore.DebugLevel 55 | } 56 | 57 | return level 58 | } 59 | 60 | // Init logger 61 | func (l *apiLogger) InitLogger() { 62 | logLevel := l.getLoggerLevel(l.cfg) 63 | 64 | logWriter := zapcore.AddSync(os.Stderr) 65 | 66 | var encoderCfg zapcore.EncoderConfig 67 | if l.cfg.Server.Mode == "Development" { 68 | encoderCfg = zap.NewDevelopmentEncoderConfig() 69 | } else { 70 | encoderCfg = zap.NewProductionEncoderConfig() 71 | } 72 | 73 | var encoder zapcore.Encoder 74 | encoderCfg.LevelKey = "LEVEL" 75 | encoderCfg.CallerKey = "CALLER" 76 | encoderCfg.TimeKey = "TIME" 77 | encoderCfg.NameKey = "NAME" 78 | encoderCfg.MessageKey = "MESSAGE" 79 | 80 | if l.cfg.Logger.Encoding == "console" { 81 | encoder = zapcore.NewConsoleEncoder(encoderCfg) 82 | } else { 83 | encoder = zapcore.NewJSONEncoder(encoderCfg) 84 | } 85 | 86 | encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder 87 | core := zapcore.NewCore(encoder, logWriter, zap.NewAtomicLevelAt(logLevel)) 88 | logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) 89 | 90 | l.sugarLogger = logger.Sugar() 91 | if err := l.sugarLogger.Sync(); err != nil { 92 | l.sugarLogger.Error(err) 93 | } 94 | } 95 | 96 | // Logger methods 97 | 98 | func (l *apiLogger) Debug(args ...interface{}) { 99 | l.sugarLogger.Debug(args...) 100 | } 101 | 102 | func (l *apiLogger) Debugf(template string, args ...interface{}) { 103 | l.sugarLogger.Debugf(template, args...) 104 | } 105 | 106 | func (l *apiLogger) Info(args ...interface{}) { 107 | l.sugarLogger.Info(args...) 108 | } 109 | 110 | func (l *apiLogger) Infof(template string, args ...interface{}) { 111 | l.sugarLogger.Infof(template, args...) 112 | } 113 | 114 | func (l *apiLogger) Warn(args ...interface{}) { 115 | l.sugarLogger.Warn(args...) 116 | } 117 | 118 | func (l *apiLogger) Warnf(template string, args ...interface{}) { 119 | l.sugarLogger.Warnf(template, args...) 120 | } 121 | 122 | func (l *apiLogger) Error(args ...interface{}) { 123 | l.sugarLogger.Error(args...) 124 | } 125 | 126 | func (l *apiLogger) Errorf(template string, args ...interface{}) { 127 | l.sugarLogger.Errorf(template, args...) 128 | } 129 | 130 | func (l *apiLogger) DPanic(args ...interface{}) { 131 | l.sugarLogger.DPanic(args...) 132 | } 133 | 134 | func (l *apiLogger) DPanicf(template string, args ...interface{}) { 135 | l.sugarLogger.DPanicf(template, args...) 136 | } 137 | 138 | func (l *apiLogger) Panic(args ...interface{}) { 139 | l.sugarLogger.Panic(args...) 140 | } 141 | 142 | func (l *apiLogger) Panicf(template string, args ...interface{}) { 143 | l.sugarLogger.Panicf(template, args...) 144 | } 145 | 146 | func (l *apiLogger) Fatal(args ...interface{}) { 147 | l.sugarLogger.Fatal(args...) 148 | } 149 | 150 | func (l *apiLogger) Fatalf(template string, args ...interface{}) { 151 | l.sugarLogger.Fatalf(template, args...) 152 | } 153 | -------------------------------------------------------------------------------- /pkg/metric/metric.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/prometheus/client_golang/prometheus/promhttp" 10 | ) 11 | 12 | // App Metrics interface 13 | type Metrics interface { 14 | IncHits(status int, method, path string) 15 | ObserveResponseTime(status int, method, path string, observeTime float64) 16 | } 17 | 18 | // Prometheus Metrics struct 19 | type PrometheusMetrics struct { 20 | HitsTotal prometheus.Counter 21 | Hits *prometheus.CounterVec 22 | Times *prometheus.HistogramVec 23 | } 24 | 25 | // Create metrics with address and name 26 | func CreateMetrics(address string, name string) (Metrics, error) { 27 | var metr PrometheusMetrics 28 | metr.HitsTotal = prometheus.NewCounter(prometheus.CounterOpts{ 29 | Name: name + "_hits_total", 30 | }) 31 | 32 | if err := prometheus.Register(metr.HitsTotal); err != nil { 33 | return nil, err 34 | } 35 | 36 | metr.Hits = prometheus.NewCounterVec( 37 | prometheus.CounterOpts{ 38 | Name: name + "_hits", 39 | }, 40 | []string{"status", "method", "path"}, 41 | ) 42 | 43 | if err := prometheus.Register(metr.Hits); err != nil { 44 | return nil, err 45 | } 46 | 47 | metr.Times = prometheus.NewHistogramVec( 48 | prometheus.HistogramOpts{ 49 | Name: name + "_times", 50 | }, 51 | []string{"status", "method", "path"}, 52 | ) 53 | 54 | if err := prometheus.Register(metr.Times); err != nil { 55 | return nil, err 56 | } 57 | 58 | if err := prometheus.Register(prometheus.NewBuildInfoCollector()); err != nil { 59 | return nil, err 60 | } 61 | 62 | go func() { 63 | router := echo.New() 64 | router.GET("/metrics", echo.WrapHandler(promhttp.Handler())) 65 | log.Printf("Metrics server is running on port: %s", address) 66 | if err := router.Start(address); err != nil { 67 | log.Fatal(err) 68 | } 69 | }() 70 | 71 | return &metr, nil 72 | } 73 | 74 | // IncHits 75 | func (metr *PrometheusMetrics) IncHits(status int, method, path string) { 76 | metr.HitsTotal.Inc() 77 | metr.Hits.WithLabelValues(strconv.Itoa(status), method, path).Inc() 78 | } 79 | 80 | // Observer response time 81 | func (metr *PrometheusMetrics) ObserveResponseTime(status int, method, path string, observeTime float64) { 82 | metr.Times.WithLabelValues(strconv.Itoa(status), method, path).Observe(observeTime) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/sanitize/sanitize.go: -------------------------------------------------------------------------------- 1 | package sanitize 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | 7 | "github.com/microcosm-cc/bluemonday" 8 | ) 9 | 10 | var sanitizer *bluemonday.Policy 11 | 12 | func init() { 13 | sanitizer = bluemonday.UGCPolicy() 14 | } 15 | 16 | // Sanitize json 17 | func SanitizeJSON(s []byte) ([]byte, error) { 18 | d := json.NewDecoder(bytes.NewReader(s)) 19 | d.UseNumber() 20 | var i interface{} 21 | err := d.Decode(&i) 22 | if err != nil { 23 | return nil, err 24 | } 25 | sanitize(i) 26 | return json.MarshalIndent(i, "", " ") 27 | } 28 | 29 | func sanitize(data interface{}) { 30 | switch d := data.(type) { 31 | case map[string]interface{}: 32 | for k, v := range d { 33 | switch tv := v.(type) { 34 | case string: 35 | d[k] = sanitizer.Sanitize(tv) 36 | case map[string]interface{}: 37 | sanitize(tv) 38 | case []interface{}: 39 | sanitize(tv) 40 | case nil: 41 | delete(d, k) 42 | } 43 | } 44 | case []interface{}: 45 | if len(d) > 0 { 46 | switch d[0].(type) { 47 | case string: 48 | for i, s := range d { 49 | d[i] = sanitizer.Sanitize(s.(string)) 50 | } 51 | case map[string]interface{}: 52 | for _, t := range d { 53 | sanitize(t) 54 | } 55 | case []interface{}: 56 | for _, t := range d { 57 | sanitize(t) 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/utils/auth.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/AleksK1NG/api-mc/pkg/httpErrors" 7 | "github.com/AleksK1NG/api-mc/pkg/logger" 8 | ) 9 | 10 | // Validate is user from owner of content 11 | func ValidateIsOwner(ctx context.Context, creatorID string, logger logger.Logger) error { 12 | user, err := GetUserFromCtx(ctx) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | if user.UserID.String() != creatorID { 18 | logger.Errorf( 19 | "ValidateIsOwner, userID: %v, creatorID: %v", 20 | user.UserID.String(), 21 | creatorID, 22 | ) 23 | return httpErrors.Forbidden 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/utils/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io/ioutil" 7 | "mime/multipart" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/labstack/echo/v4" 12 | "github.com/pkg/errors" 13 | 14 | "github.com/AleksK1NG/api-mc/config" 15 | "github.com/AleksK1NG/api-mc/internal/models" 16 | "github.com/AleksK1NG/api-mc/pkg/httpErrors" 17 | "github.com/AleksK1NG/api-mc/pkg/logger" 18 | "github.com/AleksK1NG/api-mc/pkg/sanitize" 19 | ) 20 | 21 | // Get request id from echo context 22 | func GetRequestID(c echo.Context) string { 23 | return c.Response().Header().Get(echo.HeaderXRequestID) 24 | } 25 | 26 | // ReqIDCtxKey is a key used for the Request ID in context 27 | type ReqIDCtxKey struct{} 28 | 29 | // Get ctx with timeout and request id from echo context 30 | func GetCtxWithReqID(c echo.Context) (context.Context, context.CancelFunc) { 31 | ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*15) 32 | ctx = context.WithValue(ctx, ReqIDCtxKey{}, GetRequestID(c)) 33 | return ctx, cancel 34 | } 35 | 36 | // Get context with request id 37 | func GetRequestCtx(c echo.Context) context.Context { 38 | return context.WithValue(c.Request().Context(), ReqIDCtxKey{}, GetRequestID(c)) 39 | } 40 | 41 | // Get config path for local or docker 42 | func GetConfigPath(configPath string) string { 43 | if configPath == "docker" { 44 | return "./config/config-docker" 45 | } 46 | return "./config/config-local" 47 | } 48 | 49 | // Configure jwt cookie 50 | func ConfigureJWTCookie(cfg *config.Config, jwtToken string) *http.Cookie { 51 | return &http.Cookie{ 52 | Name: cfg.Cookie.Name, 53 | Value: jwtToken, 54 | Path: "/", 55 | RawExpires: "", 56 | MaxAge: cfg.Cookie.MaxAge, 57 | Secure: cfg.Cookie.Secure, 58 | HttpOnly: cfg.Cookie.HTTPOnly, 59 | SameSite: 0, 60 | } 61 | } 62 | 63 | // Configure jwt cookie 64 | func CreateSessionCookie(cfg *config.Config, session string) *http.Cookie { 65 | return &http.Cookie{ 66 | Name: cfg.Session.Name, 67 | Value: session, 68 | Path: "/", 69 | // Domain: "/", 70 | // Expires: time.Now().Add(1 * time.Minute), 71 | RawExpires: "", 72 | MaxAge: cfg.Session.Expire, 73 | Secure: cfg.Cookie.Secure, 74 | HttpOnly: cfg.Cookie.HTTPOnly, 75 | SameSite: 0, 76 | } 77 | } 78 | 79 | // Delete session 80 | func DeleteSessionCookie(c echo.Context, sessionName string) { 81 | c.SetCookie(&http.Cookie{ 82 | Name: sessionName, 83 | Value: "", 84 | Path: "/", 85 | MaxAge: -1, 86 | }) 87 | } 88 | 89 | // UserCtxKey is a key used for the User object in the context 90 | type UserCtxKey struct{} 91 | 92 | // Get user from context 93 | func GetUserFromCtx(ctx context.Context) (*models.User, error) { 94 | user, ok := ctx.Value(UserCtxKey{}).(*models.User) 95 | if !ok { 96 | return nil, httpErrors.Unauthorized 97 | } 98 | 99 | return user, nil 100 | } 101 | 102 | // Get user ip address 103 | func GetIPAddress(c echo.Context) string { 104 | return c.Request().RemoteAddr 105 | } 106 | 107 | // Error response with logging error for echo context 108 | func ErrResponseWithLog(ctx echo.Context, logger logger.Logger, err error) error { 109 | logger.Errorf( 110 | "ErrResponseWithLog, RequestID: %s, IPAddress: %s, Error: %s", 111 | GetRequestID(ctx), 112 | GetIPAddress(ctx), 113 | err, 114 | ) 115 | return ctx.JSON(httpErrors.ErrorResponse(err)) 116 | } 117 | 118 | // Error response with logging error for echo context 119 | func LogResponseError(ctx echo.Context, logger logger.Logger, err error) { 120 | logger.Errorf( 121 | "ErrResponseWithLog, RequestID: %s, IPAddress: %s, Error: %s", 122 | GetRequestID(ctx), 123 | GetIPAddress(ctx), 124 | err, 125 | ) 126 | } 127 | 128 | // Read request body and validate 129 | func ReadRequest(ctx echo.Context, request interface{}) error { 130 | if err := ctx.Bind(request); err != nil { 131 | return err 132 | } 133 | return validate.StructCtx(ctx.Request().Context(), request) 134 | } 135 | 136 | func ReadImage(ctx echo.Context, field string) (*multipart.FileHeader, error) { 137 | image, err := ctx.FormFile(field) 138 | if err != nil { 139 | return nil, errors.WithMessage(err, "ctx.FormFile") 140 | } 141 | 142 | // Check content type of image 143 | if err = CheckImageContentType(image); err != nil { 144 | return nil, err 145 | } 146 | 147 | return image, nil 148 | } 149 | 150 | // Read sanitize and validate request 151 | func SanitizeRequest(ctx echo.Context, request interface{}) error { 152 | body, err := ioutil.ReadAll(ctx.Request().Body) 153 | if err != nil { 154 | return err 155 | } 156 | defer ctx.Request().Body.Close() 157 | 158 | sanBody, err := sanitize.SanitizeJSON(body) 159 | if err != nil { 160 | return ctx.NoContent(http.StatusBadRequest) 161 | } 162 | 163 | if err = json.Unmarshal(sanBody, request); err != nil { 164 | return err 165 | } 166 | 167 | return validate.StructCtx(ctx.Request().Context(), request) 168 | } 169 | 170 | var allowedImagesContentTypes = map[string]string{ 171 | "image/bmp": "bmp", 172 | "image/gif": "gif", 173 | "image/png": "png", 174 | "image/jpeg": "jpeg", 175 | "image/jpg": "jpg", 176 | "image/svg+xml": "svg", 177 | "image/webp": "webp", 178 | "image/tiff": "tiff", 179 | "image/vnd.microsoft.icon": "ico", 180 | } 181 | 182 | func CheckImageFileContentType(fileContent []byte) (string, error) { 183 | contentType := http.DetectContentType(fileContent) 184 | 185 | extension, ok := allowedImagesContentTypes[contentType] 186 | if !ok { 187 | return "", errors.New("this content type is not allowed") 188 | } 189 | 190 | return extension, nil 191 | } 192 | -------------------------------------------------------------------------------- /pkg/utils/images.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "mime/multipart" 6 | "net/http" 7 | "net/textproto" 8 | 9 | "github.com/google/uuid" 10 | 11 | "github.com/AleksK1NG/api-mc/pkg/httpErrors" 12 | ) 13 | 14 | var allowedImagesContentType = map[string]string{ 15 | "image/png": "png", 16 | "image/jpg": "jpg", 17 | "image/jpeg": "jpeg", 18 | } 19 | 20 | func determineFileContentType(fileHeader textproto.MIMEHeader) (string, error) { 21 | contentTypes := fileHeader["Content-Type"] 22 | if len(contentTypes) < 1 { 23 | return "", httpErrors.NotAllowedImageHeader 24 | } 25 | return contentTypes[0], nil 26 | } 27 | 28 | func CheckImageContentType(image *multipart.FileHeader) error { 29 | // Check content type from header 30 | if !IsAllowedImageHeader(image) { 31 | return httpErrors.NotAllowedImageHeader 32 | } 33 | 34 | // Check real content type 35 | imageFile, err := image.Open() 36 | if err != nil { 37 | return httpErrors.BadRequest 38 | } 39 | defer imageFile.Close() 40 | 41 | fileHeader := make([]byte, 512) 42 | if _, err = imageFile.Read(fileHeader); err != nil { 43 | return httpErrors.BadRequest 44 | } 45 | 46 | if !IsAllowedImageContentType(fileHeader) { 47 | return httpErrors.NotAllowedImageHeader 48 | } 49 | return nil 50 | } 51 | 52 | func IsAllowedImageHeader(image *multipart.FileHeader) bool { 53 | contentType, err := determineFileContentType(image.Header) 54 | if err != nil { 55 | return false 56 | } 57 | _, allowed := allowedImagesContentType[contentType] 58 | return allowed 59 | } 60 | 61 | func GetImageExtension(image *multipart.FileHeader) (string, error) { 62 | contentType, err := determineFileContentType(image.Header) 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | extension, has := allowedImagesContentType[contentType] 68 | if !has { 69 | return "", errors.New("prohibited image extension") 70 | } 71 | return extension, nil 72 | } 73 | 74 | func GetImageContentType(image []byte) (string, bool) { 75 | contentType := http.DetectContentType(image) 76 | extension, allowed := allowedImagesContentType[contentType] 77 | return extension, allowed 78 | } 79 | 80 | func IsAllowedImageContentType(image []byte) bool { 81 | _, allowed := GetImageContentType(image) 82 | return allowed 83 | } 84 | 85 | func GetUniqFileName(userID string, fileExtension string) string { 86 | randString := uuid.New().String() 87 | return "userid_" + userID + "_" + randString + "." + fileExtension 88 | } 89 | -------------------------------------------------------------------------------- /pkg/utils/jwt.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "html" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | "github.com/dgrijalva/jwt-go" 11 | 12 | "github.com/AleksK1NG/api-mc/config" 13 | "github.com/AleksK1NG/api-mc/internal/models" 14 | ) 15 | 16 | // JWT Claims struct 17 | type Claims struct { 18 | Email string `json:"email"` 19 | ID string `json:"id"` 20 | jwt.StandardClaims 21 | } 22 | 23 | // Generate new JWT Token 24 | func GenerateJWTToken(user *models.User, config *config.Config) (string, error) { 25 | // Register the JWT claims, which includes the username and expiry time 26 | claims := &Claims{ 27 | Email: user.Email, 28 | ID: user.UserID.String(), 29 | StandardClaims: jwt.StandardClaims{ 30 | ExpiresAt: time.Now().Add(time.Minute * 60).Unix(), 31 | }, 32 | } 33 | 34 | // Declare the token with the algorithm used for signing, and the claims 35 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 36 | 37 | // Register the JWT string 38 | tokenString, err := token.SignedString([]byte(config.Server.JwtSecretKey)) 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | return tokenString, nil 44 | } 45 | 46 | // Extract JWT From Request 47 | func ExtractJWTFromRequest(r *http.Request) (map[string]interface{}, error) { 48 | // Get the JWT string 49 | tokenString := ExtractBearerToken(r) 50 | 51 | // Initialize a new instance of `Claims` (here using Claims map) 52 | claims := jwt.MapClaims{} 53 | 54 | // Parse the JWT string and repositories the result in `claims`. 55 | // Note that we are passing the key in this method as well. This method will return an error 56 | // if the token is invalid (if it has expired according to the expiry time we set on sign in), 57 | // or if the signature does not match 58 | token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (jwtKey interface{}, err error) { 59 | return jwtKey, err 60 | }) 61 | 62 | if err != nil { 63 | if errors.Is(err, jwt.ErrSignatureInvalid) { 64 | return nil, errors.New("invalid token signature") 65 | } 66 | return nil, err 67 | } 68 | 69 | if !token.Valid { 70 | return nil, errors.New("invalid token ") 71 | } 72 | 73 | return claims, nil 74 | } 75 | 76 | // Extract bearer token from request Authorization header 77 | func ExtractBearerToken(r *http.Request) string { 78 | headerAuthorization := r.Header.Get("Authorization") 79 | bearerToken := strings.Split(headerAuthorization, " ") 80 | return html.EscapeString(bearerToken[1]) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/utils/pagination.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | const ( 12 | defaultSize = 10 13 | ) 14 | 15 | // Pagination query params 16 | type PaginationQuery struct { 17 | Size int `json:"size,omitempty"` 18 | Page int `json:"page,omitempty"` 19 | OrderBy string `json:"orderBy,omitempty"` 20 | } 21 | 22 | // Set page size 23 | func (q *PaginationQuery) SetSize(sizeQuery string) error { 24 | if sizeQuery == "" { 25 | q.Size = defaultSize 26 | return nil 27 | } 28 | n, err := strconv.Atoi(sizeQuery) 29 | if err != nil { 30 | return err 31 | } 32 | q.Size = n 33 | 34 | return nil 35 | } 36 | 37 | // Set page number 38 | func (q *PaginationQuery) SetPage(pageQuery string) error { 39 | if pageQuery == "" { 40 | q.Size = 0 41 | return nil 42 | } 43 | n, err := strconv.Atoi(pageQuery) 44 | if err != nil { 45 | return err 46 | } 47 | q.Page = n 48 | 49 | return nil 50 | } 51 | 52 | // Set order by 53 | func (q *PaginationQuery) SetOrderBy(orderByQuery string) { 54 | q.OrderBy = orderByQuery 55 | } 56 | 57 | // Get offset 58 | func (q *PaginationQuery) GetOffset() int { 59 | if q.Page == 0 { 60 | return 0 61 | } 62 | return (q.Page - 1) * q.Size 63 | } 64 | 65 | // Get limit 66 | func (q *PaginationQuery) GetLimit() int { 67 | return q.Size 68 | } 69 | 70 | // Get OrderBy 71 | func (q *PaginationQuery) GetOrderBy() string { 72 | return q.OrderBy 73 | } 74 | 75 | // Get OrderBy 76 | func (q *PaginationQuery) GetPage() int { 77 | return q.Page 78 | } 79 | 80 | // Get OrderBy 81 | func (q *PaginationQuery) GetSize() int { 82 | return q.Size 83 | } 84 | 85 | func (q *PaginationQuery) GetQueryString() string { 86 | return fmt.Sprintf("page=%v&size=%v&orderBy=%s", q.GetPage(), q.GetSize(), q.GetOrderBy()) 87 | } 88 | 89 | // Get pagination query struct from 90 | func GetPaginationFromCtx(c echo.Context) (*PaginationQuery, error) { 91 | q := &PaginationQuery{} 92 | if err := q.SetPage(c.QueryParam("page")); err != nil { 93 | return nil, err 94 | } 95 | if err := q.SetSize(c.QueryParam("size")); err != nil { 96 | return nil, err 97 | } 98 | q.SetOrderBy(c.QueryParam("orderBy")) 99 | 100 | return q, nil 101 | } 102 | 103 | // Get total pages int 104 | func GetTotalPages(totalCount int, pageSize int) int { 105 | d := float64(totalCount) / float64(pageSize) 106 | return int(math.Ceil(d)) 107 | } 108 | 109 | // Get has more 110 | func GetHasMore(currentPage int, totalCount int, pageSize int) bool { 111 | return currentPage < totalCount/pageSize 112 | } 113 | -------------------------------------------------------------------------------- /pkg/utils/validator.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-playground/validator/v10" 7 | ) 8 | 9 | // Use a single instance of Validate, it caches struct info 10 | var validate *validator.Validate 11 | 12 | func init() { 13 | validate = validator.New() 14 | } 15 | 16 | // Validate struct fields 17 | func ValidateStruct(ctx context.Context, s interface{}) error { 18 | return validate.StructCtx(ctx, s) 19 | } 20 | -------------------------------------------------------------------------------- /ssl/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEpDCCAowCCQDXNgOwHd80ojANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls 3 | b2NhbGhvc3QwHhcNMjAwOTIwMTYzNzU3WhcNMzAwOTE4MTYzNzU3WjAUMRIwEAYD 4 | VQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCm 5 | owweTC+08ilKKxRWrozjZIZ+ZiMxyOvbeYh177A8ZZJAdA6Jd2ymo/eflACeZNL5 6 | KARy7+nfZMrhtMhDNdGuYNAQse+pPNnu1J3wcLvKg1ul1yhfstqWCCY3uKAZ6w02 7 | C9b2fd39KhR+uDBrW6PHZGkBuNHeUTHnRquHs4YSUd+Bg1YbfaPrU5PXE0qxK1lt 8 | eXi1sGp8hPsetBRKwRhPeOZiEsy31kwdeQWqxFylSLDvB+5Sq3oXK4xTsDoFuXRB 9 | uNhToP7KqcxwYH46x6Amidw2k0u2oOlO9t8OKqMVKr10hglUtDsBl1QrN0MZbvEW 10 | Y7kgJVUPx1REMlqbB+7wkLL0NJNTjZZRg2GhGcNwUR3FK523cxSqyXmIFubUTsJ1 11 | fuAVpsbkZUEt07b/tUzBGFlM1UTjVAojvTaecEllNJOvEtp8Iv7AP0hV4Li5apPV 12 | sMHFW8hMrVeWZevNVXkIAb8eK/vVi3zZuUricePqQ4Zj2b4rJvJdeBQGDiP0fEv0 13 | z6wtvQ3gjPaaHgWVDxJVvcSgougLMpHMhKbB2fxpvUOlB5v3ldKwn1DWIaOOd52X 14 | bQLAGwdhm++fE1TusDxorAH9wHvg8qxlFioTNwTiIxYuIREwxZjqPMr2VEDZhy8J 15 | xv2CAwUwTf2xOcHH/Wtg+GA6LmSH65k87SFM9oALowIDAQABMA0GCSqGSIb3DQEB 16 | CwUAA4ICAQByBiaYzLAiznvEDmQqUlUrCil/gdmB7Vu/th1g85cJFa78QWZUiwGg 17 | pLhgsR6UWdhqbGwhcZ3dVLMTEa2jHOWG3OE9HPTEs8fckrKOBCPckKw9Lq2jk0dt 18 | 4Z/wrPMLlqyd6DnDGL7OGlfGteLbgpc01rmEnrF7VfeJN4Z2tT6//wWgp5F6pXq8 19 | An5sYeP1rpvXOV5t10xdBftDm3pX+4RFbgLP0Y4r9DDRajkVeATNy3jEUYamGRkv 20 | QWy61pYdp5Ojp61uxew/JyPnsgVINogbAE9xsoalf7V9FqfFs+X0my9w5LAk8vJu 21 | xn8x88rkr5dPsrHoCiJlMoXpKIRC89enSJ0gPshQNbGg7yTQpwWYsug+Ir35MRIz 22 | appCKvhEaZVfseAsQQpeKBa65aoMHSfQUXHOAyvYMEQRHOp0B8Benr3rJ+inzFrO 23 | jk8e9u+dYBPC9HBdv4uNgZ6Nwnfi5KurInou1rhcB+PtC60Cx4k2XO0Zqht3HnaS 24 | XX9qewFqfvifx5XQGCqv+6MxtLSZ2VFom1resuPNIZXbFSB6N0wgCEJXAojvWDQn 25 | Nw6Dvl5rW/8c7pkifMqZ1q1mcbGOUo9mO+iBd1wN1WX4ZZPXin9UjUSs4/hFG7e3 26 | 5OFl+/YSI6ImznfJ0L3FhidGF0R9HhH92/wI6zqU5GzPMQgYmbrLNg== 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /ssl/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,413CF35BD54213C4 4 | 5 | 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 54 | -----END RSA PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /ssl/instructions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Inspired from: https://github.com/grpc/grpc-java/tree/master/examples#generating-self-signed-certificates-for-use-with-grpc 3 | 4 | # Output files 5 | # ca.key: Certificate Authority private key file (this shouldn't be shared in real-life) 6 | # ca.crt: Certificate Authority trust certificate (this should be shared with users in real-life) 7 | # server.key: Server private key, password protected (this shouldn't be shared) 8 | # server.csr: Server certificate signing request (this should be shared with the CA owner) 9 | # server.crt: Server certificate signed by the CA (this would be sent back by the CA owner) - keep on server 10 | # server.pem: Conversion of server.key into a format gRPC likes (this shouldn't be shared) 11 | 12 | # Summary 13 | # Private files: ca.key, server.key, server.pem, server.crt 14 | # "Share" files: ca.crt (needed by the client), server.csr (needed by the CA) 15 | 16 | # Changes these CN's to match your hosts in your environment if needed. 17 | SERVER_CN=localhost 18 | 19 | # Step 1: Generate Certificate Authority + Trust Certificate (ca.crt) 20 | openssl genrsa -passout pass:1111 -des3 -out ca.key 4096 21 | openssl req -passin pass:1111 -new -x509 -days 3650 -key ca.key -out ca.crt -subj "/CN=${SERVER_CN}" 22 | openssl req -passin pass:1111 -new -x509 -days 3650 -key ca.key -out ca.crt -subj "/CN=localhost" 23 | 24 | # Step 2: Generate the Server Private Key (server.key) 25 | openssl genrsa -passout pass:1111 -des3 -out server.key 4096 26 | 27 | # Step 3: Get a certificate signing request from the CA (server.csr) 28 | openssl req -passin pass:1111 -new -key server.key -out server.csr -subj "/CN=${SERVER_CN}" 29 | openssl req -passin pass:1111 -new -key server.key -out server.csr -subj "/CN=localhost" 30 | 31 | # Step 4: Sign the certificate with the CA we created (it's called self signing) - server.crt 32 | openssl x509 -req -passin pass:1111 -days 3650 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt 33 | 34 | # Step 5: Convert the server certificate to .pem format (server.pem) - usable by gRPC 35 | openssl pkcs8 -topk8 -nocrypt -passin pass:1111 -in server.key -out server.pem -------------------------------------------------------------------------------- /ssl/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEnDCCAoQCAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0 3 | MB4XDTIwMDkyMDE2MzgxNFoXDTMwMDkxODE2MzgxNFowFDESMBAGA1UEAwwJbG9j 4 | YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1aT9Du9BhGS1 5 | gyoQzFqAi0sx/dVulVZQgbt6AeCqz0Rmr+ZbpmnobOX1+F8WlykKvr6m1Xdd5cYD 6 | OQiu7MmQOhnbeVoDh/pnFVPIRWuKj/mzYW9t9RkQnsm3Evv2MyRgzsBLr4o+OCpX 7 | V3nBRYhYLAOkdDuaoLMzFcu/FhnOAjDkuhY49gw5WAQzxuAs5mIRhySpMDB0a4c+ 8 | RVhS1P1Pc56CmPPyaioXRGWYeF+7YtXrMR3OLfOmcgOCqrrIqDFJqqnyUaheSDp7 9 | qEx+DRTEjhRPWiHVShpOC8XoqmaWkOxp5PxgU42aVazQNqBGRz92FZZ7OWk0Pkwj 10 | FMKNB+VklAfsLrsAysWwnG0LhF2NeE6M5a25q0nRVJtpVhu++csFqx6MAOpBN7dJ 11 | YOsnPfXXTSkm3OJXGlBZvHPawVma92ONR2Qowv5nyzSOjITk/DT0Dv2l4spp7TjN 12 | qKaNW18Kj81Tx+VdZzmQmTG2S90rFGa9BHL5J5650fzqiuU1k9VLY0USoRAZyKIt 13 | rM2ufdJKrbHFlDhmyLf/HLCwFQiXF3jCcX1ODgdFWQ6fXAyknFpdqu4wkGVwO9CE 14 | j8rYu0EKhMeG1yza+A6DswANF9mrW3wBpCtBX38b2Mks959hGEzQgMGw3KGcSzob 15 | NhNe1hB3OFCU30KOz+C6zHVbznwt6/MCAwEAATANBgkqhkiG9w0BAQUFAAOCAgEA 16 | UX6/cNhSyvn2ptPRGimZ3+4NrRjnrlh463YFeM7PkpNeH8LcjbZaKfrjaSaXP3oe 17 | ycVppFIuKF2+yRfSQ4XjYUHYWUbPE3UQr+ivMI6j8m6mzOzWaLIjzdxpag57obg2 18 | ssw3SmjbvaK/J+9PWt1lfHBCqd/SP5sBIIsMpgZraUvQ3Ba1AHcF0R025g6/JAFL 19 | 262TcdJFmLDQ9kamuVRS+tZR1BMnAQYWe47/r1iRSYUq6BPvTN4Tn6RvUWjaFJ5N 20 | W8W2YcS5RCtXktMMcWje8K+q2cvbdNYxQXWXDXvj/cFBgiszFtZg8s1/rB6YN1W5 21 | Q9c/IU31kljXLLT/t9W9SpeqmVtuS8R8ghP0TUYNSLkUU6YVGgqn6BUqCmtPq12K 22 | nAi2fxAEhK0DLG93oZd7balCwpkvkoOKh6rMnN0sqhh4hCsBl6Kj1i6PJ33Nu06l 23 | yq4yMFZCuf4CxeYr7PYbFjtVzJJAOaeP0my6iF59PY9eoHtNQNzLJfgWzzJKEQAM 24 | 7xxovUUygVUFo0hU4BDiCv3Yuimr9h3PXXhKkGHw2gVYcYMNdihuUNBJw3HQ2MXS 25 | X8ZKusm2V5ewpZmqvxIWMP1ngCqRCYpvG0ZMzbai1cLgEEZF6M7LNsGnVv1NpvcE 26 | o7KBP+t+EON3kJG4zirPINMHu14Qtl3vUwHSlqWWGZI= 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /ssl/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEWTCCAkECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0B 3 | AQEFAAOCAg8AMIICCgKCAgEA1aT9Du9BhGS1gyoQzFqAi0sx/dVulVZQgbt6AeCq 4 | z0Rmr+ZbpmnobOX1+F8WlykKvr6m1Xdd5cYDOQiu7MmQOhnbeVoDh/pnFVPIRWuK 5 | j/mzYW9t9RkQnsm3Evv2MyRgzsBLr4o+OCpXV3nBRYhYLAOkdDuaoLMzFcu/FhnO 6 | AjDkuhY49gw5WAQzxuAs5mIRhySpMDB0a4c+RVhS1P1Pc56CmPPyaioXRGWYeF+7 7 | YtXrMR3OLfOmcgOCqrrIqDFJqqnyUaheSDp7qEx+DRTEjhRPWiHVShpOC8XoqmaW 8 | kOxp5PxgU42aVazQNqBGRz92FZZ7OWk0PkwjFMKNB+VklAfsLrsAysWwnG0LhF2N 9 | eE6M5a25q0nRVJtpVhu++csFqx6MAOpBN7dJYOsnPfXXTSkm3OJXGlBZvHPawVma 10 | 92ONR2Qowv5nyzSOjITk/DT0Dv2l4spp7TjNqKaNW18Kj81Tx+VdZzmQmTG2S90r 11 | FGa9BHL5J5650fzqiuU1k9VLY0USoRAZyKItrM2ufdJKrbHFlDhmyLf/HLCwFQiX 12 | F3jCcX1ODgdFWQ6fXAyknFpdqu4wkGVwO9CEj8rYu0EKhMeG1yza+A6DswANF9mr 13 | W3wBpCtBX38b2Mks959hGEzQgMGw3KGcSzobNhNe1hB3OFCU30KOz+C6zHVbznwt 14 | 6/MCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4ICAQBqR0sahis2v7Yp2fzLR/sB26jH 15 | gmxWrTt+xMBhi52ufgKxNW6TlF2dBj1gVXv9M0gEe1bRL702L3oPUzi+siPiHXsz 16 | gFCqXwSPNrvFbKYIHgMejlzGsSypf9W9Ni0gmrRJ9u3vG7FZQzEnv5QyT40GxRnP 17 | E68vemuqY6DX1W8LqSuHNBuaeWHVudl3GP9shfvKBTB/FcS54TISmXpiFXS6wR0p 18 | 1xACgRbBiEmFH2prJrdkqxOQbL1TT8ilPnVScWA7taFYgC1BSz/fD3Tr6EGkmBLb 19 | 4xCkXjNV5nkFrgBtiKk0hen9WkOWvBZ2BOsK1fQnOwNjCthRyIdVUIUPsFleUNGb 20 | aZPamgrWoSFkcwzD8gPVn+FrpyGgJiZp1Ytx2xJxFw9cAHrZC8gBakYxZqesNvG+ 21 | 7QQe6dA5skggQIsb9vj3EKohlSoCbyVaopGkGZH7Km6aFccnR1yrv9/qTySn4FKu 22 | MfFQ8inl6IcdxUt7YbYNJSDSreLKoW/9lydZuIH1dPRlvBar5RBMZWCNd3yo3eTI 23 | nrYXpI5QIXpoHgTHjjRGXzCnZa2yH8BisksjFrHxjBZ9s3SU7oCK2x33kSAUrcpA 24 | dNHRTe9lii0dbJamqqM77f/GfBu10dGd/6KL0UP9N/qNoSFVr0xWdYLwD3+b+QtY 25 | 95TKslD7fW4wSfMNNQ== 26 | -----END CERTIFICATE REQUEST----- 27 | -------------------------------------------------------------------------------- /ssl/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,5A073541A56C8C82 4 | 5 | bs768ErJe4QN4KmxM5FM3tgLk6oGwLN98Zh2rsOgLMurmAogBGhO19pzIPLL5yXJ 6 | pUSU9inEQmFx7URnNeq9EGV6wMTGCWWpYW0Pzy+PKhiHoWPhy/bnhS7FA9uMqXF2 7 | vIxJofg/acFVRHDrIZOIFMdnTapzqk0WNgkpme82lmFV/z89xJdvA8hJJhkNaogt 8 | 12mLnOCDkazgN/mhEN+/EbeBr+5ZVYDWBA8nQFSSeb/gKN8q8efnp39+mUl4zfQb 9 | SNf75eUema7gCxMdjizJhld0wtsW+ttCqmQ8wbTExcYdSn06B7NsQtFC1mWVIIuI 10 | wz+GRIdr2Ie68kw2Gw72n7km7nrmgrkRBa5Ag1ZIZnqAkmWMJH074aRvVCFJvJ2h 11 | uoZeohlWh5jz9PwMxkHRji7JhUuKuG403p2DhIuTbDKS3sJ+sn1RDXAWRV6g7gWC 12 | SbwarNNdbp7ZdV0BCSxfRR2D7ys0GautKmNg1PNfeXPupb/4knhO6qToQNndGJVy 13 | 6Hfs9LqpN6J91jSky4oP9s2ECDvd6raBj958IVAkoAR84eEMD9amazi3wAiHvGKp 14 | f7W7D8WB/eht/ELWgjDpOINjLNbETqSmMW7l5S5T5hTXoOe2RbZi4bSSDXPCS68n 15 | 0XjRhfCjtUSbxsr03nT+6iqzFF7BoUSqPwxCkXTvcP4Ai+hO+8oQ/1+rzbyU1yXj 16 | OjiC5T6cTHl3Alb6IJPBzw8Vyr8GppOYl2H1B+NQs5fiYMHT+uR/pb39JyBI8h6t 17 | XAv+HkHQmn6xv8BVmYBPEI9xbdu8Nk8Vir4PAk7fcBIhXKwCbue/E88h6EHZlezn 18 | zKUtjokqhMYdV7sgBS8jpi1xMeCj8UPLvk1+CDDAoGbH8dbBgwBneUMbd8dgaOYQ 19 | cJCvVJnxuMQPv5oFaZOPZX7xxQiqJPFLXDzRyq8clOJ/IrUUxJhhQcz+phSqKJhu 20 | sajYVYrz2MKl/SvxqYbCexsy4oVwGTqNJMC/5uN4/OM7T650kk3A0KKDZRF8U5AY 21 | M8VynyDDElR4tKmQF1vCaisB9ylV5K/ujzUq3gtr+kKouFLtP3ssplarH1GigxcM 22 | 9yYC/fjK4XZbM4Z8zcig+26G8BltCxFFYOqaguUxze6/DxqGahxQpOJuM+6x2N6K 23 | tgQR053o/DfFIuprD3GSn2ZjIq/+tC8GmqMESMhrwSwSBudYsQpPUqPF3ZpBKc71 24 | uub8su7cKZzrsgmVWA4OlUkV/b0kwulDdDMf+w6DGkmHjfBz7G8KCS+CnQmuDw2v 25 | zxFNJHx2mJFi3TjmDMc0oI+xLZhbGnf8F+FifMZSrkqjo04aV2q91rpmF7xLNwbk 26 | N5CVrHXBjKM9iRNCBeJsWVWmJYGWPSlU4nYyh+kWK0XI6O56ciRlAc/+vdGECE6O 27 | 3FDeZocT8Ygxko3Gz4x1Mv6GNCsQy0xcv46cVkfTlM7CFqs1/9HGkJCsS5BW1ouJ 28 | 8ITG451rsMumqg7MfokUjmHuHR+b0tMh3chB1WpKuRH3Sr1VpYya4srYFTKmyR9j 29 | FF+DtAsNuidwMZ+uUu/ylB/d8kHuh73q5knzwkv5FnXQcFL2NbJgjcvHWmV2Xc0e 30 | uQYlKtHxmB6oUrPaNQHJki5xhHxG35ksKc6ebzM9jyfWj437LkgbwwOlDYI1wZ18 31 | eskWomuALZPNB6SEfox1DDGGZ2Zwm575IQfmwtGRSLAlH+8NQpgqTwNL0h7W1/GP 32 | F1MVz7CeymiGahBYFwLOeI+bOq/aVnPR4jV9/YU1yRHia4Mjno762R7HzJV0jbx3 33 | 71wz7Noho2L1OxdsJBbriO9AbzhIHA/YYnWqrfjrlFAJypzMuLpxtnnNpNoX9nCL 34 | ++Lrg+eYAz/Sonig9N69I1JbrrJ4TmlHEkbfflCxIq1HUPwlz8i2lBQrjn+lsORH 35 | JjMZomV3otF6wUgmAFUalGdF3iRjkbL4Qn9Evw0xvn+xpmwMuZmrXeu5F3/nqKgX 36 | hwMQ/cWyLV0hXjSFvQ5Mlfn+vjKeFfBf+rHfxnFXJQ+MCXFiQ2NIONgYHrmCS7aF 37 | opMULkxRrDTh/trhtk6kTYEnuC14vUH2Z4Y7e6qjNentEtDKBjAkEwMTuakQJ60B 38 | wnAWOED0ZIgDq9a9VPoNZLDKf1Tib1OKO5Fe8Mxc3dM2vhgyhZiRFJy/2OmBhKfT 39 | 5PVA1Sh38IE/BM+mL5FoPP6X+zUPmo/sxsAclvYU31Bci0xHj4JlZkDtWJfhArNN 40 | pQZG2s2hzcAv/V3WFVdE8EDULrzXa1llytaeFyPiEkAvMXOkJ3Q6wK9vIOT9wBiW 41 | UjjdpkknKmeC9lemb/G21QeC4g2wGUT0pAsMhHxbapSxg0/m7Xg/aipVJEF/S+Lz 42 | m0N64cPtaYDXNNir0/Dx3cyQ7ty29ihAqSKnBzCIdJ2WkyFnniUlCKCvkgwcyB+F 43 | 9117cxQtyi3ClNJsxoWnNMuvomVlg/WuD3Vrn/cX3cKcWqHhKSgp/tUhJJPa4n2y 44 | XApmOltG70jjKoMIUQ9MP/qTe35M1Ea4Wc5WWj+3JG+QeNL3sFx3edislcCWu6e1 45 | WGvDvoqVsYjIIZif5G/efqEk46I8dw4VZgwZWOqk9Htv0gJxcbecHCrI4NP5fuFD 46 | +NAm/3i7GaC0SNkZJCMO8ilSNB0i/n+YcuBcn/MahQvsPKS4sVaqHETnhzT1DmDA 47 | A7XsEyAv5z60T1oOLPqtBfXPc2tdmI4rm60GtEe6A/vq3BO6mKz8yDSbnFSyQzqU 48 | /sC3SkXCjmTNkZbbEwoiprdbfUkJPN5IbnH2Y4JryKtYBkI3scO16j/W6ZyuGrkm 49 | WXxfQJYGMEmLRowCAHcLMg4gv+KQ3W9t9SScdweZpkkizSCvMHgJ33YlFUMuK8vT 50 | tEpO3nGTtZuhEOCPcl4Hb4D5lgA/tV0kSoTsFE6f2mBO4QcNbWnh7u7U/sID2MW/ 51 | Iku3Q1Si2NQZ67j5qX1CBT2S409tchv82kFVJw9NipAbfrAB3BMJruHfxsyadMjk 52 | PPZnuTUPXlmq5mDQRxte6l9ydK3/6HwgJKyIMi7y9DzfNzUEAzm8AM2rBVWvHofU 53 | KCFlUbqzoFQOszRHtF9hvo5Gpv50iQ7NYsG1HWy83DR+i+pDiAHbmfcV19h+MH3z 54 | -----END RSA PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /ssl/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDVpP0O70GEZLWD 3 | KhDMWoCLSzH91W6VVlCBu3oB4KrPRGav5lumaehs5fX4XxaXKQq+vqbVd13lxgM5 4 | CK7syZA6Gdt5WgOH+mcVU8hFa4qP+bNhb231GRCeybcS+/YzJGDOwEuvij44KldX 5 | ecFFiFgsA6R0O5qgszMVy78WGc4CMOS6Fjj2DDlYBDPG4CzmYhGHJKkwMHRrhz5F 6 | WFLU/U9znoKY8/JqKhdEZZh4X7ti1esxHc4t86ZyA4KqusioMUmqqfJRqF5IOnuo 7 | TH4NFMSOFE9aIdVKGk4LxeiqZpaQ7Gnk/GBTjZpVrNA2oEZHP3YVlns5aTQ+TCMU 8 | wo0H5WSUB+wuuwDKxbCcbQuEXY14TozlrbmrSdFUm2lWG775ywWrHowA6kE3t0lg 9 | 6yc99ddNKSbc4lcaUFm8c9rBWZr3Y41HZCjC/mfLNI6MhOT8NPQO/aXiymntOM2o 10 | po1bXwqPzVPH5V1nOZCZMbZL3SsUZr0EcvknnrnR/OqK5TWT1UtjRRKhEBnIoi2s 11 | za590kqtscWUOGbIt/8csLAVCJcXeMJxfU4OB0VZDp9cDKScWl2q7jCQZXA70ISP 12 | yti7QQqEx4bXLNr4DoOzAA0X2atbfAGkK0FffxvYySz3n2EYTNCAwbDcoZxLOhs2 13 | E17WEHc4UJTfQo7P4LrMdVvOfC3r8wIDAQABAoICAEdBSe2LOszPUgK3KvcdUDYl 14 | FD1WzBUevqcmQiESL6YFaEJOkE7Gj/CSGiGGhWBRHfZUXAxiTXzvN+/zx3POHj5i 15 | lWK59OeLSopAcVFF9ubiH0PmCERw4aw0Fs1MH+cawPb0B8o6T1ooNQ1F3II2YUH8 16 | zQK/RmlGm0kvtUHHxX/RktfFxaW6mf2TGTnBVvhXyQTL42nhH1Mlvk0ekjHbcn5b 17 | Za9h3X4vH6d+QwYS18q1EkZFbJjC5MauCQysU3RVS/6Rw/IcN6Xba5bMPFZckNna 18 | SEUFd7/JWjJvBZSftqQLVZ471lzHo6vjgWZWulnU/qtgjySsw/HrLuAjqynxBkQE 19 | /Z/vOTshXWoTR+kBO1oij5C+e9r1EQbedXxnDletkdY28zaPmdMVj3T2sPehP/sO 20 | O7zxWpeawmSNji0ULxl0Q76MtgJ4yeTU3aadKOco7EOT1oUD98cnCGlzcA6hv3GD 21 | aKZtzs7cn49sFkLAAUKEDoAvzMh67gApi44/p6/f+qgfRXnXLysUaYYp650PEikc 22 | JaI/d18rNdgJpR523Q4vsF+2b+I0S2qYKrLArVdmSvJbsRVOGBnkTzsosjzbnwsZ 23 | NLeG2AD6lJhEmarXdw+zfNqwGNmPVwU2eR42Vao1v/3JiaXdEoUNtv9k9TjLmjNJ 24 | epGa22wDX5PzU+w82eZBAoIBAQDvVEmU3mvC5xovEc1x4drBDE1ESNjo2m3JV4H3 25 | /KgiIBSuFjlNT1cgnpTwUX+lWoAkk4f8QZRYeLkT8JTHXwi6DDNYdlaK8r1lxIPW 26 | 3JQvATcbHHFDSM6/UGEK00N+GCsadqsOXExLonnCcHJHQae6ktpKH0Q5mMJ7c/9k 27 | ayUCoLXmjoxueQukucAY0B1nJQUv0fM++5IJxBnisZvoDAR+ZdkLzd/vZim89SYT 28 | ibN1ExvV2zGryjnwldl/KCc/O/ki0FuP7721q7FakjblDbkmrHM31vHIroeuDNTo 29 | 2AWPJfgZNHu87anr3ZPuntfFtWExbXS7db5ihO0RqUVwrIpDAoIBAQDkhrDtV04X 30 | gsxq4Re34DukTBxJt7uHlzc3ra5ru2Z8EbKJ7bdG7X0vRIWW1ssx7IGFI93qeERv 31 | jUHeyydF8LjpY//rUY1IMGEkI+rmBYt9NN6VPKBetrznikA7ReLrZJ31zq933YQ0 32 | AnRHvKtv9wcXSzaGHSKSnN3aMjhDg7i6sqPiI7+YRRnM71CpsrP2sUk9K5t67yEx 33 | rHY4ZTWMoHinCUia6z5G71ZBIslRG5CRf/U3QnSK4LszMPwNjZyI9seDFAeHd9IC 34 | lBcusrRX1zJKUSkFvRh4gsRDjSR56nCnezkRtnQRrM0zBYUlMBRlAPUVq0G04TEt 35 | HyLK+ZXNSjSRAoIBAQDnaX+wg5R9I8rMopEdQb6slYGMukeKd9JaMdQI/nNwc8ar 36 | Qe/sUgA0GUJ4UMV1FFn9g+2kO6D+HtUOc7zYPosIok1vhxVNS0NZSLgWJLjf7nPj 37 | MhBOd/L5R/ZdakPDhAkBkKb7vsFDDPpgySumvNQ68k5CB5OHga7jghj7dyKVNOJN 38 | 6Z3eIArjH3ygQXN8zW4DfCWQy928tbI14XiX2i7qLP6+jDWwnP9Up1JG6Anu9Sgg 39 | E88mheaaO7rPWfsBCLNwNzmhprWwGTDnG6QavLc/rtXFs3+chS3KXLvt3Rsa/CK1 40 | 9GqFFuULnPeybkLC+AvfqC+MJ2CMkG1Oe3caaKtTAoIBADrabUpSh6wKZXbJDYCv 41 | YOzJJSffB4695NyUAC2Cj7w4GpDnBaJgmzLHJNhZ7O6oiBqvyAEQhB9uc55bF3wt 42 | qJGCzW/fCtGilAHotiATIX9XVFN+z5ZU3YWL10rsjqosuXmKhyoJhHiYgTXQYx5s 43 | sgjPt/UGH9c+SuxcrpzEmZiLVSVyK2+drC1ZHJ73hN1tfv0f8+TPHO9cCP4xIn9a 44 | /HeYLninSNyf2sjfmpUm0i6Gk7JtjPIPOmbOoLsk00F6vJsHV4EN3KoJVYcTQtq0 45 | cyEskbIGpvyyQLVc7h3vwJ+BXosvP+klZZtUOpv/K+FvQ68W8c8Rh8alFCLN8ER0 46 | beECggEAfQaY/uElfI8v4lL185R+H5icP5rgoz5yy2Q+MfwFJzBasM3EYvWOvPMF 47 | pUgPLbZGd82nIIGzGDOxJqPcPTA9sOaEmnkv4iOez0OqUifYeYfdRctTSgIFGTSV 48 | hKWsRxFjLxxbpBcXvjdscgSenqAScPkYYqNP51j4MPTR70VGW/QV4DkQeN1lI9P3 49 | UDTjWeVvphIX5glwJURW5Wg7TzORQSpTfwP4eHvIxoucF98L0bRNeMr2pnf2jKVx 50 | y4srmPLFZHSVJNmijOauz1rApO2Vqw1yfn5dh5dmWhon8TKl3sXx3o3jrFLr7uy5 51 | z+V8FV4U0jafzeuPSEDjf7QAV3Q2aA== 52 | -----END PRIVATE KEY----- 53 | --------------------------------------------------------------------------------