diff --git a/.coverage/.keep b/.coverage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index cbc4606..7985813 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ -config/config-local.yaml +vendor/ -vendor/ \ No newline at end of file +# we want to ignore files under this directory (ends with /*) +.coverage/* + +# we want to keep track of this .keep files (starts with !) +!.keep \ No newline at end of file diff --git a/cmd/api/main.go b/cmd/api/main.go index 8f58455..fac72e2 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -2,21 +2,53 @@ package main import ( "context" - "os" + "time" "github.com/DoWithLogic/golang-clean-architecture/config" "github.com/DoWithLogic/golang-clean-architecture/internal/app" + "github.com/DoWithLogic/golang-clean-architecture/pkg/observability" + "github.com/labstack/gommon/log" ) func main() { - env := os.Getenv("env") - if env == "" { - env = "local" + // Load the application configuration from the specified directory. + cfg, err := config.LoadConfig("config") + if err != nil { + // If an error occurs while loading the configuration, panic with the error. + panic(err) } - cfg, err := config.LoadConfig(env) + // Set the time zone to the specified value from the configuration. + _, err = time.LoadLocation(cfg.Server.TimeZone) if err != nil { - panic(err) + // If an error occurs while setting the time zone, log the error and exit the function. + log.Error("Error on setting the time zone: ", err) + return + } + + // Initialize observability components if observability is enabled in the configuration. + if cfg.Observability.Enable { + // Initialize the tracer provider for distributed tracing. + tracer, err := observability.InitTracerProvider(cfg) + if err != nil { + log.Warn("Failed to initialize tracer: ", err) + } + + // Initialize the meter provider for metrics collection. + meter, err := observability.InitMeterProvider(cfg) + if err != nil { + log.Warn("Failed to initialize meter: ", err) + } + + // Ensure that the tracer and meter are shut down when the main function exits. + defer func() { + if tracer != nil { + tracer.Shutdown(context.Background()) + } + if meter != nil { + meter.Shutdown(context.Background()) + } + }() } app.NewApp(context.Background(), cfg).Run() diff --git a/config/config-local.yaml b/config/config-local.yaml new file mode 100644 index 0000000..f54eb06 --- /dev/null +++ b/config/config-local.yaml @@ -0,0 +1,26 @@ +App: + Name: "golang-clean-architecture" + Version: "v0.0.1" + Scheme: "http" + Host: "localhost:3002" + Environment: local #local,development,staging,production + +Server: + Port: "9090" + Debug: true + TimeZone: "Asia/Jakarta" + + +Database: + Host: 127.0.0.1 + Port: 3306 + Name: users + User: root + Password: pwd + +Authentication: + Key: DoWithLogic!@# + +Observability: + Enable: false + Mode: "otlp/http" \ No newline at end of file diff --git a/config/config-local.yaml.example b/config/config-local.yaml.example deleted file mode 100644 index cdcfd4b..0000000 --- a/config/config-local.yaml.example +++ /dev/null @@ -1,21 +0,0 @@ -Server: - Name: golang-clean-architecture - Version: "v0.0.1" - RPCPort: 8080 - RESTPort: 9090 - Debug: false - Env: local - ReadTimeOut: 5s - WriteTimeOut: 5s - -Database: - Host: mysql-db - Port: 3306 - Name: users - User: mysql - Password: pwd - -Authentication: - Key: DoWithLogic!@# - SecretKey: s3cr#tK3y!@#v001 - SaltKey: s4ltK3y!@#ddv001 diff --git a/config/config.go b/config/config.go index fd07f37..0f83d79 100644 --- a/config/config.go +++ b/config/config.go @@ -1,19 +1,41 @@ package config import ( + "errors" "fmt" - "time" + "log" + "strings" "github.com/spf13/viper" ) type ( Config struct { - Database DatabaseConfig + App AppConfig Server ServerConfig + Database DatabaseConfig Authentication AuthenticationConfig + Observability ObservabilityConfig + JWT JWTConfig + } + + // AppConfig holds the configuration related to the application settings. + AppConfig struct { + Name string + Version string + Schema string + Host string + Environment string + } + + // ServerConfig holds the configuration for the server settings. + ServerConfig struct { + Port string // The port on which the server will listen. + Debug bool // Indicates if debug mode is enabled. + TimeZone string // The time zone setting for the server. } + // DatabaseConfig holds the configuration for the database connection. DatabaseConfig struct { Host string Port int @@ -22,37 +44,76 @@ type ( Password string } - ServerConfig struct { - Name string - Version string - RPCPort string - RESTPort string - Debug bool - Environment string - ReadTimeout time.Duration - WriteTimeout time.Duration + AuthenticationConfig struct { + Key string } - AuthenticationConfig struct { - Key string - SecretKey string - SaltKey string + // ObservabilityConfig holds the configuration for observability settings. + ObservabilityConfig struct { + Enable bool // Indicates if observability is enabled. + Mode string // Specifies the observability mode. + } + + JWTConfig struct { + Key string + Expired int + Label string } ) -func LoadConfig(env string) (Config, error) { - viper.SetConfigFile(fmt.Sprintf("config/config-%s.yaml", env)) +// LoadConfig loads the configuration from the specified filename. +func LoadConfig(filename string) (Config, error) { + // Create a new Viper instance. + v := viper.New() - if err := viper.ReadInConfig(); err != nil { + // Set the configuration file name, path, and environment variable settings. + v.SetConfigName(fmt.Sprintf("config/%s", filename)) + v.AddConfigPath(".") + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + // Read the configuration file. + if err := v.ReadInConfig(); err != nil { fmt.Printf("Error reading config file: %v\n", err) return Config{}, err } + // Unmarshal the configuration into the Config struct. var config Config - if err := viper.Unmarshal(&config); err != nil { + if err := v.Unmarshal(&config); err != nil { fmt.Printf("Error unmarshaling config: %v\n", err) return Config{}, err } return config, nil } + +// LoadConfigPath loads the configuration from the specified path. +func LoadConfigPath(path string) (Config, error) { + // Create a new Viper instance. + v := viper.New() + + // Set the configuration file name, path, and environment variable settings. + v.SetConfigName(path) + v.AddConfigPath(".") + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + // Read the configuration file. + if err := v.ReadInConfig(); err != nil { + // Handle the case where the configuration file is not found. + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + return Config{}, errors.New("config file not found") + } + return Config{}, err + } + + // Parse the configuration into the Config struct. + var c Config + if err := v.Unmarshal(&c); err != nil { + log.Printf("unable to decode into struct, %v", err) + return Config{}, err + } + + return c, nil +} diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..f54eb06 --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,26 @@ +App: + Name: "golang-clean-architecture" + Version: "v0.0.1" + Scheme: "http" + Host: "localhost:3002" + Environment: local #local,development,staging,production + +Server: + Port: "9090" + Debug: true + TimeZone: "Asia/Jakarta" + + +Database: + Host: 127.0.0.1 + Port: 3306 + Name: users + User: root + Password: pwd + +Authentication: + Key: DoWithLogic!@# + +Observability: + Enable: false + Mode: "otlp/http" \ No newline at end of file diff --git a/database/mysql/migration/20230924142159_add_user_table.sql b/database/mysql/migration/20230924142159_add_user_table.sql index b6f6ced..e426494 100644 --- a/database/mysql/migration/20230924142159_add_user_table.sql +++ b/database/mysql/migration/20230924142159_add_user_table.sql @@ -9,9 +9,7 @@ CREATE TABLE `users` ( `user_type` varchar(50) NOT NULL, `is_active` tinyint(1) NOT NULL, `created_at` timestamp NOT NULL, - `created_by` varchar(255) NOT NULL, `updated_at` timestamp DEFAULT NULL, - `updated_by` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/docker-compose.yml b/docker-compose.yml index 281d669..753e515 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,14 +17,7 @@ services: interval: 0.5s timeout: 10s retries: 10 - golang-clean-architecture: - build: - context: . - dockerfile: Dockerfile # Specify the path to your Dockerfile - ports: - - 8080:8080 - - 9090:9090 - volumes: - - ./config/config-local.yaml:/app/config/config-local.yaml - depends_on: - - mysql-db + entrypoint: + sh -c " + echo 'CREATE DATABASE IF NOT EXISTS users;' > /docker-entrypoint-initdb.d/init.sql; + /usr/local/bin/docker-entrypoint.sh --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" diff --git a/go.mod b/go.mod index 1d557cd..6f93e05 100644 --- a/go.mod +++ b/go.mod @@ -1,46 +1,90 @@ module github.com/DoWithLogic/golang-clean-architecture -go 1.20 +go 1.21 + +toolchain go1.21.5 require ( - github.com/DATA-DOG/go-sqlmock v1.5.0 - github.com/dgrijalva/jwt-go v3.2.0+incompatible - github.com/go-sql-driver/mysql v1.7.1 + github.com/go-faker/faker/v4 v4.4.1 + github.com/go-playground/validator/v10 v10.20.0 + github.com/go-sql-driver/mysql v1.8.1 + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/invopop/validation v0.3.0 - github.com/jmoiron/sqlx v1.3.5 - github.com/labstack/echo/v4 v4.11.1 - github.com/rs/zerolog v1.30.0 - github.com/spf13/viper v1.16.0 - github.com/stretchr/testify v1.8.4 - go.uber.org/mock v0.3.0 + github.com/jmoiron/sqlx v1.4.0 + github.com/labstack/echo-contrib v0.17.1 + github.com/labstack/echo/v4 v4.12.0 + github.com/labstack/gommon v0.4.2 + github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.32.0 + github.com/samber/lo v1.39.0 + github.com/spf13/viper v1.18.2 + github.com/stretchr/testify v1.9.0 + github.com/valyala/fasthttp v1.52.0 + go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.51.0 + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.51.0 + go.opentelemetry.io/otel v1.26.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.26.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.26.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.26.0 + go.opentelemetry.io/otel/sdk v1.26.0 + go.opentelemetry.io/otel/sdk/metric v1.26.0 + go.opentelemetry.io/otel/trace v1.26.0 + go.uber.org/mock v0.4.0 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/labstack/gommon v0.4.0 // indirect - github.com/lib/pq v1.10.9 // indirect + github.com/klauspost/compress v1.17.6 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spf13/afero v1.9.5 // indirect - github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.19.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.53.0 // indirect + github.com/prometheus/procfs v0.13.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.4.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/crypto v0.13.0 // indirect - golang.org/x/net v0.12.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/time v0.3.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/proto/otlp v1.2.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect + google.golang.org/grpc v1.63.2 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6ea0206..5a1ef3d 100644 --- a/go.sum +++ b/go.sum @@ -1,540 +1,209 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= -github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-faker/faker/v4 v4.4.1 h1:LY1jDgjVkBZWIhATCt+gkl0x9i/7wC61gZx73GTFb+Q= +github.com/go-faker/faker/v4 v4.4.1/go.mod h1:HRLrjis+tYsbFtIHufEPTAIzcZiRu0rS9EYl2Ccwme4= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/invopop/validation v0.3.0 h1:o260kbjXzoBO/ypXDSSrCLL7SxEFUXBsX09YTE9AxZw= github.com/invopop/validation v0.3.0/go.mod h1:qIBG6APYLp2Wu3/96p3idYjP8ffTKVmQBfKiZbw0Hts= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= -github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo-contrib v0.17.1 h1:7I/he7ylVKsDUieaGRZ9XxxTYOjfQwVzHzUYrNykfCU= +github.com/labstack/echo-contrib v0.17.1/go.mod h1:SnsCZtwHBAZm5uBSAtQtXQHI3wqEA73hvTn0bYMKnZA= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= +github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= +github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= +github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= -github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= +github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= -github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= +github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= -go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.51.0 h1:477zSmIXZy+324mzDsXGm1DPhHKWTFrT6iUIAlpI9f4= +go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.51.0/go.mod h1:oZWJX6UZ7QPGvMSM/2T5yXdKOtFaey2IV/IMHY+tryg= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.51.0 h1:974XTyIwHI4nHa1+uSLxHtUnlJ2DiVtAJjk7fd07p/8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.51.0/go.mod h1:ZvX/taFlN6TGaOOM6D42wrNwPKUV1nGO2FuUXkityBU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= +go.opentelemetry.io/contrib/propagators/b3 v1.26.0 h1:wgFbVA+bK2k+fGVfDOCOG4cfDAoppyr5sI2dVlh8MWM= +go.opentelemetry.io/contrib/propagators/b3 v1.26.0/go.mod h1:DDktFXxA+fyItAAM0Sbl5OBH7KOsCTjvbBdPKtoIf/k= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.26.0 h1:+hm+I+KigBy3M24/h1p/NHkUx/evbLH0PNcjpMyCHc4= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.26.0/go.mod h1:NjC8142mLvvNT6biDpaMjyz78kyEHIwAJlSX0N9P5KI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.26.0 h1:HGZWGmCVRCVyAs2GQaiHQPbDHo+ObFWeUEOd+zDnp64= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.26.0/go.mod h1:SaH+v38LSCHddyk7RGlU9uZyQoRrKao6IBnJw6Kbn+c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 h1:Waw9Wfpo/IXzOI8bCB7DIk+0JZcqqsyn1JFnAc+iam8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0/go.mod h1:wnJIG4fOqyynOnnQF/eQb4/16VlX2EJAHhHgqIqWfAo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 h1:1wp/gyxsuYtuE/JFxsQRtcCDtMrO2qMvlfXALU5wkzI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0/go.mod h1:gbTHmghkGgqxMomVQQMur1Nba4M0MQ8AYThXDUjsJ38= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.26.0 h1:0W5o9SzoR15ocYHEQfvfipzcNog1lBxOLfnex91Hk6s= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.26.0/go.mod h1:zVZ8nz+VSggWmnh6tTsJqXQ7rU4xLwRtna1M4x5jq58= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= +go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= +go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= +go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y= +go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/golang clean architecture.postman_collection.json b/golang clean architecture.postman_collection.json new file mode 100644 index 0000000..ae78f09 --- /dev/null +++ b/golang clean architecture.postman_collection.json @@ -0,0 +1,186 @@ +{ + "info": { + "_postman_id": "c006b308-a263-4461-a045-83e378d1a44e", + "name": "golang clean architecture", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "9928599" + }, + "item": [ + { + "name": "Users", + "item": [ + { + "name": "Login", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"martin.yonatan1@test.com\",\n \"password\": \"BatakReseh@123\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:9090/api/v1/users/login", + "host": [ + "localhost" + ], + "port": "9090", + "path": [ + "api", + "v1", + "users", + "login" + ] + } + }, + "response": [] + }, + { + "name": "Create", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"fullname\": \"testing\",\n \"phone_number\": \"081271717273\",\n \"email\": \"martin.yonatan1@test.com\",\n \"password\": \"BatakReseh@123\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:9090/api/v1/users", + "host": [ + "localhost" + ], + "port": "9090", + "path": [ + "api", + "v1", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Update Partial", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyLCJlbWFpbCI6Im1hcnRpbi55b25hdGFuMUB0ZXN0LmNvbSIsImV4cCI6MTY5NzU0OTk1OX0.3j5Oa4-z0InXgKHClNla1Me78hhD0jTebQyU3sp7ZxA", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"user_type\": \"premium_user\",\n \"fullname\": \"UPDATE NAME\",\n \"phone_number\": \"081278984563\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:9090/api/v1/users/update", + "host": [ + "localhost" + ], + "port": "9090", + "path": [ + "api", + "v1", + "users", + "update" + ] + } + }, + "response": [] + }, + { + "name": "Update Status", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyLCJlbWFpbCI6Im1hcnRpbi55b25hdGFuMUB0ZXN0LmNvbSIsImV4cCI6MTY5NzU0OTk1OX0.3j5Oa4-z0InXgKHClNla1Me78hhD0jTebQyU3sp7ZxA", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"status\":1 // active = 1, inactive = 0\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:9090/api/v1/users/update/status", + "host": [ + "localhost" + ], + "port": "9090", + "path": [ + "api", + "v1", + "users", + "update", + "status" + ] + } + }, + "response": [] + }, + { + "name": "Detail", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJlbWFpbCI6Im1hcnRpbi55b25hdGFuMUB0ZXN0LmNvbSIsImV4cCI6MTY5OTA2OTE2MH0.peKCj5pdg40XimFhP6rCJoHlcwSL9tPb7z90kXXUByA", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "localhost:9090/api/v1/users/detail", + "host": [ + "localhost" + ], + "port": "9090", + "path": [ + "api", + "v1", + "users", + "detail" + ] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/internal/app/app.go b/internal/app/app.go index cf9bd05..6ebafd6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -11,39 +11,31 @@ import ( _ "github.com/go-sql-driver/mysql" "github.com/DoWithLogic/golang-clean-architecture/config" + "github.com/DoWithLogic/golang-clean-architecture/internal/middleware" "github.com/DoWithLogic/golang-clean-architecture/pkg/datasource" - "github.com/DoWithLogic/golang-clean-architecture/pkg/middleware" - "github.com/DoWithLogic/golang-clean-architecture/pkg/otel/zerolog" "github.com/jmoiron/sqlx" "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" + + "github.com/samber/lo" ) type App struct { - db *sqlx.DB - echo *echo.Echo - log *zerolog.Logger - cfg config.Config + db *sqlx.DB // Database connection. + echo *echo.Echo // Echo HTTP server instance. + cfg config.Config // Configuration settings for the application. } func NewApp(ctx context.Context, cfg config.Config) *App { - db, err := datasource.NewDatabase(cfg.Database) - if err != nil { - panic(err) - } - return &App{ - db: db, - echo: echo.New(), - log: zerolog.NewZeroLog(ctx, os.Stdout), + db: lo.Must(datasource.NewDatabase(cfg.Database)), + echo: middleware.NewEchoServer(cfg), cfg: cfg, } } func (app *App) Run() error { if err := app.startService(); err != nil { - app.log.Z().Err(err).Msg("[app]StartService") - return err } @@ -57,15 +49,14 @@ func (app *App) Run() error { <-quit log.Info("Server is shutting down...") + // Create a context with a timeout of 10 seconds for the server shutdown. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - app.db.Close() + + // Shutdown gracefully. + app.db.DB.Close() app.echo.Shutdown(ctx) }() - app.echo.Debug = app.cfg.Server.Debug - app.echo.Use(middleware.AppCors()) - app.echo.Use(middleware.CacheWithRevalidation) - - return app.echo.Start(fmt.Sprintf(":%s", app.cfg.Server.RESTPort)) + return app.echo.Start(fmt.Sprintf(":%s", app.cfg.Server.Port)) } diff --git a/pkg/middleware/echo_middleware.go b/internal/middleware/cache.go similarity index 63% rename from pkg/middleware/echo_middleware.go rename to internal/middleware/cache.go index f470da4..7916623 100644 --- a/pkg/middleware/echo_middleware.go +++ b/internal/middleware/cache.go @@ -5,17 +5,8 @@ import ( "time" "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" ) -func AppCors() echo.MiddlewareFunc { - return middleware.CORSWithConfig(middleware.CORSConfig{ - AllowOrigins: []string{"*"}, // Specify the allowed origins or use a list of allowed domains - AllowMethods: []string{echo.GET, echo.HEAD, echo.PUT, echo.PATCH, echo.POST, echo.DELETE}, - AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization}, - }) -} - func CacheWithRevalidation(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { // Call the next handler diff --git a/internal/middleware/echo.go b/internal/middleware/echo.go new file mode 100644 index 0000000..7056539 --- /dev/null +++ b/internal/middleware/echo.go @@ -0,0 +1,54 @@ +package middleware + +import ( + "net/http" + + "github.com/DoWithLogic/golang-clean-architecture/config" + + "github.com/DoWithLogic/golang-clean-architecture/pkg/observability" + "github.com/go-playground/validator/v10" + + "github.com/labstack/echo-contrib/echoprometheus" + "github.com/labstack/echo/v4" + echoMiddleware "github.com/labstack/echo/v4/middleware" + "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho" +) + +type CustomValidator struct { + validator *validator.Validate +} + +func (v *CustomValidator) Validate(i interface{}) error { + return v.validator.Struct(i) +} + +// configCORS contains the CORS (Cross-Origin Resource Sharing) configuration for the server. +var configCORS = echoMiddleware.CORSConfig{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete, http.MethodPatch}, +} + +// NewEchoServer creates and configures a new Echo server instance. +// Parameters: +// - cfg: The application configuration. +// +// Returns: +// - *echo.Echo: A configured Echo server instance. +func NewEchoServer(cfg config.Config) *echo.Echo { + e := echo.New() + e.Use(echoMiddleware.RecoverWithConfig(echoMiddleware.RecoverConfig{DisableStackAll: true})) + e.Use(echoMiddleware.CORSWithConfig(configCORS)) + e.Use(echoprometheus.NewMiddleware("http")) + e.Use(LoggingMiddleware(observability.NewZeroLogHook().Z())) + e.Use(CacheWithRevalidation) + e.Validator = &CustomValidator{validator: validator.New()} + + if cfg.Observability.Enable { + e.Use(otelecho.Middleware(cfg.App.Name)) + } + + e.HTTPErrorHandler = errorHandler + e.Debug = cfg.Server.Debug + + return e +} diff --git a/internal/middleware/error_handler.go b/internal/middleware/error_handler.go new file mode 100644 index 0000000..aa1aa5f --- /dev/null +++ b/internal/middleware/error_handler.go @@ -0,0 +1,131 @@ +package middleware + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "net" + "net/http" + + "strconv" + "time" + + "github.com/DoWithLogic/golang-clean-architecture/pkg/apperror" + "github.com/DoWithLogic/golang-clean-architecture/pkg/response" + "github.com/golang-jwt/jwt" + "github.com/invopop/validation" + "github.com/labstack/echo/v4" + + "github.com/valyala/fasthttp" +) + +// errorHandler handles HTTP errors and sends a custom response based on the error type. +// Parameters: +// - err: The error that occurred. +// - c: The Echo context. +func errorHandler(err error, c echo.Context) { + // Unauthorized Error + var jwtErr *jwt.ValidationError + if errors.As(err, &jwtErr) || err.Error() == "Missing or malformed JWT" { + response.ErrorBuilder(apperror.Unauthorized(err)).Send(c) + + return + } + + var echoErr *echo.HTTPError + if errors.As(err, &echoErr) { + report, ok := err.(*echo.HTTPError) + + if !ok { + report = echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + switch report.Code { + case http.StatusNotFound: + response.ErrorBuilder(apperror.NotFound(errors.New("route not found"))).Send(c) + + return + default: + response.ErrorBuilder(err).Send(c) + + return + } + } + + // Path Parse Error + var numErr *strconv.NumError + if errors.As(err, &numErr) { + response.ErrorBuilder(apperror.BadRequest(errors.New("malformed_body"))).Send(c) + + return + } + + // handle HTTP Error + var appErr *apperror.AppError + if errors.As(err, &appErr) { + response.ErrorBuilder(err).Send(c) + + return + } + + var validatorError validation.Errors + if errors.As(err, &validatorError) { + response.ErrorBuilder(apperror.BadRequest(errors.New("validation_error"))).Send(c) + return + } + + // JSON Format Error + var jsonSyntaxErr *json.SyntaxError + if errors.As(err, &jsonSyntaxErr) { + response.ErrorBuilder(apperror.BadRequest(errors.New("malformed_body"))).Send(c) + + return + } + + // Unmarshal Error + var unmarshalErr *json.UnmarshalTypeError + if errors.As(err, &unmarshalErr) { + var translatedType string + switch unmarshalErr.Type.Name() { + // REGEX *int* + case "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float32", "float64": + translatedType = "number" + case "Time": + translatedType = "date time" + case "string": + translatedType = "string" + } + + response.ErrorBuilder(apperror.BadRequest(fmt.Errorf("the field must be a valid %s", translatedType))).Send(c) + return + } + + //time parse error + var timeParseErr *time.ParseError + if errors.As(err, &timeParseErr) { + v := timeParseErr.Value + if v == "" { + v = "empty string (``)" + } + + response.ErrorBuilder(apperror.BadRequest(fmt.Errorf("invalid time format on %s", v))).Send(c) + + return + } + + // Multipart Error + if errors.Is(err, fasthttp.ErrNoMultipartForm) { + response.ErrorBuilder(apperror.BadRequest(errors.New("invalid multipart content-type"))).Send(c) + + return + } + + //TCP connection error + var tcpErr *net.OpError + if errors.As(err, &tcpErr) { + log.Fatalf("unable to get tcp connection from %s, shutting down...", tcpErr.Addr.String()) + } + + response.ErrorBuilder(err).Send(c) +} diff --git a/internal/middleware/guard.go b/internal/middleware/guard.go new file mode 100644 index 0000000..c19ba13 --- /dev/null +++ b/internal/middleware/guard.go @@ -0,0 +1,67 @@ +package middleware + +import ( + "fmt" + + "github.com/DoWithLogic/golang-clean-architecture/config" + "github.com/DoWithLogic/golang-clean-architecture/pkg/apperror" + "github.com/DoWithLogic/golang-clean-architecture/pkg/constant" + "github.com/DoWithLogic/golang-clean-architecture/pkg/response" + "github.com/golang-jwt/jwt" + "github.com/labstack/echo/v4" + "github.com/pkg/errors" +) + +type PayloadToken struct { + Data *Data `json:"data"` + jwt.StandardClaims +} + +type Data struct { + UserID int64 `json:"user_id"` + Email string `json:"email"` +} + +// Middleware function to validate JWT token +func JWTMiddleware(cfg config.Config) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + tokenString := c.Request().Header.Get(constant.AuthorizationHeaderKey) + if tokenString == "" { + return response.ErrorBuilder(apperror.Unauthorized(errors.New("Missing JWT token"))).Send(c) + } + + // Remove "Bearer " prefix from token string + tokenString = tokenString[len("Bearer "):] + + token, err := jwt.ParseWithClaims(tokenString, &PayloadToken{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(cfg.JWT.Key), nil + }) + + if err != nil { + return response.ErrorBuilder(apperror.Unauthorized(errors.New("Invalid JWT token"))).Send(c) + } + if !token.Valid { + return response.ErrorBuilder(apperror.Unauthorized(errors.New("JWT token is not valid"))).Send(c) + } + + // Store the token claims in the request context for later use + claims := token.Claims.(*PayloadToken) + c.Set(constant.AuthCredentialKey, claims) + + return next(c) + } + } +} + +func NewTokenInformation(ctx echo.Context) (*PayloadToken, error) { + tokenInformation, ok := ctx.Get(constant.AuthCredentialKey).(*PayloadToken) + if !ok { + return tokenInformation, apperror.Unauthorized(apperror.ErrFailedGetTokenInformation) + } + + return tokenInformation, nil +} diff --git a/internal/middleware/logging.go b/internal/middleware/logging.go new file mode 100644 index 0000000..51ae74c --- /dev/null +++ b/internal/middleware/logging.go @@ -0,0 +1,72 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "io" + "time" + + "github.com/labstack/echo/v4" + "github.com/rs/zerolog" +) + +func LoggingMiddleware(logger *zerolog.Logger) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + req := c.Request() + res := c.Response() + + // Track the start time of the request + startTime := time.Now() + + if errCheck := func() error { + // Check if the request has a body + if req.Body != nil { + // Read the request body + reqBody, err := io.ReadAll(req.Body) + if err != nil { + return err + } + + // Restore the request body so it can be used in subsequent handlers + req.Body = io.NopCloser(bytes.NewBuffer(reqBody)) + + // Include request body as JSON in the log + var requestBodyJSON interface{} + if err := json.Unmarshal(reqBody, &requestBodyJSON); err != nil { + return err + } + + // Include request body in the log + logger.Info(). + Str("method", req.Method). + Str("path", req.URL.Path). + Str("ip", req.RemoteAddr). + Interface("body", requestBodyJSON). + Msg("Received request") + } + + return nil + }(); errCheck != nil { + // Log trace and span IDs using zerolog + logger.Info(). + Str("method", req.Method). + Str("path", req.URL.Path). + Str("ip", req.RemoteAddr). + Msg("Received request") + } + + // Continue to the next middleware/handler + err := next(c) + + // Log response details using zerolog + logger.Info(). + Int("status", res.Status). + Int64("size", res.Size). + Dur("duration", time.Since(startTime)). + Msg("Sent response") + + return err + } + } +} diff --git a/internal/users/delivery/http/v1/handler.go b/internal/users/delivery/http/v1/handler.go index 6214a1c..0cdc5eb 100644 --- a/internal/users/delivery/http/v1/handler.go +++ b/internal/users/delivery/http/v1/handler.go @@ -1,14 +1,12 @@ package v1 import ( - "context" - "net/http" - "time" - + "github.com/DoWithLogic/golang-clean-architecture/internal/middleware" "github.com/DoWithLogic/golang-clean-architecture/internal/users" "github.com/DoWithLogic/golang-clean-architecture/internal/users/dtos" - "github.com/DoWithLogic/golang-clean-architecture/pkg/middleware" - "github.com/DoWithLogic/golang-clean-architecture/pkg/utils/response" + "github.com/DoWithLogic/golang-clean-architecture/pkg/apperror" + "github.com/DoWithLogic/golang-clean-architecture/pkg/observability/instrumentation" + "github.com/DoWithLogic/golang-clean-architecture/pkg/response" "github.com/labstack/echo/v4" ) @@ -21,120 +19,120 @@ func NewHandlers(uc users.Usecase) *handlers { } func (h *handlers) Login(c echo.Context) error { - var ( - request dtos.UserLoginRequest - ) + ctx, span := instrumentation.NewTraceSpan(c.Request().Context(), "LoginHandler") + defer span.End() + var request dtos.UserLoginRequest if err := c.Bind(&request); err != nil { - return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusBadRequest, response.MsgFailed, err.Error())) + return response.ErrorBuilder(apperror.BadRequest(err)).Send(c) } if err := request.Validate(); err != nil { - return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusBadRequest, response.MsgFailed, err.Error())) + return response.ErrorBuilder(apperror.BadRequest(err)).Send(c) } - authData, httpCode, err := h.uc.Login(c.Request().Context(), request) + authData, err := h.uc.Login(ctx, request) if err != nil { - return c.JSON(httpCode, response.NewResponseError(httpCode, response.MsgFailed, err.Error())) + return response.ErrorBuilder(err).Send(c) } - return c.JSON(httpCode, response.NewResponse(httpCode, response.MsgSuccess, authData)) + return response.SuccessBuilder(authData).Send(c) } func (h *handlers) CreateUser(c echo.Context) error { - var ( - ctx, cancel = context.WithTimeout(c.Request().Context(), time.Duration(30*time.Second)) - payload dtos.CreateUserRequest - ) - defer cancel() + ctx, span := instrumentation.NewTraceSpan(c.Request().Context(), "CreateUserHandler") + defer span.End() + var payload dtos.CreateUserRequest if err := c.Bind(&payload); err != nil { - return c.JSON(http.StatusBadRequest, response.NewResponseError( - http.StatusBadRequest, - response.MsgFailed, - err.Error()), - ) + return response.ErrorBuilder(apperror.BadRequest(err)).Send(c) } if err := payload.Validate(); err != nil { - return c.JSON(http.StatusBadRequest, response.NewResponseError( - http.StatusBadRequest, - response.MsgFailed, - err.Error()), - ) + return response.ErrorBuilder(apperror.BadRequest(err)).Send(c) } - userID, httpCode, err := h.uc.Create(ctx, payload) + userID, err := h.uc.Create(ctx, payload) if err != nil { - return c.JSON(httpCode, response.NewResponseError( - httpCode, - response.MsgFailed, - err.Error()), - ) + return response.ErrorBuilder(err).Send(c) } - return c.JSON(http.StatusOK, response.NewResponse(http.StatusOK, response.MsgSuccess, map[string]int64{"id": userID})) + return response.SuccessBuilder(map[string]int64{"id": userID}).Send(c) } func (h *handlers) UserDetail(c echo.Context) error { - ctx, cancel := context.WithTimeout(c.Request().Context(), time.Duration(30*time.Second)) - defer cancel() + ctx, span := instrumentation.NewTraceSpan(c.Request().Context(), "UserDetailHandler") + defer span.End() - userID := c.Get("identity").(*middleware.CustomClaims).UserID + userData, err := middleware.NewTokenInformation(c) + if err != nil { + return response.ErrorBuilder(err).Send(c) + } - data, code, err := h.uc.Detail(ctx, userID) + data, err := h.uc.Detail(ctx, userData.Data.UserID) if err != nil { - return c.JSON(code, response.NewResponseError(code, response.MsgFailed, err.Error())) + return response.ErrorBuilder(err).Send(c) } - return c.JSON(code, response.NewResponse(code, response.MsgSuccess, data)) + return response.SuccessBuilder(data).Send(c) } func (h *handlers) UpdateUser(c echo.Context) error { - var ( - ctx, cancel = context.WithTimeout(c.Request().Context(), time.Duration(30*time.Second)) - identity = c.Get("identity").(*middleware.CustomClaims) - request dtos.UpdateUserRequest - ) - defer cancel() + ctx, span := instrumentation.NewTraceSpan(c.Request().Context(), "UpdateUserHandler") + defer span.End() + var request dtos.UpdateUser if err := c.Bind(&request); err != nil { - return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusBadRequest, response.MsgFailed, err.Error())) + return response.ErrorBuilder(apperror.BadRequest(err)).Send(c) } - request.UserID = identity.UserID - request.UpdateBy = identity.Email - if err := request.Validate(); err != nil { - return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusBadRequest, response.MsgFailed, err.Error())) + return response.ErrorBuilder(apperror.BadRequest(err)).Send(c) + } + + userData, err := middleware.NewTokenInformation(c) + if err != nil { + return response.ErrorBuilder(err).Send(c) } - if err := h.uc.PartialUpdate(ctx, request); err != nil { - return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusInternalServerError, response.MsgFailed, err.Error())) + args := dtos.UpdateUserRequest{ + UserID: userData.Data.UserID, + UpdateUser: request, } - return c.JSON(http.StatusOK, response.NewResponse(http.StatusOK, response.MsgSuccess, nil)) + if err := h.uc.PartialUpdate(ctx, args); err != nil { + return response.ErrorBuilder(err).Send(c) + } + + return response.SuccessBuilder(nil).Send(c) } func (h *handlers) UpdateUserStatus(c echo.Context) error { - var ( - ctx, cancel = context.WithTimeout(c.Request().Context(), time.Duration(30*time.Second)) - identity = c.Get("identity").(*middleware.CustomClaims) - request = dtos.UpdateUserStatusRequest{UserID: identity.UserID, UpdateBy: identity.Email} - ) - defer cancel() + ctx, span := instrumentation.NewTraceSpan(c.Request().Context(), "UpdateUserStatusHandler") + defer span.End() + var request dtos.UpdateUserStatus if err := c.Bind(&request); err != nil { - return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusBadRequest, response.MsgFailed, err.Error())) + return response.ErrorBuilder(apperror.BadRequest(err)).Send(c) } if err := request.Validate(); err != nil { - return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusBadRequest, response.MsgFailed, err.Error())) + return response.ErrorBuilder(apperror.BadRequest(err)).Send(c) + } + + userData, err := middleware.NewTokenInformation(c) + if err != nil { + return response.ErrorBuilder(err).Send(c) + } + + args := dtos.UpdateUserStatusRequest{ + UserID: userData.Data.UserID, + UpdateUserStatus: request, } - if err := h.uc.UpdateStatus(ctx, request); err != nil { - return c.JSON(http.StatusInternalServerError, response.NewResponseError(http.StatusInternalServerError, response.MsgFailed, err.Error())) + if err := h.uc.UpdateStatus(ctx, args); err != nil { + return response.ErrorBuilder(err).Send(c) } - return c.JSON(http.StatusOK, response.NewResponse(http.StatusOK, response.MsgSuccess, nil)) + return response.SuccessBuilder(nil).Send(c) } diff --git a/internal/users/delivery/http/v1/route.go b/internal/users/delivery/http/v1/route.go index 3a43fbf..6b0b1b1 100644 --- a/internal/users/delivery/http/v1/route.go +++ b/internal/users/delivery/http/v1/route.go @@ -2,14 +2,14 @@ package v1 import ( "github.com/DoWithLogic/golang-clean-architecture/config" - "github.com/DoWithLogic/golang-clean-architecture/pkg/middleware" + "github.com/DoWithLogic/golang-clean-architecture/internal/middleware" "github.com/labstack/echo/v4" ) func (h *handlers) UserRoutes(domain *echo.Group, cfg config.Config) { - domain.POST("/", h.CreateUser) + domain.POST("", h.CreateUser) domain.POST("/login", h.Login) - domain.GET("/detail", h.UserDetail, middleware.AuthorizeJWT(cfg)) - domain.PATCH("/update", h.UpdateUser, middleware.AuthorizeJWT(cfg)) - domain.PUT("/update/status", h.UpdateUserStatus, middleware.AuthorizeJWT(cfg)) + domain.GET("/detail", h.UserDetail, middleware.JWTMiddleware(cfg)) + domain.PATCH("/update", h.UpdateUser, middleware.JWTMiddleware(cfg)) + domain.PUT("/update/status", h.UpdateUserStatus, middleware.JWTMiddleware(cfg)) } diff --git a/internal/users/dtos/user_detail.go b/internal/users/dtos/user_detail.go index 167c95c..5a71f61 100644 --- a/internal/users/dtos/user_detail.go +++ b/internal/users/dtos/user_detail.go @@ -11,6 +11,5 @@ type ( UserType string `json:"user_type"` IsActive bool `json:"is_active"` CreatedAt time.Time `json:"created_at"` - CreatedBy string `json:"created_by"` } ) diff --git a/internal/users/dtos/user_update.go b/internal/users/dtos/user_update.go index 45d9fab..9a139ee 100644 --- a/internal/users/dtos/user_update.go +++ b/internal/users/dtos/user_update.go @@ -1,30 +1,23 @@ package dtos import ( - "github.com/DoWithLogic/golang-clean-architecture/pkg/apperror" "github.com/DoWithLogic/golang-clean-architecture/pkg/constant" "github.com/invopop/validation" ) -type UpdateUserRequest struct { - UserID int64 `json:"-"` +type UpdateUser struct { Fullname string `json:"fullname"` PhoneNumber string `json:"phone_number"` UserType string `json:"user_type"` - Email string `json:"email"` - Password string `json:"password"` - UpdateBy string `json:"-"` } -var () - -func (cup UpdateUserRequest) Validate() error { - if cup.UserType != "" && cup.UserType != constant.UserTypePremium && cup.UserType != constant.UserTypeRegular { - return apperror.ErrInvalidUserType - } +type UpdateUserRequest struct { + UserID int64 + UpdateUser +} +func (cup UpdateUser) Validate() error { return validation.ValidateStruct(&cup, - validation.Field(&cup.UserID, validation.NotNil), - validation.Field(&cup.UpdateBy, validation.NotNil), + validation.Field(&cup.UserType, validation.In(constant.UserTypePremium, constant.UserTypeRegular)), ) } diff --git a/internal/users/dtos/user_update_status.go b/internal/users/dtos/user_update_status.go index 05b062a..f33ddb9 100644 --- a/internal/users/dtos/user_update_status.go +++ b/internal/users/dtos/user_update_status.go @@ -1,24 +1,21 @@ package dtos import ( - "github.com/DoWithLogic/golang-clean-architecture/pkg/apperror" "github.com/DoWithLogic/golang-clean-architecture/pkg/constant" "github.com/invopop/validation" ) -type UpdateUserStatusRequest struct { - UserID int64 `json:"-"` - Status string `json:"status"` - UpdateBy string `json:"-"` +type UpdateUserStatus struct { + Status string `json:"status"` } -func (ussp UpdateUserStatusRequest) Validate() error { - if ussp.Status != constant.UserInactive && ussp.Status != constant.UserActive { - return apperror.ErrStatusValue - } +type UpdateUserStatusRequest struct { + UserID int64 + UpdateUserStatus +} +func (ussp UpdateUserStatus) Validate() error { return validation.ValidateStruct(&ussp, - validation.Field(&ussp.UserID, validation.NotNil), - validation.Field(&ussp.UpdateBy, validation.NotNil), + validation.Field(&ussp.Status, validation.In(constant.UserActive, constant.UserInactive)), ) } diff --git a/internal/users/entities/user_detail.go b/internal/users/entities/user_detail.go index 2243608..03de83a 100644 --- a/internal/users/entities/user_detail.go +++ b/internal/users/entities/user_detail.go @@ -2,7 +2,7 @@ package entities import "github.com/DoWithLogic/golang-clean-architecture/internal/users/dtos" -func NewUserDetail(data Users) dtos.UserDetailResponse { +func NewUserDetail(data User) dtos.UserDetailResponse { return dtos.UserDetailResponse{ UserID: data.UserID, Email: data.Email, @@ -11,6 +11,5 @@ func NewUserDetail(data Users) dtos.UserDetailResponse { UserType: data.UserType, IsActive: data.IsActive, CreatedAt: data.CreatedAt, - CreatedBy: data.CreatedBy, } } diff --git a/internal/users/entities/user_update.go b/internal/users/entities/user_update.go index 8861605..febdd67 100644 --- a/internal/users/entities/user_update.go +++ b/internal/users/entities/user_update.go @@ -6,24 +6,20 @@ import ( "github.com/DoWithLogic/golang-clean-architecture/internal/users/dtos" ) -type UpdateUsers struct { +type UpdateUser struct { UserID int64 - Email string Fullname string - Password string PhoneNumber string UserType string UpdatedAt time.Time - UpdatedBy string } -func NewUpdateUsers(data dtos.UpdateUserRequest) UpdateUsers { - return UpdateUsers{ +func NewUpdateUser(data dtos.UpdateUserRequest) UpdateUser { + return UpdateUser{ UserID: data.UserID, Fullname: data.Fullname, PhoneNumber: data.PhoneNumber, UserType: data.UserType, UpdatedAt: time.Now(), - UpdatedBy: data.UpdateBy, } } diff --git a/internal/users/entities/user_update_status.go b/internal/users/entities/user_update_status.go index de1fdad..ddd2c75 100644 --- a/internal/users/entities/user_update_status.go +++ b/internal/users/entities/user_update_status.go @@ -11,7 +11,6 @@ type UpdateUserStatus struct { UserID int64 IsActive bool UpdatedAt time.Time - UpdatedBy string } func NewUpdateUserStatus(req dtos.UpdateUserStatusRequest) UpdateUserStatus { @@ -19,6 +18,5 @@ func NewUpdateUserStatus(req dtos.UpdateUserStatusRequest) UpdateUserStatus { UserID: req.UserID, IsActive: constant.MapStatus[req.Status], UpdatedAt: time.Now(), - UpdatedBy: req.UpdateBy, } } diff --git a/internal/users/entities/users.go b/internal/users/entities/users.go index 1e6ffc5..ecd3c3c 100644 --- a/internal/users/entities/users.go +++ b/internal/users/entities/users.go @@ -5,12 +5,12 @@ import ( "github.com/DoWithLogic/golang-clean-architecture/config" "github.com/DoWithLogic/golang-clean-architecture/internal/users/dtos" + "github.com/DoWithLogic/golang-clean-architecture/pkg/app_crypto" "github.com/DoWithLogic/golang-clean-architecture/pkg/constant" - "github.com/DoWithLogic/golang-clean-architecture/pkg/utils" ) type ( - Users struct { + User struct { UserID int64 Email string Password string @@ -19,9 +19,7 @@ type ( UserType string IsActive bool CreatedAt time.Time - CreatedBy string UpdatedAt time.Time - UpdatedBy string } LockingOpt struct { @@ -29,15 +27,14 @@ type ( } ) -func NewCreateUser(data dtos.CreateUserRequest, cfg config.Config) Users { - return Users{ +func NewCreateUser(data dtos.CreateUserRequest, cfg config.Config) User { + return User{ Fullname: data.FullName, Email: data.Email, - Password: utils.Encrypt(data.Password, cfg), + Password: app_crypto.NewCrypto(cfg.Authentication.Key).EncodeSHA256(data.Password), PhoneNumber: data.PhoneNumber, UserType: constant.UserTypeRegular, IsActive: true, CreatedAt: time.Now(), - CreatedBy: "SYSTEM", } } diff --git a/internal/users/mock/repository_mock.go b/internal/users/mock/repository_mock.go index 65f1e91..2cf3107 100644 --- a/internal/users/mock/repository_mock.go +++ b/internal/users/mock/repository_mock.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: internal/users/repository.go +// +// Generated by this command: +// +// mockgen -source internal/users/repository.go -destination internal/users/mock/repository_mock.go -package=mocks +// // Package mocks is a generated GoMock package. package mocks @@ -46,43 +51,43 @@ func (m *MockRepository) Atomic(ctx context.Context, opt *sql.TxOptions, repo fu } // Atomic indicates an expected call of Atomic. -func (mr *MockRepositoryMockRecorder) Atomic(ctx, opt, repo interface{}) *gomock.Call { +func (mr *MockRepositoryMockRecorder) Atomic(ctx, opt, repo any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Atomic", reflect.TypeOf((*MockRepository)(nil).Atomic), ctx, opt, repo) } // GetUserByEmail mocks base method. -func (m *MockRepository) GetUserByEmail(arg0 context.Context, arg1 string) (entities.Users, error) { +func (m *MockRepository) GetUserByEmail(arg0 context.Context, arg1 string) (entities.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetUserByEmail", arg0, arg1) - ret0, _ := ret[0].(entities.Users) + ret0, _ := ret[0].(entities.User) ret1, _ := ret[1].(error) return ret0, ret1 } // GetUserByEmail indicates an expected call of GetUserByEmail. -func (mr *MockRepositoryMockRecorder) GetUserByEmail(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockRepositoryMockRecorder) GetUserByEmail(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockRepository)(nil).GetUserByEmail), arg0, arg1) } // GetUserByID mocks base method. -func (m *MockRepository) GetUserByID(arg0 context.Context, arg1 int64, arg2 ...entities.LockingOpt) (entities.Users, error) { +func (m *MockRepository) GetUserByID(arg0 context.Context, arg1 int64, arg2 ...entities.LockingOpt) (entities.User, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "GetUserByID", varargs...) - ret0, _ := ret[0].(entities.Users) + ret0, _ := ret[0].(entities.User) ret1, _ := ret[1].(error) return ret0, ret1 } // GetUserByID indicates an expected call of GetUserByID. -func (mr *MockRepositoryMockRecorder) GetUserByID(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockRepositoryMockRecorder) GetUserByID(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByID", reflect.TypeOf((*MockRepository)(nil).GetUserByID), varargs...) } @@ -95,13 +100,13 @@ func (m *MockRepository) IsUserExist(ctx context.Context, email string) bool { } // IsUserExist indicates an expected call of IsUserExist. -func (mr *MockRepositoryMockRecorder) IsUserExist(ctx, email interface{}) *gomock.Call { +func (mr *MockRepositoryMockRecorder) IsUserExist(ctx, email any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUserExist", reflect.TypeOf((*MockRepository)(nil).IsUserExist), ctx, email) } // SaveNewUser mocks base method. -func (m *MockRepository) SaveNewUser(arg0 context.Context, arg1 entities.Users) (int64, error) { +func (m *MockRepository) SaveNewUser(arg0 context.Context, arg1 entities.User) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveNewUser", arg0, arg1) ret0, _ := ret[0].(int64) @@ -110,13 +115,13 @@ func (m *MockRepository) SaveNewUser(arg0 context.Context, arg1 entities.Users) } // SaveNewUser indicates an expected call of SaveNewUser. -func (mr *MockRepositoryMockRecorder) SaveNewUser(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockRepositoryMockRecorder) SaveNewUser(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNewUser", reflect.TypeOf((*MockRepository)(nil).SaveNewUser), arg0, arg1) } // UpdateUserByID mocks base method. -func (m *MockRepository) UpdateUserByID(arg0 context.Context, arg1 entities.UpdateUsers) error { +func (m *MockRepository) UpdateUserByID(arg0 context.Context, arg1 entities.UpdateUser) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateUserByID", arg0, arg1) ret0, _ := ret[0].(error) @@ -124,7 +129,7 @@ func (m *MockRepository) UpdateUserByID(arg0 context.Context, arg1 entities.Upda } // UpdateUserByID indicates an expected call of UpdateUserByID. -func (mr *MockRepositoryMockRecorder) UpdateUserByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockRepositoryMockRecorder) UpdateUserByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserByID", reflect.TypeOf((*MockRepository)(nil).UpdateUserByID), arg0, arg1) } @@ -138,7 +143,7 @@ func (m *MockRepository) UpdateUserStatusByID(arg0 context.Context, arg1 entitie } // UpdateUserStatusByID indicates an expected call of UpdateUserStatusByID. -func (mr *MockRepositoryMockRecorder) UpdateUserStatusByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockRepositoryMockRecorder) UpdateUserStatusByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatusByID", reflect.TypeOf((*MockRepository)(nil).UpdateUserStatusByID), arg0, arg1) } diff --git a/internal/users/mock/usecase_mock.go b/internal/users/mock/usecase_mock.go index 0fecf6c..14baadc 100644 --- a/internal/users/mock/usecase_mock.go +++ b/internal/users/mock/usecase_mock.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: internal/users/usecase.go +// +// Generated by this command: +// +// mockgen -source internal/users/usecase.go -destination internal/users/mock/usecase_mock.go -package=mocks +// // Package mocks is a generated GoMock package. package mocks @@ -36,49 +41,46 @@ func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { } // Create mocks base method. -func (m *MockUsecase) Create(ctx context.Context, payload dtos.CreateUserRequest) (int64, int, error) { +func (m *MockUsecase) Create(ctx context.Context, payload dtos.CreateUserRequest) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", ctx, payload) ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(int) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 + ret1, _ := ret[1].(error) + return ret0, ret1 } // Create indicates an expected call of Create. -func (mr *MockUsecaseMockRecorder) Create(ctx, payload interface{}) *gomock.Call { +func (mr *MockUsecaseMockRecorder) Create(ctx, payload any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUsecase)(nil).Create), ctx, payload) } // Detail mocks base method. -func (m *MockUsecase) Detail(ctx context.Context, id int64) (dtos.UserDetailResponse, int, error) { +func (m *MockUsecase) Detail(ctx context.Context, id int64) (dtos.UserDetailResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Detail", ctx, id) ret0, _ := ret[0].(dtos.UserDetailResponse) - ret1, _ := ret[1].(int) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 + ret1, _ := ret[1].(error) + return ret0, ret1 } // Detail indicates an expected call of Detail. -func (mr *MockUsecaseMockRecorder) Detail(ctx, id interface{}) *gomock.Call { +func (mr *MockUsecaseMockRecorder) Detail(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Detail", reflect.TypeOf((*MockUsecase)(nil).Detail), ctx, id) } // Login mocks base method. -func (m *MockUsecase) Login(ctx context.Context, request dtos.UserLoginRequest) (dtos.UserLoginResponse, int, error) { +func (m *MockUsecase) Login(ctx context.Context, request dtos.UserLoginRequest) (dtos.UserLoginResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Login", ctx, request) ret0, _ := ret[0].(dtos.UserLoginResponse) - ret1, _ := ret[1].(int) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 + ret1, _ := ret[1].(error) + return ret0, ret1 } // Login indicates an expected call of Login. -func (mr *MockUsecaseMockRecorder) Login(ctx, request interface{}) *gomock.Call { +func (mr *MockUsecaseMockRecorder) Login(ctx, request any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockUsecase)(nil).Login), ctx, request) } @@ -92,7 +94,7 @@ func (m *MockUsecase) PartialUpdate(ctx context.Context, data dtos.UpdateUserReq } // PartialUpdate indicates an expected call of PartialUpdate. -func (mr *MockUsecaseMockRecorder) PartialUpdate(ctx, data interface{}) *gomock.Call { +func (mr *MockUsecaseMockRecorder) PartialUpdate(ctx, data any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PartialUpdate", reflect.TypeOf((*MockUsecase)(nil).PartialUpdate), ctx, data) } @@ -106,7 +108,7 @@ func (m *MockUsecase) UpdateStatus(ctx context.Context, req dtos.UpdateUserStatu } // UpdateStatus indicates an expected call of UpdateStatus. -func (mr *MockUsecaseMockRecorder) UpdateStatus(ctx, req interface{}) *gomock.Call { +func (mr *MockUsecaseMockRecorder) UpdateStatus(ctx, req any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockUsecase)(nil).UpdateStatus), ctx, req) } diff --git a/internal/users/repository.go b/internal/users/repository.go index 85e0014..03aabfc 100644 --- a/internal/users/repository.go +++ b/internal/users/repository.go @@ -10,10 +10,10 @@ import ( type Repository interface { Atomic(ctx context.Context, opt *sql.TxOptions, repo func(tx Repository) error) error - GetUserByID(context.Context, int64, ...entities.LockingOpt) (entities.Users, error) - GetUserByEmail(context.Context, string) (entities.Users, error) - SaveNewUser(context.Context, entities.Users) (int64, error) - UpdateUserByID(context.Context, entities.UpdateUsers) error + GetUserByID(context.Context, int64, ...entities.LockingOpt) (entities.User, error) + GetUserByEmail(context.Context, string) (entities.User, error) + SaveNewUser(context.Context, entities.User) (int64, error) + UpdateUserByID(context.Context, entities.UpdateUser) error UpdateUserStatusByID(context.Context, entities.UpdateUserStatus) error IsUserExist(ctx context.Context, email string) bool } diff --git a/internal/users/repository/repository.go b/internal/users/repository/repository.go index df1cf43..72ba9eb 100644 --- a/internal/users/repository/repository.go +++ b/internal/users/repository/repository.go @@ -39,7 +39,7 @@ func (r *repository) Atomic(ctx context.Context, opt *sql.TxOptions, repo func(t return nil } -func (repo *repository) SaveNewUser(ctx context.Context, user entities.Users) (userID int64, err error) { +func (repo *repository) SaveNewUser(ctx context.Context, user entities.User) (userID int64, err error) { args := utils.Array{ user.Email, user.Password, @@ -48,7 +48,6 @@ func (repo *repository) SaveNewUser(ctx context.Context, user entities.Users) (u user.UserType, user.IsActive, user.CreatedAt, - user.CreatedBy, } err = new(datasource.DataSource).ExecSQL(repo.conn.ExecContext(ctx, repository_query.InsertUsers, args...)).Scan(nil, &userID) @@ -59,14 +58,12 @@ func (repo *repository) SaveNewUser(ctx context.Context, user entities.Users) (u return userID, nil } -func (repo *repository) UpdateUserByID(ctx context.Context, user entities.UpdateUsers) error { +func (repo *repository) UpdateUserByID(ctx context.Context, user entities.UpdateUser) error { args := utils.Array{ - user.Email, user.Email, user.Fullname, user.Fullname, user.PhoneNumber, user.PhoneNumber, user.UserType, user.UserType, user.UpdatedAt, - user.UpdatedBy, user.UserID, } @@ -78,7 +75,7 @@ func (repo *repository) UpdateUserByID(ctx context.Context, user entities.Update return nil } -func (repo *repository) GetUserByID(ctx context.Context, userID int64, options ...entities.LockingOpt) (userData entities.Users, err error) { +func (repo *repository) GetUserByID(ctx context.Context, userID int64, options ...entities.LockingOpt) (userData entities.User, err error) { args := utils.Array{ userID, } @@ -92,7 +89,6 @@ func (repo *repository) GetUserByID(ctx context.Context, userID int64, options . &userData.UserType, &userData.IsActive, &userData.CreatedAt, - &userData.CreatedBy, } } @@ -113,7 +109,6 @@ func (repo *repository) UpdateUserStatusByID(ctx context.Context, req entities.U args := utils.Array{ req.IsActive, req.UpdatedAt, - req.UpdatedBy, req.UserID, } @@ -144,23 +139,28 @@ func (repo *repository) IsUserExist(ctx context.Context, email string) bool { return id != 0 } -func (repo *repository) GetUserByEmail(ctx context.Context, email string) (userDetail entities.Users, err error) { +func (repo *repository) GetUserByEmail(ctx context.Context, email string) (userData entities.User, err error) { args := utils.Array{ email, } row := func(idx int) utils.Array { return utils.Array{ - &userDetail.UserID, - &userDetail.Email, - &userDetail.Password, + &userData.UserID, + &userData.Email, + &userData.Password, + &userData.Fullname, + &userData.PhoneNumber, + &userData.UserType, + &userData.IsActive, + &userData.CreatedAt, } } err = new(datasource.DataSource).QuerySQL(repo.conn.QueryxContext(ctx, repository_query.GetUserByEmail, args...)).Scan(row) if err != nil { - return entities.Users{}, err + return entities.User{}, err } - return userDetail, err + return userData, err } diff --git a/internal/users/repository/repository_query/users/get_detail_by_email.sql b/internal/users/repository/repository_query/users/get_detail_by_email.sql index 1ede9c3..8de11d1 100644 --- a/internal/users/repository/repository_query/users/get_detail_by_email.sql +++ b/internal/users/repository/repository_query/users/get_detail_by_email.sql @@ -1,6 +1,11 @@ -SELECT +SELECT u.id, u.email, - u.password + u.password, + u.fullname, + u.phone_number, + u.user_type, + u.is_active, + u.created_at FROM users u WHERE u.email = ? \ No newline at end of file diff --git a/internal/users/repository/repository_query/users/insert.sql b/internal/users/repository/repository_query/users/insert.sql index a34c356..aa11b4f 100644 --- a/internal/users/repository/repository_query/users/insert.sql +++ b/internal/users/repository/repository_query/users/insert.sql @@ -5,6 +5,5 @@ INSERT INTO users ( phone_number, user_type, is_active, - created_at, - created_by -)VALUE (?, ?, ?, ?, ?, ?, ?, ?) \ No newline at end of file + created_at +)VALUE (?, ?, ?, ?, ?, ?, ?) \ No newline at end of file diff --git a/internal/users/repository/repository_query/users/select.sql b/internal/users/repository/repository_query/users/select.sql index ace5edd..d0e43f5 100644 --- a/internal/users/repository/repository_query/users/select.sql +++ b/internal/users/repository/repository_query/users/select.sql @@ -5,7 +5,6 @@ SELECT u.phone_number, u.user_type, u.is_active, - u.created_at, - u.created_by + u.created_at FROM users u WHERE u.id = ? diff --git a/internal/users/repository/repository_query/users/update.sql b/internal/users/repository/repository_query/users/update.sql index 215ce88..ef6b5d7 100644 --- a/internal/users/repository/repository_query/users/update.sql +++ b/internal/users/repository/repository_query/users/update.sql @@ -1,8 +1,6 @@ UPDATE users SET - email = CASE WHEN ? != '' THEN ? ELSE email END, fullname = CASE WHEN ? != '' THEN ? ELSE fullname END, phone_number = CASE WHEN ? != '' THEN ? ELSE phone_number END, user_type = CASE WHEN ? != '' THEN ? ELSE user_type END, - updated_at = ?, - updated_by = ? + updated_at = ? WHERE id = ? \ No newline at end of file diff --git a/internal/users/repository/repository_query/users/update_status_by_id.sql b/internal/users/repository/repository_query/users/update_status_by_id.sql index efcb56b..a7bb3c7 100644 --- a/internal/users/repository/repository_query/users/update_status_by_id.sql +++ b/internal/users/repository/repository_query/users/update_status_by_id.sql @@ -1,5 +1,4 @@ UPDATE users SET is_active = ?, - updated_at = ?, - updated_by = ? + updated_at = ? WHERE id = ? \ No newline at end of file diff --git a/internal/users/repository/repository_test.go b/internal/users/repository/repository_test.go index 32375a8..402d82a 100644 --- a/internal/users/repository/repository_test.go +++ b/internal/users/repository/repository_test.go @@ -2,80 +2,144 @@ package repository_test import ( "context" + "database/sql" "testing" "time" - sqlmock "github.com/DATA-DOG/go-sqlmock" + "github.com/DoWithLogic/golang-clean-architecture/config" + "github.com/DoWithLogic/golang-clean-architecture/internal/users" "github.com/DoWithLogic/golang-clean-architecture/internal/users/entities" "github.com/DoWithLogic/golang-clean-architecture/internal/users/repository" - "github.com/DoWithLogic/golang-clean-architecture/internal/users/repository/repository_query" "github.com/DoWithLogic/golang-clean-architecture/pkg/constant" + "github.com/DoWithLogic/golang-clean-architecture/pkg/datasource" + "github.com/go-faker/faker/v4" + + _ "github.com/go-sql-driver/mysql" + "github.com/jmoiron/sqlx" - "github.com/stretchr/testify/assert" + "github.com/samber/lo" "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" ) +var cfg config.Config +var db *sqlx.DB + +func init() { + cfg = lo.Must(config.LoadConfigPath("../../../config/config-local")) + db = lo.Must(datasource.NewDatabase(cfg.Database)) + +} + +func Test_repository_SaveNewUser(t *testing.T) { + repo := repository.NewRepository(db) + + t.Run("positive_SaveNewUser", func(t *testing.T) { + newUserData := entities.User{ + Email: faker.Email(), + Password: faker.Password(), + Fullname: faker.Name(), + PhoneNumber: faker.Phonenumber(), + UserType: constant.UserTypePremium, + CreatedAt: time.Now(), + } + + userID, err := repo.SaveNewUser(context.Background(), newUserData) + require.NoError(t, err) + require.NotEmpty(t, userID) + + userData, err := repo.GetUserByID(context.Background(), userID) + require.NoError(t, err) + require.Equal(t, userData.Email, newUserData.Email) + require.Equal(t, userData.Fullname, newUserData.Fullname) + require.Equal(t, userData.PhoneNumber, newUserData.PhoneNumber) + require.Equal(t, userData.UserType, newUserData.UserType) + }) + +} + func Test_repository_UpdateUserByID(t *testing.T) { - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - require.NoError(t, err) - ctrl := gomock.NewController(t) - defer ctrl.Finish() - defer db.Close() - - var ( - conn = sqlx.NewDb(db, "sqlmock") - repo = repository.NewRepository(conn) - ) - - currentTime := time.Now() - userID := int64(1) - - t.Run("UpdateUserByID_positive_case", func(t *testing.T) { - user := entities.Users{ - Fullname: "martin yonatan pasaribu", - PhoneNumber: "08121213131414", + repo := repository.NewRepository(db) + + t.Run("positive_UpdateUserByID", func(t *testing.T) { + newUserData := entities.User{ + Email: faker.Email(), + Password: faker.Password(), + Fullname: faker.Name(), + PhoneNumber: faker.Phonenumber(), UserType: constant.UserTypePremium, - IsActive: true, - CreatedAt: currentTime, - CreatedBy: "admin", + CreatedAt: time.Now(), } - mock. - ExpectExec(repository_query.InsertUsers). - WithArgs( - user.Fullname, - user.PhoneNumber, - user.IsActive, - user.UserType, - currentTime, - user.CreatedBy, - ).WillReturnResult(sqlmock.NewResult(userID, 1)) - - userID, err := repo.SaveNewUser(context.Background(), user) - assert.NoError(t, err) - assert.NotEmpty(t, userID) - - argsUpdateUser := entities.UpdateUsers{ + userID, err := repo.SaveNewUser(context.Background(), newUserData) + require.NoError(t, err) + require.NotEmpty(t, userID) + + userData, err := repo.GetUserByID(context.Background(), userID) + require.NoError(t, err) + require.Equal(t, userData.Email, newUserData.Email) + require.Equal(t, userData.Fullname, newUserData.Fullname) + require.Equal(t, userData.PhoneNumber, newUserData.PhoneNumber) + require.Equal(t, userData.UserType, newUserData.UserType) + + updateUserData := entities.UpdateUser{ UserID: userID, - Fullname: "edited name", - PhoneNumber: "081122334455", - UpdatedAt: currentTime, - UpdatedBy: "admin", + Fullname: "updated name", + PhoneNumber: "081212121313", + UserType: constant.UserTypeRegular, + UpdatedAt: time.Now(), } - mock.ExpectExec(repository_query.UpdateUsers). - WithArgs( - argsUpdateUser.Fullname, argsUpdateUser.Fullname, - argsUpdateUser.PhoneNumber, argsUpdateUser.PhoneNumber, - argsUpdateUser.UserType, argsUpdateUser.UserType, - argsUpdateUser.UpdatedAt, - argsUpdateUser.UpdatedBy, - argsUpdateUser.UserID, - ).WillReturnResult(sqlmock.NewResult(userID, 1)) - - err = repo.UpdateUserByID(context.Background(), argsUpdateUser) + err = repo.UpdateUserByID(context.Background(), updateUserData) + require.NoError(t, err) + + userDataAfterUpdate, err := repo.GetUserByID(context.Background(), userID) require.NoError(t, err) + require.Equal(t, userDataAfterUpdate.Fullname, updateUserData.Fullname) + require.Equal(t, userDataAfterUpdate.PhoneNumber, updateUserData.PhoneNumber) + require.Equal(t, userDataAfterUpdate.UserType, updateUserData.UserType) + }) } + +func Test_repository_UpdateUserStatusByID(t *testing.T) { + repo := repository.NewRepository(db) + + t.Run("positive_UpdateUserStatusByID", func(t *testing.T) { + newUserData := entities.User{ + Email: faker.Email(), + Password: faker.Password(), + Fullname: faker.Name(), + PhoneNumber: faker.Phonenumber(), + UserType: constant.UserTypePremium, + CreatedAt: time.Now(), + } + + userID, err := repo.SaveNewUser(context.Background(), newUserData) + require.NoError(t, err) + require.NotEmpty(t, userID) + + exist := repo.IsUserExist(context.Background(), newUserData.Email) + require.Equal(t, exist, true) + + updateUserStatusData := entities.UpdateUserStatus{ + UserID: userID, + IsActive: true, + UpdatedAt: time.Now(), + } + + repo.Atomic(context.Background(), &sql.TxOptions{}, func(tx users.Repository) error { + _, err := tx.GetUserByID(context.Background(), userID, entities.LockingOpt{PessimisticLocking: true}) + require.NoError(t, err) + + err = tx.UpdateUserStatusByID(context.Background(), updateUserStatusData) + require.NoError(t, err) + + return nil + }) + + userDataAfterUpdateStatus, err := repo.GetUserByEmail(context.Background(), newUserData.Email) + require.NoError(t, err) + require.Equal(t, userDataAfterUpdateStatus.IsActive, updateUserStatusData.IsActive) + }) +} diff --git a/internal/users/usecase.go b/internal/users/usecase.go index 55f61b2..e7fba4a 100644 --- a/internal/users/usecase.go +++ b/internal/users/usecase.go @@ -7,9 +7,9 @@ import ( ) type Usecase interface { - Login(ctx context.Context, request dtos.UserLoginRequest) (response dtos.UserLoginResponse, httpCode int, err error) - Create(ctx context.Context, payload dtos.CreateUserRequest) (userID int64, httpCode int, err error) + Login(ctx context.Context, request dtos.UserLoginRequest) (response dtos.UserLoginResponse, err error) + Create(ctx context.Context, payload dtos.CreateUserRequest) (userID int64, err error) PartialUpdate(ctx context.Context, data dtos.UpdateUserRequest) error UpdateStatus(ctx context.Context, req dtos.UpdateUserStatusRequest) error - Detail(ctx context.Context, id int64) (detail dtos.UserDetailResponse, httpCode int, err error) + Detail(ctx context.Context, id int64) (detail dtos.UserDetailResponse, err error) } diff --git a/internal/users/usecase/usecase.go b/internal/users/usecase/usecase.go index d30324d..ecdd7e5 100644 --- a/internal/users/usecase/usecase.go +++ b/internal/users/usecase/usecase.go @@ -3,18 +3,17 @@ package usecase import ( "context" "database/sql" - "net/http" "strings" "time" "github.com/DoWithLogic/golang-clean-architecture/config" + "github.com/DoWithLogic/golang-clean-architecture/internal/middleware" "github.com/DoWithLogic/golang-clean-architecture/internal/users" "github.com/DoWithLogic/golang-clean-architecture/internal/users/dtos" "github.com/DoWithLogic/golang-clean-architecture/internal/users/entities" + "github.com/DoWithLogic/golang-clean-architecture/pkg/app_crypto" "github.com/DoWithLogic/golang-clean-architecture/pkg/apperror" - "github.com/DoWithLogic/golang-clean-architecture/pkg/middleware" - "github.com/DoWithLogic/golang-clean-architecture/pkg/utils" - "github.com/dgrijalva/jwt-go" + "github.com/golang-jwt/jwt" ) type ( @@ -28,48 +27,52 @@ func NewUseCase(repo users.Repository, cfg config.Config) users.Usecase { return &usecase{repo, cfg} } -func (uc *usecase) Login(ctx context.Context, request dtos.UserLoginRequest) (response dtos.UserLoginResponse, httpCode int, err error) { +func (uc *usecase) Login(ctx context.Context, request dtos.UserLoginRequest) (response dtos.UserLoginResponse, err error) { dataLogin, err := uc.repo.GetUserByEmail(ctx, request.Email) if err != nil { - return response, http.StatusInternalServerError, err + return response, apperror.InternalServerError(err) } - if !strings.EqualFold(utils.Decrypt(dataLogin.Password, uc.cfg), request.Password) { - return response, http.StatusUnauthorized, apperror.ErrInvalidPassword + if !strings.EqualFold(dataLogin.Password, app_crypto.NewCrypto(uc.cfg.Authentication.Key).EncodeSHA256(request.Password)) { + return response, apperror.Unauthorized(apperror.ErrInvalidPassword) } - identityData := middleware.CustomClaims{ - UserID: dataLogin.UserID, - Email: dataLogin.Email, + claims := middleware.PayloadToken{ + Data: &middleware.Data{ + UserID: dataLogin.UserID, + Email: dataLogin.Email, + }, StandardClaims: jwt.StandardClaims{ - ExpiresAt: time.Now().Add(time.Minute * 15).Unix(), + ExpiresAt: time.Now().Add(time.Minute * 60).Unix(), }, } - token, err := middleware.GenerateJWT(identityData, uc.cfg.Authentication.Key) - if err != nil { - return response, http.StatusInternalServerError, apperror.ErrFailedGenerateJWT - } + // Calculate the expiration time in seconds + expiresIn := claims.ExpiresAt - time.Now().Unix() - response = dtos.UserLoginResponse{ - AccessToken: token, - ExpiredAt: utils.UnixToDuration(identityData.ExpiresAt), + // Create token with claims + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Generate encoded token and send it as response. + tokenString, err := token.SignedString([]byte(uc.cfg.JWT.Key)) + if err != nil { + return response, apperror.InternalServerError(err) } - return response, http.StatusOK, nil + return dtos.UserLoginResponse{AccessToken: tokenString, ExpiredAt: expiresIn}, nil } -func (uc *usecase) Create(ctx context.Context, payload dtos.CreateUserRequest) (userID int64, httpCode int, err error) { +func (uc *usecase) Create(ctx context.Context, payload dtos.CreateUserRequest) (userID int64, err error) { if exist := uc.repo.IsUserExist(ctx, payload.Email); exist { - return userID, http.StatusConflict, apperror.ErrEmailAlreadyExist + return userID, apperror.Conflict(apperror.ErrEmailAlreadyExist) } userID, err = uc.repo.SaveNewUser(ctx, entities.NewCreateUser(payload, uc.cfg)) if err != nil { - return userID, http.StatusInternalServerError, err + return userID, apperror.InternalServerError(err) } - return userID, http.StatusOK, nil + return userID, nil } func (uc *usecase) PartialUpdate(ctx context.Context, data dtos.UpdateUserRequest) error { @@ -77,38 +80,28 @@ func (uc *usecase) PartialUpdate(ctx context.Context, data dtos.UpdateUserReques opt := entities.LockingOpt{ PessimisticLocking: true, } - _, err := tx.GetUserByID(ctx, data.UserID, opt) - if err != nil { - return err - } - err = tx.UpdateUserByID(ctx, entities.NewUpdateUsers(data)) - if err != nil { + if _, err := tx.GetUserByID(ctx, data.UserID, opt); err != nil { return err } - return nil + return tx.UpdateUserByID(ctx, entities.NewUpdateUser(data)) }) } func (uc *usecase) UpdateStatus(ctx context.Context, req dtos.UpdateUserStatusRequest) error { - _, err := uc.repo.GetUserByID(ctx, req.UserID, entities.LockingOpt{}) - if err != nil { - return err - } - - if err := uc.repo.UpdateUserStatusByID(ctx, entities.NewUpdateUserStatus(req)); err != nil { + if _, err := uc.repo.GetUserByID(ctx, req.UserID, entities.LockingOpt{}); err != nil { return err } - return nil + return uc.repo.UpdateUserStatusByID(ctx, entities.NewUpdateUserStatus(req)) } -func (uc *usecase) Detail(ctx context.Context, id int64) (detail dtos.UserDetailResponse, httpCode int, err error) { +func (uc *usecase) Detail(ctx context.Context, id int64) (detail dtos.UserDetailResponse, err error) { userDetail, err := uc.repo.GetUserByID(ctx, id) if err != nil { - return detail, http.StatusInternalServerError, err + return detail, apperror.InternalServerError(err) } - return entities.NewUserDetail(userDetail), http.StatusOK, nil + return entities.NewUserDetail(userDetail), nil } diff --git a/internal/users/usecase/usecase_test.go b/internal/users/usecase/usecase_test.go index 3e7958d..3d24a40 100644 --- a/internal/users/usecase/usecase_test.go +++ b/internal/users/usecase/usecase_test.go @@ -5,7 +5,6 @@ import ( "database/sql" "errors" "fmt" - "net/http" "testing" "time" @@ -14,25 +13,26 @@ import ( "github.com/DoWithLogic/golang-clean-architecture/internal/users/entities" mocks "github.com/DoWithLogic/golang-clean-architecture/internal/users/mock" "github.com/DoWithLogic/golang-clean-architecture/internal/users/usecase" + "github.com/DoWithLogic/golang-clean-architecture/pkg/app_crypto" "github.com/DoWithLogic/golang-clean-architecture/pkg/apperror" "github.com/DoWithLogic/golang-clean-architecture/pkg/constant" - "github.com/DoWithLogic/golang-clean-architecture/pkg/utils" + "github.com/go-faker/faker/v4" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) -func createUserMatcher(user entities.Users) gomock.Matcher { +func createUserMatcher(user entities.User) gomock.Matcher { return eqUserMatcher{ users: user, } } type eqUserMatcher struct { - users entities.Users + users entities.User } func (e eqUserMatcher) Matches(x interface{}) bool { - arg, ok := x.(entities.Users) + arg, ok := x.(entities.User) if !ok { return false } @@ -57,9 +57,7 @@ func Test_usecase_CreateUser(t *testing.T) { repo, config.Config{ Authentication: config.AuthenticationConfig{ - Key: "DoWithLogic!@#", - SecretKey: "s3cr#tK3y!@#v001", - SaltKey: "s4ltK3y!@#ddv001", + Key: "DoWithLogic!@#", }, }, ) @@ -77,7 +75,7 @@ func Test_usecase_CreateUser(t *testing.T) { repo.EXPECT(). SaveNewUser(ctx, createUserMatcher( - entities.Users{ + entities.User{ Fullname: newUser.FullName, PhoneNumber: newUser.PhoneNumber, UserType: constant.UserTypeRegular, @@ -86,18 +84,16 @@ func Test_usecase_CreateUser(t *testing.T) { )). Return(int64(1), nil) - userID, httpCode, err := uc.Create(ctx, newUser) + userID, err := uc.Create(ctx, newUser) require.NoError(t, err) - require.Equal(t, httpCode, http.StatusOK) require.NotNil(t, userID) }) t.Run("negative_email_already_use", func(t *testing.T) { repo.EXPECT().IsUserExist(ctx, newUser.Email).Return(true) - userID, httpCode, err := uc.Create(ctx, newUser) + userID, err := uc.Create(ctx, newUser) require.EqualError(t, apperror.ErrEmailAlreadyExist, err.Error()) - require.Equal(t, httpCode, http.StatusConflict) require.Equal(t, userID, int64(0)) }) @@ -107,7 +103,7 @@ func Test_usecase_CreateUser(t *testing.T) { repo.EXPECT(). SaveNewUser(ctx, createUserMatcher( - entities.Users{ + entities.User{ Fullname: "fullname", PhoneNumber: "081236548974", UserType: constant.UserTypeRegular, @@ -116,10 +112,9 @@ func Test_usecase_CreateUser(t *testing.T) { )). Return(int64(0), sql.ErrNoRows) - userID, httpCode, err := uc.Create(ctx, newUser) + userID, err := uc.Create(ctx, newUser) require.Error(t, err) require.EqualError(t, sql.ErrNoRows, err.Error()) - require.Equal(t, httpCode, http.StatusInternalServerError) require.Equal(t, userID, int64(0)) }) } @@ -136,15 +131,16 @@ func Test_usecase_UpdateUserStatus(t *testing.T) { ) args := dtos.UpdateUserStatusRequest{ - UserID: 1, - Status: constant.UserActive, - UpdateBy: "martin@test.com", + UserID: 1, + UpdateUserStatus: dtos.UpdateUserStatus{ + Status: constant.UserActive, + }, } t.Run("positive_case_UpdateUserStatus", func(t *testing.T) { repo.EXPECT(). GetUserByID(ctx, args.UserID, gomock.Any()). - Return(entities.Users{UserID: 1, IsActive: true}, nil) + Return(entities.User{UserID: 1, IsActive: true}, nil) repo.EXPECT(). UpdateUserStatusByID(ctx, gomock.Any()). @@ -157,7 +153,7 @@ func Test_usecase_UpdateUserStatus(t *testing.T) { t.Run("negative_case_UpdateUserStatus_GetUserByID_err", func(t *testing.T) { repo.EXPECT(). GetUserByID(ctx, args.UserID, gomock.Any()). - Return(entities.Users{}, errors.New("something errors")) + Return(entities.User{}, errors.New("something errors")) err := uc.UpdateStatus(ctx, args) require.Error(t, err) @@ -166,7 +162,7 @@ func Test_usecase_UpdateUserStatus(t *testing.T) { t.Run("negative_case_UpdateUserStatus_err", func(t *testing.T) { repo.EXPECT(). GetUserByID(ctx, args.UserID, gomock.Any()). - Return(entities.Users{UserID: 1, IsActive: true}, nil) + Return(entities.User{UserID: 1, IsActive: true}, nil) repo.EXPECT(). UpdateUserStatusByID(ctx, gomock.Any()). @@ -188,7 +184,7 @@ func Test_usecase_Detail(t *testing.T) { var id int64 = 1 - returnedDetail := entities.Users{ + returnedDetail := entities.User{ UserID: id, Email: "test@test.com", Fullname: "test", @@ -196,24 +192,21 @@ func Test_usecase_Detail(t *testing.T) { UserType: constant.UserTypePremium, IsActive: true, CreatedAt: time.Now(), - CreatedBy: "SYSTEM", } t.Run("detail_positive", func(t *testing.T) { repo.EXPECT().GetUserByID(ctx, id).Return(returnedDetail, nil) - detail, httpCode, err := uc.Detail(ctx, id) + detail, err := uc.Detail(ctx, id) require.NoError(t, err) - require.Equal(t, httpCode, http.StatusOK) require.Equal(t, detail, entities.NewUserDetail(returnedDetail)) }) t.Run("detail_negative_failed_query_detail", func(t *testing.T) { - repo.EXPECT().GetUserByID(ctx, id).Return(entities.Users{}, sql.ErrNoRows) + repo.EXPECT().GetUserByID(ctx, id).Return(entities.User{}, sql.ErrNoRows) - detail, httpCode, err := uc.Detail(ctx, id) + detail, err := uc.Detail(ctx, id) require.EqualError(t, err, sql.ErrNoRows.Error()) - require.Equal(t, httpCode, http.StatusInternalServerError) require.Equal(t, detail, dtos.UserDetailResponse{}) }) @@ -228,11 +221,7 @@ func Test_usecase_Login(t *testing.T) { email = "martin@test.com" config = config.Config{ - Authentication: config.AuthenticationConfig{ - Key: "DoWithLogic!@#", - SecretKey: "s3cr#tK3y!@#v001", - SaltKey: "s4ltK3y!@#ddv001", - }, + Authentication: config.AuthenticationConfig{Key: "DoWithLogic!@#"}, } ) @@ -240,18 +229,17 @@ func Test_usecase_Login(t *testing.T) { repo := mocks.NewMockRepository(ctrl) uc := usecase.NewUseCase(repo, config) - returnedUser := entities.Users{ + returnedUser := entities.User{ UserID: 1, Email: email, - Password: utils.Encrypt(password, config), + Password: app_crypto.NewCrypto(config.Authentication.Key).EncodeSHA256(password), } t.Run("login_positive", func(t *testing.T) { repo.EXPECT().GetUserByEmail(ctx, email).Return(returnedUser, nil) - authData, code, err := uc.Login(ctx, dtos.UserLoginRequest{Email: email, Password: password}) + authData, err := uc.Login(ctx, dtos.UserLoginRequest{Email: email, Password: password}) require.NoError(t, err) - require.Equal(t, code, http.StatusOK) require.NotNil(t, authData) }) @@ -259,19 +247,17 @@ func Test_usecase_Login(t *testing.T) { t.Run("login_negative_invalid_password", func(t *testing.T) { repo.EXPECT().GetUserByEmail(ctx, email).Return(returnedUser, nil) - authData, code, err := uc.Login(ctx, dtos.UserLoginRequest{Email: email, Password: "testingpwd"}) + authData, err := uc.Login(ctx, dtos.UserLoginRequest{Email: email, Password: "testingpwd"}) require.EqualError(t, apperror.ErrInvalidPassword, err.Error()) - require.Equal(t, code, http.StatusUnauthorized) require.Equal(t, authData, dtos.UserLoginResponse{}) }) t.Run("login_negative_failed_query_email", func(t *testing.T) { - repo.EXPECT().GetUserByEmail(ctx, email).Return(entities.Users{}, sql.ErrNoRows) + repo.EXPECT().GetUserByEmail(ctx, email).Return(entities.User{}, sql.ErrNoRows) - authData, code, err := uc.Login(ctx, dtos.UserLoginRequest{Email: email, Password: password}) + authData, err := uc.Login(ctx, dtos.UserLoginRequest{Email: email, Password: password}) require.EqualError(t, err, sql.ErrNoRows.Error()) - require.Equal(t, code, http.StatusInternalServerError) require.Equal(t, authData, dtos.UserLoginResponse{}) }) @@ -284,15 +270,16 @@ func Test_usecase_PartialUpdate(t *testing.T) { var ( request = dtos.UpdateUserRequest{ - Fullname: "update name", - UserID: 1, + UpdateUser: dtos.UpdateUser{ + Fullname: faker.Name(), + PhoneNumber: faker.Phonenumber(), + }, + UserID: 1, } config = config.Config{ Authentication: config.AuthenticationConfig{ - Key: "DoWithLogic!@#", - SecretKey: "s3cr#tK3y!@#v001", - SaltKey: "s4ltK3y!@#ddv001", + Key: "DoWithLogic!@#", }, } ) diff --git a/makefile b/makefile index 88d84d0..84211d5 100644 --- a/makefile +++ b/makefile @@ -1,51 +1,98 @@ -.PHONY: help database-up database-down migration-up migration-down local run - -help: - @echo "Available targets:" - @echo " make database-up - Start the database container" - @echo " make database-down - Stop and remove the database container" - @echo " make migration-up - Run database migrations" - @echo " make migration-down - Rollback database migrations" - @echo " make local - Run the application locally" - @echo " make run - Start the database, run migrations, and start the application locally" - @echo " make down - Shutdown the database and down migrations" - - - # Directory where migration files are located MIGRATION_DIR := database/mysql/migration +IS_IN_PROGRESS = "is in progress ..." -# This target waits for the MySQL container to become available -wait-for-mysql: - @echo "Waiting for MySQL container to start..." - @until docker compose exec mysql-db mysql -umysql -ppwd -hlocalhost -e "SELECT 1"; do \ - sleep 6; \ - done - @echo "MySQL is up and running!" - -database-up: - docker compose up mysql-db -d - -service-up: - docker compose up golang-clean-architecture -d +.PHONY: all +all: env install mod -docker-down: - docker compose down - -migration-up: wait-for-mysql +## help: prints this help message +.PHONY: help +help: + @echo "Usage: \n" + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +## env: will setup env +.PHONY: env +env: + @echo "make env ${IS_IN_PROGRESS}" + @go env -w GO111MODULE=on + @go env -w GOBIN=`go env GOPATH`/bin + @go env -w GOPROXY=https://proxy.golang.org,direct + +## mod: will pull all dependency +.PHONY: mod +mod: + @echo "make mod ${IS_IN_PROGRESS}" + @rm -rf ./vendor ./go.sum + @go mod tidy + @go mod vendor + +## setup: Set up database temporary for integration testing +.PHONY: setup +setup: + @echo "make setup ${IS_IN_PROGRESS}" + @docker-compose up -d + @sleep 8 + +## down: Set down database temporary for integration testing +.PHONY: down +down: + @echo "make down ${IS_IN_PROGRESS}" + @docker-compose down -t 1 + +## run: run for running app on local +.PHONY: run +run: + @go run cmd/api/main.go + + +.PHONY: migration-up +migration-up: GOOSE_DRIVER=mysql GOOSE_DBSTRING="mysql:pwd@tcp(localhost:3306)/users?parseTime=true" goose -dir=$(MIGRATION_DIR) up +.PHONY: migration-down migration-down: GOOSE_DRIVER=mysql GOOSE_DBSTRING="mysql:pwd@tcp(localhost:3306)/users?parseTime=true" goose -dir=$(MIGRATION_DIR) down - -run: database-up migration-up service-up - -down : migration-down docker-down - +.PHONY: mock-repository mock-repository: mockgen -source internal/users/repository.go -destination internal/users/mock/repository_mock.go -package=mocks +.PHONY: mock-usecase mock-usecase: mockgen -source internal/users/usecase.go -destination internal/users/mock/usecase_mock.go -package=mocks + +## unit-test: will test with unit tags +.PHONY: unit-test +unit-test: + @echo "make unit-test ${IS_IN_PROGRESS}" + @go clean -testcache + @go test \ + --race -count=1 -cpu=1 -parallel=1 -timeout=90s -failfast -vet= \ + -cover -covermode=atomic -coverprofile=./.coverage/unit.out \ + ./internal/users/usecase/... + +## integration-test: will test with integration tags +.PHONY: integration-test +integration-test: + @echo "make integration-test ${IS_IN_PROGRESS}" + @go clean -testcache + @go test --race -timeout=90s -failfast \ + -vet= -cover -covermode=atomic -coverprofile=./.coverage/integration.out -tags=integration \ + ./internal/users/repository/... + +## run-integration-test: will run integration test with any dependencies +.PHONY: run-integration-test +run-integration-test:setup migration-up integration-test migration-down down + +## tests: run tests(integration, unit & e2e testing) and any dependencies +.PHONY: tests +tests:run-integration-test unit-test + +## cover: will report all test coverage +.PHONY: cover +cover: + @make -s cover-with type=integration + @make -s cover-with type=unit + diff --git a/pkg/app_crypto/cipher.go b/pkg/app_crypto/cipher.go new file mode 100644 index 0000000..58d4ef4 --- /dev/null +++ b/pkg/app_crypto/cipher.go @@ -0,0 +1,83 @@ +package app_crypto + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "io" +) + +// Cipher represents a cryptographic cipher using the AES (Advanced Encryption Standard) algorithm in CBC (Cipher Block Chaining) mode. +type Cipher struct { + block cipher.Block // The underlying block cipher. +} + +// NewES256 creates a new Cipher instance with the specified key for AES encryption. +// Parameters: +// - key: The encryption key. +// +// Returns: +// - *Cipher: A pointer to the newly created Cipher instance. +// - error: An error if the cipher creation fails. +func NewES256(key []byte) (*Cipher, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + return &Cipher{block: block}, nil +} + +// Decrypt decrypts the given ciphertext using the AES-CBC decryption algorithm. +// Parameters: +// - ciphertext: The ciphertext to decrypt. +// +// Returns: +// - []byte: The decrypted plaintext. +// - error: An error if decryption fails. +func (c *Cipher) Decrypt(ciphertext []byte) ([]byte, error) { + if len(ciphertext) < aes.BlockSize { + return nil, errors.New("ciphertext too short") + } + + iv := ciphertext[:aes.BlockSize] + ciphertext = ciphertext[aes.BlockSize:] + + if len(ciphertext)%aes.BlockSize != 0 { + return nil, errors.New("ciphertext is not a multiple of the block size") + } + + mode := cipher.NewCBCDecrypter(c.block, iv) + mode.CryptBlocks(ciphertext, ciphertext) + + // Remove padding + padding := int(ciphertext[len(ciphertext)-1]) + return ciphertext[:len(ciphertext)-padding], nil +} + +// Encrypt encrypts the given plaintext using the AES-CBC encryption algorithm. +// Parameters: +// - plaintext: The plaintext to encrypt. +// +// Returns: +// - []byte: The encrypted ciphertext. +// - error: An error if encryption fails. +func (c *Cipher) Encrypt(plaintext []byte) ([]byte, error) { + padding := aes.BlockSize - len(plaintext)%aes.BlockSize + paddedPlaintext := append(plaintext, bytes.Repeat([]byte{byte(padding)}, padding)...) + + iv := make([]byte, aes.BlockSize) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + + ciphertext := make([]byte, aes.BlockSize+len(paddedPlaintext)) + copy(ciphertext[:aes.BlockSize], iv) + + mode := cipher.NewCBCEncrypter(c.block, iv) + mode.CryptBlocks(ciphertext[aes.BlockSize:], paddedPlaintext) + + return ciphertext, nil +} diff --git a/pkg/app_crypto/cipher_test.go b/pkg/app_crypto/cipher_test.go new file mode 100644 index 0000000..1cac7a4 --- /dev/null +++ b/pkg/app_crypto/cipher_test.go @@ -0,0 +1,91 @@ +package app_crypto_test + +import ( + "bytes" + "crypto/aes" + "crypto/rand" + "testing" + + "github.com/DoWithLogic/golang-clean-architecture/pkg/app_crypto" +) + +func TestCipherEncryptDecrypt(t *testing.T) { + key := make([]byte, aes.BlockSize) + if _, err := rand.Read(key); err != nil { + t.Fatal(err) + } + + cipher, err := app_crypto.NewES256(key) + if err != nil { + t.Fatal(err) + } + + plaintext := []byte("This is a test message.") + + // Test encryption + ciphertext, err := cipher.Encrypt(plaintext) + if err != nil { + t.Fatal(err) + } + + // Test decryption + decryptedText, err := cipher.Decrypt(ciphertext) + if err != nil { + t.Fatal(err) + } + + // Ensure the decrypted text matches the original plaintext + if !bytes.Equal(decryptedText, plaintext) { + t.Errorf("Decrypted text doesn't match original plaintext. Expected %s, got %s", plaintext, decryptedText) + } +} + +func TestInvalidCiphertextLength(t *testing.T) { + key := make([]byte, aes.BlockSize) + if _, err := rand.Read(key); err != nil { + t.Fatal(err) + } + + cipher, err := app_crypto.NewES256(key) + if err != nil { + t.Fatal(err) + } + + // Attempt to decrypt invalid ciphertext length + invalidCiphertext := make([]byte, aes.BlockSize-1) + _, err = cipher.Decrypt(invalidCiphertext) + + if err == nil { + t.Error("Expected error for invalid ciphertext length, but got none.") + } else { + expectedErrorMsg := "ciphertext too short" + if err.Error() != expectedErrorMsg { + t.Errorf("Expected error message '%s', but got '%s'", expectedErrorMsg, err.Error()) + } + } +} + +func TestInvalidBlockSize(t *testing.T) { + key := make([]byte, aes.BlockSize) + if _, err := rand.Read(key); err != nil { + t.Fatal(err) + } + + cipher, err := app_crypto.NewES256(key) + if err != nil { + t.Fatal(err) + } + + // Attempt to decrypt ciphertext with invalid block size + invalidCiphertext := make([]byte, aes.BlockSize+1) + _, err = cipher.Decrypt(invalidCiphertext) + + if err == nil { + t.Error("Expected error for invalid block size, but got none.") + } else { + expectedErrorMsg := "ciphertext is not a multiple of the block size" + if err.Error() != expectedErrorMsg { + t.Errorf("Expected error message '%s', but got '%s'", expectedErrorMsg, err.Error()) + } + } +} diff --git a/pkg/app_crypto/crypto.go b/pkg/app_crypto/crypto.go new file mode 100644 index 0000000..439abbc --- /dev/null +++ b/pkg/app_crypto/crypto.go @@ -0,0 +1,123 @@ +package app_crypto + +import ( + "crypto/cipher" + "crypto/des" + "crypto/hmac" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "fmt" + "io" +) + +type Crypto struct { + key string +} + +func NewCrypto(key string) *Crypto { + return &Crypto{key: key} +} + +// EncodeSHA1HMACBase64 : encrypt to SHA1HMAC input key, data String. Output to String in Base64 format +func (c *Crypto) EncodeSHA1HMACBase64(data ...string) string { + return c.EncodeBASE64(c.ComputeSHA1HMAC(data...)) +} + +// EncodeSHA1HMAC : encrypt to SHA1HMAC input key, data String. Output to String in Base16/Hex format +func (c *Crypto) EncodeSHA1HMAC(data ...string) string { + return fmt.Sprintf("%x", c.ComputeSHA1HMAC(data...)) +} + +// ComputeSHA1HMAC : encrypt to SHA1HMAC input key, data String. Output to String +func (c *Crypto) ComputeSHA1HMAC(data ...string) string { + h := hmac.New(sha1.New, []byte(c.key)) + for _, v := range data { + io.WriteString(h, v) + } + return string(h.Sum(nil)) +} + +func (c *Crypto) EncodeSHA256HMACBase64(data ...string) string { + return c.EncodeBASE64(c.ComputeSHA256HMAC(data...)) +} + +func (c *Crypto) EncodeSHA256HMAC(data ...string) string { + return fmt.Sprintf("%x", c.ComputeSHA256HMAC(data...)) +} + +func (c *Crypto) ComputeSHA256HMAC(data ...string) string { + h := hmac.New(sha256.New, []byte(c.key)) + for _, v := range data { + io.WriteString(h, v) + } + return string(h.Sum(nil)) +} + +func (c *Crypto) EncodeSHA512HMACBase64(data ...string) string { + return c.EncodeBASE64(c.ComputeSHA512HMAC(data...)) +} + +func (c *Crypto) EncodeSHA512HMAC(data ...string) string { + return fmt.Sprintf("%x", c.ComputeSHA512HMAC(data...)) +} + +func (c *Crypto) ComputeSHA512HMAC(data ...string) string { + h := hmac.New(sha512.New, []byte(c.key)) + for _, v := range data { + io.WriteString(h, v) + } + return string(h.Sum(nil)) +} + +// EncodeMD5 : encrypt to MD5 input string, output to string +func (c *Crypto) EncodeMD5(text string) string { + h := md5.New() + h.Write([]byte(text)) + return hex.EncodeToString(h.Sum(nil)) +} + +func (c *Crypto) EncodeMD5Base64(text string) string { + h := md5.New() + h.Write([]byte(text)) + // return EncodeBASE64(hex.EncodeToString(h.Sum(nil))) + return base64.StdEncoding.EncodeToString((h.Sum(nil))) +} + +// EncodeBASE64 : Encrypt to Base64. Input string, output string +func (c *Crypto) EncodeBASE64(text string) string { + return base64.StdEncoding.EncodeToString([]byte(text)) +} + +// DecodeBASE64 : Decrypt Base64. Input string, output string +func (c *Crypto) DecodeBASE64(text string) (string, error) { + byt, err := base64.StdEncoding.DecodeString(text) + return string(byt), err +} + +// EncodeBASE64URL : Encrypt to Base64URL. Input string, output text +func (c *Crypto) EncodeBASE64URL(text string) string { + return base64.URLEncoding.EncodeToString([]byte(text)) +} + +// EncodeDES : Encrypt to DES. input string, output chiper +func (c *Crypto) EncodeDES(text string) (cipher.Block, error) { + desKey, _ := hex.DecodeString(text) + cipher, err := des.NewTripleDESCipher(desKey) + return cipher, err +} + +// EncodeSHA256: Encrypt to SHA256. input string, output text +func (c *Crypto) EncodeSHA256(text string) string { + h := sha256.Sum256([]byte(text)) + return fmt.Sprintf("%x", h) +} + +// EncodeSHA512 Encrypt to SHA512. input string, output text +func (c *Crypto) EncodeSHA512(text string) string { + h := sha512.Sum512([]byte(text)) + return fmt.Sprintf("%x", h) +} diff --git a/pkg/app_crypto/crypto_test.go b/pkg/app_crypto/crypto_test.go new file mode 100644 index 0000000..0b36f27 --- /dev/null +++ b/pkg/app_crypto/crypto_test.go @@ -0,0 +1,120 @@ +package app_crypto_test + +import ( + "testing" + + "github.com/DoWithLogic/golang-clean-architecture/pkg/app_crypto" +) + +func TestCrypto_EncodeSHA1HMACBase64(t *testing.T) { + c := app_crypto.NewCrypto("secretKey") + result := c.EncodeSHA1HMACBase64("data1", "data2") + + // You need to replace the expectedValue with the actual HMAC SHA1 Base64 value based on your secret key and input data. + expectedValue := "BC9mO9N3TM9DeXyopI7eFXJ78pM=" + if result != expectedValue { + t.Errorf("Expected %s, got %s", expectedValue, result) + } +} + +func TestCrypto_EncodeSHA256HMAC(t *testing.T) { + c := app_crypto.NewCrypto("secretKey") + result := c.EncodeSHA256HMAC("data1", "data2") + + // You need to replace the expectedValue with the actual HMAC SHA256 value based on your secret key and input data. + expectedValue := "9cdb1c43d56c85451dfb630f66748778380ed7e7d114fab185f1b82adb1b658d" + if result != expectedValue { + t.Errorf("Expected %s, got %s", expectedValue, result) + } +} + +func TestCrypto_EncodeSHA512HMACBase64(t *testing.T) { + c := app_crypto.NewCrypto("secretKey") + result := c.EncodeSHA512HMACBase64("data1", "data2") + + // You need to replace the expectedValue with the actual HMAC SHA512 Base64 value based on your secret key and input data. + expectedValue := "28PiVYGlVn8XQrfF0rC9LXRmZcG1VYTK+akgZn3LBuxXRcwDGdaTKB05KfhehZXV1gO43Ie+s4NfHu17Bw16zA==" + if result != expectedValue { + t.Errorf("Expected %s, got %s", expectedValue, result) + } +} + +func TestCrypto_EncodeMD5(t *testing.T) { + c := app_crypto.NewCrypto("secretKey") + result := c.EncodeMD5("text") + + // You need to replace the expectedValue with the actual MD5 value based on your input text. + expectedValue := "1cb251ec0d568de6a929b520c4aed8d1" + if result != expectedValue { + t.Errorf("Expected %s, got %s", expectedValue, result) + } +} + +func TestCrypto_EncodeMD5Base64(t *testing.T) { + c := app_crypto.NewCrypto("secretKey") + result := c.EncodeMD5Base64("text") + + // You need to replace the expectedValue with the actual MD5 Base64 value based on your input text. + expectedValue := "HLJR7A1WjeapKbUgxK7Y0Q==" + if result != expectedValue { + t.Errorf("Expected %s, got %s", expectedValue, result) + } +} + +func TestCrypto_EncodeBASE64(t *testing.T) { + c := app_crypto.NewCrypto("secretKey") + result := c.EncodeBASE64("text") + + // You need to replace the expectedValue with the actual Base64 value based on your input text. + expectedValue := "dGV4dA==" + if result != expectedValue { + t.Errorf("Expected %s, got %s", expectedValue, result) + } +} + +func TestCrypto_DecodeBASE64(t *testing.T) { + c := app_crypto.NewCrypto("secretKey") + encodedText := c.EncodeBASE64("text") + result, err := c.DecodeBASE64(encodedText) + + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if result != "text" { + t.Errorf("Expected %s, got %s", "text", result) + } +} + +func TestCrypto_EncodeBASE64URL(t *testing.T) { + c := app_crypto.NewCrypto("secretKey") + result := c.EncodeBASE64URL("text") + + // You need to replace the expectedValue with the actual Base64URL value based on your input text. + expectedValue := "dGV4dA==" + if result != expectedValue { + t.Errorf("Expected %s, got %s", expectedValue, result) + } +} + +func TestCrypto_EncodeSHA256(t *testing.T) { + c := app_crypto.NewCrypto("secretKey") + result := c.EncodeSHA256("text") + + // You need to replace the expectedValue with the actual SHA256 value based on your input text. + expectedValue := "982d9e3eb996f559e633f4d194def3761d909f5a3b647d1a851fead67c32c9d1" + if result != expectedValue { + t.Errorf("Expected %s, got %s", expectedValue, result) + } +} + +func TestCrypto_EncodeSHA512(t *testing.T) { + c := app_crypto.NewCrypto("secretKey") + result := c.EncodeSHA512("text") + + // You need to replace the expectedValue with the actual SHA512 value based on your input text. + expectedValue := "eaf2c12742cb8c161bcbd84b032b9bb98999a23282542672ca01cc6edd268f7dce9987ad6b2bc79305634f89d90b90102bcd59a57e7135b8e3ceb93c0597117b" + if result != expectedValue { + t.Errorf("Expected %s, got %s", expectedValue, result) + } +} diff --git a/pkg/apperror/errors.go b/pkg/apperror/errors.go index 4c2242f..09f76b4 100644 --- a/pkg/apperror/errors.go +++ b/pkg/apperror/errors.go @@ -1,6 +1,10 @@ package apperror -import "errors" +import ( + "errors" + "net/http" + "strings" +) var ( ErrEmailAlreadyExist = errors.New("email already exist") @@ -9,4 +13,76 @@ var ( ErrFailedGenerateJWT = errors.New("failed generate access token") ErrInvalidIsActive = errors.New("invalid is_active") ErrStatusValue = errors.New("status should be 0 or 1") + + ErrFailedGetTokenInformation = errors.New("failed to get token information") ) + +type AppError struct { + Code int + Err error + Message string +} + +func Equals(err error, expectedErr error) bool { + return strings.EqualFold(err.Error(), expectedErr.Error()) +} + +func (h AppError) Error() string { + return h.Err.Error() +} + +func BadRequest(err error) error { + return &AppError{ + Code: http.StatusBadRequest, + Message: "bad_request", + Err: err, + } +} + +func InternalServerError(err error) error { + return &AppError{ + Code: http.StatusInternalServerError, + Message: "internal_server_error", + Err: err, + } +} + +func Unauthorized(err error) error { + return &AppError{ + Code: http.StatusUnauthorized, + Message: "unauthorized", + Err: err, + } +} + +func Forbidden(err error) error { + return &AppError{ + Code: http.StatusForbidden, + Message: "forbidden", + Err: err, + } +} + +func NotFound(err error) error { + return &AppError{ + Code: http.StatusNotFound, + Message: "not_found", + Err: err, + } +} + +func Conflict(err error) error { + return &AppError{ + Code: http.StatusConflict, + Message: "Conflict", + Err: err, + } +} + +func GatewayTimeout(err error) error { + return &AppError{ + Code: http.StatusGatewayTimeout, + Message: "gateway_timeout", + Err: err, + } +} diff --git a/pkg/constant/constant.go b/pkg/constant/constant.go index 49202e3..106921e 100644 --- a/pkg/constant/constant.go +++ b/pkg/constant/constant.go @@ -7,11 +7,16 @@ const ( ) const ( - UserInactive = "0" - UserActive = "1" + UserInactive = "inactive" + UserActive = "active" ) var MapStatus = map[string]bool{ UserInactive: false, UserActive: true, } + +const ( + AuthorizationHeaderKey = "Authorization" + AuthCredentialKey = "authCredential" +) diff --git a/pkg/datasource/sql.go b/pkg/datasource/sql.go index cad6cf4..ad20969 100644 --- a/pkg/datasource/sql.go +++ b/pkg/datasource/sql.go @@ -8,9 +8,9 @@ import ( "io" "os" - "github.com/DoWithLogic/golang-clean-architecture/pkg/otel/zerolog" "github.com/DoWithLogic/golang-clean-architecture/pkg/utils" "github.com/jmoiron/sqlx" + "github.com/rs/zerolog" ) type ( @@ -53,7 +53,7 @@ var ( _ Conn = (*sqlx.Conn)(nil) _ Conn = (*sqlx.DB)(nil) _ ConnTx = (*sqlx.Tx)(nil) - log = zerolog.NewZeroLog(context.Background(), os.Stdout) + log = zerolog.New(os.Stdout) ) // datasource errors @@ -66,13 +66,13 @@ var ( func (x exec) Scan(rowsAffected, lastInsertID *int64) error { if x.err != nil { - log.Z().Err(x.err).Msg("[database:exec]error not nil") + log.Err(x.err).Msg("[database:exec]error not nil") return x.err } if x.sqlResult == nil { - log.Z().Err(sql.ErrNoRows).Msg("[database:exec]rows is nil") + log.Err(sql.ErrNoRows).Msg("[database:exec]rows is nil") return ErrDataNotFound } @@ -80,12 +80,12 @@ func (x exec) Scan(rowsAffected, lastInsertID *int64) error { if rowsAffected != nil { n, err := x.sqlResult.RowsAffected() if err != nil { - log.Z().Err(err).Msg("[database:exec]scan rows affected error") + log.Err(err).Msg("[database:exec]scan rows affected error") return err } if n < 1 { - log.Z().Err(ErrDataNotFound).Msg("[database:exec]rows affected") + log.Err(ErrDataNotFound).Msg("[database:exec]rows affected") return ErrDataNotFound } @@ -95,7 +95,7 @@ func (x exec) Scan(rowsAffected, lastInsertID *int64) error { if lastInsertID != nil { n, err := x.sqlResult.LastInsertId() if err != nil { - log.Z().Err(err).Msg("[database:exec]last inserted id error") + log.Err(err).Msg("[database:exec]last inserted id error") } else { *lastInsertID = int64(n) } @@ -106,13 +106,13 @@ func (x exec) Scan(rowsAffected, lastInsertID *int64) error { func (x query) Scan(row func(i int) utils.Array) error { if x.err != nil { - log.Z().Err(x.err).Msg("[database:query]error not nil") + log.Err(x.err).Msg("[database:query]error not nil") return x.err } if x.sqlRows == nil { - log.Z().Err(sql.ErrNoRows).Msg("[database:query]rows is nil") + log.Err(sql.ErrNoRows).Msg("[database:query]rows is nil") return ErrDataNotFound } @@ -125,13 +125,13 @@ func (x query) Scan(row func(i int) utils.Array) error { columns, err := x.sqlRows.Columns() if err != nil { - log.Z().Err(err).Msg("[database:query]columns") + log.Err(err).Msg("[database:query]columns") return err } if len(columns) < 1 { - log.Z().Err(ErrNoColumnReturned).Msg("[database:query]count columns length") + log.Err(ErrNoColumnReturned).Msg("[database:query]count columns length") return ErrNoColumnReturned } @@ -139,7 +139,7 @@ func (x query) Scan(row func(i int) utils.Array) error { var idx int = 0 for x.sqlRows.Next() { if x.sqlRows.Err() != nil { - log.Z().Err(x.sqlRows.Err()).Msg("[database:query]error to scan sql rows") + log.Err(x.sqlRows.Err()).Msg("[database:query]error to scan sql rows") return x.sqlRows.Err() } @@ -154,13 +154,13 @@ func (x query) Scan(row func(i int) utils.Array) error { if len(row(idx)) != len(columns) { err := fmt.Errorf("%w: [%d] columns on [%d] destinations", ErrInvalidArguments, len(columns), len(row(idx))) - log.Z().Err(err).Msg("[database:query]error invalid args to scan") + log.Err(err).Msg("[database:query]error invalid args to scan") return err } if err = x.sqlRows.Scan(row(idx)...); err != nil { - log.Z().Err(err).Msg("[database:query] failed to scan row") + log.Err(err).Msg("[database:query] failed to scan row") return err } @@ -181,7 +181,7 @@ func (DataSource) QuerySQL(sqlRows *sqlx.Rows, err error) Query { func (DataSource) EndTx(tx *sqlx.Tx, err error) error { if tx == nil { - log.Z().Err(ErrInvalidTransaction).Msg("[database:EndTx]") + log.Err(ErrInvalidTransaction).Msg("[database:EndTx]") return ErrInvalidTransaction } @@ -192,14 +192,14 @@ func (DataSource) EndTx(tx *sqlx.Tx, err error) error { msg = fmt.Sprintf("%s failed: (%s)", msg, errR.Error()) } - log.Z().Err(err).Msg(fmt.Sprintf("[database:EndTx]%s", msg)) + log.Err(err).Msg(fmt.Sprintf("[database:EndTx]%s", msg)) return err } // we try to commit here if err = tx.Commit(); err != nil { - log.Z().Err(err).Msg("[database:EndTx]Commit") + log.Err(err).Msg("[database:EndTx]Commit") return err } diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go deleted file mode 100644 index 185db26..0000000 --- a/pkg/middleware/auth.go +++ /dev/null @@ -1,76 +0,0 @@ -package middleware - -import ( - "errors" - "net/http" - "strings" - - "github.com/dgrijalva/jwt-go" - - "github.com/DoWithLogic/golang-clean-architecture/config" - "github.com/DoWithLogic/golang-clean-architecture/pkg/utils/response" - "github.com/labstack/echo/v4" -) - -// CustomClaims represents the custom claims you want to include in the JWT payload. -type CustomClaims struct { - UserID int64 `json:"user_id"` - Email string `json:"email"` - jwt.StandardClaims -} - -func GenerateJWT(data CustomClaims, secretKey string) (string, error) { - // Create the token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, data) - - // Sign the token with the secret key - tokenString, err := token.SignedString([]byte(secretKey)) - if err != nil { - return "", err - } - - return tokenString, nil -} - -func AuthorizeJWT(cfg config.Config) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - auth, err := extractBearerToken(c) - if err != nil { - return c.JSON(http.StatusUnauthorized, response.NewResponseError(http.StatusUnauthorized, response.MsgFailed, err.Error())) - } - - token, err := jwt.ParseWithClaims(*auth, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(cfg.Authentication.Key), nil - }) - - if err != nil { - return c.JSON(http.StatusUnauthorized, response.NewResponseError(http.StatusUnauthorized, response.MsgFailed, err.Error())) - } - - if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { - c.Set("identity", claims) - - return next(c) - } - - return c.JSON(http.StatusUnauthorized, response.NewResponseError(http.StatusUnauthorized, response.MsgFailed, err.Error())) - } - } -} - -func extractBearerToken(c echo.Context) (*string, error) { - authData := c.Request().Header.Get("Authorization") - if authData == "" { - return nil, errors.New("authorization can't be nil") - } - parts := strings.Split(authData, " ") - if len(parts) < 2 { - return nil, errors.New("invalid authorization value") - } - if parts[0] != "Bearer" { - return nil, errors.New("auth should be bearer") - } - - return &parts[1], nil -} diff --git a/pkg/observability/instrumentation/trace.go b/pkg/observability/instrumentation/trace.go new file mode 100644 index 0000000..63014fb --- /dev/null +++ b/pkg/observability/instrumentation/trace.go @@ -0,0 +1,79 @@ +package instrumentation + +import ( + "context" + "net/http/httptrace" + + "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +// tracer: Global instance of the OpenTelemetry tracer, initialized with the service name "btb-cms". +var tracer = otel.Tracer("golang-clean-architecture") + +// NewTraceSpan: Starts a new trace span with the given context and name, incorporating any baggage attributes into the span attributes. +// Parameters: +// - ctx: Context containing baggage attributes. +// - name: Name of the new trace span. +// Returns: +// - Context with the new span added. +// - The newly created trace span. +func NewTraceSpan(ctx context.Context, name string) (context.Context, trace.Span) { + return tracer.Start( + ctx, + name, + trace.WithAttributes(ctxBaggageToAttributes(ctx)...), + ) +} + +// NewTraceSpanWithoutBaggage: Starts a new trace span without incorporating any baggage attributes. +// Parameters: +// - ctx: Context for the new trace span. +// - name: Name of the new trace span. +// Returns: +// - Context with the new span added. +// - The newly created trace span. +func NewTraceSpanWithoutBaggage(ctx context.Context, name string) (context.Context, trace.Span) { + return tracer.Start( + ctx, + name, + ) +} + +// NewTraceHttpClient: Returns a new HTTP client trace for OpenTelemetry using the provided context. +// Parameters: +// - ctx: Context for the HTTP client trace. +// Returns: +// - A new HTTP client trace. +func NewTraceHttpClient(ctx context.Context) *httptrace.ClientTrace { + return otelhttptrace.NewClientTrace(ctx) +} + +// RecordSpanError: Records an error on the provided trace span and sets the status of the span to 'Error'. +// Parameters: +// - span: Trace span on which to record the error. +// - err: Error to record. +func RecordSpanError(span trace.Span, err error) { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) +} + +// ctxBaggageToAttributes: Converts baggage attributes from the context into OpenTelemetry attribute key-values. +// Parameters: +// - ctx: Context containing baggage attributes. +// Returns: +// - Slice of attribute key-values converted from the baggage attributes. +func ctxBaggageToAttributes(ctx context.Context) []attribute.KeyValue { + var attributes []attribute.KeyValue + + bag := baggage.FromContext(ctx) + for _, member := range bag.Members() { + attributes = append(attributes, attribute.String(member.Key(), member.Value())) + } + + return attributes +} diff --git a/pkg/observability/metrics.go b/pkg/observability/metrics.go new file mode 100644 index 0000000..05d79f1 --- /dev/null +++ b/pkg/observability/metrics.go @@ -0,0 +1,67 @@ +package observability + +import ( + "context" + "time" + + "github.com/DoWithLogic/golang-clean-architecture/config" + "github.com/pkg/errors" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" +) + +// OTLP communication modes +const ( + OTLP_HTTP_MODE = "otlp/http" // HTTP mode for OTLP communication + OTLP_GRPC_MODE = "otlp/grpc" // gRPC mode for OTLP communication + CONSOLE_MODE = "console" // Console mode, possibly for debugging +) + +// InitMeterProvider initializes and returns an OpenTelemetry MeterProvider based on the provided configuration. +// The function sets up a metric exporter based on the observability mode specified in the configuration and creates a MeterProvider with the specified interval for exporting metrics. +// +// Parameters: +// - config: Configuration containing observability mode and application details. +// +// Returns: +// - A pointer to an OpenTelemetry MeterProvider. +// - An error if any occurs during initialization or setting up the provider. +func InitMeterProvider(config config.Config) (*sdkmetric.MeterProvider, error) { + var ( + exporter sdkmetric.Exporter + err error + ) + + // Determine the type of metric exporter based on the observability mode. + switch config.Observability.Mode { + case OTLP_HTTP_MODE: + exporter, err = otlpmetrichttp.New( + context.Background(), + otlpmetrichttp.WithInsecure(), + ) + case OTLP_GRPC_MODE: + exporter, err = otlpmetricgrpc.New( + context.Background(), + otlpmetricgrpc.WithInsecure(), + ) + default: + return nil, errors.New("invalid observability mode") + } + + if err != nil { + return nil, err + } + + // Create and configure the MeterProvider with a periodic reader for exporting metrics. + mp := sdkmetric.NewMeterProvider( + sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exporter, sdkmetric.WithInterval(2*time.Second))), + sdkmetric.WithResource(initResource(config.App.Name, config.App.Version, config.App.Environment)), + ) + + // Set the MeterProvider as the global meter provider. + otel.SetMeterProvider(mp) + + return mp, nil +} diff --git a/pkg/observability/resource.go b/pkg/observability/resource.go new file mode 100644 index 0000000..64597fb --- /dev/null +++ b/pkg/observability/resource.go @@ -0,0 +1,43 @@ +package observability + +import ( + "context" + + sdkresource "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" +) + +// initResource initializes and returns an OpenTelemetry resource based on the provided application details. +// This function creates a composite resource that combines default resource attributes with additional attributes such as OS, process, container, host, service name, service version, +// and deployment environment. +// +// Parameters: +// - appName: Name of the application. +// - appVersion: Version of the application. +// - appEnv: Environment (e.g., development, production) of the application. +// +// Returns: +// - A pointer to an OpenTelemetry resource with the combined attributes. +func initResource(appName string, appVersion string, appEnv string) *sdkresource.Resource { + // Create additional resource attributes based on the application details. + extraResource, _ := sdkresource.New( + context.Background(), + sdkresource.WithOS(), + sdkresource.WithProcess(), + sdkresource.WithContainer(), + sdkresource.WithHost(), + sdkresource.WithAttributes( + semconv.ServiceName(appName), + semconv.ServiceVersion(appVersion), + semconv.DeploymentEnvironment(appEnv), + ), + ) + + // Merge the additional resource attributes with the default attributes. + resource, _ := sdkresource.Merge( + sdkresource.Default(), + extraResource, + ) + + return resource +} diff --git a/pkg/observability/tracer.go b/pkg/observability/tracer.go new file mode 100644 index 0000000..f1162f6 --- /dev/null +++ b/pkg/observability/tracer.go @@ -0,0 +1,57 @@ +package observability + +import ( + "context" + + "github.com/DoWithLogic/golang-clean-architecture/config" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +// initializeTracerProvider initializes and configures a tracer provider based on the observability mode from the configuration. +// It supports OTLP gRPC, OTLP HTTP, and stdout (default) exporters for trace data. +// Returns a tracer provider instance or an error if initialization fails. +func InitTracerProvider(config config.Config) (*sdktrace.TracerProvider, error) { + var ( + exporter sdktrace.SpanExporter + err error + ) + + // Determine the exporter type based on the observability mode. + switch config.Observability.Mode { + case OTLP_GRPC_MODE: + exporter, err = otlptracegrpc.New( + context.Background(), + otlptracegrpc.WithInsecure(), + ) + case OTLP_HTTP_MODE: + exporter, err = otlptracehttp.New( + context.Background(), + otlptracehttp.WithInsecure(), + ) + default: + exporter, err = stdouttrace.New(stdouttrace.WithPrettyPrint()) + } + + if err != nil { + return nil, err + } + + // Configure and set up the tracer provider with the chosen exporter and other necessary configurations. + tracerProvider := sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.AlwaysSample()), + sdktrace.WithBatcher(exporter), + sdktrace.WithResource(initResource(config.App.Name, config.App.Version, config.App.Environment)), + ) + + // Set the global tracer provider for the OpenTelemetry API. + otel.SetTracerProvider(tracerProvider) + // Set the text map propagator for trace context and baggage. + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + + return tracerProvider, nil +} diff --git a/pkg/observability/zerolog.go b/pkg/observability/zerolog.go new file mode 100644 index 0000000..ebfaa8f --- /dev/null +++ b/pkg/observability/zerolog.go @@ -0,0 +1,71 @@ +package observability + +import ( + "context" + "io" + "log" + "os" + "strings" + + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/trace" +) + +// Logger: A utility structure containing both a standard Go logger and a zerolog logger. +// It also provides methods to retrieve each logger instance and to set the log level dynamically. +type ( + Logger struct { + standard *log.Logger // standard: Standard Go logger. + zerolog *zerolog.Logger // zerolog: Zerolog logger. + } +) + +// TracingHook: Custom hook for zerolog that extracts tracing information from the context and adds it to the log entry. +type TracingHook struct{} + +// Run: Implements the Run method of the zerolog.Hook interface to customize log events with tracing information. +func (h TracingHook) Run(e *zerolog.Event, level zerolog.Level, msg string) { + ctx := e.GetCtx() + span := trace.SpanContextFromContext(ctx) + + if span.HasTraceID() { + e.Str("span_id", span.SpanID().String()).Str("trace_id", span.TraceID().String()) + } + +} + +// NewZeroLogHook: Creates a new logger with a zerolog hook for tracing. +func NewZeroLogHook() *Logger { + z := zerolog.New(os.Stdout).Hook(TracingHook{}).With().Timestamp().Stack().Logger() + + return &Logger{log.New(z, "", 0), &z} +} + +// NewZeroLog: Creates a new logger with tracing information extracted from the provided context. +func NewZeroLog(ctx context.Context, c ...io.Writer) *Logger { + span := trace.SpanContextFromContext(ctx) + + z := zerolog.New(os.Stdout).With().Timestamp(). + Str("span_id", span.SpanID().String()). + Str("trace_id", span.TraceID().String()). + Stack().Logger() + + return &Logger{log.New(z, "", 0), &z} +} + +// S: Returns the standard Go logger from the Logger structure. +func (x *Logger) S() *log.Logger { return x.standard } + +// Z: Returns the zerolog logger from the Logger structure. +func (x *Logger) Z() *zerolog.Logger { return x.zerolog } + +// Level: Sets the log level for the zerolog logger and updates the standard logger accordingly. +func (x *Logger) Level(level string) *Logger { + lv, err := zerolog.ParseLevel(strings.ToLower(level)) + if err == nil { + *x.zerolog = x.zerolog.Level(lv) + x.standard.SetOutput(x.zerolog) + } + + return x +} diff --git a/pkg/otel/zerolog/zerolog.go b/pkg/otel/zerolog/zerolog.go deleted file mode 100644 index 5bd3cf4..0000000 --- a/pkg/otel/zerolog/zerolog.go +++ /dev/null @@ -1,73 +0,0 @@ -package zerolog - -import ( - "context" - "io" - "log" - "os" - "strconv" - "strings" - "time" - - "github.com/rs/zerolog" -) - -type ( - Logger struct { - standard *log.Logger - zerolog *zerolog.Logger - Out, Err *os.File - } -) - -var ( - logID = strconv.FormatInt(time.Now().UnixMicro(), 36) - sOUT = os.Stdout - sERR = os.Stderr -) - -func NewZeroLog(ctx context.Context, c ...io.Writer) *Logger { - ws, z := make([]io.Writer, 0), new(zerolog.Logger) - - for j := 0; j < len(c); j++ { - if c[j] != nil { - ws = append(ws, c[j]) - } - } - - if len(c) > 0 { - switch len(ws) { - case 0: - *z = zerolog.Nop() - case 1: - *z = zerolog.New(ws[0]).With().Timestamp().Stack().Logger() - default: - *z = zerolog.New(zerolog.MultiLevelWriter(ws...)) - } - } else if zz := zerolog.Ctx(ctx); zz != nil { - *z = *zz - } - - dir := os.TempDir() - tempOUT, _ := os.Create(dir + "/golang-clean-architecture-" + logID + "-out.log") - tempERR, _ := os.Create(dir + "/golang-clean-architecture-" + logID + "-err.log") - - return &Logger{log.New(z, "", 0), z, tempOUT, tempERR} -} - -func (x *Logger) S() *log.Logger { return x.standard } -func (x *Logger) Z() *zerolog.Logger { return x.zerolog } -func (x *Logger) Level(level string) *Logger { - lv, err := zerolog.ParseLevel(strings.ToLower(level)) - if err == nil { - *x.zerolog = x.zerolog.Level(lv) - x.standard.SetOutput(x.zerolog) - } - - return x -} -func (x *Logger) Unswap() { - os.Stdout, os.Stderr = sOUT, sERR - - log.SetOutput(os.Stderr) -} diff --git a/pkg/response/response.go b/pkg/response/response.go new file mode 100644 index 0000000..29aa1cd --- /dev/null +++ b/pkg/response/response.go @@ -0,0 +1,121 @@ +package response + +import ( + "net/http" + + "github.com/DoWithLogic/golang-clean-architecture/pkg/apperror" + "github.com/DoWithLogic/golang-clean-architecture/pkg/observability/instrumentation" + "github.com/labstack/echo/v4" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +const ( + INTERNAL_SERVER_ERROR = "internal_server_error" + SUCCESS = "success" +) + +// FailedResponse represents a failed response structure for API responses. +type FailedResponse struct { + Code int `json:"code" example:"500"` // HTTP status code. + Message string `json:"message" example:"internal_server_error"` // Message corresponding to the status code. + Error string `json:"error" example:"{$err}"` // error message. +} + +// BasicResponse represents a failed response structure for API responses. +type BasicResponse struct { + Code int `json:"code" example:"500"` // HTTP status code. + Message string `json:"message" example:"internal_server_error"` // Message corresponding to the status code. + Error string `json:"error" example:"{$err}"` // error message. + Data interface{} `json:"data,omitempty"` +} + +// BasicBuilder constructs a BasicBuilder based on the provided error. +func BasicBuilder(result BasicResponse) BasicResponse { + return result +} + +// Send sends the CustomResponse as a JSON response using the provided Echo context. +func (c BasicResponse) Send(ctx echo.Context) error { + trace.SpanFromContext(ctx.Request().Context()).SetStatus(codes.Ok, http.StatusText(c.Code)) + return ctx.JSON(c.Code, c) +} + +// ErrorBuilder constructs a FailedResponse based on the provided error. +func ErrorBuilder(err error) FailedResponse { + var appErr *apperror.AppError + if errors.As(err, &appErr) { + ae := err.(*apperror.AppError) + + return FailedResponse{ + Code: ae.Code, + Message: ae.Message, + Error: ae.Error(), + } + } + + var errString = INTERNAL_SERVER_ERROR + if err != nil { + errString = err.Error() + } + + return FailedResponse{ + Code: http.StatusInternalServerError, + Message: INTERNAL_SERVER_ERROR, + Error: errString, + } +} + +// Send sends the CustomResponse as a JSON response using the provided Echo context. +func (x FailedResponse) Send(c echo.Context) error { + instrumentation.RecordSpanError(trace.SpanFromContext(c.Request().Context()), errors.New(x.Error)) + + return c.JSON(x.Code, x) +} + +// SuccessResponse represents a success response structure for API responses. +type SuccessResponse struct { + Success + Meta +} + +type ResponseFormat struct { + Code int `json:"code" example:"200"` // HTTP status code. + Message string `json:"message" example:"success"` +} + +type Success struct { + ResponseFormat + Data interface{} `json:"data,omitempty"` // data payload. +} + +type Meta struct { + Meta interface{} `json:"meta,omitempty"` //pagination payload. + Success +} + +// SuccessBuilder constructs a CustomResponse with a Success status and the provided response data. +func SuccessBuilder(response interface{}, meta ...interface{}) SuccessResponse { + result := SuccessResponse{ + Success: Success{ + ResponseFormat: ResponseFormat{ + Code: http.StatusOK, + Message: SUCCESS, + }, + Data: response, + }, + } + + if len(meta) > 0 { + result.Meta.Meta = meta[0] + } + + return result +} + +// Send sends the CustomResponse as a JSON response using the provided Echo context. +func (c SuccessResponse) Send(ctx echo.Context) error { + trace.SpanFromContext(ctx.Request().Context()).SetStatus(codes.Ok, http.StatusText(c.Code)) + return ctx.JSON(c.Code, c) +} diff --git a/pkg/utils/crypto.go b/pkg/utils/crypto.go index d55ab27..91eed98 100644 --- a/pkg/utils/crypto.go +++ b/pkg/utils/crypto.go @@ -3,77 +3,58 @@ package utils import ( "crypto/aes" "crypto/cipher" + "crypto/rand" "encoding/base64" - - "github.com/DoWithLogic/golang-clean-architecture/config" + "io" ) -func Encrypt(pwd string, cfg config.Config) string { - return encrypt(pwd, []byte(cfg.Authentication.SecretKey), []byte(cfg.Authentication.SaltKey)) -} - -func Decrypt(pwd string, cfg config.Config) string { - return decrypt(pwd, []byte(cfg.Authentication.SecretKey), []byte(cfg.Authentication.SaltKey)) -} - -func encrypt(text string, key, salt []byte) string { +func Encrypt(text, secretKey, salt string) string { plaintext := []byte(text) + key := []byte(secretKey) + saltBytes := []byte(salt) - // Create a new AES cipher block block, err := aes.NewCipher(key) if err != nil { - panic(err.Error()) - } - - // Create a GCM (Galois/Counter Mode) cipher using AES - gcm, err := cipher.NewGCM(block) - if err != nil { - panic(err.Error()) + panic(err) } - // Create a nonce by concatenating salt and random bytes. Nonce must be unique for each encryption - nonce := make([]byte, gcm.NonceSize()) - copy(nonce, salt) + cipherText := make([]byte, aes.BlockSize+len(plaintext)) - // Encrypt the data using AES-GCM - ciphertext := gcm.Seal(nil, nonce, plaintext, nil) + // Use the salt in the initialization vector + iv := cipherText[:aes.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + panic(err) + } - // Include the nonce in the encrypted data - encryptedData := append(nonce, ciphertext...) + stream := cipher.NewCFBEncrypter(block, iv) + stream.XORKeyStream(cipherText[aes.BlockSize:], plaintext) - return base64.StdEncoding.EncodeToString(encryptedData) + return base64.StdEncoding.EncodeToString(append(saltBytes, cipherText...)) } -func decrypt(encryptedText string, key, salt []byte) string { - // Decode base64 - encryptedData, err := base64.StdEncoding.DecodeString(encryptedText) +func Decrypt(encryptedText, secretKey, salt string) string { + ciphertext, err := base64.StdEncoding.DecodeString(encryptedText) if err != nil { - panic(err.Error()) + panic(err) } - // Create a new AES cipher block + key := []byte(secretKey) + saltBytes := []byte(salt) + block, err := aes.NewCipher(key) if err != nil { - panic(err.Error()) + panic(err) } - // Create a GCM (Galois/Counter Mode) cipher using AES - gcm, err := cipher.NewGCM(block) - if err != nil { - panic(err.Error()) + if len(ciphertext) < aes.BlockSize { + panic("ciphertext too short") } - // Nonce size is determined by the choice of GCM mode and its associated size for the given key - nonceSize := gcm.NonceSize() + iv := ciphertext[:aes.BlockSize] + ciphertext = ciphertext[aes.BlockSize:] - // Extract the nonce from the encrypted data - nonce, encryptedMessage := encryptedData[:nonceSize], encryptedData[nonceSize:] - - // Decrypt the data using AES-GCM - plaintext, err := gcm.Open(nil, nonce, encryptedMessage, nil) - if err != nil { - panic(err.Error()) - } + stream := cipher.NewCFBDecrypter(block, iv) + stream.XORKeyStream(ciphertext, ciphertext) - return string(plaintext) + return string(ciphertext[len(saltBytes):]) } diff --git a/pkg/utils/response/response.go b/pkg/utils/response/response.go deleted file mode 100644 index 0142dd0..0000000 --- a/pkg/utils/response/response.go +++ /dev/null @@ -1,52 +0,0 @@ -package response - -import ( - "net/http" -) - -type ( - Message map[string]string - - Response struct { - Status int `json:"status"` - Message Message `json:"message"` - Errors []CaptureError `json:"errors,omitempty"` - Data interface{} `json:"data,omitempty"` - Meta interface{} `json:"meta,omitempty"` - Header http.Header `json:"header,omitempty"` - Body interface{} `json:"body,omitempty"` - } - - CaptureError struct { - Details string `json:"details"` - Message string `json:"message"` - } -) - -var ( - Text = http.StatusText - - MsgSuccess = map[string]string{"en": "Success", "id": "Sukses"} - MsgFailed = map[string]string{"en": "Failed", "id": "Gagal"} -) - -func NewResponse(statusCode int, message Message, data interface{}) Response { - return Response{ - Status: statusCode, - Message: MsgSuccess, - Data: data, - } -} - -func NewResponseError(statusCode int, messageStatus Message, details string) Response { - return Response{ - Status: statusCode, - Message: messageStatus, - Errors: []CaptureError{ - { - Message: Text(statusCode), - Details: details, - }, - }, - } -}