diff --git a/.gitignore b/.gitignore index a9a04ed..0472a60 100644 --- a/.gitignore +++ b/.gitignore @@ -14,9 +14,13 @@ # Binaries cmd/client/main -cmd/client/agent +cmd/client/client_darwin_amd64 +cmd/client/client_linux_amd64 +cmd/client/client_windows_amd64 cmd/server/main cmd/server/server +cmd/migrator/main +cmd/migrator/migrator # Dependency directories (remove the comment below to include it) vendor/ @@ -25,5 +29,16 @@ vendor/ .idea .vscode -# Data -data/ +# Configs +config/migrator_client.yaml +config/migrator_server.yaml +config/server.yaml +config/server_test.yaml +config/client.yaml +config/client_test.yaml + +# log +*.log + +# client +__debug_bin* \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..d2dd5af --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,56 @@ +linters: + enable-all: true + disable: + - nlreturn + - varnamelen + - wsl + - perfsprint + - depguard + - forbidigo + - exhaustruct + - tenv + - tagalign + - nosprintfhostport + - lll + - loggercheck + - paralleltest + - tagliatelle + - gochecknoglobals + - nonamedreturns + - wrapcheck + - gocognit + - cyclop + - exhaustive + - funlen + - nilnil + - mnd + - godox + - gocyclo + - gochecknoinits + +linters-settings: + revive: + rules: + - name: var-naming + arguments: + - ["ID"] + - ["VM"] + - - skipPackageNameChecks: true + + stylecheck: + checks: ["all", "-ST1003"] + + gci: + sections: + - standard # Standard lib + - default # External dependencies + - prefix(github.com/bjlag/go-keeper) # Internal packages + + ireturn: + allow: + - anon + - error + - empty + - stdlib + - github\.com\/charmbracelet\/bubbletea\.Model + - github\.com\/golang-migrate\/migrate\/v4\/database\.Driver \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a523552 --- /dev/null +++ b/Makefile @@ -0,0 +1,102 @@ +BUILD_VERSION ?= "v1.0.0" +BUILD_DATE ?= $(shell date +'%Y/%m/%d %H:%M:%S') + +NAME = $(shell basename "$(PWD)") +DIR = $(shell pwd) +DOCKER_FILE = "docker-compose.local.yaml" + +.DEFAULT_GOAL := up + +.PHONY: all +all: help + +## up: start app +.PHONY: up +up: docker-up wait-db migrate + +## down: stop app +.PHONY: down +down: docker-down + +wait-db: + @echo " > Wait DB" + @sleep 5 + +## clean: stop app and remove volumes +.PHONY: clean +clean: docker-down-clear + +## docker-up: start docker +.PHONY: docker-up +docker-up: + @echo " > Start docker" + @docker-compose -f $(DIR)/docker/$(DOCKER_FILE) up -d + +## docker-down: stop docker +.PHONY: docker-down +docker-down: + @echo " > Stop docker" + @docker-compose -f $(DIR)/docker/$(DOCKER_FILE) down --remove-orphans + +## docker-down-clear: stop docker and remove volumes +.PHONY: docker-down-clear +docker-down-clear: + @echo " > Stop docker and remove volumes" + @docker-compose -f $(DIR)/docker/$(DOCKER_FILE) down -v --remove-orphans + +## migrate: apply migrations +.PHONY: migrate +migrate: + @echo " > Apply migrations" + @go run $(DIR)/cmd/migrator -c="./config/migrator_server.yaml" + +## lint: start linter +.PHONY: lint +lint: + @echo " > Start linter" + @golangci-lint run + +## fmt: start fmt +.PHONY: fmt +fmt: + @echo " > Start fmt" + @goimports -local "github.com/bjlag/go-keeper" -d -w $$(find . -type f -name '*.go' -not -path "*_mock.go" -not -path "./internal/generated/*") + +## test: start testing +.PHONY: test +test: + @echo " > Testing" + @go test -v $(DIR)/... + +## tidy: start `go mod tidy` +.PHONY: tidy +tidy: + @echo " > Go mod tidy" + @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go mod tidy + +## proto: generate grpc client/server from proto files +.PHONY: proto +proto: + @echo " > Generate gRPC" + @protoc --go_out=. --go_opt=paths=import --go-grpc_out=. --go-grpc_opt=paths=import --go-grpc_opt=require_unimplemented_servers=false proto/* + +## doc: open documentation +.PHONY: doc +doc: + godoc -http=:8888 -play + +build-client: + @echo " > Building client for OSX" + @GOOS=darwin GOARCH=amd64 go build -ldflags "-X main.buildVersion=$(BUILD_VERSION) -X 'main.buildDate=$(BUILD_DATE)'" -o ./cmd/client/client_darwin_amd64 ./cmd/client/. + @echo " > Building client for Linux" + @GOOS=linux GOARCH=amd64 go build -ldflags "-X main.buildVersion=$(BUILD_VERSION) -X 'main.buildDate=$(BUILD_DATE)'" -o ./cmd/client/client_linux_amd64 ./cmd/client/. + @echo " > Building client for Windows" + @GOOS=windows GOARCH=amd64 go build -ldflags "-X main.buildVersion=$(BUILD_VERSION) -X 'main.buildDate=$(BUILD_DATE)'" -o ./cmd/client/client_windows_amd64 ./cmd/client/. + +.PHONY: help +help: Makefile + @echo + @echo " Choose a command run in "$(NAME)":" + @echo + @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' + @echo \ No newline at end of file diff --git a/README.md b/README.md index 048e7bd..2a6bf15 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # go-keeper +Выпускная работа на курсе "Яндекс Практикум: Продвинутый Go-разработчик". + GoKeeper представляет собой клиент-серверную систему, позволяющую пользователю надёжно и безопасно хранить логины, пароли, бинарные данные и прочую приватную информацию. diff --git a/cmd/client/main.go b/cmd/client/main.go index 7905807..b8896bd 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -1,5 +1,71 @@ +// Отвечает за запуск клиента. +// +// Конфигурация указывается через флаг -c, описывается в YAML файле: +// - пример ./config/client.yaml.dist +// +// Флаг -version выведет текущую версию и дату сборки. package main +import ( + "context" + "flag" + "fmt" + logNative "log" + "os/signal" + "syscall" + + "github.com/ilyakaznacheev/cleanenv" + "go.uber.org/zap" + + "github.com/bjlag/go-keeper/internal/app/client" + "github.com/bjlag/go-keeper/internal/infrastructure/logger" +) + +const ( + // Конфигурация по умолчанию, если не передать флаг конфигурации при старте приложения. + configPathDefault = "./config/client.yaml" +) + +var ( + viewVersion bool + buildVersion string + buildDate string +) + func main() { + defer func() { + if r := recover(); r != nil { + logNative.Fatalf("panic occurred: %v", r) + } + }() + + var configPath string + + flag.StringVar(&configPath, "c", configPathDefault, "Path to config file") + flag.BoolVar(&viewVersion, "version", false, "View build version and data") + flag.Parse() + + if viewVersion { + fmt.Printf("Version: %s\nBuild: %s\n", buildVersion, buildDate) + return + } + + var cfg client.Config + if err := cleanenv.ReadConfig(configPath, &cfg); err != nil { + panic(err) + } + + log := logger.Get(cfg.Env) + defer func() { + _ = log.Sync() + }() + + log.Debug("Config loaded", zap.Any("config", cfg)) + + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT) + defer cancel() + if err := client.NewApp(cfg, log).Run(ctx); err != nil { + panic(err) + } } diff --git a/cmd/migrator/main.go b/cmd/migrator/main.go new file mode 100644 index 0000000..c2c3614 --- /dev/null +++ b/cmd/migrator/main.go @@ -0,0 +1,95 @@ +// Отвечает за применение миграций к базе данных. +// Поддерживает PostgreSQL и SQLite. +// +// Конфигурация указывается через флаг -c, описывается в YAML файле: +// - пример для клиента ./config/migrator_client.yaml.dist +// - пример для сервера ./config/migrator_server.yaml.dist +package main + +import ( + "errors" + "flag" + "io/fs" + logNative "log" + + "github.com/golang-migrate/migrate/v4" + "github.com/ilyakaznacheev/cleanenv" + "github.com/jmoiron/sqlx" + "go.uber.org/zap" + + "github.com/bjlag/go-keeper/internal/infrastructure/db/pg" + "github.com/bjlag/go-keeper/internal/infrastructure/db/sqlite" + "github.com/bjlag/go-keeper/internal/infrastructure/logger" + "github.com/bjlag/go-keeper/internal/infrastructure/migrator" +) + +func main() { + defer func() { + if r := recover(); r != nil { + logNative.Fatalf("panic occurred: %v", r) + } + }() + + var configPath string + + flag.StringVar(&configPath, "c", "", "Path to config file") + flag.Parse() + + var cfg migrator.Config + if err := cleanenv.ReadConfig(configPath, &cfg); err != nil { + panic(err) + } + + log := logger.Get(cfg.Env) + defer func() { + _ = log.Sync() + }() + + log.Debug("Config loaded", zap.Any("config", cfg)) + log = log.With(zap.Any("db_type", cfg.Database.Type)) + + var ( + err error + db *sqlx.DB + ) + + switch cfg.Database.Type { + case migrator.TypePG: + dbConf := cfg.Database + db, err = pg.New(pg.GetDSN(dbConf.Host, dbConf.Port, dbConf.Name, dbConf.User, dbConf.Password)).Connect() + case migrator.TypeSqlite: + db, err = sqlite.New("./client.db").Connect() + } + defer func() { + _ = db.Close() + }() + + if err != nil { + log.Error("Failed to open db", zap.Error(err)) + panic(err) + } + + m, err := migrator.Get(db, cfg.Database.Type, cfg.Database.Name, cfg.SourcePath, cfg.MigrationsTable) + if err != nil { + log.Error("Failed to initialize migrator", zap.Error(err)) + panic(err) + } + + log.Info("Applying migrations") + + if err = m.Up(); err != nil { + if errors.Is(err, migrate.ErrNoChange) { + log.Info("No changes") + return + } + var e *fs.PathError + if errors.As(err, &e) { + log.Info("No migration files") + return + } + log.Error("Migration failed", zap.Error(err)) + return + } + + log.Info("Migrations applied") +} diff --git a/cmd/server/main.go b/cmd/server/main.go index 7905807..46e27b7 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,5 +1,78 @@ +// Отвечает за запуск сервера. +// +// Конфигурация указывается через флаг -c, описывается в YAML файле: +// - пример ./config/server.yaml.dist package main +import ( + "context" + "flag" + "fmt" + logNative "log" + "net" + "os/signal" + "syscall" + + "github.com/ilyakaznacheev/cleanenv" + "go.uber.org/zap" + + "github.com/bjlag/go-keeper/internal/app/server" + "github.com/bjlag/go-keeper/internal/infrastructure/auth" + "github.com/bjlag/go-keeper/internal/infrastructure/db/pg" + "github.com/bjlag/go-keeper/internal/infrastructure/logger" +) + +const ( + // Конфигурация по умолчанию, если не передать флаг конфигурации при старте приложения. + configPathDefault = "./config/server.yaml" +) + func main() { + defer func() { + if r := recover(); r != nil { + logNative.Fatalf("panic occurred: %v", r) + } + }() + + var configPath string + + flag.StringVar(&configPath, "c", configPathDefault, "Path to config file") + flag.Parse() + + var cfg server.Config + if err := cleanenv.ReadConfig(configPath, &cfg); err != nil { + panic(err) + } + + log := logger.Get(cfg.Env) + defer func() { + _ = log.Sync() + }() + + log.Debug("Config loaded", zap.Any("config", cfg)) + + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT) + defer cancel() + + dbCfg := cfg.Database + db, err := pg.New(pg.GetDSN(dbCfg.Host, dbCfg.Port, dbCfg.Name, dbCfg.User, dbCfg.Password)).Connect() + if err != nil { + log.Error("Failed to get db connection", zap.Error(err)) + panic(err) + } + defer func() { + _ = db.Close() + }() + + jwt := auth.NewJWT(cfg.Auth.SecretKey, cfg.Auth.AccessTokenExp, cfg.Auth.RefreshTokenExp) + + listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", cfg.Address.Host, cfg.Address.Port)) + if err != nil { + log.Error("Failed to listen", zap.Error(err)) + panic(err) + } + if err = server.NewApp(db, jwt, listener, log).Run(ctx); err != nil { + panic(err) + } } diff --git a/config/client.yaml.dist b/config/client.yaml.dist new file mode 100644 index 0000000..12d5a2a --- /dev/null +++ b/config/client.yaml.dist @@ -0,0 +1,18 @@ +env: dev + +migration: + sourcePath: "./migrations/client" + table: "migrations" + +server: + host: localhost + port: 8080 + +masterKey: + saltLength: 16 + iterCount: 100000 + length: 32 + +database: + dir: data + prefix: client \ No newline at end of file diff --git a/config/migrator_client.yaml.dist b/config/migrator_client.yaml.dist new file mode 100644 index 0000000..4d94d49 --- /dev/null +++ b/config/migrator_client.yaml.dist @@ -0,0 +1,12 @@ +env: dev + +sourcePath: "./migrations/client" +migrationsTable: "migrations" + +database: + type: sqlite + host: localhost + port: 5444 + name: master + user: postgres + password: secret \ No newline at end of file diff --git a/config/migrator_server.yaml.dist b/config/migrator_server.yaml.dist new file mode 100644 index 0000000..228ebb1 --- /dev/null +++ b/config/migrator_server.yaml.dist @@ -0,0 +1,12 @@ +env: dev + +sourcePath: "./migrations/server" +migrationsTable: "migrations" + +database: + type: pg + host: localhost + port: 5444 + name: master + user: postgres + password: secret diff --git a/config/server.yaml.dist b/config/server.yaml.dist new file mode 100644 index 0000000..255972f --- /dev/null +++ b/config/server.yaml.dist @@ -0,0 +1,17 @@ +env: dev + +address: + host: localhost + port: 8080 + +auth: + accessTokenExp: 5m + refreshTokenExp: 720h # 30 days + secretKey: secret + +database: + host: localhost + port: 5444 + name: master + user: postgres + password: secret \ No newline at end of file diff --git a/config/server_test.yaml.dist b/config/server_test.yaml.dist new file mode 100644 index 0000000..2916cb0 --- /dev/null +++ b/config/server_test.yaml.dist @@ -0,0 +1,17 @@ +env: test + +migration: + sourcePath: "./migrations/server" + table: "migrations" + +auth: + accessTokenExp: 5m + refreshTokenExp: 2h + secretKey: secret + +container: + pg: + tag: 16.4-alpine3.20 + dbName: master_test + dbUser: postgres + dbPassword: secret \ No newline at end of file diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..f59ec20 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/docker/docker-compose.local.yaml b/docker/docker-compose.local.yaml new file mode 100644 index 0000000..0feeeda --- /dev/null +++ b/docker/docker-compose.local.yaml @@ -0,0 +1,19 @@ +version: "3.8" +services: + pg: + image: postgres:16.4-alpine3.20 + container_name: go-keeper-server-db + restart: always + environment: + - POSTGRES_DB=master + - POSTGRES_PASSWORD=secret + ports: + - "5444:5432" + volumes: + - pg:/var/lib/postgresql/data + healthcheck: + test: [ "CMD-SHELL", "pg_isready" ] + interval: 2s + +volumes: + pg: diff --git a/go.mod b/go.mod index c8a2e76..479be37 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,135 @@ module github.com/bjlag/go-keeper -go 1.23 +go 1.23.0 + +require ( + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.3.3 + github.com/charmbracelet/lipgloss v1.0.0 + github.com/golang-jwt/jwt/v4 v4.5.1 + github.com/golang-migrate/migrate/v4 v4.18.2 + github.com/google/uuid v1.6.0 + github.com/ilyakaznacheev/cleanenv v1.5.0 + github.com/jackc/pgx/v5 v5.7.2 + github.com/jmoiron/sqlx v1.4.0 + github.com/lib/pq v1.10.9 + github.com/mattn/go-sqlite3 v1.14.24 + github.com/stretchr/testify v1.10.0 + github.com/testcontainers/testcontainers-go v0.35.0 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.36.0 + golang.org/x/sync v0.12.0 + google.golang.org/grpc v1.71.0 + google.golang.org/protobuf v1.36.5 +) + +require ( + cel.dev/expr v0.22.0 // indirect + cloud.google.com/go v0.119.0 // indirect + cloud.google.com/go/auth v0.15.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/iam v1.4.2 // indirect + cloud.google.com/go/longrunning v0.6.6 // indirect + cloud.google.com/go/monitoring v1.24.1 // indirect + cloud.google.com/go/spanner v1.77.0 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.2.0+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-testfixtures/testfixtures/v3 v3.14.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/googleapis/go-sql-spanner v1.11.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/klauspost/compress v1.17.7 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/api v0.226.0 // indirect + google.golang.org/genproto v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..90f3f3c --- /dev/null +++ b/go.sum @@ -0,0 +1,1835 @@ +cel.dev/expr v0.22.0 h1:+hFFhLPmquBImfs1BiN2PZmkr5ASse2ZOuaxIs9e4R8= +cel.dev/expr v0.22.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +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 v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= +cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go v0.119.0 h1:tw7OjErMzJKbbjaEHkrt60KQrK5Wus/boCZ7tm5/RNE= +cloud.google.com/go v0.119.0/go.mod h1:fwB8QLzTcNevxqi8dcpR+hoMIs3jBherGS9VUBDAW08= +cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= +cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= +cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= +cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= +cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= +cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= +cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= +cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= +cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= +cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= +cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= +cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= +cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= +cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= +cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= +cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= +cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= +cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= +cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= +cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= +cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= +cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= +cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= +cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= +cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= +cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= +cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= +cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= +cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= +cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= +cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= +cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= +cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= +cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= +cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= +cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= +cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= +cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= +cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= +cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= +cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= +cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= +cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= +cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= +cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= +cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= +cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= +cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= +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/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= +cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= +cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= +cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= +cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= +cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= +cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= +cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= +cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= +cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= +cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= +cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= +cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= +cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= +cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= +cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= +cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= +cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= +cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= +cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= +cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= +cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= +cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= +cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= +cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= +cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= +cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= +cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= +cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= +cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= +cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= +cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= +cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= +cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= +cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= +cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= +cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= +cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= +cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= +cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= +cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= +cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= +cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= +cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= +cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= +cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= +cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= +cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= +cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= +cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= +cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= +cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= +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/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= +cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= +cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= +cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= +cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= +cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= +cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= +cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= +cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= +cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= +cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= +cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= +cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= +cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= +cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= +cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= +cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= +cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= +cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= +cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= +cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= +cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= +cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= +cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= +cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= +cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= +cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= +cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= +cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= +cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= +cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= +cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= +cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= +cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= +cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= +cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= +cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= +cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= +cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= +cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= +cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= +cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= +cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= +cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iam v1.4.2 h1:4AckGYAYsowXeHzsn/LCKWIwSWLkdb0eGjH8wWkd27Q= +cloud.google.com/go/iam v1.4.2/go.mod h1:REGlrt8vSlh4dfCJfSEcNjLGq75wW75c5aU3FLOYq34= +cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= +cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= +cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= +cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= +cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= +cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= +cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= +cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= +cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= +cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= +cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= +cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= +cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= +cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= +cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= +cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= +cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= +cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= +cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= +cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw= +cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw= +cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= +cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= +cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= +cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= +cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= +cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= +cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= +cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= +cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= +cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= +cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= +cloud.google.com/go/monitoring v1.24.1 h1:vKiypZVFD/5a3BbQMvI4gZdl8445ITzXFh257XBgrS0= +cloud.google.com/go/monitoring v1.24.1/go.mod h1:Z05d1/vn9NaujqY2voG6pVQXoJGbp+r3laV+LySt9K0= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= +cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= +cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= +cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= +cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= +cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= +cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= +cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= +cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= +cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= +cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= +cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= +cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= +cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= +cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= +cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= +cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= +cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= +cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= +cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= +cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= +cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= +cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= +cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= +cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= +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/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= +cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= +cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= +cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= +cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= +cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= +cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= +cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= +cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= +cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= +cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= +cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= +cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= +cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= +cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= +cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= +cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= +cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= +cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= +cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= +cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= +cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= +cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= +cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= +cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= +cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= +cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= +cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= +cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= +cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= +cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= +cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= +cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= +cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= +cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= +cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= +cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= +cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= +cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= +cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= +cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= +cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= +cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= +cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= +cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= +cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= +cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= +cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= +cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= +cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= +cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= +cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= +cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= +cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= +cloud.google.com/go/spanner v1.77.0 h1:SR2ceRiwFUysdjo09CLM4IYgoTZtCzH1jGmptfvqgVw= +cloud.google.com/go/spanner v1.77.0/go.mod h1:SyWlGAC2Ssp1wCTl/bT3nedt/8/I8sIqPzkmigOfvbI= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= +cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= +cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= +cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= +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= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= +cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= +cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= +cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= +cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= +cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= +cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= +cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= +cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= +cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= +cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= +cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= +cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= +cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= +cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= +cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= +cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= +cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= +cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= +cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= +cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= +cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= +cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= +cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= +cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= +cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= +cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= +cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= +cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= +cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= +cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= +cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= +cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= +cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= +cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= +cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= +cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= +cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= +cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= +cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= +cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= +cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= +cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= +cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 h1:DBjmt6/otSdULyJdVg2BlG0qGZO5tKL4VzOs0jpvw5Q= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= +github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= +github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.3.3 h1:WpU6fCY0J2vDWM3zfS3vIDi/ULq3SYphZhkAGGvmEUY= +github.com/charmbracelet/bubbletea v1.3.3/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +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/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +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/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8= +github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +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/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= +github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= +github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= +github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= +github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= +github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= +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-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/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-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +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/go-testfixtures/testfixtures/v3 v3.14.0 h1:aRt5qyH2XjzFgCC5NizNs6QrzjO7rC4pQZ1oJpPIdo8= +github.com/go-testfixtures/testfixtures/v3 v3.14.0/go.mod h1:HHb6Yd8spzm6aFZU6jwBj9qFvVUNNkx5nGbjG4UHeOE= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8= +github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +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/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +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/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +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.3/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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +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/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +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/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/go-sql-spanner v1.11.2 h1:G5mvQXSuuoISyElJuz6Ce1ax6GO/2yDoks+wA/PBKVM= +github.com/googleapis/go-sql-spanner v1.11.2/go.mod h1:PMXTYVjFrP4zX1HB+M6hBkiEF6CUKUZP4DeJG1SvWQ0= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +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/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +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/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= +github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +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/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= +github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo= +github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +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= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +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.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +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-20190820162420-60c769a6c586/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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/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-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +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/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +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/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +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/lint v0.0.0-20210508222113-6edffad5e616/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/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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-20201110031124-69a78807bb2b/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-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +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/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +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/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +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-20190916202348-b4ddaad3f8a3/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-20200116001909-b77594299b42/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-20201204225414-ed752295db88/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-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/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-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +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.5/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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +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.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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-20190206041539-40960b6deb8e/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-20190927191325-030b2cf1153e/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-20200619180055-7c47624df98f/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-20201124115921-2c860bdd6e78/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-20210106214847-113979e3529a/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/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +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= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= +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/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= +google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= +google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= +google.golang.org/api v0.226.0 h1:9A29y1XUD+YRXfnHkO66KggxHBZWg9LsTGqm7TkUvtQ= +google.golang.org/api v0.226.0/go.mod h1:WP/0Xm4LVvMOCldfvOISnWquSRWbG2kArDZcg+W2DbY= +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-20200513103714-09dca8ec2884/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-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= +google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= +google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= +google.golang.org/genproto v0.0.0-20250313205543-e70fdf4c4cb4 h1:kCjWYliqPA8g5z87mbjnf/cdgQqMzBfp9xYre5qKu2A= +google.golang.org/genproto v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:SqIx1NV9hcvqdLHo7uNZDS5lrUJybQ3evo3+z/WBfA0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY= +google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +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.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +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/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +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= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +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= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= +modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +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/internal/app/client/app.go b/internal/app/client/app.go new file mode 100644 index 0000000..5ff0a2b --- /dev/null +++ b/internal/app/client/app.go @@ -0,0 +1,193 @@ +// Package client настраивает и запускает клиент. +package client + +import ( + "context" + "crypto/md5" //nolint:gosec + "errors" + "fmt" + "path" + + tea "github.com/charmbracelet/bubbletea" + "github.com/golang-migrate/migrate/v4" + "github.com/jmoiron/sqlx" + "go.uber.org/zap" + + formCreate "github.com/bjlag/go-keeper/internal/cli/model/create" + "github.com/bjlag/go-keeper/internal/cli/model/item/bank_card" + "github.com/bjlag/go-keeper/internal/cli/model/item/file" + "github.com/bjlag/go-keeper/internal/cli/model/item/password" + syncItem "github.com/bjlag/go-keeper/internal/cli/model/item/sync" + "github.com/bjlag/go-keeper/internal/cli/model/item/text" + "github.com/bjlag/go-keeper/internal/cli/model/list" + formLogin "github.com/bjlag/go-keeper/internal/cli/model/login" + "github.com/bjlag/go-keeper/internal/cli/model/master" + formRegister "github.com/bjlag/go-keeper/internal/cli/model/register" + "github.com/bjlag/go-keeper/internal/fetcher/item" + crypt "github.com/bjlag/go-keeper/internal/infrastructure/crypt/cipher" + "github.com/bjlag/go-keeper/internal/infrastructure/crypt/master_key" + "github.com/bjlag/go-keeper/internal/infrastructure/db/sqlite" + "github.com/bjlag/go-keeper/internal/infrastructure/migrator" + rpc "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" + backups "github.com/bjlag/go-keeper/internal/infrastructure/store/client/backup" + sItem "github.com/bjlag/go-keeper/internal/infrastructure/store/client/item" + "github.com/bjlag/go-keeper/internal/infrastructure/store/client/option" + "github.com/bjlag/go-keeper/internal/infrastructure/store/client/token" + "github.com/bjlag/go-keeper/internal/usecase/client/backup" + "github.com/bjlag/go-keeper/internal/usecase/client/item/create" + "github.com/bjlag/go-keeper/internal/usecase/client/item/edit" + "github.com/bjlag/go-keeper/internal/usecase/client/item/remove" + itemSync "github.com/bjlag/go-keeper/internal/usecase/client/item/sync" + "github.com/bjlag/go-keeper/internal/usecase/client/login" + mkey "github.com/bjlag/go-keeper/internal/usecase/client/master_key" + "github.com/bjlag/go-keeper/internal/usecase/client/register" + "github.com/bjlag/go-keeper/internal/usecase/client/restore" + "github.com/bjlag/go-keeper/internal/usecase/client/sync" +) + +type App struct { + cfg Config + log *zap.Logger +} + +func NewApp(cfg Config, log *zap.Logger) *App { + return &App{ + cfg: cfg, + log: log, + } +} + +func (a *App) Run(ctx context.Context) error { + const op = "app.Run" + + tokens := token.NewStore() + + client, err := rpc.NewRPCClient(a.cfg.Server.Host, a.cfg.Server.Port, tokens, a.log) + if err != nil { + a.log.Error("Failed to create rpc client", zap.Error(err)) + return fmt.Errorf("%s:%w", op, err) + } + defer func() { + _ = client.Close() + }() + + email, pass, err := a.login(client, tokens) + if err != nil { + a.log.Error("Failed to login", zap.Error(err)) + return fmt.Errorf("%s:%w", op, err) + } + if email == "" { + return nil + } + + db, err := a.initDB(email) + if err != nil { + a.log.Error("Failed to init db", zap.Error(err)) + return fmt.Errorf("%s:%w", op, err) + } + defer func() { + _ = db.Close() + }() + + salter := master_key.NewSaltGenerator(a.cfg.MasterKey.SaltLength) + keymaker := master_key.NewKeyGenerator(a.cfg.MasterKey.IterCount, a.cfg.MasterKey.Length) + cipher := new(crypt.Cipher) + + storeItem := sItem.NewStore(db) + storeOption := option.NewStore(db) + storeBackup := backups.NewStore(db) + + ucMasterKey := mkey.NewUsecase(tokens, storeOption, salter, keymaker) + err = ucMasterKey.Do(ctx, mkey.Data{Password: pass}) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + ucSync := sync.NewUsecase(client, storeItem, tokens, cipher) + ucItemSync := itemSync.NewUsecase(client, storeItem, tokens, cipher) + ucCreateItem := create.NewUsecase(client, storeItem, tokens, cipher) + ucSaveItem := edit.NewUsecase(client, storeItem, tokens, cipher) + ucRemoveItem := remove.NewUsecase(client, storeItem) + ucBackup := backup.NewUsecase(storeItem, tokens, storeBackup, cipher) + ucRestore := restore.NewUsecase(storeItem, tokens, storeBackup, cipher) + + fetchItem := item.NewFetcher(storeItem) + + frmSync := syncItem.InitModel(ucItemSync) + frmPasswordItem := password.InitModel(ucCreateItem, ucSaveItem, ucRemoveItem, frmSync) + frmTextItem := text.InitModel(ucCreateItem, ucSaveItem, ucRemoveItem, frmSync) + frmBankCardItem := bank_card.InitModel(ucCreateItem, ucSaveItem, ucRemoveItem, frmSync) + frmFileItem := file.InitModel(ucCreateItem, ucSaveItem, ucRemoveItem, frmSync) + + err = ucRestore.Do(ctx) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + m := master.InitModel( + master.WithCreatForm(formCreate.InitModel(frmPasswordItem, frmTextItem, frmBankCardItem, frmFileItem)), + master.WithListForm(list.InitModel(ucSync, fetchItem, frmPasswordItem, frmTextItem, frmBankCardItem, frmFileItem)), + ) + defer func() { + err = ucBackup.Do(ctx) + if err != nil { + a.log.Error("Failed to backup", zap.Error(err)) + } + }() + + _, err = tea.NewProgram(m, tea.WithAltScreen(), tea.WithContext(ctx)).Run() + if err != nil { + a.log.Error("failed to run cli program", zap.Error(err)) + } + + return err +} + +func (a *App) login(client *rpc.RPCClient, tokens *token.Store) (email string, pass string, err error) { + ucLogin := login.NewUsecase(client, tokens) + ucRegister := register.NewUsecase(client, tokens) + + frmRegister := formRegister.InitModel(ucRegister) + frmLogin := formLogin.InitModel(ucLogin, frmRegister) + + mLogin, err := tea.NewProgram(frmLogin, tea.WithAltScreen()).Run() + if err != nil { + a.log.Error("Failed to run cli program for login", zap.Error(err)) + return + } + + ml, ok := mLogin.(*formLogin.Model) + if !ok { + panic("failed to run model cli program") + } + + email = ml.UserEmail() + pass = ml.UserPass() + + return +} + +func (a *App) initDB(email string) (db *sqlx.DB, err error) { + emailHash := md5.Sum([]byte(email)) //nolint:gosec + dbName := fmt.Sprintf("%s_%x.db", a.cfg.Database.Prefix, emailHash) + pathToDB := path.Join(a.cfg.Database.Dir, dbName) + + db, err = sqlite.New(pathToDB).Connect() + if err != nil { + return + } + + m, err := migrator.Get(db, migrator.TypeSqlite, "", a.cfg.Migration.SourcePath, a.cfg.Migration.Table) + if err != nil { + return + } + + if err = m.Up(); err != nil { + if !errors.Is(err, migrate.ErrNoChange) { + return + } + err = nil + } + + return +} diff --git a/internal/app/client/config.go b/internal/app/client/config.go new file mode 100644 index 0000000..9796daf --- /dev/null +++ b/internal/app/client/config.go @@ -0,0 +1,41 @@ +package client + +// Config хранит конфигурацию клиента. +type Config struct { + // Env окружение. + Env string `yaml:"env" env:"ENV" env-default:"dev" env-description:"Environment" json:"env"` + + // Migration настройки магратора. + Migration struct { + // SourcePath путь до файлов миграций. + SourcePath string `yaml:"sourcePath" env:"MIGRATION_SOURCE_PATH" env-description:"Path to migration source" json:"source_path"` + // Table название таблицы с примененными миграциями. + Table string `yaml:"table" env:"MIGRATION_TABLE" env-description:"Migration table" json:"table"` + } `yaml:"migration" json:"migration"` + + // Server содержит настройки подключения к серверу. + Server struct { + // Host хост сервера. + Host string `yaml:"host" env:"SERVER_HOST" env-description:"Server host" json:"host"` + // Port порт сервера. + Port int `yaml:"port" env:"SERVER_PORT" env-description:"Server port" json:"port"` + } `yaml:"server" json:"server"` + + // MasterKey настройки мастер ключа. + MasterKey struct { + // SaltLength длина соли. + SaltLength int `yaml:"saltLength" env:"SALT_LENGTH" env-description:"Salt length" json:"salt_length"` + // IterCount количество итерация при генерации мастер ключа. + IterCount int `yaml:"iterCount" env:"MASTER_KEY_ITER_COUNT" env-description:"Master key iteration" json:"iter_count"` + // Length длина мастер ключа. + Length int `yaml:"length" env:"MASTER_KEY_LENGTH" env-description:"Master key length" json:"length"` + } `yaml:"masterKey" json:"master_key"` + + // Database настройки подключения к базе данных клиента. + Database struct { + // Dir директория, где будут лежать файлы БД. + Dir string `yaml:"dir" env:"DB_DIR" env-description:"Database directory" json:"dir"` + // Prefix префикс в названии файла базы данных. + Prefix string `yaml:"prefix" env:"DB_PREFIX" env-description:"Database prefix" json:"prefix"` + } `yaml:"database" json:"database"` +} diff --git a/internal/app/server/app.go b/internal/app/server/app.go new file mode 100644 index 0000000..ea6387f --- /dev/null +++ b/internal/app/server/app.go @@ -0,0 +1,90 @@ +// Package server настраивает и запускает сервер. +package server + +import ( + "context" + "fmt" + "net" + + "github.com/jmoiron/sqlx" + "go.uber.org/zap" + + "github.com/bjlag/go-keeper/internal/infrastructure/auth" + "github.com/bjlag/go-keeper/internal/infrastructure/rpc/server" + "github.com/bjlag/go-keeper/internal/infrastructure/store/server/item" + "github.com/bjlag/go-keeper/internal/infrastructure/store/server/user" + rpcCreateItem "github.com/bjlag/go-keeper/internal/rpc/create_item" + rpcDeleteItem "github.com/bjlag/go-keeper/internal/rpc/delete_item" + rpcGetAllItems "github.com/bjlag/go-keeper/internal/rpc/get_all_items" + rpcGetByGUID "github.com/bjlag/go-keeper/internal/rpc/get_by_guid" + rpcLogin "github.com/bjlag/go-keeper/internal/rpc/login" + rpcRefreshTokens "github.com/bjlag/go-keeper/internal/rpc/refresh_tokens" + rpcRegister "github.com/bjlag/go-keeper/internal/rpc/register" + rpcUpdateItem "github.com/bjlag/go-keeper/internal/rpc/update_item" + "github.com/bjlag/go-keeper/internal/usecase/server/item/create" + "github.com/bjlag/go-keeper/internal/usecase/server/item/get_all" + "github.com/bjlag/go-keeper/internal/usecase/server/item/get_by_guid" + "github.com/bjlag/go-keeper/internal/usecase/server/item/remove" + "github.com/bjlag/go-keeper/internal/usecase/server/item/update" + "github.com/bjlag/go-keeper/internal/usecase/server/user/login" + rt "github.com/bjlag/go-keeper/internal/usecase/server/user/refresh_tokens" + "github.com/bjlag/go-keeper/internal/usecase/server/user/register" +) + +type App struct { + db *sqlx.DB + jwt *auth.JWT + listener net.Listener + log *zap.Logger +} + +func NewApp(db *sqlx.DB, jwt *auth.JWT, listener net.Listener, log *zap.Logger) *App { + return &App{ + db: db, + jwt: jwt, + listener: listener, + log: log, + } +} + +func (a *App) Run(ctx context.Context) error { + const op = "app.Run" + + userStore := user.NewStore(a.db) + dataStore := item.NewStore(a.db) + + ucRegister := register.NewUsecase(userStore, a.jwt) + ucLogin := login.NewUsecase(userStore, a.jwt) + ucRefreshTokens := rt.NewUsecase(userStore, a.jwt) + ucCreateItem := create.NewUsecase(dataStore) + ucUpdateItem := update.NewUsecase(dataStore) + ucRemoveItem := remove.NewUsecase(dataStore) + + // todo вынести в фетчер + ucGetByGUID := get_by_guid.NewUsecase(dataStore) + ucGetAllData := get_all.NewUsecase(dataStore) + + s := server.NewRPCServer( + server.WithListener(a.listener), + server.WithJWT(a.jwt), + server.WithLogger(a.log), + + server.WithHandler(server.RegisterMethod, rpcRegister.New(ucRegister).Handle), + server.WithHandler(server.LoginMethod, rpcLogin.New(ucLogin).Handle), + server.WithHandler(server.RefreshTokensMethod, rpcRefreshTokens.New(ucRefreshTokens).Handle), + server.WithHandler(server.GetByGUIDMethod, rpcGetByGUID.New(ucGetByGUID).Handle), + server.WithHandler(server.GetAllItemsMethod, rpcGetAllItems.New(ucGetAllData).Handle), + server.WithHandler(server.CreateItemMethod, rpcCreateItem.New(ucCreateItem).Handle), + server.WithHandler(server.UpdateItemMethod, rpcUpdateItem.New(ucUpdateItem).Handle), + server.WithHandler(server.DeleteItemMethod, rpcDeleteItem.New(ucRemoveItem).Handle), + ) + + if err := s.Start(ctx); err != nil { + a.log.Error("Failed to start gRPC server", zap.Error(err)) + return fmt.Errorf("%s: %w", op, err) + } + + a.log.Info("Server shutdown gracefully") + + return nil +} diff --git a/internal/app/server/config.go b/internal/app/server/config.go new file mode 100644 index 0000000..d2fceaf --- /dev/null +++ b/internal/app/server/config.go @@ -0,0 +1,41 @@ +package server + +import "time" + +// Config хранит конфигурацию сервера. +type Config struct { + // Env окружение. + Env string `yaml:"env" env:"ENV" env-default:"dev" env-description:"Environment" json:"env"` + + // Address содержит адрес сервера. + Address struct { + // Host хост сервера. + Host string `yaml:"host" env:"ADDRESS_HOST" env-description:"Server host" json:"host"` + // Port порт сервера. + Port int `yaml:"port" env:"ADDRESS_PORT" env-description:"Server port" json:"port"` + } `yaml:"address" json:"address"` + + // Auth настройки авторизации. + Auth struct { + // AccessTokenExp время жизни access токена. + AccessTokenExp time.Duration `yaml:"accessTokenExp" env:"ACCESS_TOKEN_EXP" env-description:"Access token expiration" json:"access_token_exp"` + // RefreshTokenExp время жизни refresh токена. + RefreshTokenExp time.Duration `yaml:"refreshTokenExp" env:"REFRESH_TOKEN_EXP" env-description:"Refresh token expiration" json:"refresh_token_exp"` + // SecretKey секретный ключ. + SecretKey string `yaml:"secretKey" env:"SECRET_KEY" env-description:"Secret key" json:"secret_key"` + } `yaml:"auth" json:"auth"` + + // Database настройки подключения к базе данных клиента. + Database struct { + // Host хост базы. + Host string `yaml:"host" env:"DB_HOST" env-description:"Database host" json:"host"` + // Port порт базы. + Port string `yaml:"port" env:"DB_PORT" env-description:"Database port" json:"port"` + // Name название базы данных. + Name string `yaml:"name" env:"DB_NAME" env-description:"Database name" json:"name"` + // User пользователь. + User string `yaml:"user" env:"DB_USER" env-description:"Database user" json:"user"` + // Password пароль. + Password string `yaml:"password" env:"DB_PASSWORD" env-description:"Database password" json:"password"` + } `yaml:"database" json:"database"` +} diff --git a/internal/cli/common/error.go b/internal/cli/common/error.go new file mode 100644 index 0000000..80ed7a1 --- /dev/null +++ b/internal/cli/common/error.go @@ -0,0 +1,42 @@ +package common + +import ( + "errors" + "strings" +) + +var ErrInvalidElement = errors.New("invalid element") + +type ValidateError struct { + errors []string +} + +func NewValidateError() *ValidateError { + return &ValidateError{} +} + +func (e *ValidateError) AddError(msg string) { + e.errors = append(e.errors, msg) +} + +func (e *ValidateError) HasErrors() bool { + return len(e.errors) > 0 +} + +func (e *ValidateError) Error() string { + return strings.Join(e.errors, "\n") +} + +type FormError struct { + text string +} + +func NewFormError(text string) *FormError { + return &FormError{ + text: text, + } +} + +func (e *FormError) Error() string { + return e.text +} diff --git a/internal/cli/common/key.go b/internal/cli/common/key.go new file mode 100644 index 0000000..410f1ea --- /dev/null +++ b/internal/cli/common/key.go @@ -0,0 +1,85 @@ +package common + +import "github.com/charmbracelet/bubbles/key" + +type Map struct { + New key.Binding + Edit key.Binding + Delete key.Binding + Up key.Binding + Down key.Binding + Right key.Binding + Left key.Binding + Enter key.Binding + Help key.Binding + Quit key.Binding + Back key.Binding + Tab key.Binding + Navigation key.Binding +} + +func (k Map) ShortHelp() []key.Binding { + return []key.Binding{k.Navigation, k.Enter, k.Quit} +} + +func (k Map) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.Navigation, k.Enter}, + {k.Help, k.Quit}, + } +} + +var Keys = Map{ + New: key.NewBinding( + key.WithKeys("n"), + key.WithHelp("n", "добавить"), + ), + Edit: key.NewBinding( + key.WithKeys("e"), + key.WithHelp("e", "редактировать"), + ), + Delete: key.NewBinding( + key.WithKeys("d"), + key.WithHelp("d", "удалить"), + ), + Navigation: key.NewBinding( + key.WithKeys("up", "down", "tab"), + key.WithHelp("↑/↓/tab", "navigation"), + ), + Up: key.NewBinding( + key.WithKeys("up"), + key.WithHelp("↑", "наверх"), + ), + Down: key.NewBinding( + key.WithKeys("down"), + key.WithHelp("↓", "вниз"), + ), + Tab: key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "табуляция"), + ), + Right: key.NewBinding( + key.WithKeys("right", "l"), + key.WithHelp("→/l", "направо"), + ), + Left: key.NewBinding( + key.WithKeys("left", "h"), + key.WithHelp("←/l", "налево"), + ), + Enter: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "выполнить"), + ), + Help: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "help"), + ), + Quit: key.NewBinding( + key.WithKeys("ctrl+c"), + key.WithHelp("ctrl+c", "выход"), + ), + Back: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "назад"), + ), +} diff --git a/internal/cli/element/button/button.go b/internal/cli/element/button/button.go new file mode 100644 index 0000000..595bb3a --- /dev/null +++ b/internal/cli/element/button/button.go @@ -0,0 +1,77 @@ +package button + +import ( + "fmt" + + "github.com/charmbracelet/lipgloss" + + "github.com/bjlag/go-keeper/internal/cli/style" +) + +// Button описывает кнопку в UI. +type Button struct { + // text содержит текст кнопки. + text string + // focus признак имеет ли кнопка фокус. + focus bool + // FocusedStyle стиль кнопки, когда она в состоянии фокуса + FocusedStyle lipgloss.Style + // BlurredStyle стиль кнопки, когда она не в фокусе. + BlurredStyle lipgloss.Style +} + +// NewButton создает экземпляр кнопки. +func NewButton(text string) Button { + return Button{ + text: text, + FocusedStyle: lipgloss.NewStyle(), + BlurredStyle: lipgloss.NewStyle(), + } +} + +// Option тип опции кнопки. +type Option func(m *Button) + +// WithFocused меняет состояние кнопки на фокус. +func WithFocused() Option { + return func(m *Button) { + m.Focus() + } +} + +// CreateDefaultButton создает экземпляр кнопки с заранее примененными стилями. +// Через аргумент opts можно передать дополнительные настройки. +func CreateDefaultButton(text string, opts ...Option) Button { + b := NewButton(text) + b.FocusedStyle = style.FocusedStyle + b.BlurredStyle = style.BlurredStyle + + for _, opt := range opts { + opt(&b) + } + + return b +} + +// String формирует строковое представление кнопки для вывода в UI. +func (b *Button) String() string { + if b.focus { + return fmt.Sprintf("[ %s ]", b.FocusedStyle.Render(b.text)) + } + return fmt.Sprintf("[ %s ]", b.BlurredStyle.Render(b.text)) +} + +// Focus меняет состояние на фокус. +func (b *Button) Focus() { + b.focus = true +} + +// Blur снимает фокус с кнопки. +func (b *Button) Blur() { + b.focus = false +} + +// Focused возвращает состояние фокуса кнопки. +func (b *Button) Focused() bool { + return b.focus +} diff --git a/internal/cli/element/list/list.go b/internal/cli/element/list/list.go new file mode 100644 index 0000000..c920717 --- /dev/null +++ b/internal/cli/element/list/list.go @@ -0,0 +1,106 @@ +package list + +import ( + "fmt" + "io" + "strings" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + + "github.com/bjlag/go-keeper/internal/cli/style" + "github.com/bjlag/go-keeper/internal/domain/client" +) + +// CreateDefaultList создает список [list.Model] с заранее определенными стилями и элементами списка. +func CreateDefaultList(title string, with, height int, itemDelegate list.ItemDelegate, items ...list.Item) list.Model { + l := list.New(items, itemDelegate, with, height) + + l.Title = title + l.SetShowStatusBar(false) + l.SetFilteringEnabled(false) + l.Styles.Title = style.ListTitleStyle + l.Styles.PaginationStyle = style.ListPaginationStyle + l.Styles.HelpStyle = style.ListHelpStyle + + return l +} + +// Category описывает элемент списка для категорий. +type Category struct { + Category client.Category + Title string +} + +func (i Category) FilterValue() string { return "" } + +type CategoryDelegate struct{} + +func (d CategoryDelegate) Height() int { + return 1 +} + +func (d CategoryDelegate) Spacing() int { + return 0 +} + +func (d CategoryDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { + return nil +} + +func (d CategoryDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(Category) + if !ok { + return + } + + str := fmt.Sprintf("%d. %s", index+1, i.Title) + + fn := style.ListItemStyle.Render + if index == m.Index() { + fn = func(s ...string) string { + return style.SelectedListItemStyle.Render("> " + strings.Join(s, " ")) + } + } + + _, _ = fmt.Fprint(w, fn(str)) +} + +// Item описывает элемент списка для конкретной записи: пароль, текст и пр. +type Item struct { + Model client.Item +} + +func (i Item) FilterValue() string { return "" } + +type ItemDelegate struct{} + +func (d ItemDelegate) Height() int { + return 1 +} + +func (d ItemDelegate) Spacing() int { + return 0 +} + +func (d ItemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { + return nil +} + +func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(Item) + if !ok { + return + } + + str := fmt.Sprintf("%d. %s", index+1, i.Model.Title) + + fn := style.ListItemStyle.Render + if index == m.Index() { + fn = func(s ...string) string { + return style.SelectedListItemStyle.Render("> " + strings.Join(s, " ")) + } + } + + _, _ = fmt.Fprint(w, fn(str)) +} diff --git a/internal/cli/element/textarea/textarea.go b/internal/cli/element/textarea/textarea.go new file mode 100644 index 0000000..d96aa9e --- /dev/null +++ b/internal/cli/element/textarea/textarea.go @@ -0,0 +1,34 @@ +package textarea + +import ( + "github.com/charmbracelet/bubbles/textarea" +) + +// Option описывает тип опции элемента. +type Option func(m *textarea.Model) + +// WithFocused настроит элемент в фокусе. +func WithFocused() Option { + return func(m *textarea.Model) { + m.Focus() + } +} + +// WithValue настроит элемент с определенным значением. +func WithValue(value string) Option { + return func(m *textarea.Model) { + m.SetValue(value) + } +} + +// CreateDefaultTextArea создает [textarea.Model] с заранее определенными настройками. +func CreateDefaultTextArea(placeholder string, opts ...Option) textarea.Model { + m := textarea.New() + m.Placeholder = placeholder + + for _, opt := range opts { + opt(&m) + } + + return m +} diff --git a/internal/cli/element/textinput/textinput.go b/internal/cli/element/textinput/textinput.go new file mode 100644 index 0000000..53b6dba --- /dev/null +++ b/internal/cli/element/textinput/textinput.go @@ -0,0 +1,40 @@ +package textinput + +import ( + "github.com/charmbracelet/bubbles/textinput" + + "github.com/bjlag/go-keeper/internal/cli/style" +) + +// Option описывает тип опции элемента. +type Option func(m *textinput.Model) + +// WithFocused настроит элемент в фокусе. +func WithFocused() Option { + return func(m *textinput.Model) { + m.Focus() + } +} + +// WithValue настроит элемент с определенным значением. +func WithValue(value string) Option { + return func(m *textinput.Model) { + m.SetValue(value) + } +} + +// CreateDefaultTextInput создает [textinput.Model] с заранее определенными настройками. +func CreateDefaultTextInput(placeholder string, opts ...Option) textinput.Model { + m := textinput.New() + + m.Cursor.Style = style.CursorStyle + m.PlaceholderStyle = style.BlurredStyle + m.CharLimit = 50 + m.Placeholder = placeholder + + for _, opt := range opts { + opt(&m) + } + + return m +} diff --git a/internal/cli/element/util.go b/internal/cli/element/util.go new file mode 100644 index 0000000..1bd993c --- /dev/null +++ b/internal/cli/element/util.go @@ -0,0 +1,19 @@ +package element + +import ( + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/textinput" +) + +// GetValue вспомогательная функция, которая достает из переданного elements элемент +// в позиции pos и возвращает значение модели. +func GetValue(elements []interface{}, pos int) string { + switch e := elements[pos].(type) { + case textinput.Model: + return e.Value() + case textarea.Model: + return e.Value() + } + + return "" +} diff --git a/internal/cli/message/message.go b/internal/cli/message/message.go new file mode 100644 index 0000000..860eccc --- /dev/null +++ b/internal/cli/message/message.go @@ -0,0 +1,52 @@ +package message + +import ( + tea "github.com/charmbracelet/bubbletea" + + "github.com/bjlag/go-keeper/internal/domain/client" +) + +type ( + // OpenLoginMsg сообщение указывает, что надо открыть модель для аутентификации. + OpenLoginMsg struct{} + + // OpenRegisterMsg сообщение указывает, что надо открыть модель для регистрации. + OpenRegisterMsg struct { + // LoginModel содержит ссылку на модель аутентификации, чтобы на нее можно было вернуться + // в случае отказа от регистрации. + LoginModel tea.Model + } + + // SuccessLoginMsg вспомогательное сообщение, что какое-то событие выполнилось успешно, например, аутентификация. + SuccessLoginMsg struct { + Email string + Password string + } + + // OpenCategoriesMsg сообщение используется для открытия списка категорий. + OpenCategoriesMsg struct{} + + // OpenItemsMsg сообщение используется для открытия списка элементов определенной категории. + OpenItemsMsg struct { + // Category содержит категорию элементов, которые надо вывести. + Category client.Category + } + + // BackMsg используется для возврата в предыдущую модель, например, из элемента в список элементов. + BackMsg struct { + // State состояние модели, в которое надо вернуться. + State int + // Item каким элементом надо заменить данные в модели, в которую возвращаемся. + Item *client.Item + } + + // OpenItemMsg содержит информацию, какой элемент надо открыть. + OpenItemMsg struct { + // BackModel модель, в которую надо вернуться, в случае отмены. + BackModel tea.Model + // BackState состояние, в которое надо вернуть модель, в случае отмены. + BackState int + // Item каким элементом надо заменить данные в модели, в которую возвращаемся. + Item *client.Item + } +) diff --git a/internal/cli/model/create/model.go b/internal/cli/model/create/model.go new file mode 100644 index 0000000..a0f602a --- /dev/null +++ b/internal/cli/model/create/model.go @@ -0,0 +1,125 @@ +// Package create описывает модель, которая выводит UI для создания элемента. +package create + +import ( + "errors" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + + "github.com/bjlag/go-keeper/internal/cli/common" + elist "github.com/bjlag/go-keeper/internal/cli/element/list" + "github.com/bjlag/go-keeper/internal/cli/message" + "github.com/bjlag/go-keeper/internal/cli/style" + "github.com/bjlag/go-keeper/internal/domain/client" +) + +var errUnsupportedCategory = errors.New("unsupported category") + +const ( + defaultWidth = 40 + listHeight = 14 +) + +type Model struct { + main tea.Model + help help.Model + header string + categories list.Model + err error + + formPassword tea.Model + formText tea.Model + formBankCard tea.Model + formFile tea.Model +} + +func InitModel( + formPassword tea.Model, + formText tea.Model, + formBankCard tea.Model, + formFile tea.Model, +) *Model { + return &Model{ + help: help.New(), + header: "Создание", + categories: elist.CreateDefaultList("Выберите категорию:", defaultWidth, listHeight, elist.CategoryDelegate{}, + elist.Category{Category: client.CategoryPassword, Title: client.CategoryPassword.String()}, + elist.Category{Category: client.CategoryText, Title: client.CategoryText.String()}, + elist.Category{Category: client.CategoryFile, Title: client.CategoryFile.String()}, + elist.Category{Category: client.CategoryBankCard, Title: client.CategoryBankCard.String()}, + ), + + formPassword: formPassword, + formText: formText, + formBankCard: formBankCard, + formFile: formFile, + } +} + +func (f *Model) SetMainModel(m tea.Model) { + f.main = m +} + +func (f *Model) Init() tea.Cmd { + return nil +} + +func (f *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case message.OpenItemMsg: + return f, nil + case tea.KeyMsg: + switch { + case key.Matches(msg, common.Keys.Enter): + if c, ok := f.categories.SelectedItem().(elist.Category); ok { + itemMsg := message.OpenItemMsg{ + BackModel: f, + } + + switch c.Category { + case client.CategoryPassword: + return f.formPassword.Update(itemMsg) + case client.CategoryText: + return f.formText.Update(itemMsg) + case client.CategoryFile: + return f.formFile.Update(itemMsg) + case client.CategoryBankCard: + return f.formBankCard.Update(itemMsg) + default: + f.err = errUnsupportedCategory + return f, nil + } + } + + return f, nil + case key.Matches(msg, common.Keys.Back): + return f.main.Update(message.BackMsg{}) + } + } + + var cmd tea.Cmd + f.categories, cmd = f.categories.Update(msg) + + return f, cmd +} + +func (f *Model) View() string { + var b strings.Builder + + b.WriteString(style.TitleStyle.Render(f.header)) + b.WriteRune('\n') + + b.WriteString(f.categories.View()) + + // выводим прочие ошибки + if f.err != nil { + b.WriteRune('\n') + b.WriteString(style.ErrorBlockStyle.Render(f.err.Error())) + } + + return b.String() +} diff --git a/internal/cli/model/item/bank_card/action.go b/internal/cli/model/item/bank_card/action.go new file mode 100644 index 0000000..7c6c7f5 --- /dev/null +++ b/internal/cli/model/item/bank_card/action.go @@ -0,0 +1,39 @@ +package bank_card + +import ( + "context" + + "github.com/bjlag/go-keeper/internal/cli/element" + "github.com/bjlag/go-keeper/internal/domain/client" +) + +func (m *Model) createAction() error { + item := client.NewBankCardItem( + element.GetValue(m.elements, posCreateTitle), + element.GetValue(m.elements, posCreateNumber), + element.GetValue(m.elements, posCreateCVV), + element.GetValue(m.elements, posCreateExpiry), + element.GetValue(m.elements, posCreateNotes), + ) + + return m.usecaseCreate.Do(context.TODO(), item) +} + +func (m *Model) editAction() error { + item := client.NewBankCardItem( + element.GetValue(m.elements, posEditTitle), + element.GetValue(m.elements, posEditNumber), + element.GetValue(m.elements, posEditCVV), + element.GetValue(m.elements, posEditExpiry), + element.GetValue(m.elements, posEditNotes), + ) + item.GUID = m.guid + item.CreatedAt = m.item.CreatedAt + item.UpdatedAt = m.item.UpdatedAt + + return m.usecaseEdit.Do(context.TODO(), item) +} + +func (m *Model) deleteAction() error { + return m.usecaseDelete.Do(context.TODO(), m.guid) +} diff --git a/internal/cli/model/item/bank_card/model.go b/internal/cli/model/item/bank_card/model.go new file mode 100644 index 0000000..425a5a3 --- /dev/null +++ b/internal/cli/model/item/bank_card/model.go @@ -0,0 +1,332 @@ +// Package bank_card описывает модель для работы UI с банковскими картами. +package bank_card + +import ( + "errors" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/google/uuid" + + "github.com/bjlag/go-keeper/internal/cli/common" + "github.com/bjlag/go-keeper/internal/cli/element/button" + tarea "github.com/bjlag/go-keeper/internal/cli/element/textarea" + tinput "github.com/bjlag/go-keeper/internal/cli/element/textinput" + "github.com/bjlag/go-keeper/internal/cli/message" + "github.com/bjlag/go-keeper/internal/cli/style" + "github.com/bjlag/go-keeper/internal/domain/client" + "github.com/bjlag/go-keeper/internal/usecase/client/item/create" + "github.com/bjlag/go-keeper/internal/usecase/client/item/edit" + "github.com/bjlag/go-keeper/internal/usecase/client/item/remove" +) + +const ( + posEditTitle int = iota + posEditNumber + posEditExpiry + posEditCVV + posEditNotes + posEditEditBtn + posEditDeleteBtn + posEditBackBtn +) + +const ( + posCreateTitle int = iota + posCreateNumber + posCreateExpiry + posCreateCVV + posCreateNotes + posCreateSaveBtn + posCreateBackBtn +) + +type state int + +const ( + stateCreate state = iota + stateEdit +) + +var ( + errUnsupportedCommand = errors.New("unsupported command") + errInvalidValuePassword = errors.New("invalid value password") +) + +type Model struct { + help help.Model + header string + state state + elements []interface{} + pos int + err error + + backModel tea.Model + backState int + + guid uuid.UUID + item *client.Item + category client.Category + + formSync tea.Model + + usecaseCreate *create.Usecase + usecaseEdit *edit.Usecase + usecaseDelete *remove.Usecase +} + +func InitModel(usecaseCreate *create.Usecase, usecaseSave *edit.Usecase, usecaseDelete *remove.Usecase, formSync tea.Model) *Model { + return &Model{ + help: help.New(), + header: "Банковская карта", + state: stateCreate, + + formSync: formSync, + + usecaseCreate: usecaseCreate, + usecaseEdit: usecaseSave, + usecaseDelete: usecaseDelete, + } +} + +func (m *Model) Init() tea.Cmd { + return nil +} + +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + for i := range m.elements { + if e, ok := m.elements[i].(textinput.Model); ok { + e.Width = msg.Width + } + } + return m, nil + case message.BackMsg: + if msg.Item != nil { + m.item = msg.Item + } + return m, nil + case message.OpenItemMsg: + m.backState = msg.BackState + m.backModel = msg.BackModel + + if msg.Item != nil { + m.state = stateEdit + m.header = msg.Item.Title + m.item = msg.Item + m.guid = msg.Item.GUID + m.category = msg.Item.Category + + value, ok := msg.Item.Value.(*client.BankCard) + if !ok { + m.err = errInvalidValuePassword + return m, nil + } + + m.elements = []interface{}{ + posEditTitle: tinput.CreateDefaultTextInput("Название", tinput.WithValue(msg.Item.Title), tinput.WithFocused()), + posEditNumber: tinput.CreateDefaultTextInput("Номер", tinput.WithValue(value.Number)), + posEditExpiry: tinput.CreateDefaultTextInput("Истекает", tinput.WithValue(value.Expiry)), + posEditCVV: tinput.CreateDefaultTextInput("CVV", tinput.WithValue(value.CVV)), + posEditNotes: tarea.CreateDefaultTextArea("Заметки", tarea.WithValue(msg.Item.Notes)), + posEditEditBtn: button.CreateDefaultButton("Изменить"), + posEditDeleteBtn: button.CreateDefaultButton("Удалить"), + posEditBackBtn: button.CreateDefaultButton("Назад"), + } + + return m, nil + } + + m.state = stateCreate + m.header = "Новая банковская карта" + m.elements = []interface{}{ + posCreateTitle: tinput.CreateDefaultTextInput("Название", tinput.WithFocused()), + posCreateNumber: tinput.CreateDefaultTextInput("Номер"), + posCreateExpiry: tinput.CreateDefaultTextInput("Истекает"), + posCreateCVV: tinput.CreateDefaultTextInput("CVV"), + posCreateNotes: tarea.CreateDefaultTextArea("Заметки"), + posCreateSaveBtn: button.CreateDefaultButton("Сохранить"), + posCreateBackBtn: button.CreateDefaultButton("Назад"), + } + + return m, nil + case tea.KeyMsg: + switch { + case key.Matches(msg, common.Keys.Quit): + return m, tea.Quit + case key.Matches(msg, common.Keys.Navigation): + if key.Matches(msg, common.Keys.Down, common.Keys.Tab) { + m.pos++ + } else { + m.pos-- + } + + if m.pos > len(m.elements)-1 { + m.pos = 0 + } else if m.pos < 0 { + m.pos = len(m.elements) - 1 + } + + for i := range m.elements { + switch e := m.elements[i].(type) { + case textinput.Model: + if i == m.pos { + e.Focus() + m.elements[i] = style.SetFocusStyle(e) + continue + } + + e.Blur() + m.elements[i] = style.SetNoStyle(e) + case textarea.Model: + if i == m.pos { + e.Focus() + m.elements[i] = e + continue + } + + e.Blur() + m.elements[i] = e + case button.Button: + if i == m.pos { + e.Focus() + m.elements[i] = e + continue + } + e.Blur() + m.elements[i] = e + } + } + + return m, nil + case key.Matches(msg, common.Keys.Enter): + m.err = nil + + if m.state == stateCreate { + switch m.pos { + case posCreateSaveBtn: + m.err = m.createAction() + return m, nil + case posCreateBackBtn: + return m.backModel.Update(message.BackMsg{ + State: m.backState, + }) + default: + m.err = errUnsupportedCommand + } + + return m, nil + } + + switch m.pos { + case posEditEditBtn: + err := m.editAction() + if err != nil && errors.Is(err, edit.ErrConflict) { + return m.formSync.Update(message.OpenItemMsg{ + BackModel: m, + Item: m.item, + }) + } + + m.err = err + return m, nil + case posEditDeleteBtn: + m.err = m.deleteAction() + return m, nil + case posEditBackBtn: + return m.backModel.Update(message.BackMsg{ + State: m.backState, + }) + default: + m.err = errUnsupportedCommand + } + + return m, nil + case key.Matches(msg, common.Keys.Back): + return m.backModel.Update(message.BackMsg{ + State: m.backState, + }) + } + } + + return m, m.updateInputs(msg) +} + +func (m *Model) View() string { + var b strings.Builder + + b.WriteString(style.TitleStyle.Render(m.header)) + b.WriteRune('\n') + + b.WriteString("Категория: ") + b.WriteString(m.category.String()) + b.WriteRune('\n') + + for i := range m.elements { + switch e := m.elements[i].(type) { + case textinput.Model: + b.WriteString(e.Placeholder) + b.WriteRune('\n') + b.WriteString(e.View()) + b.WriteRune('\n') + b.WriteRune('\n') + case textarea.Model: + b.WriteString(e.Placeholder) + b.WriteRune('\n') + b.WriteString(e.View()) + b.WriteRune('\n') + b.WriteRune('\n') + } + } + + b.WriteRune('\n') + + for i := range m.elements { + if e, ok := m.elements[i].(button.Button); ok { + b.WriteString(e.String()) + b.WriteRune('\n') + } + } + + var ( + errValidate *common.ValidateError + errForm *common.FormError + ) + + // выводим ошибки валидации + if m.err != nil && (errors.As(m.err, &errValidate) || errors.As(m.err, &errForm)) { + b.WriteString(style.ErrorBlockStyle.Render(m.err.Error())) + b.WriteRune('\n') + } + + b.WriteRune('\n') + b.WriteString(m.help.View(common.Keys)) + + // выводим прочие ошибки + if m.err != nil && !(errors.As(m.err, &errValidate) || errors.As(m.err, &errForm)) { + b.WriteRune('\n') + b.WriteString(style.ErrorBlockStyle.Render(m.err.Error())) + } + + return b.String() +} + +func (m *Model) updateInputs(msg tea.Msg) tea.Cmd { + cmds := make([]tea.Cmd, len(m.elements)) + + for i := range m.elements { + switch e := m.elements[i].(type) { + case textinput.Model: + m.elements[i], cmds[i] = e.Update(msg) + case textarea.Model: + m.elements[i], cmds[i] = e.Update(msg) + } + } + + return tea.Batch(cmds...) +} diff --git a/internal/cli/model/item/file/action.go b/internal/cli/model/item/file/action.go new file mode 100644 index 0000000..9c15581 --- /dev/null +++ b/internal/cli/model/item/file/action.go @@ -0,0 +1,57 @@ +package file + +import ( + "context" + "errors" + "os" + "path/filepath" + + "github.com/bjlag/go-keeper/internal/cli/element" + "github.com/bjlag/go-keeper/internal/domain/client" +) + +var ( + errNoFileSelected = errors.New("no file selected") + errFileNotExist = errors.New("file does not exist") +) + +func (m *Model) createAction() error { + if m.selectedFile == "" { + return errNoFileSelected + } + + data, err := os.ReadFile(m.selectedFile) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return errFileNotExist + } + return err + } + + item := client.NewFileItem( + element.GetValue(m.elements, posCreateTitle), + filepath.Base(m.selectedFile), + data, + element.GetValue(m.elements, posCreateNotes), + ) + + return m.usecaseCreate.Do(context.TODO(), item) +} + +func (m *Model) editAction() error { + item := client.NewFileItem( + element.GetValue(m.elements, posEditTitle), + m.selectedFile, + m.fileData, + element.GetValue(m.elements, posEditNotes), + ) + item.GUID = m.guid + item.CreatedAt = m.item.CreatedAt + item.UpdatedAt = m.item.UpdatedAt + + return m.usecaseEdit.Do(context.TODO(), item) +} + +func (m *Model) deleteAction() error { + return m.usecaseDelete.Do(context.TODO(), m.guid) +} diff --git a/internal/cli/model/item/file/model.go b/internal/cli/model/item/file/model.go new file mode 100644 index 0000000..8899da4 --- /dev/null +++ b/internal/cli/model/item/file/model.go @@ -0,0 +1,411 @@ +// Package file описывает модель для работы UI с файлами (бинарными данными). +package file + +import ( + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/charmbracelet/bubbles/filepicker" + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/google/uuid" + + "github.com/bjlag/go-keeper/internal/cli/common" + "github.com/bjlag/go-keeper/internal/cli/element/button" + tarea "github.com/bjlag/go-keeper/internal/cli/element/textarea" + tinput "github.com/bjlag/go-keeper/internal/cli/element/textinput" + "github.com/bjlag/go-keeper/internal/cli/message" + "github.com/bjlag/go-keeper/internal/cli/style" + "github.com/bjlag/go-keeper/internal/domain/client" + "github.com/bjlag/go-keeper/internal/usecase/client/item/create" + "github.com/bjlag/go-keeper/internal/usecase/client/item/edit" + "github.com/bjlag/go-keeper/internal/usecase/client/item/remove" +) + +var errFileNotSupported = errors.New("не поддерживается") + +const ( + posEditTitle int = iota + posEditNotes + posEditEditBtn + posEditDeleteBtn + posEditBackBtn +) + +const ( + posCreateTitle int = iota + posCreateNotes + posCreateSelectFileBtn + posCreateSaveBtn + posCreateBackBtn +) + +type state int + +const ( + stateCreate state = iota + stateEdit +) + +var ( + errUnsupportedCommand = errors.New("unsupported command") + errInvalidValue = errors.New("invalid value") +) + +type clearErrorMsg struct{} + +func clearErrorAfter(t time.Duration) tea.Cmd { + return tea.Tick(t, func(_ time.Time) tea.Msg { + return clearErrorMsg{} + }) +} + +type Model struct { + help help.Model + header string + state state + elements []interface{} + pos int + err error + + filepicker filepicker.Model + selectedFile string + selectFileMode bool + fileData []byte + + backModel tea.Model + backState int + item *client.Item + + guid uuid.UUID + category client.Category + + formSync tea.Model + + usecaseCreate *create.Usecase + usecaseEdit *edit.Usecase + usecaseDelete *remove.Usecase +} + +func InitModel(usecaseCreate *create.Usecase, usecaseSave *edit.Usecase, usecaseDelete *remove.Usecase, formSync tea.Model) *Model { + fp := filepicker.New() + fp.AllowedTypes = []string{".txt", ".md", ".jpg", ".jpeg", ".png"} + fp.CurrentDirectory, _ = os.UserHomeDir() + fp.Height = 30 + + return &Model{ + help: help.New(), + header: "Файл", + state: stateCreate, + filepicker: fp, + + formSync: formSync, + + usecaseCreate: usecaseCreate, + usecaseEdit: usecaseSave, + usecaseDelete: usecaseDelete, + } +} + +func (m *Model) Init() tea.Cmd { + return nil +} + +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case clearErrorMsg: + m.err = nil + case tea.WindowSizeMsg: + for i := range m.elements { + if e, ok := m.elements[i].(textinput.Model); ok { + e.Width = msg.Width + } + } + return m, nil + case message.BackMsg: + if msg.Item != nil { + m.item = msg.Item + } + return m, nil + case message.OpenItemMsg: + m.backState = msg.BackState + m.backModel = msg.BackModel + + if msg.Item != nil { + m.state = stateEdit + m.header = msg.Item.Title + m.guid = msg.Item.GUID + m.category = msg.Item.Category + m.item = msg.Item + + value, ok := msg.Item.Value.(*client.File) + if !ok { + m.err = errInvalidValue + return m, nil + } + + m.selectedFile = value.Name + m.fileData = value.Data + + m.elements = []interface{}{ + posEditTitle: tinput.CreateDefaultTextInput("Название", tinput.WithValue(msg.Item.Title), tinput.WithFocused()), + posEditNotes: tarea.CreateDefaultTextArea("Заметки", tarea.WithValue(msg.Item.Notes)), + posEditEditBtn: button.CreateDefaultButton("Изменить"), + posEditDeleteBtn: button.CreateDefaultButton("Удалить"), + posEditBackBtn: button.CreateDefaultButton("Назад"), + } + + return m, nil + } + + m.state = stateCreate + m.header = "Новый файл" + m.elements = []interface{}{ + posCreateTitle: tinput.CreateDefaultTextInput("Название", tinput.WithFocused()), + posCreateNotes: tarea.CreateDefaultTextArea("Заметки"), + posCreateSelectFileBtn: button.CreateDefaultButton("Выбрать файл"), + posCreateSaveBtn: button.CreateDefaultButton("Сохранить"), + posCreateBackBtn: button.CreateDefaultButton("Назад"), + } + + return m, nil + case tea.KeyMsg: + switch { + case key.Matches(msg, common.Keys.Quit): + return m, tea.Quit + case key.Matches(msg, common.Keys.Navigation): + if !m.selectFileMode { + m.navigate(msg) + return m, nil + } + case key.Matches(msg, common.Keys.Enter): + m.err = nil + + if m.selectFileMode { + var cmd tea.Cmd + m.filepicker, cmd = m.filepicker.Update(msg) + + if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect { + m.selectedFile = path + m.selectFileMode = false + } + + if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect { + m.err = fmt.Errorf("%s %w", path, errFileNotSupported) + m.selectedFile = "" + return m, tea.Batch(cmd, clearErrorAfter(2*time.Second)) + } + + return m, cmd + } + + if m.state == stateCreate { + switch m.pos { + case posCreateSelectFileBtn: + m.selectFileMode = true + return m, m.filepicker.Init() + case posCreateSaveBtn: + m.err = m.createAction() + return m, nil + case posCreateBackBtn: + return m.backModel.Update(message.BackMsg{ + State: m.backState, + }) + default: + m.err = errUnsupportedCommand + } + + return m, nil + } + + switch m.pos { + case posEditEditBtn: + err := m.editAction() + if err != nil && errors.Is(err, edit.ErrConflict) { + return m.formSync.Update(message.OpenItemMsg{ + BackModel: m, + Item: m.item, + }) + } + + m.err = err + return m, nil + case posEditDeleteBtn: + m.err = m.deleteAction() + return m, nil + case posEditBackBtn: + return m.backModel.Update(message.BackMsg{ + State: m.backState, + }) + default: + m.err = errUnsupportedCommand + } + + return m, nil + case key.Matches(msg, common.Keys.Back): + if m.selectFileMode { + m.selectFileMode = false + return m.Update(message.OpenItemMsg{ + BackModel: m.backModel, + BackState: m.backState, + Item: m.item, + }) + } + + return m.backModel.Update(message.BackMsg{ + State: m.backState, + }) + } + } + + if m.selectFileMode { + var cmd tea.Cmd + m.filepicker, cmd = m.filepicker.Update(msg) + return m, cmd + } + + return m, m.updateInputs(msg) +} + +func (m *Model) View() string { + var b strings.Builder + + b.WriteString(style.TitleStyle.Render(m.header)) + b.WriteRune('\n') + + if m.selectFileMode { + switch { + case m.err != nil: + b.WriteString(m.err.Error()) + case m.selectedFile == "": + b.WriteString("Выберите файл:") + default: + b.WriteString("Выбранный файл: " + m.selectedFile) + } + + b.WriteString("\n\n" + m.filepicker.View() + "\n") + return b.String() + } + + b.WriteString("Категория: ") + b.WriteString(m.category.String()) + b.WriteRune('\n') + + for i := range m.elements { + switch e := m.elements[i].(type) { + case textinput.Model: + b.WriteString(e.Placeholder) + b.WriteRune('\n') + b.WriteString(e.View()) + b.WriteRune('\n') + b.WriteRune('\n') + case textarea.Model: + b.WriteString(e.Placeholder) + b.WriteRune('\n') + b.WriteString(e.View()) + b.WriteRune('\n') + b.WriteRune('\n') + } + } + + if m.selectedFile != "" { + b.WriteString("Файл: " + m.selectedFile) + b.WriteRune('\n') + b.WriteRune('\n') + } + + for i := range m.elements { + if e, ok := m.elements[i].(button.Button); ok { + b.WriteString(e.String()) + b.WriteRune('\n') + } + } + + var ( + errValidate *common.ValidateError + errForm *common.FormError + ) + + // выводим ошибки валидации + if m.err != nil && (errors.As(m.err, &errValidate) || errors.As(m.err, &errForm)) { + b.WriteString(style.ErrorBlockStyle.Render(m.err.Error())) + b.WriteRune('\n') + } + + b.WriteRune('\n') + b.WriteString(m.help.View(common.Keys)) + + // выводим прочие ошибки + if m.err != nil && !(errors.As(m.err, &errValidate) || errors.As(m.err, &errForm)) { + b.WriteRune('\n') + b.WriteString(style.ErrorBlockStyle.Render(m.err.Error())) + } + + return b.String() +} + +func (m *Model) updateInputs(msg tea.Msg) tea.Cmd { + cmds := make([]tea.Cmd, len(m.elements)) + + for i := range m.elements { + switch e := m.elements[i].(type) { + case textinput.Model: + m.elements[i], cmds[i] = e.Update(msg) + case textarea.Model: + m.elements[i], cmds[i] = e.Update(msg) + } + } + + return tea.Batch(cmds...) +} + +func (m *Model) navigate(msg tea.KeyMsg) { + if key.Matches(msg, common.Keys.Down, common.Keys.Tab) { + m.pos++ + } else { + m.pos-- + } + + if m.pos > len(m.elements)-1 { + m.pos = 0 + } else if m.pos < 0 { + m.pos = len(m.elements) - 1 + } + + for i := range m.elements { + switch e := m.elements[i].(type) { + case textinput.Model: + if i == m.pos { + e.Focus() + m.elements[i] = style.SetFocusStyle(e) + continue + } + + e.Blur() + m.elements[i] = style.SetNoStyle(e) + case textarea.Model: + if i == m.pos { + e.Focus() + m.elements[i] = e + continue + } + + e.Blur() + m.elements[i] = e + case button.Button: + if i == m.pos { + e.Focus() + m.elements[i] = e + continue + } + e.Blur() + m.elements[i] = e + } + } +} diff --git a/internal/cli/model/item/password/action.go b/internal/cli/model/item/password/action.go new file mode 100644 index 0000000..70a5ca2 --- /dev/null +++ b/internal/cli/model/item/password/action.go @@ -0,0 +1,37 @@ +package password + +import ( + "context" + + "github.com/bjlag/go-keeper/internal/cli/element" + "github.com/bjlag/go-keeper/internal/domain/client" +) + +func (m *Model) createAction() error { + item := client.NewPasswordItem( + element.GetValue(m.elements, posCreateTitle), + element.GetValue(m.elements, posCreateLogin), + element.GetValue(m.elements, posCreatePassword), + element.GetValue(m.elements, posCreateNotes), + ) + + return m.usecaseCreate.Do(context.TODO(), item) +} + +func (m *Model) editAction() error { + item := client.NewPasswordItem( + element.GetValue(m.elements, posEditTitle), + element.GetValue(m.elements, posEditLogin), + element.GetValue(m.elements, posEditPassword), + element.GetValue(m.elements, posEditNotes), + ) + item.GUID = m.guid + item.CreatedAt = m.item.CreatedAt + item.UpdatedAt = m.item.UpdatedAt + + return m.usecaseEdit.Do(context.TODO(), item) +} + +func (m *Model) deleteAction() error { + return m.usecaseDelete.Do(context.TODO(), m.guid) +} diff --git a/internal/cli/model/item/password/model.go b/internal/cli/model/item/password/model.go new file mode 100644 index 0000000..43779c7 --- /dev/null +++ b/internal/cli/model/item/password/model.go @@ -0,0 +1,328 @@ +// Package password описывает модель для работы UI с паролями. +package password + +import ( + "errors" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/google/uuid" + + "github.com/bjlag/go-keeper/internal/cli/common" + "github.com/bjlag/go-keeper/internal/cli/element/button" + tarea "github.com/bjlag/go-keeper/internal/cli/element/textarea" + tinput "github.com/bjlag/go-keeper/internal/cli/element/textinput" + "github.com/bjlag/go-keeper/internal/cli/message" + "github.com/bjlag/go-keeper/internal/cli/style" + "github.com/bjlag/go-keeper/internal/domain/client" + "github.com/bjlag/go-keeper/internal/usecase/client/item/create" + "github.com/bjlag/go-keeper/internal/usecase/client/item/edit" + "github.com/bjlag/go-keeper/internal/usecase/client/item/remove" +) + +const ( + posEditTitle int = iota + posEditLogin + posEditPassword + posEditNotes + posEditEditBtn + posEditDeleteBtn + posEditBackBtn +) + +const ( + posCreateTitle int = iota + posCreateLogin + posCreatePassword + posCreateNotes + posCreateSaveBtn + posCreateBackBtn +) + +type state int + +const ( + stateCreate state = iota + stateEdit +) + +var ( + errUnsupportedCommand = errors.New("unsupported command") + errInvalidValuePassword = errors.New("invalid value password") +) + +type Model struct { + help help.Model + header string + state state + elements []interface{} + pos int + err error + + backModel tea.Model + backState int + + guid uuid.UUID + item *client.Item + category client.Category + + formSync tea.Model + + usecaseCreate *create.Usecase + usecaseEdit *edit.Usecase + usecaseDelete *remove.Usecase +} + +func InitModel(usecaseCreate *create.Usecase, usecaseSave *edit.Usecase, usecaseDelete *remove.Usecase, formSync tea.Model) *Model { + return &Model{ + help: help.New(), + header: "Пароль", + state: stateCreate, + + formSync: formSync, + + usecaseCreate: usecaseCreate, + usecaseEdit: usecaseSave, + usecaseDelete: usecaseDelete, + } +} + +func (m *Model) Init() tea.Cmd { + return nil +} + +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + for i := range m.elements { + if e, ok := m.elements[i].(textinput.Model); ok { + e.Width = msg.Width + } + } + return m, nil + case message.BackMsg: + if msg.Item != nil { + m.item = msg.Item + } + return m, nil + case message.OpenItemMsg: + m.backState = msg.BackState + m.backModel = msg.BackModel + + if msg.Item != nil { + m.state = stateEdit + m.header = msg.Item.Title + m.item = msg.Item + m.guid = msg.Item.GUID + m.category = msg.Item.Category + + value, ok := msg.Item.Value.(*client.Password) + if !ok { + m.err = errInvalidValuePassword + return m, nil + } + + m.elements = []interface{}{ + posEditTitle: tinput.CreateDefaultTextInput("Название", tinput.WithValue(msg.Item.Title), tinput.WithFocused()), + posEditLogin: tinput.CreateDefaultTextInput("Логин", tinput.WithValue(value.Login)), + posEditPassword: tinput.CreateDefaultTextInput("Пароль", tinput.WithValue(value.Password)), + posEditNotes: tarea.CreateDefaultTextArea("Заметки", tarea.WithValue(msg.Item.Notes)), + posEditEditBtn: button.CreateDefaultButton("Изменить"), + posEditDeleteBtn: button.CreateDefaultButton("Удалить"), + posEditBackBtn: button.CreateDefaultButton("Назад"), + } + + return m, nil + } + + m.state = stateCreate + m.header = "Новый пароль" + m.elements = []interface{}{ + posCreateTitle: tinput.CreateDefaultTextInput("Название", tinput.WithFocused()), + posCreateLogin: tinput.CreateDefaultTextInput("Логин"), + posCreatePassword: tinput.CreateDefaultTextInput("Пароль"), + posCreateNotes: tarea.CreateDefaultTextArea("Заметки"), + posCreateSaveBtn: button.CreateDefaultButton("Сохранить"), + posCreateBackBtn: button.CreateDefaultButton("Назад"), + } + + return m, nil + case tea.KeyMsg: + switch { + case key.Matches(msg, common.Keys.Quit): + return m, tea.Quit + case key.Matches(msg, common.Keys.Navigation): + if key.Matches(msg, common.Keys.Down, common.Keys.Tab) { + m.pos++ + } else { + m.pos-- + } + + if m.pos > len(m.elements)-1 { + m.pos = 0 + } else if m.pos < 0 { + m.pos = len(m.elements) - 1 + } + + for i := range m.elements { + switch e := m.elements[i].(type) { + case textinput.Model: + if i == m.pos { + e.Focus() + m.elements[i] = style.SetFocusStyle(e) + continue + } + + e.Blur() + m.elements[i] = style.SetNoStyle(e) + case textarea.Model: + if i == m.pos { + e.Focus() + m.elements[i] = e + continue + } + + e.Blur() + m.elements[i] = e + case button.Button: + if i == m.pos { + e.Focus() + m.elements[i] = e + continue + } + e.Blur() + m.elements[i] = e + } + } + + return m, nil + case key.Matches(msg, common.Keys.Enter): + m.err = nil + + if m.state == stateCreate { + switch m.pos { + case posCreateSaveBtn: + m.err = m.createAction() + return m, nil + case posCreateBackBtn: + return m.backModel.Update(message.BackMsg{ + State: m.backState, + }) + default: + m.err = errUnsupportedCommand + } + + return m, nil + } + + switch m.pos { + case posEditEditBtn: + err := m.editAction() + if err != nil && errors.Is(err, edit.ErrConflict) { + return m.formSync.Update(message.OpenItemMsg{ + BackModel: m, + Item: m.item, + }) + } + + m.err = err + return m, nil + case posEditDeleteBtn: + m.err = m.deleteAction() + return m, nil + case posEditBackBtn: + return m.backModel.Update(message.BackMsg{ + State: m.backState, + }) + default: + m.err = errUnsupportedCommand + } + + return m, nil + case key.Matches(msg, common.Keys.Back): + return m.backModel.Update(message.BackMsg{ + State: m.backState, + }) + } + } + + return m, m.updateInputs(msg) +} + +func (m *Model) View() string { + var b strings.Builder + + b.WriteString(style.TitleStyle.Render(m.header)) + b.WriteRune('\n') + + b.WriteString("Категория: ") + b.WriteString(m.category.String()) + b.WriteRune('\n') + + for i := range m.elements { + switch e := m.elements[i].(type) { + case textinput.Model: + b.WriteString(e.Placeholder) + b.WriteRune('\n') + b.WriteString(e.View()) + b.WriteRune('\n') + b.WriteRune('\n') + case textarea.Model: + b.WriteString(e.Placeholder) + b.WriteRune('\n') + b.WriteString(e.View()) + b.WriteRune('\n') + b.WriteRune('\n') + } + } + + b.WriteRune('\n') + + for i := range m.elements { + if e, ok := m.elements[i].(button.Button); ok { + b.WriteString(e.String()) + b.WriteRune('\n') + } + } + + var ( + errValidate *common.ValidateError + errForm *common.FormError + ) + + // выводим ошибки валидации + if m.err != nil && (errors.As(m.err, &errValidate) || errors.As(m.err, &errForm)) { + b.WriteString(style.ErrorBlockStyle.Render(m.err.Error())) + b.WriteRune('\n') + } + + b.WriteRune('\n') + b.WriteString(m.help.View(common.Keys)) + + // выводим прочие ошибки + if m.err != nil && !(errors.As(m.err, &errValidate) || errors.As(m.err, &errForm)) { + b.WriteRune('\n') + b.WriteString(style.ErrorBlockStyle.Render(m.err.Error())) + } + + return b.String() +} + +func (m *Model) updateInputs(msg tea.Msg) tea.Cmd { + cmds := make([]tea.Cmd, len(m.elements)) + + for i := range m.elements { + switch e := m.elements[i].(type) { + case textinput.Model: + m.elements[i], cmds[i] = e.Update(msg) + case textarea.Model: + m.elements[i], cmds[i] = e.Update(msg) + } + } + + return tea.Batch(cmds...) +} diff --git a/internal/cli/model/item/sync/action.go b/internal/cli/model/item/sync/action.go new file mode 100644 index 0000000..1a2f7e9 --- /dev/null +++ b/internal/cli/model/item/sync/action.go @@ -0,0 +1,14 @@ +package sync + +import "context" + +func (m *Model) syncAction() error { + item, err := m.usecaseSync.Do(context.TODO(), m.item.GUID) + if err != nil { + return err + } + + m.item = *item + + return nil +} diff --git a/internal/cli/model/item/sync/model.go b/internal/cli/model/item/sync/model.go new file mode 100644 index 0000000..2f965b5 --- /dev/null +++ b/internal/cli/model/item/sync/model.go @@ -0,0 +1,213 @@ +// Package sync описывает модель для работы UI при синхронизации клиента с сервером. +package sync + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + + "github.com/bjlag/go-keeper/internal/cli/common" + "github.com/bjlag/go-keeper/internal/cli/element/button" + "github.com/bjlag/go-keeper/internal/cli/message" + "github.com/bjlag/go-keeper/internal/cli/style" + "github.com/bjlag/go-keeper/internal/domain/client" + itemSync "github.com/bjlag/go-keeper/internal/usecase/client/item/sync" +) + +const ( + posSyncBtn int = iota + posCancelBtn +) + +type Model struct { + help help.Model + header string + elements []button.Button + pos int + err error + + item client.Item + prevModel tea.Model + + usecaseSync *itemSync.Usecase +} + +func InitModel(usecaseSync *itemSync.Usecase) *Model { + return &Model{ + help: help.New(), + usecaseSync: usecaseSync, + } +} + +func (m *Model) Init() tea.Cmd { + return nil +} + +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case message.OpenItemMsg: + m.prevModel = msg.BackModel + + if msg.Item == nil { + return m.prevModel.Update(msg) + } + + m.item = *msg.Item + m.header = "Синхронизация" + + switch m.item.Category { + case client.CategoryPassword: + m.header += fmt.Sprintf(" пароля: %s", m.item.Title) + case client.CategoryText: + m.header += fmt.Sprintf(" текста: %s", m.item.Title) + case client.CategoryFile: + m.header += fmt.Sprintf(" файла: %s", m.item.Title) + case client.CategoryBankCard: + m.header += fmt.Sprintf(" банковской карты: %s", m.item.Title) + } + + m.elements = []button.Button{ + posSyncBtn: button.CreateDefaultButton("Синхронизировать"), + posCancelBtn: button.CreateDefaultButton("Отмена"), + } + + return m, nil + case tea.KeyMsg: + switch { + case key.Matches(msg, common.Keys.Quit): + return m, tea.Quit + case key.Matches(msg, common.Keys.Navigation): + if key.Matches(msg, common.Keys.Down, common.Keys.Tab) { + m.pos++ + } else { + m.pos-- + } + + if m.pos > len(m.elements)-1 { + m.pos = 0 + } else if m.pos < 0 { + m.pos = len(m.elements) - 1 + } + + for i, e := range m.elements { + if i == m.pos { + e.Focus() + m.elements[i] = e + continue + } + e.Blur() + m.elements[i] = e + } + + return m, nil + case key.Matches(msg, common.Keys.Enter): + m.err = nil + + switch m.pos { + case posSyncBtn: + err := m.syncAction() + if err != nil { + m.err = err + return m, nil + } + + return m.prevModel.Update(message.BackMsg{ + Item: &m.item, + }) + case posCancelBtn: + return m.prevModel.Update(message.BackMsg{ + Item: &m.item, + }) + } + + return m, nil + case key.Matches(msg, common.Keys.Back): + return m.prevModel.Update(message.BackMsg{}) + } + } + + return m, nil +} + +func (m *Model) View() string { + var b strings.Builder + + b.WriteString(style.TitleStyle.Render(m.header)) + b.WriteRune('\n') + + b.WriteString("Версия на сервере отличается от версии на клиенте.\n") + b.WriteString("Синхронизируйте сначала данные с сервером, после меняйте запись.\n") + b.WriteRune('\n') + + b.WriteString("Запись на сервере:\n") + b.WriteRune('\n') + + b.WriteString("Название") + b.WriteRune('\n') + b.WriteString(m.item.Title) + b.WriteRune('\n') + b.WriteRune('\n') + + switch v := m.item.Value.(type) { + case *client.Password: + b.WriteString("Логин") + b.WriteRune('\n') + b.WriteString(v.Login) + b.WriteRune('\n') + b.WriteRune('\n') + + b.WriteString("Пароль") + b.WriteRune('\n') + b.WriteString(v.Password) + b.WriteRune('\n') + b.WriteRune('\n') + case *client.File: + b.WriteString("Название файла") + b.WriteRune('\n') + b.WriteString(v.Name) + b.WriteRune('\n') + b.WriteRune('\n') + case *client.BankCard: + b.WriteString("Номер карты") + b.WriteRune('\n') + b.WriteString(v.Number) + b.WriteRune('\n') + b.WriteRune('\n') + + b.WriteString("Истекает") + b.WriteRune('\n') + b.WriteString(v.Expiry) + b.WriteRune('\n') + b.WriteRune('\n') + + b.WriteString("CVV") + b.WriteRune('\n') + b.WriteString(v.CVV) + b.WriteRune('\n') + b.WriteRune('\n') + } + + b.WriteString("Заметки") + b.WriteRune('\n') + b.WriteString(m.item.Notes) + b.WriteRune('\n') + b.WriteRune('\n') + + b.WriteRune('\n') + + for _, e := range m.elements { + b.WriteString(e.String()) + b.WriteRune('\n') + } + + // выводим прочие ошибки + if m.err != nil { + b.WriteRune('\n') + b.WriteString(style.ErrorBlockStyle.Render(m.err.Error())) + } + + return b.String() +} diff --git a/internal/cli/model/item/text/action.go b/internal/cli/model/item/text/action.go new file mode 100644 index 0000000..e79b6d2 --- /dev/null +++ b/internal/cli/model/item/text/action.go @@ -0,0 +1,33 @@ +package text + +import ( + "context" + + "github.com/bjlag/go-keeper/internal/cli/element" + "github.com/bjlag/go-keeper/internal/domain/client" +) + +func (m *Model) createAction() error { + item := client.NewTextItem( + element.GetValue(m.elements, posCreateTitle), + element.GetValue(m.elements, posCreateNotes), + ) + + return m.usecaseCreate.Do(context.TODO(), item) +} + +func (m *Model) editAction() error { + item := client.NewTextItem( + element.GetValue(m.elements, posEditTitle), + element.GetValue(m.elements, posEditNotes), + ) + item.GUID = m.guid + item.CreatedAt = m.item.CreatedAt + item.UpdatedAt = m.item.UpdatedAt + + return m.usecaseEdit.Do(context.TODO(), item) +} + +func (m *Model) deleteAction() error { + return m.usecaseDelete.Do(context.TODO(), m.guid) +} diff --git a/internal/cli/model/item/text/model.go b/internal/cli/model/item/text/model.go new file mode 100644 index 0000000..467cdcd --- /dev/null +++ b/internal/cli/model/item/text/model.go @@ -0,0 +1,309 @@ +// Package text описывает модель для работы UI с текстом. +package text + +import ( + "errors" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/google/uuid" + + "github.com/bjlag/go-keeper/internal/cli/common" + "github.com/bjlag/go-keeper/internal/cli/element/button" + tarea "github.com/bjlag/go-keeper/internal/cli/element/textarea" + tinput "github.com/bjlag/go-keeper/internal/cli/element/textinput" + "github.com/bjlag/go-keeper/internal/cli/message" + "github.com/bjlag/go-keeper/internal/cli/style" + "github.com/bjlag/go-keeper/internal/domain/client" + "github.com/bjlag/go-keeper/internal/usecase/client/item/create" + "github.com/bjlag/go-keeper/internal/usecase/client/item/edit" + "github.com/bjlag/go-keeper/internal/usecase/client/item/remove" +) + +const ( + posEditTitle int = iota + posEditNotes + posEditEditBtn + posEditDeleteBtn + posEditBackBtn +) + +const ( + posCreateTitle int = iota + posCreateNotes + posCreateSaveBtn + posCreateBackBtn +) + +type state int + +const ( + stateCreate state = iota + stateEdit +) + +var errUnsupportedCommand = errors.New("unsupported command") + +type Model struct { + help help.Model + header string + state state + elements []interface{} + pos int + err error + + backModel tea.Model + backState int + + guid uuid.UUID + item *client.Item + category client.Category + + formSync tea.Model + + usecaseCreate *create.Usecase + usecaseEdit *edit.Usecase + usecaseDelete *remove.Usecase +} + +func InitModel(usecaseCreate *create.Usecase, usecaseSave *edit.Usecase, usecaseDelete *remove.Usecase, formSync tea.Model) *Model { + return &Model{ + help: help.New(), + header: "Текст", + state: stateCreate, + + formSync: formSync, + + usecaseCreate: usecaseCreate, + usecaseEdit: usecaseSave, + usecaseDelete: usecaseDelete, + } +} + +func (m *Model) Init() tea.Cmd { + return nil +} + +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + for i := range m.elements { + if e, ok := m.elements[i].(textinput.Model); ok { + e.Width = msg.Width + } + } + return m, nil + case message.BackMsg: + if msg.Item != nil { + m.item = msg.Item + } + return m, nil + case message.OpenItemMsg: + m.backState = msg.BackState + m.backModel = msg.BackModel + + if msg.Item != nil { + m.state = stateEdit + m.header = msg.Item.Title + m.item = msg.Item + m.guid = msg.Item.GUID + m.category = msg.Item.Category + + m.elements = []interface{}{ + posEditTitle: tinput.CreateDefaultTextInput("Название", tinput.WithValue(msg.Item.Title), tinput.WithFocused()), + posEditNotes: tarea.CreateDefaultTextArea("Текст", tarea.WithValue(msg.Item.Notes)), + posEditEditBtn: button.CreateDefaultButton("Изменить"), + posEditDeleteBtn: button.CreateDefaultButton("Удалить"), + posEditBackBtn: button.CreateDefaultButton("Назад"), + } + + return m, nil + } + + m.state = stateCreate + m.header = "Новый текст" + m.elements = []interface{}{ + posCreateTitle: tinput.CreateDefaultTextInput("Название", tinput.WithFocused()), + posCreateNotes: tarea.CreateDefaultTextArea("Текст"), + posCreateSaveBtn: button.CreateDefaultButton("Сохранить"), + posCreateBackBtn: button.CreateDefaultButton("Назад"), + } + + return m, nil + case tea.KeyMsg: + switch { + case key.Matches(msg, common.Keys.Quit): + return m, tea.Quit + case key.Matches(msg, common.Keys.Navigation): + if key.Matches(msg, common.Keys.Down, common.Keys.Tab) { + m.pos++ + } else { + m.pos-- + } + + if m.pos > len(m.elements)-1 { + m.pos = 0 + } else if m.pos < 0 { + m.pos = len(m.elements) - 1 + } + + for i := range m.elements { + switch e := m.elements[i].(type) { + case textinput.Model: + if i == m.pos { + e.Focus() + m.elements[i] = style.SetFocusStyle(e) + continue + } + + e.Blur() + m.elements[i] = style.SetNoStyle(e) + case textarea.Model: + if i == m.pos { + e.Focus() + m.elements[i] = e + continue + } + + e.Blur() + m.elements[i] = e + case button.Button: + if i == m.pos { + e.Focus() + m.elements[i] = e + continue + } + e.Blur() + m.elements[i] = e + } + } + + return m, nil + case key.Matches(msg, common.Keys.Enter): + m.err = nil + + if m.state == stateCreate { + switch m.pos { + case posCreateSaveBtn: + m.err = m.createAction() + return m, nil + case posCreateBackBtn: + return m.backModel.Update(message.BackMsg{ + State: m.backState, + }) + default: + m.err = errUnsupportedCommand + } + } + + switch m.pos { + case posEditEditBtn: + err := m.editAction() + if err != nil && errors.Is(err, edit.ErrConflict) { + return m.formSync.Update(message.OpenItemMsg{ + BackModel: m, + Item: m.item, + }) + } + + m.err = err + return m, nil + case posEditDeleteBtn: + m.err = m.deleteAction() + return m, nil + case posEditBackBtn: + return m.backModel.Update(message.BackMsg{ + State: m.backState, + }) + default: + m.err = errUnsupportedCommand + } + + return m, nil + case key.Matches(msg, common.Keys.Back): + return m.backModel.Update(message.BackMsg{ + State: m.backState, + }) + } + } + + return m, m.updateInputs(msg) +} + +func (m *Model) View() string { + var b strings.Builder + + b.WriteString(style.TitleStyle.Render(m.header)) + b.WriteRune('\n') + + b.WriteString("Категория: ") + b.WriteString(m.category.String()) + b.WriteRune('\n') + + for i := range m.elements { + switch e := m.elements[i].(type) { + case textinput.Model: + b.WriteString(e.Placeholder) + b.WriteRune('\n') + b.WriteString(e.View()) + b.WriteRune('\n') + b.WriteRune('\n') + case textarea.Model: + b.WriteString(e.Placeholder) + b.WriteRune('\n') + b.WriteString(e.View()) + b.WriteRune('\n') + b.WriteRune('\n') + } + } + + b.WriteRune('\n') + + for i := range m.elements { + if e, ok := m.elements[i].(button.Button); ok { + b.WriteString(e.String()) + b.WriteRune('\n') + } + } + + var ( + errValidate *common.ValidateError + errForm *common.FormError + ) + + // выводим ошибки валидации + if m.err != nil && (errors.As(m.err, &errValidate) || errors.As(m.err, &errForm)) { + b.WriteString(style.ErrorBlockStyle.Render(m.err.Error())) + b.WriteRune('\n') + } + + b.WriteRune('\n') + b.WriteString(m.help.View(common.Keys)) + + // выводим прочие ошибки + if m.err != nil && !(errors.As(m.err, &errValidate) || errors.As(m.err, &errForm)) { + b.WriteRune('\n') + b.WriteString(style.ErrorBlockStyle.Render(m.err.Error())) + } + + return b.String() +} + +func (m *Model) updateInputs(msg tea.Msg) tea.Cmd { + cmds := make([]tea.Cmd, len(m.elements)) + + for i := range m.elements { + switch e := m.elements[i].(type) { + case textinput.Model: + m.elements[i], cmds[i] = e.Update(msg) + case textarea.Model: + m.elements[i], cmds[i] = e.Update(msg) + } + } + + return tea.Batch(cmds...) +} diff --git a/internal/cli/model/list/model.go b/internal/cli/model/list/model.go new file mode 100644 index 0000000..059a9f0 --- /dev/null +++ b/internal/cli/model/list/model.go @@ -0,0 +1,225 @@ +// Package list описывает модель для работы UI списка категорий и элементов. +package list + +import ( + "context" + "errors" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + + "github.com/bjlag/go-keeper/internal/cli/common" + elist "github.com/bjlag/go-keeper/internal/cli/element/list" + "github.com/bjlag/go-keeper/internal/cli/message" + "github.com/bjlag/go-keeper/internal/cli/style" + "github.com/bjlag/go-keeper/internal/domain/client" + "github.com/bjlag/go-keeper/internal/fetcher/item" + "github.com/bjlag/go-keeper/internal/usecase/client/sync" +) + +var errUnsupportedCategory = errors.New("unsupported category") + +const ( + stateCategoryList int = iota + stateItemList +) + +const ( + defaultWidth = 40 + listHeight = 14 +) + +type Model struct { + main tea.Model + help help.Model + state int + header string + categories list.Model + items list.Model + err error + + selectedCategory client.Category + + formPassword tea.Model + formText tea.Model + formBankCard tea.Model + formFile tea.Model + + usecaseSync *sync.Usecase + fetcherItem *item.Fetcher +} + +func InitModel( + usecaseSync *sync.Usecase, + fetcherItem *item.Fetcher, + formPassword tea.Model, + formText tea.Model, + formBankCard tea.Model, + formFile tea.Model, +) *Model { + f := &Model{ + help: help.New(), + header: "Категории", + categories: elist.CreateDefaultList("Категории:", defaultWidth, listHeight, elist.CategoryDelegate{}), + items: elist.CreateDefaultList("Пароли:", defaultWidth, listHeight, elist.ItemDelegate{}), + + usecaseSync: usecaseSync, + fetcherItem: fetcherItem, + + formPassword: formPassword, + formText: formText, + formBankCard: formBankCard, + formFile: formFile, + } + + return f +} + +func (f *Model) SetMainModel(m tea.Model) { + f.main = m +} + +func (f *Model) Init() tea.Cmd { + return nil +} + +func (f *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + f.categories.SetWidth(msg.Width) + f.items.SetWidth(msg.Width) + return f, nil + case message.BackMsg: + switch msg.State { + case stateCategoryList: + return f.Update(message.OpenCategoriesMsg{}) + case stateItemList: + return f.Update(message.OpenItemsMsg{}) + } + case message.OpenCategoriesMsg: + f.state = stateCategoryList + + f.err = f.usecaseSync.Do(context.TODO()) + + f.categories.SetItems(nil) + f.categories.InsertItem(len(f.categories.Items()), elist.Category{Category: client.CategoryPassword, Title: client.CategoryPassword.String()}) + f.categories.InsertItem(len(f.categories.Items()), elist.Category{Category: client.CategoryText, Title: client.CategoryText.String()}) + f.categories.InsertItem(len(f.categories.Items()), elist.Category{Category: client.CategoryFile, Title: client.CategoryFile.String()}) + f.categories.InsertItem(len(f.categories.Items()), elist.Category{Category: client.CategoryBankCard, Title: client.CategoryBankCard.String()}) + + return f, nil + case message.OpenItemsMsg: + f.state = stateItemList + + if c, ok := f.categories.SelectedItem().(elist.Category); ok { + f.items.Title = c.Title + ":" + } + + items, err := f.fetcherItem.ItemsByCategory(context.TODO(), f.selectedCategory) + if err != nil { + f.err = err + return f, nil + } + + f.items.SetItems(nil) + for _, model := range items { + f.items.InsertItem(len(f.categories.Items()), elist.Item{ + Model: model, + }) + } + + return f, nil + case tea.KeyMsg: + switch { + case key.Matches(msg, common.Keys.Quit): + return f, tea.Quit + case key.Matches(msg, common.Keys.Enter): + switch f.state { + case stateCategoryList: + if c, ok := f.categories.SelectedItem().(elist.Category); ok { + f.selectedCategory = c.Category + return f.Update(message.OpenItemsMsg{ + Category: c.Category, + }) + } + case stateItemList: + if i, ok := f.items.SelectedItem().(elist.Item); ok { + f.selectedCategory = i.Model.Category + + switch i.Model.Category { + case client.CategoryPassword: + return f.formPassword.Update(message.OpenItemMsg{ + BackModel: f, + BackState: f.state, + Item: &i.Model, + }) + case client.CategoryText: + return f.formText.Update(message.OpenItemMsg{ + BackModel: f, + BackState: f.state, + Item: &i.Model, + }) + case client.CategoryBankCard: + return f.formBankCard.Update(message.OpenItemMsg{ + BackModel: f, + BackState: f.state, + Item: &i.Model, + }) + case client.CategoryFile: + return f.formFile.Update(message.OpenItemMsg{ + BackModel: f, + BackState: f.state, + Item: &i.Model, + }) + default: + f.err = errUnsupportedCategory + } + } + } + + return f, nil + case key.Matches(msg, common.Keys.Back): + switch f.state { + case stateCategoryList: + return f.main.Update(message.BackMsg{}) + case stateItemList: + return f.Update(message.OpenCategoriesMsg{}) + } + } + } + + var cmd tea.Cmd + switch f.state { + case stateCategoryList: + f.categories, cmd = f.categories.Update(msg) + case stateItemList: + f.items, cmd = f.items.Update(msg) + } + + return f, cmd +} + +func (f *Model) View() string { + var b strings.Builder + + b.WriteString(style.TitleStyle.Render(f.header)) + b.WriteRune('\n') + + switch f.state { + case stateCategoryList: + b.WriteString(f.categories.View()) + case stateItemList: + b.WriteString(f.items.View()) + } + + // выводим прочие ошибки + if f.err != nil { + b.WriteRune('\n') + b.WriteString(style.ErrorBlockStyle.Render(f.err.Error())) + } + + return b.String() +} diff --git a/internal/cli/model/login/model.go b/internal/cli/model/login/model.go new file mode 100644 index 0000000..90eed7e --- /dev/null +++ b/internal/cli/model/login/model.go @@ -0,0 +1,283 @@ +// Package login описывает модель для работы UI аутентификации пользователя. +package login + +import ( + "context" + "errors" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/bjlag/go-keeper/internal/cli/common" + "github.com/bjlag/go-keeper/internal/cli/element/button" + tinput "github.com/bjlag/go-keeper/internal/cli/element/textinput" + "github.com/bjlag/go-keeper/internal/cli/message" + "github.com/bjlag/go-keeper/internal/cli/style" + "github.com/bjlag/go-keeper/internal/infrastructure/validator" + "github.com/bjlag/go-keeper/internal/usecase/client/login" +) + +const ( + posEmail int = iota + posPassword + posSubmitBtn + posRegisterBtn + posCloseBtn +) + +var errPasswordInvalid = common.NewFormError("Неверный email или пароль") + +type Model struct { + help help.Model + header string + elements []interface{} + pos int + err error + + fromRegister tea.Model + usecaseLogin *login.Usecase + + userEmail string + userPassword string +} + +func (f *Model) UserEmail() string { + return f.userEmail +} + +func (f *Model) UserPass() string { + return f.userPassword +} + +func InitModel(usecaseLogin *login.Usecase, fromRegister tea.Model) *Model { + f := &Model{ + help: help.New(), + header: "Авторизация", + elements: []interface{}{ + posEmail: tinput.CreateDefaultTextInput("Email"), + posPassword: tinput.CreateDefaultTextInput("Пароль"), + posSubmitBtn: button.CreateDefaultButton("Вход"), + posRegisterBtn: button.CreateDefaultButton("Регистрация"), + posCloseBtn: button.CreateDefaultButton("Закрыть"), + }, + + fromRegister: fromRegister, + usecaseLogin: usecaseLogin, + } + + for i := range f.elements { + if e, ok := f.elements[i].(textinput.Model); ok { + if i == posEmail { + e.Focus() + f.elements[i] = style.SetFocusStyle(e) + continue + } + e.EchoMode = textinput.EchoPassword + e.EchoCharacter = '•' + f.elements[i] = e + } + } + + return f +} + +func (f *Model) Init() tea.Cmd { + return nil +} + +func (f *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + for i := range f.elements { + if e, ok := f.elements[i].(textinput.Model); ok { + e.Width = msg.Width + } + } + return f, nil + case message.OpenLoginMsg: + for i := range f.elements { + if e, ok := f.elements[i].(textinput.Model); ok { + e.SetValue("") + f.elements[i] = e + } + } + case message.SuccessLoginMsg: + f.userEmail = msg.Email + f.userPassword = msg.Password + + return f, tea.Quit + case tea.KeyMsg: + switch { + case key.Matches(msg, common.Keys.Quit): + return f, tea.Quit + case key.Matches(msg, common.Keys.Navigation): + if key.Matches(msg, common.Keys.Down, common.Keys.Tab) { + f.pos++ + } else { + f.pos-- + } + + if f.pos > len(f.elements)-1 { + f.pos = 0 + } else if f.pos < 0 { + f.pos = len(f.elements) - 1 + } + + for i := range f.elements { + switch e := f.elements[i].(type) { + case textinput.Model: + if i == f.pos { + e.Focus() + f.elements[i] = style.SetFocusStyle(e) + continue + } + + e.Blur() + f.elements[i] = style.SetNoStyle(e) + case button.Button: + if i == f.pos { + e.Focus() + f.elements[i] = e + continue + } + e.Blur() + f.elements[i] = e + } + } + + return f, nil + case key.Matches(msg, common.Keys.Enter): + f.err = nil + + switch { + case f.pos == posSubmitBtn || f.pos == posEmail || f.pos == posPassword: + return f.submit() + case f.pos == posRegisterBtn: + return f.fromRegister.Update(message.OpenRegisterMsg{ + LoginModel: f, + }) + case f.pos == posCloseBtn: + return f, tea.Quit + } + + return f, nil + } + } + + return f, f.updateInputs(msg) +} + +func (f *Model) View() string { + var b strings.Builder + + b.WriteString(style.TitleStyle.Render(f.header)) + b.WriteRune('\n') + + for i := range f.elements { + if e, ok := f.elements[i].(textinput.Model); ok { + b.WriteString(e.View()) + b.WriteRune('\n') + } + } + + b.WriteRune('\n') + + for i := range f.elements { + if e, ok := f.elements[i].(button.Button); ok { + b.WriteString(e.String()) + b.WriteRune('\n') + } + } + + var ( + errValidate *common.ValidateError + errForm *common.FormError + ) + + // выводим ошибки валидации + if f.err != nil && (errors.As(f.err, &errValidate) || errors.As(f.err, &errForm)) { + b.WriteString(style.ErrorBlockStyle.Render(f.err.Error())) + b.WriteRune('\n') + } + + b.WriteRune('\n') + b.WriteString(f.help.View(common.Keys)) + + // выводим прочие ошибки + if f.err != nil && !(errors.As(f.err, &errValidate) || errors.As(f.err, &errForm)) { + b.WriteRune('\n') + b.WriteString(style.ErrorBlockStyle.Render(f.err.Error())) + } + + return b.String() +} + +func (f *Model) submit() (tea.Model, tea.Cmd) { + errValidate := common.NewValidateError() + + email, ok := f.elements[posEmail].(textinput.Model) + if !ok { + f.err = common.ErrInvalidElement + return f, nil + } + password, ok := f.elements[posPassword].(textinput.Model) + if !ok { + f.err = common.ErrInvalidElement + return f, nil + } + + if !validator.ValidateEmail(email.Value()) { + f.elements[posEmail] = style.SetErrorStyle(email) + errValidate.AddError("Неверно заполнен email") + } + + if password.Value() == "" { + f.elements[posPassword] = style.SetErrorStyle(password) + errValidate.AddError("Не заполнен пароль") + } + + if errValidate.HasErrors() { + f.err = errValidate + return f, nil + } + + err := f.usecaseLogin.Do(context.TODO(), login.Data{ + Email: email.Value(), + Password: password.Value(), + }) + if err != nil { + if s, ok := status.FromError(err); ok { + if s.Code() == codes.Unauthenticated { + f.err = errPasswordInvalid + return f, nil + } + f.err = common.NewFormError(s.Message()) + return f, nil + } + + f.err = err + return f, nil + } + + f.userEmail = email.Value() + f.userPassword = password.Value() + + return f, tea.Quit +} + +func (f *Model) updateInputs(msg tea.Msg) tea.Cmd { + cmds := make([]tea.Cmd, len(f.elements)) + + for i := range f.elements { + if m, ok := f.elements[i].(textinput.Model); ok { + f.elements[i], cmds[i] = m.Update(msg) + } + } + + return tea.Batch(cmds...) +} diff --git a/internal/cli/model/master/model.go b/internal/cli/model/master/model.go new file mode 100644 index 0000000..26d73ff --- /dev/null +++ b/internal/cli/model/master/model.go @@ -0,0 +1,142 @@ +// Package master описывает основную модель, которая стартует при запуске CLI приложения. +package master + +import ( + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + + "github.com/bjlag/go-keeper/internal/cli/common" + "github.com/bjlag/go-keeper/internal/cli/element/button" + "github.com/bjlag/go-keeper/internal/cli/message" + "github.com/bjlag/go-keeper/internal/cli/model/create" + listf "github.com/bjlag/go-keeper/internal/cli/model/list" + "github.com/bjlag/go-keeper/internal/cli/style" +) + +const ( + posViewBtn int = iota + posCreateBtn + posCloseBtn +) + +type Model struct { + help help.Model + header string + elements []interface{} + pos int + err error + + formCreate *create.Model + formList *listf.Model +} + +func InitModel(opts ...Option) *Model { + m := &Model{ + help: help.New(), + header: "Go Keeper", + elements: []interface{}{ + posViewBtn: button.CreateDefaultButton("Просмотр", button.WithFocused()), + posCreateBtn: button.CreateDefaultButton("Создать"), + posCloseBtn: button.CreateDefaultButton("Выйти"), + }, + } + + for _, opt := range opts { + opt(m) + } + + return m +} + +func (m *Model) Init() tea.Cmd { + return nil +} + +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if msg == nil { + return m, nil + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, common.Keys.Quit): + return m, tea.Quit + case key.Matches(msg, common.Keys.Enter): + switch m.pos { + case posViewBtn: + return m.formList.Update(message.OpenCategoriesMsg{}) + case posCreateBtn: + return m.formCreate.Update(message.OpenItemMsg{}) + case posCloseBtn: + return m, tea.Quit + } + case key.Matches(msg, common.Keys.Navigation): + if key.Matches(msg, common.Keys.Down, common.Keys.Tab) { + m.pos++ + } else { + m.pos-- + } + + if m.pos > len(m.elements)-1 { + m.pos = 0 + } else if m.pos < 0 { + m.pos = len(m.elements) - 1 + } + + for i := range m.elements { + if e, ok := m.elements[i].(button.Button); ok { + if i == m.pos { + e.Focus() + m.elements[i] = e + continue + } + e.Blur() + m.elements[i] = e + } + } + + return m, nil + } + + case message.BackMsg: + return m.Update(nil) + + // Forms + case message.OpenCategoriesMsg: + return m.formList.Update(msg) + case message.OpenItemsMsg: + return m.formList.Update(msg) + + // Success + case message.SuccessLoginMsg: + return m.Update(nil) + } + + return m.Update(nil) +} + +func (m *Model) View() string { + var b strings.Builder + + b.WriteString(style.TitleStyle.Render(m.header)) + b.WriteRune('\n') + + for i := range m.elements { + if e, ok := m.elements[i].(button.Button); ok { + b.WriteString(e.String()) + b.WriteRune('\n') + } + } + + // выводим прочие ошибки + if m.err != nil { + b.WriteRune('\n') + b.WriteString(style.ErrorBlockStyle.Render(m.err.Error())) + } + + return b.String() +} diff --git a/internal/cli/model/master/option.go b/internal/cli/model/master/option.go new file mode 100644 index 0000000..17b3cd4 --- /dev/null +++ b/internal/cli/model/master/option.go @@ -0,0 +1,22 @@ +package master + +import ( + "github.com/bjlag/go-keeper/internal/cli/model/create" + "github.com/bjlag/go-keeper/internal/cli/model/list" +) + +type Option func(*Model) + +func WithCreatForm(form *create.Model) Option { + return func(m *Model) { + form.SetMainModel(m) + m.formCreate = form + } +} + +func WithListForm(form *list.Model) Option { + return func(m *Model) { + form.SetMainModel(m) + m.formList = form + } +} diff --git a/internal/cli/model/register/model.go b/internal/cli/model/register/model.go new file mode 100644 index 0000000..b2a30e3 --- /dev/null +++ b/internal/cli/model/register/model.go @@ -0,0 +1,260 @@ +// Package register описывает модель для работы UI регистрации пользователя. +package register + +import ( + "context" + "errors" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/bjlag/go-keeper/internal/cli/common" + "github.com/bjlag/go-keeper/internal/cli/element/button" + tinput "github.com/bjlag/go-keeper/internal/cli/element/textinput" + "github.com/bjlag/go-keeper/internal/cli/message" + "github.com/bjlag/go-keeper/internal/cli/style" + "github.com/bjlag/go-keeper/internal/infrastructure/validator" + "github.com/bjlag/go-keeper/internal/usecase/client/register" +) + +const ( + posEmail int = iota + posPassword + posSubmitBtn + posBackBtn +) + +var errUserAlreadyRegistered = common.NewFormError("Пользователь уже зарегистрирован") + +type Model struct { + help help.Model + header string + elements []interface{} + pos int + err error + + loginModel tea.Model + usecaseRegister *register.Usecase +} + +func InitModel(usecaseRegister *register.Usecase) *Model { + f := &Model{ + help: help.New(), + header: "Регистрация", + elements: []interface{}{ + posEmail: tinput.CreateDefaultTextInput("Email"), + posPassword: tinput.CreateDefaultTextInput("Пароль"), + posSubmitBtn: button.CreateDefaultButton("Регистрация"), + posBackBtn: button.CreateDefaultButton("Назад"), + }, + + usecaseRegister: usecaseRegister, + } + + for i := range f.elements { + if e, ok := f.elements[i].(textinput.Model); ok { + if i == posEmail { + e.TextStyle = style.FocusedStyle + e.PromptStyle = style.FocusedStyle + e.Focus() + + f.elements[i] = e + continue + } + e.EchoMode = textinput.EchoPassword + e.EchoCharacter = '•' + f.elements[i] = e + } + } + + return f +} + +func (f *Model) Init() tea.Cmd { + return nil +} + +func (f *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + for i := range f.elements { + if e, ok := f.elements[i].(textinput.Model); ok { + e.Width = msg.Width + } + } + return f, nil + case message.OpenRegisterMsg: + f.loginModel = msg.LoginModel + case tea.KeyMsg: + switch { + case key.Matches(msg, common.Keys.Quit): + return f, tea.Quit + case key.Matches(msg, common.Keys.Navigation): + if key.Matches(msg, common.Keys.Down, common.Keys.Tab) { + f.pos++ + } else { + f.pos-- + } + + if f.pos > len(f.elements)-1 { + f.pos = 0 + } else if f.pos < 0 { + f.pos = len(f.elements) - 1 + } + + for i := range f.elements { + switch e := f.elements[i].(type) { + case textinput.Model: + if i == f.pos { + e.Focus() + f.elements[i] = style.SetFocusStyle(e) + continue + } + + e.Blur() + f.elements[i] = style.SetNoStyle(e) + case button.Button: + if i == f.pos { + e.Focus() + f.elements[i] = e + continue + } + e.Blur() + f.elements[i] = e + } + } + + return f, nil + case key.Matches(msg, common.Keys.Back): + return f.loginModel.Update(message.BackMsg{}) + case key.Matches(msg, common.Keys.Enter): + f.err = nil + + switch { + case f.pos == posSubmitBtn || f.pos == posEmail || f.pos == posPassword: + return f.submit() + case f.pos == posBackBtn: + return f.loginModel.Update(message.OpenLoginMsg{}) + } + + return f, nil + } + } + + return f, f.updateInputs(msg) +} + +func (f *Model) View() string { + var b strings.Builder + + b.WriteString(style.TitleStyle.Render(f.header)) + b.WriteRune('\n') + + for i := range f.elements { + if e, ok := f.elements[i].(textinput.Model); ok { + b.WriteString(e.View()) + b.WriteRune('\n') + } + } + + b.WriteRune('\n') + + for i := range f.elements { + if e, ok := f.elements[i].(button.Button); ok { + b.WriteString(e.String()) + b.WriteRune('\n') + } + } + + var ( + errValidate *common.ValidateError + errForm *common.FormError + ) + + // выводим ошибки валидации + if f.err != nil && (errors.As(f.err, &errValidate) || errors.As(f.err, &errForm)) { + b.WriteString(style.ErrorBlockStyle.Render(f.err.Error())) + b.WriteRune('\n') + } + + b.WriteRune('\n') + b.WriteString(f.help.View(common.Keys)) + + // выводим прочие ошибки + if f.err != nil && !(errors.As(f.err, &errValidate) || errors.As(f.err, &errForm)) { + b.WriteRune('\n') + b.WriteString(style.ErrorBlockStyle.Render(f.err.Error())) + } + + return b.String() +} + +func (f *Model) submit() (tea.Model, tea.Cmd) { + errValidate := common.NewValidateError() + + email, ok := f.elements[posEmail].(textinput.Model) + if !ok { + f.err = common.ErrInvalidElement + return f, nil + } + password, ok := f.elements[posPassword].(textinput.Model) + if !ok { + f.err = common.ErrInvalidElement + return f, nil + } + + if !validator.ValidateEmail(email.Value()) { + f.elements[posEmail] = style.SetErrorStyle(email) + errValidate.AddError("Неправильный email") + } + + if !validator.ValidatePassword(password.Value()) { + f.elements[posPassword] = style.SetErrorStyle(password) + errValidate.AddError("Недостаточно сложный пароль") + } + + if errValidate.HasErrors() { + f.err = errValidate + return f, nil + } + + err := f.usecaseRegister.Do(context.TODO(), register.Data{ + Email: email.Value(), + Password: password.Value(), + }) + if err != nil { + if s, ok := status.FromError(err); ok { + if s.Code() == codes.AlreadyExists { + f.err = errUserAlreadyRegistered + return f, nil + } + f.err = common.NewFormError(s.Message()) + return f, nil + } + + f.err = err + return f, nil + } + + return f.loginModel.Update(message.SuccessLoginMsg{ + Email: email.Value(), + Password: password.Value(), + }) +} + +func (f *Model) updateInputs(msg tea.Msg) tea.Cmd { + cmds := make([]tea.Cmd, len(f.elements)) + + for i := range f.elements { + if m, ok := f.elements[i].(textinput.Model); ok { + f.elements[i], cmds[i] = m.Update(msg) + } + } + + return tea.Batch(cmds...) +} diff --git a/internal/cli/style/style.go b/internal/cli/style/style.go new file mode 100644 index 0000000..ade6bcd --- /dev/null +++ b/internal/cli/style/style.go @@ -0,0 +1,53 @@ +// Package style содержит стили CLI приложения, которые используются для вывода UI. +package style + +import ( + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/lipgloss" +) + +var ( + TitleStyle = lipgloss.NewStyle(). + Bold(true). + BorderStyle(lipgloss.RoundedBorder()). + BorderBottom(true). + Foreground(lipgloss.Color("39")). + Margin(1, 0) + + NoStyle = lipgloss.NewStyle() + CursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252")) + FocusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39")) + BlurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) + ErrorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196")) + + ListTitleStyle = lipgloss.NewStyle().MarginLeft(2) + ListItemStyle = lipgloss.NewStyle().PaddingLeft(4) + SelectedListItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) + ListPaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) + ListHelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) + + ErrorBlockStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("245")). + Padding(0, 1). + MarginTop(1). + Foreground(lipgloss.Color("196")) +) + +func SetNoStyle(input textinput.Model) textinput.Model { + input.PromptStyle = NoStyle + input.TextStyle = NoStyle + return input +} + +func SetFocusStyle(input textinput.Model) textinput.Model { + input.PromptStyle = FocusedStyle + input.TextStyle = FocusedStyle + return input +} + +func SetErrorStyle(input textinput.Model) textinput.Model { + input.PromptStyle = ErrorStyle + return input +} diff --git a/internal/domain/client/backup.go b/internal/domain/client/backup.go new file mode 100644 index 0000000..cbc9d60 --- /dev/null +++ b/internal/domain/client/backup.go @@ -0,0 +1,21 @@ +package client + +import ( + "time" + + "github.com/google/uuid" +) + +type Backup struct { + GUID uuid.UUID + Value []byte +} + +type BackupValue struct { + Category Category `json:"category"` + Title string `json:"title"` + Value *[]byte `json:"value"` + Notes string `json:"notes"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/internal/domain/client/encrypted_data.go b/internal/domain/client/encrypted_data.go new file mode 100644 index 0000000..2eabfb6 --- /dev/null +++ b/internal/domain/client/encrypted_data.go @@ -0,0 +1,57 @@ +package client + +import ( + "encoding/json" + "fmt" +) + +// EncryptedData описывает зашифрованные данные, которые приходят с сервера. +type EncryptedData struct { + // Title название элемента. + Title string `json:"title"` + // Category категория элемента: пароль, текст, файл и пр. + Category Category `json:"category_id"` + // Value у каждой категории свой набор данных, после расшифровки хранится в байтах, по сути это JSON. + Value *[]byte `json:"value,omitempty"` + // Notes какие-то заметки, которые можно указать у элемента. + Notes string `json:"notes"` +} + +func (d *EncryptedData) UnmarshalJSON(data []byte) error { + type Alias EncryptedData + + alias := &struct { + *Alias + Value *json.RawMessage `json:"value,omitempty"` + }{ + Alias: (*Alias)(d), + } + + if err := json.Unmarshal(data, alias); err != nil { + return fmt.Errorf("unmarshal data: %w", err) + } + + if alias.Value != nil { + value := []byte(*alias.Value) + d.Value = &value + } + + return nil +} + +func (d EncryptedData) MarshalJSON() ([]byte, error) { + type Alias EncryptedData + + alias := struct { + Alias + Value json.RawMessage `json:"value,omitempty"` + }{ + Alias: Alias(d), + } + + if d.Value != nil { + alias.Value = *d.Value + } + + return json.Marshal(alias) +} diff --git a/internal/domain/client/item.go b/internal/domain/client/item.go new file mode 100644 index 0000000..fa09335 --- /dev/null +++ b/internal/domain/client/item.go @@ -0,0 +1,149 @@ +package client + +import ( + "time" + + "github.com/google/uuid" +) + +// Category тип под категорию. +type Category int + +const ( + // CategoryPassword пароль. + CategoryPassword Category = iota + // CategoryText текст. + CategoryText + // CategoryFile файл (бинарные данные). + CategoryFile + // CategoryBankCard банковская карта. + CategoryBankCard +) + +// String возвращает строковое представление категории. +func (c Category) String() string { + switch c { + case CategoryPassword: + return "Пароль" + case CategoryText: + return "Текст" + case CategoryFile: + return "Файл" + case CategoryBankCard: + return "Банковская карта" + } + return "" +} + +// RawItem описывает элемент как он приходит с сервера. +type RawItem struct { + GUID uuid.UUID + Category Category + Title string + Value *[]byte + Notes string + CreatedAt time.Time + UpdatedAt time.Time +} + +// Item содержит элемент после разбора. +// В Value будет лежать структура в зависимости от категории Category - Password, File, BankCard. +type Item struct { + GUID uuid.UUID + Category Category + Title string + Value interface{} + Notes string + CreatedAt time.Time + UpdatedAt time.Time +} + +// NewItem создает новый элемент. +func NewItem(category Category, title string, value interface{}, note string) Item { + now := time.Now() + return Item{ + GUID: uuid.New(), + Category: category, + Title: title, + Value: value, + Notes: note, + CreatedAt: now, + UpdatedAt: now, + } +} + +// NewPasswordItem создает новый элемент категории CategoryPassword. +func NewPasswordItem(title, login, password, note string) Item { + return NewItem( + CategoryPassword, + title, + Password{ + Login: login, + Password: password, + }, + note, + ) +} + +// NewTextItem создает новый элемент категории CategoryText. +func NewTextItem(title, note string) Item { + return NewItem( + CategoryText, + title, + nil, + note, + ) +} + +// NewBankCardItem создает новый элемент категории CategoryBankCard. +func NewBankCardItem(title, number, cvv, expiry, note string) Item { + return NewItem( + CategoryBankCard, + title, + BankCard{ + Number: number, + CVV: cvv, + Expiry: expiry, + }, + note, + ) +} + +// NewFileItem создает новый элемент категории CategoryFile. +func NewFileItem(title, name string, data []byte, note string) Item { + return NewItem( + CategoryFile, + title, + File{ + Name: name, + Data: data, + }, + note, + ) +} + +// Password описывает пароль. +type Password struct { + // Login логин. + Login string `json:"login"` + // Password пароль. + Password string `json:"password"` +} + +// File файл. +type File struct { + // Name название файла. + Name string `json:"path"` + // Data контент файла. + Data []byte `json:"data"` +} + +// BankCard банковская карта. +type BankCard struct { + // Number номер карты. + Number string `json:"number"` + // CVV код. + CVV string `json:"cvv"` + // Expiry действует до. + Expiry string `json:"exp"` +} diff --git a/internal/domain/client/option.go b/internal/domain/client/option.go new file mode 100644 index 0000000..a2ac430 --- /dev/null +++ b/internal/domain/client/option.go @@ -0,0 +1,14 @@ +package client + +const ( + // OptSaltKey слаг под опцию, которая хранит соль для генерации мастер ключа. + OptSaltKey = "salt" +) + +// Option описывает опцию, которую храним в БД клиента. +type Option struct { + // Slug какое-то название опции. + Slug string + // Value значение опции. + Value string +} diff --git a/internal/domain/server/data/item.go b/internal/domain/server/data/item.go new file mode 100644 index 0000000..a6c0850 --- /dev/null +++ b/internal/domain/server/data/item.go @@ -0,0 +1,27 @@ +package data + +import ( + "time" + + "github.com/google/uuid" +) + +// Item описывает секретные данные. +type Item struct { + // GUID уникальный идентификатор. + GUID uuid.UUID + // UserGUID идентификатор пользователя, которому принадлежат данные. + UserGUID uuid.UUID + // EncryptedData сами секретные данные в зашифрованном виде. + EncryptedData []byte + // CreatedAt дата и время создания записи. + CreatedAt time.Time + // UpdatedAt дата и время обновления записи. + UpdatedAt time.Time +} + +// UpdatedItem описывает данные для обновления. +type UpdatedItem struct { + EncryptedData []byte + UpdatedAt time.Time +} diff --git a/internal/domain/server/user/user.go b/internal/domain/server/user/user.go new file mode 100644 index 0000000..78281c7 --- /dev/null +++ b/internal/domain/server/user/user.go @@ -0,0 +1,21 @@ +package user + +import ( + "time" + + "github.com/google/uuid" +) + +// User описывает пользователя. +type User struct { + // GUID уникальный идентификатор пользователя. + GUID uuid.UUID + // Email пользователя. + Email string + // PasswordHash хеш пароля. + PasswordHash string + // CreatedAt дата и время создания пользователя. + CreatedAt time.Time + // UpdatedAt дата и время обновления пользователя. + UpdatedAt time.Time +} diff --git a/internal/fetcher/item/contract.go b/internal/fetcher/item/contract.go new file mode 100644 index 0000000..21c0de1 --- /dev/null +++ b/internal/fetcher/item/contract.go @@ -0,0 +1,11 @@ +package item + +import ( + "context" + + model "github.com/bjlag/go-keeper/internal/domain/client" +) + +type itemStore interface { + ItemsByCategory(ctx context.Context, category model.Category) ([]model.RawItem, error) +} diff --git a/internal/fetcher/item/fetcher.go b/internal/fetcher/item/fetcher.go new file mode 100644 index 0000000..f060d45 --- /dev/null +++ b/internal/fetcher/item/fetcher.go @@ -0,0 +1,73 @@ +// Package item отвечает за получение данных по элементам. +package item + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + model "github.com/bjlag/go-keeper/internal/domain/client" +) + +const prefixOp = "fetcher.item" + +// ErrUnknownCategory ошибка на случай если получены данные, значение которых не известны. +// Неизвестно, в какую модель их раскладывать. +var ErrUnknownCategory = errors.New("unknown category") + +type Fetcher struct { + itemStore itemStore +} + +func NewFetcher(itemStore itemStore) *Fetcher { + return &Fetcher{ + itemStore: itemStore, + } +} + +// ItemsByCategory получает элементы из локальной базы клиента по указанной категории. +func (u *Fetcher) ItemsByCategory(ctx context.Context, category model.Category) ([]model.Item, error) { + const op = prefixOp + ".ItemsByCategory" + + rawItems, err := u.itemStore.ItemsByCategory(ctx, category) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + items := make([]model.Item, len(rawItems)) + for i, item := range rawItems { + var v interface{} + if item.Value != nil { + switch item.Category { + case model.CategoryPassword: + v = &model.Password{} + case model.CategoryText: + break + case model.CategoryFile: + v = &model.File{} + case model.CategoryBankCard: + v = &model.BankCard{} + default: + return nil, fmt.Errorf("%w: %d", ErrUnknownCategory, item.Category) + } + + err = json.Unmarshal(*item.Value, &v) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + } + + items[i] = model.Item{ + GUID: item.GUID, + Category: item.Category, + Title: item.Title, + Value: v, + Notes: item.Notes, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + } + + return items, nil +} diff --git a/internal/generated/rpc/keeper.pb.go b/internal/generated/rpc/keeper.pb.go new file mode 100644 index 0000000..620c1f9 --- /dev/null +++ b/internal/generated/rpc/keeper.pb.go @@ -0,0 +1,1023 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.4 +// protoc v5.29.3 +// source: proto/keeper.proto + +package rpc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// auth +type RegisterIn struct { + state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterIn) Reset() { + *x = RegisterIn{} + mi := &file_proto_keeper_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterIn) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterIn) ProtoMessage() {} + +func (x *RegisterIn) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterIn.ProtoReflect.Descriptor instead. +func (*RegisterIn) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{0} +} + +func (x *RegisterIn) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *RegisterIn) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +type RegisterOut struct { + state protoimpl.MessageState `protogen:"open.v1"` + AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + RefreshToken string `protobuf:"bytes,2,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterOut) Reset() { + *x = RegisterOut{} + mi := &file_proto_keeper_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterOut) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterOut) ProtoMessage() {} + +func (x *RegisterOut) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterOut.ProtoReflect.Descriptor instead. +func (*RegisterOut) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{1} +} + +func (x *RegisterOut) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +func (x *RegisterOut) GetRefreshToken() string { + if x != nil { + return x.RefreshToken + } + return "" +} + +type LoginIn struct { + state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LoginIn) Reset() { + *x = LoginIn{} + mi := &file_proto_keeper_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoginIn) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginIn) ProtoMessage() {} + +func (x *LoginIn) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoginIn.ProtoReflect.Descriptor instead. +func (*LoginIn) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{2} +} + +func (x *LoginIn) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *LoginIn) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +type LoginOut struct { + state protoimpl.MessageState `protogen:"open.v1"` + AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + RefreshToken string `protobuf:"bytes,2,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LoginOut) Reset() { + *x = LoginOut{} + mi := &file_proto_keeper_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoginOut) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginOut) ProtoMessage() {} + +func (x *LoginOut) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoginOut.ProtoReflect.Descriptor instead. +func (*LoginOut) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{3} +} + +func (x *LoginOut) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +func (x *LoginOut) GetRefreshToken() string { + if x != nil { + return x.RefreshToken + } + return "" +} + +type RefreshTokensIn struct { + state protoimpl.MessageState `protogen:"open.v1"` + RefreshToken string `protobuf:"bytes,1,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RefreshTokensIn) Reset() { + *x = RefreshTokensIn{} + mi := &file_proto_keeper_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RefreshTokensIn) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshTokensIn) ProtoMessage() {} + +func (x *RefreshTokensIn) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RefreshTokensIn.ProtoReflect.Descriptor instead. +func (*RefreshTokensIn) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{4} +} + +func (x *RefreshTokensIn) GetRefreshToken() string { + if x != nil { + return x.RefreshToken + } + return "" +} + +type RefreshTokensOut struct { + state protoimpl.MessageState `protogen:"open.v1"` + AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + RefreshToken string `protobuf:"bytes,2,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RefreshTokensOut) Reset() { + *x = RefreshTokensOut{} + mi := &file_proto_keeper_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RefreshTokensOut) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshTokensOut) ProtoMessage() {} + +func (x *RefreshTokensOut) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RefreshTokensOut.ProtoReflect.Descriptor instead. +func (*RefreshTokensOut) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{5} +} + +func (x *RefreshTokensOut) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +func (x *RefreshTokensOut) GetRefreshToken() string { + if x != nil { + return x.RefreshToken + } + return "" +} + +// data +type GetByGuidIn struct { + state protoimpl.MessageState `protogen:"open.v1"` + Guid string `protobuf:"bytes,1,opt,name=guid,proto3" json:"guid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetByGuidIn) Reset() { + *x = GetByGuidIn{} + mi := &file_proto_keeper_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetByGuidIn) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetByGuidIn) ProtoMessage() {} + +func (x *GetByGuidIn) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetByGuidIn.ProtoReflect.Descriptor instead. +func (*GetByGuidIn) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{6} +} + +func (x *GetByGuidIn) GetGuid() string { + if x != nil { + return x.Guid + } + return "" +} + +type GetByGuidOut struct { + state protoimpl.MessageState `protogen:"open.v1"` + Guid string `protobuf:"bytes,1,opt,name=guid,proto3" json:"guid,omitempty"` + EncryptedData []byte `protobuf:"bytes,2,opt,name=encryptedData,proto3" json:"encryptedData,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=CreatedAt,proto3" json:"CreatedAt,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=UpdatedAt,proto3" json:"UpdatedAt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetByGuidOut) Reset() { + *x = GetByGuidOut{} + mi := &file_proto_keeper_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetByGuidOut) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetByGuidOut) ProtoMessage() {} + +func (x *GetByGuidOut) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetByGuidOut.ProtoReflect.Descriptor instead. +func (*GetByGuidOut) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{7} +} + +func (x *GetByGuidOut) GetGuid() string { + if x != nil { + return x.Guid + } + return "" +} + +func (x *GetByGuidOut) GetEncryptedData() []byte { + if x != nil { + return x.EncryptedData + } + return nil +} + +func (x *GetByGuidOut) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *GetByGuidOut) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type GetAllItemsIn struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAllItemsIn) Reset() { + *x = GetAllItemsIn{} + mi := &file_proto_keeper_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAllItemsIn) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAllItemsIn) ProtoMessage() {} + +func (x *GetAllItemsIn) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAllItemsIn.ProtoReflect.Descriptor instead. +func (*GetAllItemsIn) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{8} +} + +func (x *GetAllItemsIn) GetLimit() uint32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *GetAllItemsIn) GetOffset() uint32 { + if x != nil { + return x.Offset + } + return 0 +} + +type GetAllItemsOut struct { + state protoimpl.MessageState `protogen:"open.v1"` + Items []*Item `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAllItemsOut) Reset() { + *x = GetAllItemsOut{} + mi := &file_proto_keeper_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAllItemsOut) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAllItemsOut) ProtoMessage() {} + +func (x *GetAllItemsOut) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAllItemsOut.ProtoReflect.Descriptor instead. +func (*GetAllItemsOut) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{9} +} + +func (x *GetAllItemsOut) GetItems() []*Item { + if x != nil { + return x.Items + } + return nil +} + +type Item struct { + state protoimpl.MessageState `protogen:"open.v1"` + Guid string `protobuf:"bytes,1,opt,name=guid,proto3" json:"guid,omitempty"` + EncryptedData []byte `protobuf:"bytes,2,opt,name=encryptedData,proto3" json:"encryptedData,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=CreatedAt,proto3" json:"CreatedAt,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=UpdatedAt,proto3" json:"UpdatedAt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Item) Reset() { + *x = Item{} + mi := &file_proto_keeper_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Item) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Item) ProtoMessage() {} + +func (x *Item) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Item.ProtoReflect.Descriptor instead. +func (*Item) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{10} +} + +func (x *Item) GetGuid() string { + if x != nil { + return x.Guid + } + return "" +} + +func (x *Item) GetEncryptedData() []byte { + if x != nil { + return x.EncryptedData + } + return nil +} + +func (x *Item) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *Item) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type CreateItemIn struct { + state protoimpl.MessageState `protogen:"open.v1"` + Guid string `protobuf:"bytes,1,opt,name=guid,proto3" json:"guid,omitempty"` + EncryptedData []byte `protobuf:"bytes,2,opt,name=encryptedData,proto3" json:"encryptedData,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=CreatedAt,proto3" json:"CreatedAt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateItemIn) Reset() { + *x = CreateItemIn{} + mi := &file_proto_keeper_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateItemIn) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateItemIn) ProtoMessage() {} + +func (x *CreateItemIn) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateItemIn.ProtoReflect.Descriptor instead. +func (*CreateItemIn) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{11} +} + +func (x *CreateItemIn) GetGuid() string { + if x != nil { + return x.Guid + } + return "" +} + +func (x *CreateItemIn) GetEncryptedData() []byte { + if x != nil { + return x.EncryptedData + } + return nil +} + +func (x *CreateItemIn) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +type UpdateItemIn struct { + state protoimpl.MessageState `protogen:"open.v1"` + Guid string `protobuf:"bytes,1,opt,name=guid,proto3" json:"guid,omitempty"` + EncryptedData []byte `protobuf:"bytes,2,opt,name=encryptedData,proto3" json:"encryptedData,omitempty"` + Version int64 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateItemIn) Reset() { + *x = UpdateItemIn{} + mi := &file_proto_keeper_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateItemIn) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateItemIn) ProtoMessage() {} + +func (x *UpdateItemIn) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateItemIn.ProtoReflect.Descriptor instead. +func (*UpdateItemIn) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{12} +} + +func (x *UpdateItemIn) GetGuid() string { + if x != nil { + return x.Guid + } + return "" +} + +func (x *UpdateItemIn) GetEncryptedData() []byte { + if x != nil { + return x.EncryptedData + } + return nil +} + +func (x *UpdateItemIn) GetVersion() int64 { + if x != nil { + return x.Version + } + return 0 +} + +type UpdateItemOut struct { + state protoimpl.MessageState `protogen:"open.v1"` + NewVersion int64 `protobuf:"varint,1,opt,name=newVersion,proto3" json:"newVersion,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateItemOut) Reset() { + *x = UpdateItemOut{} + mi := &file_proto_keeper_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateItemOut) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateItemOut) ProtoMessage() {} + +func (x *UpdateItemOut) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateItemOut.ProtoReflect.Descriptor instead. +func (*UpdateItemOut) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{13} +} + +func (x *UpdateItemOut) GetNewVersion() int64 { + if x != nil { + return x.NewVersion + } + return 0 +} + +type DeleteItemIn struct { + state protoimpl.MessageState `protogen:"open.v1"` + Guid string `protobuf:"bytes,1,opt,name=guid,proto3" json:"guid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteItemIn) Reset() { + *x = DeleteItemIn{} + mi := &file_proto_keeper_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteItemIn) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteItemIn) ProtoMessage() {} + +func (x *DeleteItemIn) ProtoReflect() protoreflect.Message { + mi := &file_proto_keeper_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteItemIn.ProtoReflect.Descriptor instead. +func (*DeleteItemIn) Descriptor() ([]byte, []int) { + return file_proto_keeper_proto_rawDescGZIP(), []int{14} +} + +func (x *DeleteItemIn) GetGuid() string { + if x != nil { + return x.Guid + } + return "" +} + +var File_proto_keeper_proto protoreflect.FileDescriptor + +var file_proto_keeper_proto_rawDesc = string([]byte{ + 0x0a, 0x12, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x1a, 0x1f, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, + 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3e, 0x0a, 0x0a, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x55, 0x0a, 0x0b, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4f, 0x75, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x23, 0x0a, 0x0d, + 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x22, 0x3b, 0x0a, 0x07, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x12, 0x14, 0x0a, 0x05, + 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x52, + 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x4f, 0x75, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x23, 0x0a, + 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x22, 0x36, 0x0a, 0x0f, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x73, 0x49, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, + 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, + 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x5a, 0x0a, 0x10, 0x52, 0x65, + 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x4f, 0x75, 0x74, 0x12, 0x21, + 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, + 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x21, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x42, 0x79, 0x47, + 0x75, 0x69, 0x64, 0x49, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x75, 0x69, 0x64, 0x22, 0xbc, 0x01, 0x0a, 0x0c, 0x47, 0x65, + 0x74, 0x42, 0x79, 0x47, 0x75, 0x69, 0x64, 0x4f, 0x75, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x75, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x75, 0x69, 0x64, 0x12, 0x24, + 0x0a, 0x0d, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, + 0x44, 0x61, 0x74, 0x61, 0x12, 0x38, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x38, + 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x3d, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x41, + 0x6c, 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x49, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, + 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, + 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x22, 0x34, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x41, 0x6c, + 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x4f, 0x75, 0x74, 0x12, 0x22, 0x0a, 0x05, 0x69, 0x74, 0x65, + 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6b, 0x65, 0x65, 0x70, 0x65, + 0x72, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xb4, 0x01, + 0x0a, 0x04, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x75, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x75, 0x69, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x65, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x0d, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, + 0x12, 0x38, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x64, 0x41, 0x74, 0x22, 0x82, 0x01, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, + 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x75, 0x69, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x65, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0d, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12, + 0x38, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x62, 0x0a, 0x0c, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x75, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x75, 0x69, 0x64, 0x12, 0x24, 0x0a, + 0x0d, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, + 0x61, 0x74, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x2f, 0x0a, + 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x4f, 0x75, 0x74, 0x12, 0x1e, + 0x0a, 0x0a, 0x6e, 0x65, 0x77, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0a, 0x6e, 0x65, 0x77, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x22, + 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x12, 0x12, + 0x0a, 0x04, 0x67, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x75, + 0x69, 0x64, 0x32, 0xd6, 0x03, 0x0a, 0x06, 0x4b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x12, 0x33, 0x0a, + 0x08, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x12, 0x2e, 0x6b, 0x65, 0x65, 0x70, + 0x65, 0x72, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x1a, 0x13, 0x2e, + 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4f, + 0x75, 0x74, 0x12, 0x2a, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x0f, 0x2e, 0x6b, 0x65, + 0x65, 0x70, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x1a, 0x10, 0x2e, 0x6b, + 0x65, 0x65, 0x70, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x4f, 0x75, 0x74, 0x12, 0x42, + 0x0a, 0x0d, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, + 0x17, 0x2e, 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x49, 0x6e, 0x1a, 0x18, 0x2e, 0x6b, 0x65, 0x65, 0x70, 0x65, + 0x72, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x4f, + 0x75, 0x74, 0x12, 0x36, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x42, 0x79, 0x47, 0x75, 0x69, 0x64, 0x12, + 0x13, 0x2e, 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x79, 0x47, 0x75, + 0x69, 0x64, 0x49, 0x6e, 0x1a, 0x14, 0x2e, 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x2e, 0x47, 0x65, + 0x74, 0x42, 0x79, 0x47, 0x75, 0x69, 0x64, 0x4f, 0x75, 0x74, 0x12, 0x3c, 0x0a, 0x0b, 0x47, 0x65, + 0x74, 0x41, 0x6c, 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x15, 0x2e, 0x6b, 0x65, 0x65, 0x70, + 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6c, 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x49, 0x6e, + 0x1a, 0x16, 0x2e, 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6c, 0x6c, + 0x49, 0x74, 0x65, 0x6d, 0x73, 0x4f, 0x75, 0x74, 0x12, 0x3a, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x14, 0x2e, 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x2e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x1a, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x74, + 0x65, 0x6d, 0x12, 0x14, 0x2e, 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x1a, 0x15, 0x2e, 0x6b, 0x65, 0x65, 0x70, 0x65, + 0x72, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x4f, 0x75, 0x74, 0x12, + 0x3a, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x14, 0x2e, + 0x6b, 0x65, 0x65, 0x70, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, + 0x6d, 0x49, 0x6e, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x18, 0x5a, 0x16, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, + 0x64, 0x2f, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_proto_keeper_proto_rawDescOnce sync.Once + file_proto_keeper_proto_rawDescData []byte +) + +func file_proto_keeper_proto_rawDescGZIP() []byte { + file_proto_keeper_proto_rawDescOnce.Do(func() { + file_proto_keeper_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_keeper_proto_rawDesc), len(file_proto_keeper_proto_rawDesc))) + }) + return file_proto_keeper_proto_rawDescData +} + +var file_proto_keeper_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_proto_keeper_proto_goTypes = []any{ + (*RegisterIn)(nil), // 0: keeper.RegisterIn + (*RegisterOut)(nil), // 1: keeper.RegisterOut + (*LoginIn)(nil), // 2: keeper.LoginIn + (*LoginOut)(nil), // 3: keeper.LoginOut + (*RefreshTokensIn)(nil), // 4: keeper.RefreshTokensIn + (*RefreshTokensOut)(nil), // 5: keeper.RefreshTokensOut + (*GetByGuidIn)(nil), // 6: keeper.GetByGuidIn + (*GetByGuidOut)(nil), // 7: keeper.GetByGuidOut + (*GetAllItemsIn)(nil), // 8: keeper.GetAllItemsIn + (*GetAllItemsOut)(nil), // 9: keeper.GetAllItemsOut + (*Item)(nil), // 10: keeper.Item + (*CreateItemIn)(nil), // 11: keeper.CreateItemIn + (*UpdateItemIn)(nil), // 12: keeper.UpdateItemIn + (*UpdateItemOut)(nil), // 13: keeper.UpdateItemOut + (*DeleteItemIn)(nil), // 14: keeper.DeleteItemIn + (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 16: google.protobuf.Empty +} +var file_proto_keeper_proto_depIdxs = []int32{ + 15, // 0: keeper.GetByGuidOut.CreatedAt:type_name -> google.protobuf.Timestamp + 15, // 1: keeper.GetByGuidOut.UpdatedAt:type_name -> google.protobuf.Timestamp + 10, // 2: keeper.GetAllItemsOut.items:type_name -> keeper.Item + 15, // 3: keeper.Item.CreatedAt:type_name -> google.protobuf.Timestamp + 15, // 4: keeper.Item.UpdatedAt:type_name -> google.protobuf.Timestamp + 15, // 5: keeper.CreateItemIn.CreatedAt:type_name -> google.protobuf.Timestamp + 0, // 6: keeper.Keeper.Register:input_type -> keeper.RegisterIn + 2, // 7: keeper.Keeper.Login:input_type -> keeper.LoginIn + 4, // 8: keeper.Keeper.RefreshTokens:input_type -> keeper.RefreshTokensIn + 6, // 9: keeper.Keeper.GetByGuid:input_type -> keeper.GetByGuidIn + 8, // 10: keeper.Keeper.GetAllItems:input_type -> keeper.GetAllItemsIn + 11, // 11: keeper.Keeper.CreateItem:input_type -> keeper.CreateItemIn + 12, // 12: keeper.Keeper.UpdateItem:input_type -> keeper.UpdateItemIn + 14, // 13: keeper.Keeper.DeleteItem:input_type -> keeper.DeleteItemIn + 1, // 14: keeper.Keeper.Register:output_type -> keeper.RegisterOut + 3, // 15: keeper.Keeper.Login:output_type -> keeper.LoginOut + 5, // 16: keeper.Keeper.RefreshTokens:output_type -> keeper.RefreshTokensOut + 7, // 17: keeper.Keeper.GetByGuid:output_type -> keeper.GetByGuidOut + 9, // 18: keeper.Keeper.GetAllItems:output_type -> keeper.GetAllItemsOut + 16, // 19: keeper.Keeper.CreateItem:output_type -> google.protobuf.Empty + 13, // 20: keeper.Keeper.UpdateItem:output_type -> keeper.UpdateItemOut + 16, // 21: keeper.Keeper.DeleteItem:output_type -> google.protobuf.Empty + 14, // [14:22] is the sub-list for method output_type + 6, // [6:14] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_proto_keeper_proto_init() } +func file_proto_keeper_proto_init() { + if File_proto_keeper_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_keeper_proto_rawDesc), len(file_proto_keeper_proto_rawDesc)), + NumEnums: 0, + NumMessages: 15, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_keeper_proto_goTypes, + DependencyIndexes: file_proto_keeper_proto_depIdxs, + MessageInfos: file_proto_keeper_proto_msgTypes, + }.Build() + File_proto_keeper_proto = out.File + file_proto_keeper_proto_goTypes = nil + file_proto_keeper_proto_depIdxs = nil +} diff --git a/internal/generated/rpc/keeper_grpc.pb.go b/internal/generated/rpc/keeper_grpc.pb.go new file mode 100644 index 0000000..c235924 --- /dev/null +++ b/internal/generated/rpc/keeper_grpc.pb.go @@ -0,0 +1,390 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.29.3 +// source: proto/keeper.proto + +package rpc + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Keeper_Register_FullMethodName = "/keeper.Keeper/Register" + Keeper_Login_FullMethodName = "/keeper.Keeper/Login" + Keeper_RefreshTokens_FullMethodName = "/keeper.Keeper/RefreshTokens" + Keeper_GetByGuid_FullMethodName = "/keeper.Keeper/GetByGuid" + Keeper_GetAllItems_FullMethodName = "/keeper.Keeper/GetAllItems" + Keeper_CreateItem_FullMethodName = "/keeper.Keeper/CreateItem" + Keeper_UpdateItem_FullMethodName = "/keeper.Keeper/UpdateItem" + Keeper_DeleteItem_FullMethodName = "/keeper.Keeper/DeleteItem" +) + +// KeeperClient is the client API for Keeper service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type KeeperClient interface { + // auth + Register(ctx context.Context, in *RegisterIn, opts ...grpc.CallOption) (*RegisterOut, error) + Login(ctx context.Context, in *LoginIn, opts ...grpc.CallOption) (*LoginOut, error) + RefreshTokens(ctx context.Context, in *RefreshTokensIn, opts ...grpc.CallOption) (*RefreshTokensOut, error) + // data + GetByGuid(ctx context.Context, in *GetByGuidIn, opts ...grpc.CallOption) (*GetByGuidOut, error) + GetAllItems(ctx context.Context, in *GetAllItemsIn, opts ...grpc.CallOption) (*GetAllItemsOut, error) + CreateItem(ctx context.Context, in *CreateItemIn, opts ...grpc.CallOption) (*emptypb.Empty, error) + UpdateItem(ctx context.Context, in *UpdateItemIn, opts ...grpc.CallOption) (*UpdateItemOut, error) + DeleteItem(ctx context.Context, in *DeleteItemIn, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type keeperClient struct { + cc grpc.ClientConnInterface +} + +func NewKeeperClient(cc grpc.ClientConnInterface) KeeperClient { + return &keeperClient{cc} +} + +func (c *keeperClient) Register(ctx context.Context, in *RegisterIn, opts ...grpc.CallOption) (*RegisterOut, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RegisterOut) + err := c.cc.Invoke(ctx, Keeper_Register_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *keeperClient) Login(ctx context.Context, in *LoginIn, opts ...grpc.CallOption) (*LoginOut, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LoginOut) + err := c.cc.Invoke(ctx, Keeper_Login_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *keeperClient) RefreshTokens(ctx context.Context, in *RefreshTokensIn, opts ...grpc.CallOption) (*RefreshTokensOut, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RefreshTokensOut) + err := c.cc.Invoke(ctx, Keeper_RefreshTokens_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *keeperClient) GetByGuid(ctx context.Context, in *GetByGuidIn, opts ...grpc.CallOption) (*GetByGuidOut, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetByGuidOut) + err := c.cc.Invoke(ctx, Keeper_GetByGuid_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *keeperClient) GetAllItems(ctx context.Context, in *GetAllItemsIn, opts ...grpc.CallOption) (*GetAllItemsOut, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetAllItemsOut) + err := c.cc.Invoke(ctx, Keeper_GetAllItems_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *keeperClient) CreateItem(ctx context.Context, in *CreateItemIn, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Keeper_CreateItem_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *keeperClient) UpdateItem(ctx context.Context, in *UpdateItemIn, opts ...grpc.CallOption) (*UpdateItemOut, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateItemOut) + err := c.cc.Invoke(ctx, Keeper_UpdateItem_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *keeperClient) DeleteItem(ctx context.Context, in *DeleteItemIn, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Keeper_DeleteItem_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// KeeperServer is the server API for Keeper service. +// All implementations should embed UnimplementedKeeperServer +// for forward compatibility. +type KeeperServer interface { + // auth + Register(context.Context, *RegisterIn) (*RegisterOut, error) + Login(context.Context, *LoginIn) (*LoginOut, error) + RefreshTokens(context.Context, *RefreshTokensIn) (*RefreshTokensOut, error) + // data + GetByGuid(context.Context, *GetByGuidIn) (*GetByGuidOut, error) + GetAllItems(context.Context, *GetAllItemsIn) (*GetAllItemsOut, error) + CreateItem(context.Context, *CreateItemIn) (*emptypb.Empty, error) + UpdateItem(context.Context, *UpdateItemIn) (*UpdateItemOut, error) + DeleteItem(context.Context, *DeleteItemIn) (*emptypb.Empty, error) +} + +// UnimplementedKeeperServer should be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedKeeperServer struct{} + +func (UnimplementedKeeperServer) Register(context.Context, *RegisterIn) (*RegisterOut, error) { + return nil, status.Errorf(codes.Unimplemented, "method Register not implemented") +} +func (UnimplementedKeeperServer) Login(context.Context, *LoginIn) (*LoginOut, error) { + return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") +} +func (UnimplementedKeeperServer) RefreshTokens(context.Context, *RefreshTokensIn) (*RefreshTokensOut, error) { + return nil, status.Errorf(codes.Unimplemented, "method RefreshTokens not implemented") +} +func (UnimplementedKeeperServer) GetByGuid(context.Context, *GetByGuidIn) (*GetByGuidOut, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetByGuid not implemented") +} +func (UnimplementedKeeperServer) GetAllItems(context.Context, *GetAllItemsIn) (*GetAllItemsOut, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetAllItems not implemented") +} +func (UnimplementedKeeperServer) CreateItem(context.Context, *CreateItemIn) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateItem not implemented") +} +func (UnimplementedKeeperServer) UpdateItem(context.Context, *UpdateItemIn) (*UpdateItemOut, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateItem not implemented") +} +func (UnimplementedKeeperServer) DeleteItem(context.Context, *DeleteItemIn) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteItem not implemented") +} +func (UnimplementedKeeperServer) testEmbeddedByValue() {} + +// UnsafeKeeperServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to KeeperServer will +// result in compilation errors. +type UnsafeKeeperServer interface { + mustEmbedUnimplementedKeeperServer() +} + +func RegisterKeeperServer(s grpc.ServiceRegistrar, srv KeeperServer) { + // If the following call pancis, it indicates UnimplementedKeeperServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Keeper_ServiceDesc, srv) +} + +func _Keeper_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RegisterIn) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KeeperServer).Register(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Keeper_Register_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KeeperServer).Register(ctx, req.(*RegisterIn)) + } + return interceptor(ctx, in, info, handler) +} + +func _Keeper_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LoginIn) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KeeperServer).Login(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Keeper_Login_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KeeperServer).Login(ctx, req.(*LoginIn)) + } + return interceptor(ctx, in, info, handler) +} + +func _Keeper_RefreshTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RefreshTokensIn) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KeeperServer).RefreshTokens(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Keeper_RefreshTokens_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KeeperServer).RefreshTokens(ctx, req.(*RefreshTokensIn)) + } + return interceptor(ctx, in, info, handler) +} + +func _Keeper_GetByGuid_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetByGuidIn) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KeeperServer).GetByGuid(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Keeper_GetByGuid_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KeeperServer).GetByGuid(ctx, req.(*GetByGuidIn)) + } + return interceptor(ctx, in, info, handler) +} + +func _Keeper_GetAllItems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAllItemsIn) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KeeperServer).GetAllItems(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Keeper_GetAllItems_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KeeperServer).GetAllItems(ctx, req.(*GetAllItemsIn)) + } + return interceptor(ctx, in, info, handler) +} + +func _Keeper_CreateItem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateItemIn) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KeeperServer).CreateItem(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Keeper_CreateItem_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KeeperServer).CreateItem(ctx, req.(*CreateItemIn)) + } + return interceptor(ctx, in, info, handler) +} + +func _Keeper_UpdateItem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateItemIn) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KeeperServer).UpdateItem(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Keeper_UpdateItem_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KeeperServer).UpdateItem(ctx, req.(*UpdateItemIn)) + } + return interceptor(ctx, in, info, handler) +} + +func _Keeper_DeleteItem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteItemIn) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KeeperServer).DeleteItem(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Keeper_DeleteItem_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KeeperServer).DeleteItem(ctx, req.(*DeleteItemIn)) + } + return interceptor(ctx, in, info, handler) +} + +// Keeper_ServiceDesc is the grpc.ServiceDesc for Keeper service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Keeper_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "keeper.Keeper", + HandlerType: (*KeeperServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Register", + Handler: _Keeper_Register_Handler, + }, + { + MethodName: "Login", + Handler: _Keeper_Login_Handler, + }, + { + MethodName: "RefreshTokens", + Handler: _Keeper_RefreshTokens_Handler, + }, + { + MethodName: "GetByGuid", + Handler: _Keeper_GetByGuid_Handler, + }, + { + MethodName: "GetAllItems", + Handler: _Keeper_GetAllItems_Handler, + }, + { + MethodName: "CreateItem", + Handler: _Keeper_CreateItem_Handler, + }, + { + MethodName: "UpdateItem", + Handler: _Keeper_UpdateItem_Handler, + }, + { + MethodName: "DeleteItem", + Handler: _Keeper_DeleteItem_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/keeper.proto", +} diff --git a/internal/infrastructure/auth/context.go b/internal/infrastructure/auth/context.go new file mode 100644 index 0000000..2ef33fe --- /dev/null +++ b/internal/infrastructure/auth/context.go @@ -0,0 +1,31 @@ +package auth + +import ( + "context" + + "github.com/google/uuid" +) + +type ctxKeyUserGUID int + +const UserGUIDKey ctxKeyUserGUID = 0 + +func UserGUIDWithCtx(ctx context.Context, guid uuid.UUID) context.Context { + if ctxGUID, ok := ctx.Value(UserGUIDKey).(string); ok { + if ctxGUID == guid.String() { + return ctx + } + } + + return context.WithValue(ctx, UserGUIDKey, guid.String()) +} + +func UserGUIDFromCtx(ctx context.Context) uuid.UUID { + if s, ok := ctx.Value(UserGUIDKey).(string); ok { + if guid, err := uuid.Parse(s); err == nil { + return guid + } + } + + return uuid.Nil +} diff --git a/internal/infrastructure/auth/jwt.go b/internal/infrastructure/auth/jwt.go new file mode 100644 index 0000000..be6d6fa --- /dev/null +++ b/internal/infrastructure/auth/jwt.go @@ -0,0 +1,186 @@ +// Package auth отвечает за выпуск JWT токенов и работу с ними. +package auth + +import ( + "errors" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" +) + +const ( + prefixOp = "auth.jwt." + + subjectAccessToken = "access_token" + subjectRefreshToken = "refresh_token" +) + +var ( + ErrInvalidToken = errors.New("invalid token") + ErrUnexpectedSigningMethod = errors.New("unexpected signing method") + ErrUnexpectedTypeToken = errors.New("unexpected type token") +) + +type Claims struct { + jwt.RegisteredClaims +} + +type JWT struct { + secretKey string + accessTokenExp time.Duration + refreshTokenExp time.Duration +} + +func NewJWT(secretKey string, accessTokenExp, refreshTokenExp time.Duration) *JWT { + return &JWT{ + secretKey: secretKey, + accessTokenExp: accessTokenExp, + refreshTokenExp: refreshTokenExp, + } +} + +// GetUserGUIDFromAccessToken получает GUID пользователя из переданного access токена. +func (g *JWT) GetUserGUIDFromAccessToken(tokenString string) (uuid.UUID, error) { + const op = prefixOp + "GetUserGUIDFromAccessToken" + + token, clams, err := g.ParseToken(tokenString, subjectAccessToken) + if err != nil { + var e *jwt.ValidationError + if errors.As(err, &e) { + return uuid.Nil, ErrInvalidToken + } + return uuid.Nil, fmt.Errorf("%s: %w", op, err) + } + + if !token.Valid || clams.Issuer == "" { + return uuid.Nil, ErrInvalidToken + } + + guid, err := uuid.Parse(clams.Issuer) + if err != nil { + return uuid.Nil, fmt.Errorf("%s: %w", op, err) + } + + return guid, nil +} + +// GetUserGUIDFromRefreshToken получает GUID пользователя из переданного refresh токена. +func (g *JWT) GetUserGUIDFromRefreshToken(tokenString string) (uuid.UUID, error) { + const op = prefixOp + "GetUserGUIDFromRefreshToken" + + token, clams, err := g.ParseToken(tokenString, subjectRefreshToken) + if err != nil { + var e *jwt.ValidationError + if errors.As(err, &e) { + return uuid.Nil, ErrInvalidToken + } + return uuid.Nil, fmt.Errorf("%s: %w", op, err) + } + + if !token.Valid || clams.Issuer == "" { + return uuid.Nil, ErrInvalidToken + } + + guid, err := uuid.Parse(clams.Issuer) + if err != nil { + return uuid.Nil, fmt.Errorf("%s: %w", op, err) + } + + return guid, nil +} + +// GenerateTokens генерирует и возвращает пару токенов: access и refresh. +func (g *JWT) GenerateTokens(uuid string) (accessToken string, refreshToken string, err error) { + const op = prefixOp + "GenerateTokens" + + var claim *Claims + + accessToken, claim, err = g.GenerateAccessToken(uuid) + if err != nil { + err = fmt.Errorf("%s: %w", op, err) + return + } + + refreshToken, err = g.GenerateRefreshToken(claim) + if err != nil { + err = fmt.Errorf("%s: %w", op, err) + } + + return +} + +// GenerateAccessToken генерирует и возвращает access токен. +// В claims токена кладет GUID переданного пользователя. +// Используется для аутентификации пользователя. +func (g *JWT) GenerateAccessToken(uuid string) (string, *Claims, error) { + const op = prefixOp + "GenerateAccessToken" + + now := time.Now() + claim := &Claims{ + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: uuid, + ExpiresAt: jwt.NewNumericDate(now.Add(g.accessTokenExp)), + Subject: subjectAccessToken, + IssuedAt: jwt.NewNumericDate(now), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim) + tokenString, err := token.SignedString([]byte(g.secretKey)) + if err != nil { + return "", nil, fmt.Errorf("%s: %w", op, err) + } + + return tokenString, claim, nil +} + +// GenerateRefreshToken генерирует и возвращает refresh токен. +// В claims токена кладет GUID переданного пользователя. +// Используется для обновления access и refresh токенов. +func (g *JWT) GenerateRefreshToken(cl *Claims) (string, error) { + const op = prefixOp + "GenerateRefreshToken" + + now := time.Now() + claim := &Claims{ + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: cl.Issuer, + ExpiresAt: jwt.NewNumericDate(now.Add(g.refreshTokenExp)), + Subject: subjectRefreshToken, + IssuedAt: jwt.NewNumericDate(now), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim) + tokenString, err := token.SignedString([]byte(g.secretKey)) + if err != nil { + return "", fmt.Errorf("%s: %w", op, err) + } + + return tokenString, nil +} + +// ParseToken парсит токен из его строкового представления в tokenString. +func (g *JWT) ParseToken(tokenString, subjectClaim string) (*jwt.Token, *Claims, error) { + const op = prefixOp + "ParseToken" + + c := &Claims{} + fn := func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("%w: %v", ErrUnexpectedSigningMethod, t.Header["alg"]) + } + claim, ok := t.Claims.(*Claims) + if ok && claim.Subject != subjectClaim { + return nil, fmt.Errorf("%w: %v", ErrUnexpectedTypeToken, claim.Subject) + } + return []byte(g.secretKey), nil + } + + t, err := jwt.ParseWithClaims(tokenString, c, fn) + if err != nil { + return nil, nil, fmt.Errorf("%s: %w", op, err) + } + + return t, c, nil +} diff --git a/internal/infrastructure/crypt/cipher/cipher.go b/internal/infrastructure/crypt/cipher/cipher.go new file mode 100644 index 0000000..2bf6cd1 --- /dev/null +++ b/internal/infrastructure/crypt/cipher/cipher.go @@ -0,0 +1,66 @@ +// Package cipher отвечает за шифрование и дешифровку данных по переданному ключу. +package cipher + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "io" + "sync" +) + +type Cipher struct { + once sync.Once + gcmInstance cipher.AEAD +} + +// Encrypt шифрует переданные данные data используя переданный ключ key. +func (c *Cipher) Encrypt(data, key []byte) ([]byte, error) { + err := c.setup(key) + if err != nil { + return nil, err + } + + nonce := make([]byte, c.gcmInstance.NonceSize()) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return nil, err + } + + return c.gcmInstance.Seal(nonce, nonce, data, nil), nil +} + +// Decrypt дешифрует переданные зашифрованные данные в encryptedData используя переданный ключ key. +func (c *Cipher) Decrypt(encryptedData, key []byte) ([]byte, error) { + err := c.setup(key) + if err != nil { + return nil, err + } + + nonceSize := c.gcmInstance.NonceSize() + nonce, cipheredText := encryptedData[:nonceSize], encryptedData[nonceSize:] + + return c.gcmInstance.Open(nil, nonce, cipheredText, nil) +} + +func (c *Cipher) setup(key []byte) error { + var err error + + c.once.Do(func() { + var aesBlock cipher.Block + aesBlock, err = aes.NewCipher(key) + if err != nil { + return + } + + var gcmInstance cipher.AEAD + gcmInstance, err = cipher.NewGCM(aesBlock) + if err != nil { + return + } + + c.gcmInstance = gcmInstance + }) + + return err +} diff --git a/internal/infrastructure/crypt/master_key/keygen.go b/internal/infrastructure/crypt/master_key/keygen.go new file mode 100644 index 0000000..eef9723 --- /dev/null +++ b/internal/infrastructure/crypt/master_key/keygen.go @@ -0,0 +1,27 @@ +package master_key + +import ( + "crypto/sha512" + + "golang.org/x/crypto/pbkdf2" +) + +// KeyGenerator генератор мастер ключа на основе алгоритма PBKDF2. +// В iteCount количество итераций при генерации ключа. +// В keyLen длина ключа. +type KeyGenerator struct { + iteCount int + keyLen int +} + +func NewKeyGenerator(iterCount, keyLen int) *KeyGenerator { + return &KeyGenerator{ + iteCount: iterCount, + keyLen: keyLen, + } +} + +// GenerateMasterKey генерирует мастер ключ для переданного пароля password используя соль salt. +func (g KeyGenerator) GenerateMasterKey(password, salt []byte) []byte { + return pbkdf2.Key(password, salt, g.iteCount, g.keyLen, sha512.New) +} diff --git a/internal/infrastructure/crypt/master_key/salt.go b/internal/infrastructure/crypt/master_key/salt.go new file mode 100644 index 0000000..37953ee --- /dev/null +++ b/internal/infrastructure/crypt/master_key/salt.go @@ -0,0 +1,44 @@ +package master_key + +import ( + "crypto/rand" + "encoding/base64" +) + +// Salt соль для мастер ключа. +type Salt []byte + +// NewSalt создает соль указанной длины length. +func NewSalt(length int) (*Salt, error) { + salt := make([]byte, length) + _, err := rand.Read(salt) + if err != nil { + return nil, err + } + + s := Salt(salt) + + return &s, nil +} + +// ParseString создание структуры Salt из строкового представления соли. +func ParseString(salt string) (*Salt, error) { + b, err := base64.StdEncoding.DecodeString(salt) + if err != nil { + return nil, err + } + + s := Salt(b) + + return &s, nil +} + +// Value возвращает значение соли. +func (s Salt) Value() []byte { + return s +} + +// ToString encoding Salt в строку. +func (s Salt) ToString() string { + return base64.StdEncoding.EncodeToString(s) +} diff --git a/internal/infrastructure/crypt/master_key/saltgen.go b/internal/infrastructure/crypt/master_key/saltgen.go new file mode 100644 index 0000000..a5693b3 --- /dev/null +++ b/internal/infrastructure/crypt/master_key/saltgen.go @@ -0,0 +1,19 @@ +package master_key + +// SaltGenerator генератор соли Salt. +// В length длина соли. +type SaltGenerator struct { + length int +} + +// NewSaltGenerator создает генератор соли с указанной длиной в length. +func NewSaltGenerator(length int) *SaltGenerator { + return &SaltGenerator{ + length: length, + } +} + +// GenerateSalt генерирует соль. +func (g SaltGenerator) GenerateSalt() (*Salt, error) { + return NewSalt(g.length) +} diff --git a/internal/infrastructure/db/pg/dsn.go b/internal/infrastructure/db/pg/dsn.go new file mode 100644 index 0000000..def2b4b --- /dev/null +++ b/internal/infrastructure/db/pg/dsn.go @@ -0,0 +1,15 @@ +package pg + +import "fmt" + +// GetDSN вспомогательная функция для получения валидного DSN для подключения БД PostgreSQL. +func GetDSN(host, port, name, user, pass string) string { + return fmt.Sprintf( + "postgresql://%s:%s@%s:%s/%s?sslmode=disable", + user, + pass, + host, + port, + name, + ) +} diff --git a/internal/infrastructure/db/pg/dsn_test.go b/internal/infrastructure/db/pg/dsn_test.go new file mode 100644 index 0000000..0b74e94 --- /dev/null +++ b/internal/infrastructure/db/pg/dsn_test.go @@ -0,0 +1,15 @@ +package pg_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/bjlag/go-keeper/internal/infrastructure/db/pg" +) + +func TestGetDSN(t *testing.T) { + got := pg.GetDSN("host", "1111", "database_name", "user", "pass") + + assert.Equal(t, "postgresql://user:pass@host:1111/database_name?sslmode=disable", got) +} diff --git a/internal/infrastructure/db/pg/pg.go b/internal/infrastructure/db/pg/pg.go new file mode 100644 index 0000000..372ca21 --- /dev/null +++ b/internal/infrastructure/db/pg/pg.go @@ -0,0 +1,45 @@ +// Package pg отвечает за работу с базой данных PostgreSQL. +package pg + +import ( + "fmt" + "time" + + _ "github.com/jackc/pgx/v5/stdlib" // pgx driver + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" // postgres driver +) + +const ( + maxOpenConnects = 5 + maxIdleConnects = 5 + connMaxLifetime = 5 * time.Minute + connMaxIdleTime = 5 * time.Minute +) + +type PG struct { + dsn string +} + +func New(dsn string) *PG { + return &PG{ + dsn: dsn, + } +} + +// Connect создает подключение к базе. +func (p *PG) Connect() (*sqlx.DB, error) { + const op = "pg.Connect" + + db, err := sqlx.Connect("pgx", p.dsn) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + db.SetMaxOpenConns(maxOpenConnects) + db.SetMaxIdleConns(maxIdleConnects) + db.SetConnMaxLifetime(connMaxLifetime) + db.SetConnMaxIdleTime(connMaxIdleTime) + + return db, nil +} diff --git a/internal/infrastructure/db/pg/pg_test.go b/internal/infrastructure/db/pg/pg_test.go new file mode 100644 index 0000000..2e2eed6 --- /dev/null +++ b/internal/infrastructure/db/pg/pg_test.go @@ -0,0 +1,15 @@ +package pg_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/bjlag/go-keeper/internal/infrastructure/db/pg" +) + +func TestPG_Connect(t *testing.T) { + got, err := pg.New("bad_dsn").Connect() + assert.Error(t, err) //nolint:testifylint + assert.Nil(t, got) +} diff --git a/internal/infrastructure/db/sqlite/sqlite.go b/internal/infrastructure/db/sqlite/sqlite.go new file mode 100644 index 0000000..07c2be7 --- /dev/null +++ b/internal/infrastructure/db/sqlite/sqlite.go @@ -0,0 +1,31 @@ +// Package sqlite отвечает за работу с базой данных SQLite. +package sqlite + +import ( + "fmt" + + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" +) + +type SQLite struct { + dsn string +} + +func New(dsn string) *SQLite { + return &SQLite{ + dsn: dsn, + } +} + +// Connect создает подключение к базе. +func (l SQLite) Connect() (*sqlx.DB, error) { + const op = "sqlite.Connect" + + db, err := sqlx.Connect("sqlite3", l.dsn) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + return db, nil +} diff --git a/internal/infrastructure/logger/logger.go b/internal/infrastructure/logger/logger.go new file mode 100644 index 0000000..ca044c9 --- /dev/null +++ b/internal/infrastructure/logger/logger.go @@ -0,0 +1,74 @@ +// Package logger отвечает за создание логгера и работу с ним. +package logger + +import ( + "context" + "os" + "runtime" + "sync" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type ctxKeyLogger int + +// IDLoggerKey ID ключа для работы с логгером через контекст. +const IDLoggerKey ctxKeyLogger = 0 + +var ( + once sync.Once + logger *zap.Logger +) + +// Get получить логгер. Используется паттерн Singletone. +// При первом получении создается экземпляр логгера и кладется в глобальную переменную пакета. +func Get(env string) *zap.Logger { + once.Do(func() { + if env == "test" { + logger = zap.NewNop() + return + } + + var config zap.Config + + if env == "prod" { + config = zap.NewProductionConfig() + } else { + config = zap.NewDevelopmentConfig() + } + + config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + config.InitialFields = map[string]interface{}{ + "env": env, + "pid": os.Getpid(), + "go_version": runtime.Version(), + } + + logger = zap.Must(config.Build()) + }) + + return logger +} + +// FromCtx получает логгер из контекста. +func FromCtx(ctx context.Context) *zap.Logger { + if l, ok := ctx.Value(IDLoggerKey).(*zap.Logger); ok { + return l + } else if l := logger; l != nil { + return l + } + + return zap.NewNop() +} + +// WithCtx кладет логгер в контекст. +func WithCtx(ctx context.Context, l *zap.Logger) context.Context { + if lp, ok := ctx.Value(IDLoggerKey).(*zap.Logger); ok { + if lp == l { + return ctx + } + } + + return context.WithValue(ctx, IDLoggerKey, l) +} diff --git a/internal/infrastructure/migrator/config.go b/internal/infrastructure/migrator/config.go new file mode 100644 index 0000000..0c53904 --- /dev/null +++ b/internal/infrastructure/migrator/config.go @@ -0,0 +1,27 @@ +package migrator + +// Config хранит конфигурацию мигратора. +type Config struct { + // Env окружение. + Env string `yaml:"env" env:"ENV" env-default:"dev" env-description:"Environment" json:"env"` + // SourcePath путь до файлов миграций. + SourcePath string `yaml:"sourcePath" env:"MIGRATIONS_SOURCE_PATH" env-default:"./migrations" env-description:"Migrations source path" json:"source_path"` + // MigrationsTable название таблицы с примененными миграциями. + MigrationsTable string `yaml:"migrationsTable" env:"MIGRATIONS_TABLE" env-default:"migrations" env-description:"Migrations table name" json:"migrations_table"` + + // Database настройки подключения к базе данной, к которой применяются миграции. + Database struct { + // Type тип базы данных: pg - PostgreSQL, sqlite - SQLite. + Type DBType `yaml:"type" env:"DB_TYPE" env-default:"pg" env-description:"Database type" json:"type"` + // Host хост базы. + Host string `yaml:"host" env:"DB_HOST" env-description:"Database host" json:"host"` + // Port порт базы. + Port string `yaml:"port" env:"DB_PORT" env-description:"Database port" json:"port"` + // Name название базы данных. + Name string `yaml:"name" env:"DB_NAME" env-description:"Database name" json:"name"` + // User пользователь. + User string `yaml:"user" env:"DB_USER" env-description:"Database user" json:"user"` + // Password пароль. + Password string `yaml:"password" env:"DB_PASSWORD" env-description:"Database password" json:"password"` + } `yaml:"database" json:"database"` +} diff --git a/internal/infrastructure/migrator/migrator.go b/internal/infrastructure/migrator/migrator.go new file mode 100644 index 0000000..4a128f6 --- /dev/null +++ b/internal/infrastructure/migrator/migrator.go @@ -0,0 +1,57 @@ +package migrator + +import ( + "fmt" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database" + "github.com/golang-migrate/migrate/v4/database/pgx/v5" + "github.com/golang-migrate/migrate/v4/database/sqlite3" + _ "github.com/golang-migrate/migrate/v4/source/file" + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" +) + +type DBType string + +// Константы содержат поддерживаемые базы данных. +const ( + TypePG DBType = "pg" + TypeSqlite DBType = "sqlite" +) + +// Get возвращает настроенный экземпляр мигратора. +func Get(db *sqlx.DB, dbType DBType, dbName, sourcePath, migrationsTable string) (*migrate.Migrate, error) { + const op = "migrator.Init" + + driver, err := getDBDriver(db, dbType, migrationsTable) + if err != nil { + return nil, fmt.Errorf("%s get driver: %w", op, err) + } + + m, err := migrate.NewWithDatabaseInstance( + fmt.Sprintf("file://%s", sourcePath), + dbName, + driver, + ) + if err != nil { + return nil, fmt.Errorf("%s create migrator instance: %w", op, err) + } + + return m, nil +} + +func getDBDriver(db *sqlx.DB, dbType DBType, migrationsTable string) (database.Driver, error) { + switch dbType { + case TypePG: + return pgx.WithInstance(db.DB, &pgx.Config{ + MigrationsTable: migrationsTable, + }) + case TypeSqlite: + return sqlite3.WithInstance(db.DB, &sqlite3.Config{ + MigrationsTable: migrationsTable, + }) + default: + return nil, fmt.Errorf("uknown database type: %s", dbType) //nolint:err113 + } +} diff --git a/internal/infrastructure/rpc/client/client.go b/internal/infrastructure/rpc/client/client.go new file mode 100644 index 0000000..75a1540 --- /dev/null +++ b/internal/infrastructure/rpc/client/client.go @@ -0,0 +1,46 @@ +// Package client содержит логику для работы с сервером через RPC протокол. +package client + +import ( + "fmt" + + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/internal/infrastructure/rpc/interceptor" + "github.com/bjlag/go-keeper/internal/infrastructure/store/client/token" +) + +type RPCClient struct { + conn *grpc.ClientConn + client rpc.KeeperClient +} + +// NewRPCClient создает gRPC подключение к серверу расположенному на хосте serverHost и порте serverPort. +// tokensStore используется для интерцептора отвечающего за авторизацию клиента. +// log используется для интерцептора по логированию запросов. +func NewRPCClient(serverHost string, serverPort int, tokensStore *token.Store, log *zap.Logger) (*RPCClient, error) { + conn, err := grpc.NewClient( + fmt.Sprintf("%s:%d", serverHost, serverPort), + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithChainUnaryInterceptor( + interceptor.LoggerClientInterceptor(log), + interceptor.AuthClientInterceptor(tokensStore), + ), + ) + if err != nil { + return nil, err + } + + return &RPCClient{ + conn: conn, + client: rpc.NewKeeperClient(conn), + }, nil +} + +// Close закрывает подключение. +func (c RPCClient) Close() error { + return c.conn.Close() +} diff --git a/internal/infrastructure/rpc/client/create_item.go b/internal/infrastructure/rpc/client/create_item.go new file mode 100644 index 0000000..1949be6 --- /dev/null +++ b/internal/infrastructure/rpc/client/create_item.go @@ -0,0 +1,37 @@ +package client + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/bjlag/go-keeper/internal/generated/rpc" +) + +// CreateItemIn данные создаваемого элемента. +type CreateItemIn struct { + GUID uuid.UUID // GUID создаваемого элемента. + EncryptedData []byte // EncryptedData секретные данные элемента в зашифрованном виде. + CreatedAt time.Time // CreatedAt дата и время создания элемента. +} + +// CreateItem метод для создания элемента. +func (c RPCClient) CreateItem(ctx context.Context, in *CreateItemIn) error { + const op = "client.rpc.CreateItem" + + rpcIn := &rpc.CreateItemIn{ + Guid: in.GUID.String(), + EncryptedData: in.EncryptedData, + CreatedAt: timestamppb.New(in.CreatedAt), + } + + _, err := c.client.CreateItem(ctx, rpcIn) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} diff --git a/internal/infrastructure/rpc/client/delete_item.go b/internal/infrastructure/rpc/client/delete_item.go new file mode 100644 index 0000000..b0eabb6 --- /dev/null +++ b/internal/infrastructure/rpc/client/delete_item.go @@ -0,0 +1,31 @@ +package client + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/bjlag/go-keeper/internal/generated/rpc" +) + +// DeleteItemIn данные удаляемого элемента. +type DeleteItemIn struct { + GUID uuid.UUID // GUID удаляемого элемента. +} + +// DeleteItem метод для удаления элемента. +func (c RPCClient) DeleteItem(ctx context.Context, in *DeleteItemIn) error { + const op = "client.rpc.DeleteItem" + + rpcIn := &rpc.DeleteItemIn{ + Guid: in.GUID.String(), + } + + _, err := c.client.DeleteItem(ctx, rpcIn) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} diff --git a/internal/infrastructure/rpc/client/get_all_items.go b/internal/infrastructure/rpc/client/get_all_items.go new file mode 100644 index 0000000..4558f4b --- /dev/null +++ b/internal/infrastructure/rpc/client/get_all_items.go @@ -0,0 +1,68 @@ +package client + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + + "github.com/bjlag/go-keeper/internal/generated/rpc" +) + +// GetAllItemsIn параметры запроса. +type GetAllItemsIn struct { + Limit uint32 // Limit сколько получить данных максимум. + Offset uint32 // Offset с какой позиции получать данные. +} + +// GetAllItemsOut результат работы. +type GetAllItemsOut struct { + Items []GetAllDataItem // Items список полученных элементов. +} + +// GetAllDataItem данные полученного элемента. +type GetAllDataItem struct { + GUID uuid.UUID // GUID идентификатор. + EncryptedData []byte // EncryptedData зашифрованные данные. + CreatedAt time.Time // CreatedAt дата и время создания. + UpdatedAt time.Time // UpdatedAt дата и время обновления. +} + +// GetAllItems метод для получения всех данных авторизованного пользователя. +func (c RPCClient) GetAllItems(ctx context.Context, in *GetAllItemsIn) (*GetAllItemsOut, error) { + const op = "client.rpc.GetAllItems" + + rpcIn := &rpc.GetAllItemsIn{ + Limit: in.Limit, + Offset: in.Offset, + } + + out, err := c.client.GetAllItems(ctx, rpcIn) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + if out == nil || len(out.GetItems()) == 0 { + return nil, nil + } + + items := make([]GetAllDataItem, len(out.GetItems())) + for i, item := range out.GetItems() { + guid, err := uuid.Parse(item.GetGuid()) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + items[i] = GetAllDataItem{ + GUID: guid, + EncryptedData: item.GetEncryptedData(), + CreatedAt: item.GetCreatedAt().AsTime(), + UpdatedAt: item.GetUpdatedAt().AsTime(), + } + } + + return &GetAllItemsOut{ + Items: items, + }, nil +} diff --git a/internal/infrastructure/rpc/client/get_by_guid.go b/internal/infrastructure/rpc/client/get_by_guid.go new file mode 100644 index 0000000..197ab34 --- /dev/null +++ b/internal/infrastructure/rpc/client/get_by_guid.go @@ -0,0 +1,50 @@ +package client + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + + "github.com/bjlag/go-keeper/internal/generated/rpc" +) + +// GetByGUIDIn параметры запроса. +type GetByGUIDIn struct { + GUID uuid.UUID // GUID элемента, данные которого надо получить. +} + +// GetByGUIDOut результат работы. +type GetByGUIDOut struct { + GUID uuid.UUID // GUID идентификатор элемента. + EncryptedData []byte // EncryptedData зашифрованные данные элемента. + CreatedAt time.Time // CreatedAt дата и время создания элемента. + UpdatedAt time.Time // UpdatedAt дата и время обновления элемента. +} + +// GetByGUID метод для получения элемента по его GUID. +func (c RPCClient) GetByGUID(ctx context.Context, in *GetByGUIDIn) (*GetByGUIDOut, error) { + const op = "client.rpc.GetByGUID" + + rpcIn := &rpc.GetByGuidIn{ + Guid: in.GUID.String(), + } + + out, err := c.client.GetByGuid(ctx, rpcIn) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + itemGUID, err := uuid.Parse(out.GetGuid()) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + return &GetByGUIDOut{ + GUID: itemGUID, + EncryptedData: out.GetEncryptedData(), + CreatedAt: out.GetCreatedAt().AsTime(), + UpdatedAt: out.GetUpdatedAt().AsTime(), + }, nil +} diff --git a/internal/infrastructure/rpc/client/login.go b/internal/infrastructure/rpc/client/login.go new file mode 100644 index 0000000..b608fcd --- /dev/null +++ b/internal/infrastructure/rpc/client/login.go @@ -0,0 +1,40 @@ +package client + +import ( + "context" + "fmt" + + "github.com/bjlag/go-keeper/internal/generated/rpc" +) + +// LoginIn параметры запроса. +type LoginIn struct { + Email string // Email пользователя. + Password string // Password пароль пользователя. +} + +// LoginOut результат. +type LoginOut struct { + AccessToken string // AccessToken access токен. + RefreshToken string // RefreshToken refresh токен. +} + +// Login метод для аутентификации пользователя. +func (c RPCClient) Login(ctx context.Context, in LoginIn) (*LoginOut, error) { + const op = "client.rpc.Login" + + rpcIn := &rpc.LoginIn{ + Email: in.Email, + Password: in.Password, + } + + out, err := c.client.Login(ctx, rpcIn) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + return &LoginOut{ + AccessToken: out.GetAccessToken(), + RefreshToken: out.GetRefreshToken(), + }, nil +} diff --git a/internal/infrastructure/rpc/client/refresh_tokens.go b/internal/infrastructure/rpc/client/refresh_tokens.go new file mode 100644 index 0000000..ea0c02f --- /dev/null +++ b/internal/infrastructure/rpc/client/refresh_tokens.go @@ -0,0 +1,38 @@ +package client + +import ( + "context" + "fmt" + + "github.com/bjlag/go-keeper/internal/generated/rpc" +) + +// RefreshTokensIn параметры запроса. +type RefreshTokensIn struct { + RefreshToken string // RefreshToken refresh токен. +} + +// RefreshTokensOut результат. +type RefreshTokensOut struct { + AccessToken string // AccessToken access токен. + RefreshToken string // RefreshToken refresh токен. +} + +// RefreshTokens метод для обновления токенов используя refresh токен. +func (c RPCClient) RefreshTokens(ctx context.Context, in RefreshTokensIn) (*RefreshTokensOut, error) { + const op = "client.rpc.RefreshTokens" + + rpcIn := &rpc.RefreshTokensIn{ + RefreshToken: in.RefreshToken, + } + + out, err := c.client.RefreshTokens(ctx, rpcIn) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + return &RefreshTokensOut{ + AccessToken: out.GetAccessToken(), + RefreshToken: out.GetRefreshToken(), + }, nil +} diff --git a/internal/infrastructure/rpc/client/register.go b/internal/infrastructure/rpc/client/register.go new file mode 100644 index 0000000..44a4c11 --- /dev/null +++ b/internal/infrastructure/rpc/client/register.go @@ -0,0 +1,40 @@ +package client + +import ( + "context" + "fmt" + + "github.com/bjlag/go-keeper/internal/generated/rpc" +) + +// RegisterIn параметры запроса. +type RegisterIn struct { + Email string // Email пользователя. + Password string // Password пароль пользователя. +} + +// RegisterOut результат. +type RegisterOut struct { + AccessToken string // AccessToken access токен. + RefreshToken string // RefreshToken refresh токен. +} + +// Register метод для регистрации пользователя. +func (c RPCClient) Register(ctx context.Context, in RegisterIn) (*RegisterOut, error) { + const op = "client.rpc.Register" + + rpcIn := &rpc.RegisterIn{ + Email: in.Email, + Password: in.Password, + } + + out, err := c.client.Register(ctx, rpcIn) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + return &RegisterOut{ + AccessToken: out.GetAccessToken(), + RefreshToken: out.GetRefreshToken(), + }, nil +} diff --git a/internal/infrastructure/rpc/client/update_item.go b/internal/infrastructure/rpc/client/update_item.go new file mode 100644 index 0000000..cf72d38 --- /dev/null +++ b/internal/infrastructure/rpc/client/update_item.go @@ -0,0 +1,35 @@ +package client + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/bjlag/go-keeper/internal/generated/rpc" +) + +// UpdateItemIn параметры запроса. +type UpdateItemIn struct { + GUID uuid.UUID // GUID идентификатор обновляемого элемента. + EncryptedData []byte // EncryptedData зашифрованные данные элемента + Version int64 // Version версия, с которой обновляем элемент (текущая версия). +} + +// UpdateItem метод для обновления элемента. +func (c RPCClient) UpdateItem(ctx context.Context, in *UpdateItemIn) (int64, error) { + const op = "client.rpc.UpdateItem" + + rpcIn := &rpc.UpdateItemIn{ + Guid: in.GUID.String(), + EncryptedData: in.EncryptedData, + Version: in.Version, + } + + rpcOut, err := c.client.UpdateItem(ctx, rpcIn) + if err != nil { + return 0, fmt.Errorf("%s: %w", op, err) + } + + return rpcOut.GetNewVersion(), nil +} diff --git a/internal/infrastructure/rpc/interceptor/auth.go b/internal/infrastructure/rpc/interceptor/auth.go new file mode 100644 index 0000000..c7c7bc1 --- /dev/null +++ b/internal/infrastructure/rpc/interceptor/auth.go @@ -0,0 +1,106 @@ +package interceptor + +import ( + "context" + "errors" + "fmt" + "strings" + + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/internal/infrastructure/auth" + "github.com/bjlag/go-keeper/internal/infrastructure/store/client/token" +) + +const ( + // authMeta мета заголовок, в котором лежит access токен. + authMeta = "authorization" + // bearerAuth метод аутентификации. + bearerAuth = "Bearer" +) + +// methodSkip содержит gRPC методы, у которых не надо проверять аутентификацию. +var methodSkip = map[string]struct{}{ + rpc.Keeper_Login_FullMethodName: {}, + rpc.Keeper_Register_FullMethodName: {}, + rpc.Keeper_RefreshTokens_FullMethodName: {}, +} + +// CheckAccessTokenServerInterceptor клиентский интерцептор, который перед запросом к серверу кладет мета заголовок +// в запрос с access токеном, чтобы сервер мог проверить аутентифицирован пользователь или нет. +func CheckAccessTokenServerInterceptor(jwt *auth.JWT, log *zap.Logger) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if _, ok := methodSkip[info.FullMethod]; ok { + return handler(ctx, req) + } + + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + meta := md.Get(authMeta) + if len(meta) == 0 { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + accessToken, found := strings.CutPrefix(meta[0], bearerAuth) + if !found { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + userGUID, err := jwt.GetUserGUIDFromAccessToken(strings.TrimLeft(accessToken, " ")) + if err != nil { + if errors.Is(err, auth.ErrInvalidToken) { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + log.Error("Failed to get user GUID", zap.Error(err)) + return nil, status.Errorf(codes.Internal, "internal error") + } + + return handler(auth.UserGUIDWithCtx(ctx, userGUID), req) + } +} + +// AuthClientInterceptor интерцептор, который на стороне сервера, перед исполнением запроса, проверяет +// по переданной информации с клиента, аутентифицирован пользователь или нет. +func AuthClientInterceptor(tokens *token.Store) grpc.UnaryClientInterceptor { + return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + if _, ok := methodSkip[method]; ok { + return invoker(ctx, method, req, reply, cc, opts...) + } + + ctx = metadata.AppendToOutgoingContext(ctx, authMeta, fmt.Sprintf("%s %s", bearerAuth, tokens.AccessToken())) + err := invoker(ctx, method, req, reply, cc, opts...) + if status.Code(err) == codes.PermissionDenied { + in := &rpc.RefreshTokensIn{ + RefreshToken: tokens.RefreshToken(), + } + out := &rpc.RefreshTokensOut{} + err = cc.Invoke(ctx, rpc.Keeper_RefreshTokens_FullMethodName, in, out) + if err != nil { + if status.Code(err) == codes.FailedPrecondition { + return status.Errorf(codes.PermissionDenied, err.Error()) + } + return err + } + + tokens.SaveTokens(out.GetAccessToken(), out.GetRefreshToken()) + + md, ok := metadata.FromOutgoingContext(ctx) + if !ok { + md = metadata.New(nil) + } + + md.Set(authMeta, fmt.Sprintf("%s %s", bearerAuth, tokens.AccessToken())) + err = invoker(metadata.NewOutgoingContext(ctx, md), method, req, reply, cc, opts...) + } + + return err + } +} diff --git a/internal/infrastructure/rpc/interceptor/logger.go b/internal/infrastructure/rpc/interceptor/logger.go new file mode 100644 index 0000000..2406834 --- /dev/null +++ b/internal/infrastructure/rpc/interceptor/logger.go @@ -0,0 +1,46 @@ +package interceptor + +import ( + "context" + + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/status" + + "github.com/bjlag/go-keeper/internal/infrastructure/logger" +) + +// LoggerServerInterceptor интерцептор логирующий запросы на стороне сервера. +func LoggerServerInterceptor(log *zap.Logger) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + hLog := log + hLog = hLog. + With(zap.String("method", info.FullMethod)). + With(zap.Any("request", req)) + + resp, err := handler(logger.WithCtx(ctx, hLog), req) + + hLog.Info("Got RPC request", + zap.Error(err), + zap.String("code", status.Code(err).String()), + ) + + return resp, err + } +} + +// LoggerClientInterceptor интерцептор логирующий запросы на стороне клиента. +func LoggerClientInterceptor(log *zap.Logger) grpc.UnaryClientInterceptor { + return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + err := invoker(ctx, method, req, reply, cc, opts...) + + hLog := log + hLog.Info("Send RPC request", + zap.String("method", method), + zap.Any("request", req), + zap.String("code", status.Code(err).String()), + ) + + return err + } +} diff --git a/internal/infrastructure/rpc/server/method.go b/internal/infrastructure/rpc/server/method.go new file mode 100644 index 0000000..a695431 --- /dev/null +++ b/internal/infrastructure/rpc/server/method.go @@ -0,0 +1,152 @@ +package server + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + pb "github.com/bjlag/go-keeper/internal/generated/rpc" +) + +// Зарегистрированные методы. +const ( + RegisterMethod = "Register" + LoginMethod = "Login" + RefreshTokensMethod = "RefreshTokens" + GetByGUIDMethod = "GetByGUID" + GetAllItemsMethod = "GetAllItems" + CreateItemMethod = "CreateItem" + UpdateItemMethod = "UpdateItem" + DeleteItemMethod = "DeleteItem" +) + +// Register регистрация пользователя. +func (s RPCServer) Register(ctx context.Context, in *pb.RegisterIn) (*pb.RegisterOut, error) { + handler, err := s.getHandler(RegisterMethod) + if err != nil { + return nil, err + } + + h, ok := handler.(func(context.Context, *pb.RegisterIn) (*pb.RegisterOut, error)) + if !ok { + return nil, status.Errorf(codes.Internal, "handler for %s method not found", RegisterMethod) + } + + return h(ctx, in) +} + +// Login аутентификация пользователя. +func (s RPCServer) Login(ctx context.Context, in *pb.LoginIn) (*pb.LoginOut, error) { + handler, err := s.getHandler(LoginMethod) + if err != nil { + return nil, err + } + + h, ok := handler.(func(context.Context, *pb.LoginIn) (*pb.LoginOut, error)) + if !ok { + return nil, status.Errorf(codes.Internal, "handler for %s method not found", LoginMethod) + } + + return h(ctx, in) +} + +// RefreshTokens обновление токенов. +func (s RPCServer) RefreshTokens(ctx context.Context, in *pb.RefreshTokensIn) (*pb.RefreshTokensOut, error) { + handler, err := s.getHandler(RefreshTokensMethod) + if err != nil { + return nil, err + } + + h, ok := handler.(func(context.Context, *pb.RefreshTokensIn) (*pb.RefreshTokensOut, error)) + if !ok { + return nil, status.Errorf(codes.Internal, "handler for %s method not found", RefreshTokensMethod) + } + + return h(ctx, in) +} + +// GetByGuid получение элемента по его GUID. +func (s RPCServer) GetByGuid(ctx context.Context, in *pb.GetByGuidIn) (*pb.GetByGuidOut, error) { //nolint:revive + handler, err := s.getHandler(GetByGUIDMethod) + if err != nil { + return nil, err + } + + h, ok := handler.(func(context.Context, *pb.GetByGuidIn) (*pb.GetByGuidOut, error)) + if !ok { + return nil, status.Errorf(codes.Internal, "handler for %s method not found", GetByGUIDMethod) + } + + return h(ctx, in) +} + +// GetAllItems получение всех элементов. +func (s RPCServer) GetAllItems(ctx context.Context, in *pb.GetAllItemsIn) (*pb.GetAllItemsOut, error) { + handler, err := s.getHandler(GetAllItemsMethod) + if err != nil { + return nil, err + } + + h, ok := handler.(func(context.Context, *pb.GetAllItemsIn) (*pb.GetAllItemsOut, error)) + if !ok { + return nil, status.Errorf(codes.Internal, "handler for %s method not found", GetAllItemsMethod) + } + + return h(ctx, in) +} + +// CreateItem создание элемента. +func (s RPCServer) CreateItem(ctx context.Context, in *pb.CreateItemIn) (*emptypb.Empty, error) { + handler, err := s.getHandler(CreateItemMethod) + if err != nil { + return nil, err + } + + h, ok := handler.(func(context.Context, *pb.CreateItemIn) (*emptypb.Empty, error)) + if !ok { + return nil, status.Errorf(codes.Internal, "handler for %s method not found", CreateItemMethod) + } + + return h(ctx, in) +} + +// UpdateItem обновление элемента. +func (s RPCServer) UpdateItem(ctx context.Context, in *pb.UpdateItemIn) (*pb.UpdateItemOut, error) { + handler, err := s.getHandler(UpdateItemMethod) + if err != nil { + return nil, err + } + + h, ok := handler.(func(context.Context, *pb.UpdateItemIn) (*pb.UpdateItemOut, error)) + if !ok { + return nil, status.Errorf(codes.Internal, "handler for %s method not found", UpdateItemMethod) + } + + return h(ctx, in) +} + +// DeleteItem удаление элемента. +func (s RPCServer) DeleteItem(ctx context.Context, in *pb.DeleteItemIn) (*emptypb.Empty, error) { + handler, err := s.getHandler(DeleteItemMethod) + if err != nil { + return nil, err + } + + h, ok := handler.(func(context.Context, *pb.DeleteItemIn) (*emptypb.Empty, error)) + if !ok { + return nil, status.Errorf(codes.Internal, "handler for %s method not found", DeleteItemMethod) + } + + return h(ctx, in) +} + +func (s RPCServer) getHandler(name string) (any, error) { + handler, ok := s.handlers[name] + if !ok { + return nil, status.Errorf(codes.NotFound, "handler for %s methos not found", name) + } + + return handler, nil +} diff --git a/internal/infrastructure/rpc/server/option.go b/internal/infrastructure/rpc/server/option.go new file mode 100644 index 0000000..fae01e5 --- /dev/null +++ b/internal/infrastructure/rpc/server/option.go @@ -0,0 +1,40 @@ +package server + +import ( + "net" + + "go.uber.org/zap" + + "github.com/bjlag/go-keeper/internal/infrastructure/auth" +) + +// Option тип параметра сервера. +type Option func(*RPCServer) + +// WithListener передача сетевого прослушивателя сервера. +func WithListener(listener net.Listener) Option { + return func(s *RPCServer) { + s.listener = listener + } +} + +// WithLogger передача логгера. +func WithLogger(logger *zap.Logger) Option { + return func(s *RPCServer) { + s.log = logger + } +} + +// WithJWT передача сервиса для работы с JWT токенами. +func WithJWT(jwt *auth.JWT) Option { + return func(s *RPCServer) { + s.jwt = jwt + } +} + +// WithHandler регистрация обработчика запроса по указанному имени. +func WithHandler(method string, handler any) Option { + return func(s *RPCServer) { + s.handlers[method] = handler + } +} diff --git a/internal/infrastructure/rpc/server/server.go b/internal/infrastructure/rpc/server/server.go new file mode 100644 index 0000000..e9de25d --- /dev/null +++ b/internal/infrastructure/rpc/server/server.go @@ -0,0 +1,72 @@ +// Package server отвечает за работу gRPC сервера. +package server + +import ( + "context" + "fmt" + "net" + + "go.uber.org/zap" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + + pb "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/internal/infrastructure/auth" + "github.com/bjlag/go-keeper/internal/infrastructure/rpc/interceptor" +) + +type RPCServer struct { + pb.UnimplementedKeeperServer + + listener net.Listener + handlers map[string]any + jwt *auth.JWT + log *zap.Logger +} + +func NewRPCServer(opts ...Option) *RPCServer { + s := &RPCServer{ + handlers: make(map[string]any), + } + + for _, opt := range opts { + opt(s) + } + + return s +} + +func (s RPCServer) Start(ctx context.Context) error { + const op = "server.rpc.Start" + + s.log.Info("Starting gRPC server", + zap.String("addr", s.listener.Addr().String()), + ) + + grpcServer := grpc.NewServer( + grpc.ChainUnaryInterceptor( + interceptor.LoggerServerInterceptor(s.log), + interceptor.CheckAccessTokenServerInterceptor(s.jwt, s.log), + ), + ) + + pb.RegisterKeeperServer(grpcServer, s) + + g, gCtx := errgroup.WithContext(ctx) + g.Go(func() error { + return grpcServer.Serve(s.listener) + }) + g.Go(func() error { + <-gCtx.Done() + + s.log.Info("Shutting down gRPC server") + grpcServer.GracefulStop() + + return nil + }) + if err := g.Wait(); err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} diff --git a/internal/infrastructure/store/client/backup/model.go b/internal/infrastructure/store/client/backup/model.go new file mode 100644 index 0000000..d6701f3 --- /dev/null +++ b/internal/infrastructure/store/client/backup/model.go @@ -0,0 +1,34 @@ +package backup + +import ( + "github.com/google/uuid" + + "github.com/bjlag/go-keeper/internal/domain/client" +) + +type row struct { + GUID uuid.UUID `db:"guid"` + Value []byte `db:"value"` +} + +func fromModel(item client.Backup) row { + return row{ + GUID: item.GUID, + Value: item.Value, + } +} + +func (r row) toModel() client.Backup { + return client.Backup{ + GUID: r.GUID, + Value: r.Value, + } +} + +func toModels(rows []row) []client.Backup { + items := make([]client.Backup, len(rows)) + for i, item := range rows { + items[i] = item.toModel() + } + return items +} diff --git a/internal/infrastructure/store/client/backup/store.go b/internal/infrastructure/store/client/backup/store.go new file mode 100644 index 0000000..dad4728 --- /dev/null +++ b/internal/infrastructure/store/client/backup/store.go @@ -0,0 +1,68 @@ +package backup + +import ( + "context" + "fmt" + + "github.com/jmoiron/sqlx" + + "github.com/bjlag/go-keeper/internal/domain/client" +) + +const prefixOp = "store.backup." + +type Store struct { + db *sqlx.DB +} + +func NewStore(db *sqlx.DB) *Store { + return &Store{ + db: db, + } +} + +func (s *Store) Save(ctx context.Context, items []client.Backup) error { + const op = prefixOp + "Save" + + rows := make([]row, 0, len(items)) + for _, item := range items { + rows = append(rows, fromModel(item)) + } + + query := ` + INSERT INTO backup (guid, value) VALUES (:guid, :value) + ON CONFLICT (guid) DO UPDATE + SET value = excluded.value; + ` + + if _, err := s.db.NamedExecContext(ctx, query, rows); err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} + +func (s *Store) Get(ctx context.Context) ([]client.Backup, error) { + const op = prefixOp + "Get" + + query := `SELECT guid, value FROM backup` + + var rows []row + if err := s.db.SelectContext(ctx, &rows, query); err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + return toModels(rows), nil +} + +func (s *Store) Erase(ctx context.Context) error { + const op = prefixOp + "Erase" + + query := `DELETE FROM backup;` + + if _, err := s.db.ExecContext(ctx, query); err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} diff --git a/internal/infrastructure/store/client/item/model.go b/internal/infrastructure/store/client/item/model.go new file mode 100644 index 0000000..855ad4a --- /dev/null +++ b/internal/infrastructure/store/client/item/model.go @@ -0,0 +1,57 @@ +package item + +import ( + "encoding/json" + "time" + + "github.com/google/uuid" + + model "github.com/bjlag/go-keeper/internal/domain/client" +) + +type row struct { + GUID uuid.UUID `db:"guid"` + Category model.Category `db:"category_id"` + Title string `db:"title"` + Value *[]byte `db:"value"` + Notes string `db:"notes"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func toRow(model model.Item) (row, error) { + value, err := json.Marshal(model.Value) + if err != nil { + return row{}, err + } + + return row{ + GUID: model.GUID, + Category: model.Category, + Title: model.Title, + Value: &value, + Notes: model.Notes, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + }, nil +} + +func toModels(rows []row) []model.RawItem { + items := make([]model.RawItem, len(rows)) + for i, r := range rows { + items[i] = r.toModel() + } + return items +} + +func (r *row) toModel() model.RawItem { + return model.RawItem{ + GUID: r.GUID, + Category: r.Category, + Title: r.Title, + Value: r.Value, + Notes: r.Notes, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + } +} diff --git a/internal/infrastructure/store/client/item/store.go b/internal/infrastructure/store/client/item/store.go new file mode 100644 index 0000000..70ca7ae --- /dev/null +++ b/internal/infrastructure/store/client/item/store.go @@ -0,0 +1,197 @@ +// Package item отвечает за работу с элементами в базе данных на стороне клиента. +package item + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + + model "github.com/bjlag/go-keeper/internal/domain/client" +) + +const prefixOp = "store.item." + +type Store struct { + db *sqlx.DB +} + +func NewStore(db *sqlx.DB) *Store { + return &Store{ + db: db, + } +} + +// SaveItem сохраняет переданный элемент в базе. +func (s *Store) SaveItem(ctx context.Context, item model.Item) error { + const op = prefixOp + "SaveItem" + + query := ` + UPDATE items + SET title = :title, + value = :value, + notes = :notes, + updated_at = :updated_at + WHERE guid = :guid + ` + + r, err := toRow(item) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + _, err = s.db.NamedExecContext(ctx, query, r) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} + +// CreateItem создает переданную модель элемента в базе. +func (s *Store) CreateItem(ctx context.Context, item model.Item) error { + const op = prefixOp + "CreateItem" + + query := ` + INSERT INTO items(guid, category_id, title, value, notes, updated_at, created_at) + VALUES (:guid, :category_id, :title, :value, :notes, :updated_at, :created_at) + ` + + var value *[]byte + + if item.Value != nil { + v, err := json.Marshal(item.Value) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + value = &v + } + + args := row{ + GUID: item.GUID, + Category: item.Category, + Title: item.Title, + Value: value, + Notes: item.Notes, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + _, err := s.db.NamedExecContext(ctx, query, args) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} + +// DeleteItem удаляет элемент с переданным GUID из базы. +func (s *Store) DeleteItem(ctx context.Context, guid uuid.UUID) error { + const op = prefixOp + "DeleteItem" + + query := ` + DELETE FROM items WHERE guid = $1 + ` + + _, err := s.db.ExecContext(ctx, query, guid) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} + +// SaveItems сохраняет несколько элементов. +func (s *Store) SaveItems(ctx context.Context, items []model.RawItem) error { + const op = prefixOp + "SaveItems" + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + defer func() { + _ = tx.Rollback() + }() + + for _, i := range items { + query := ` + INSERT INTO items (guid, category_id, title, value, notes, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (guid) DO UPDATE SET + title = excluded.title, + value = excluded.value, + notes = excluded.notes, + updated_at = excluded.updated_at; + ` + + _, err := tx.ExecContext(ctx, query, i.GUID, i.Category, i.Title, i.Value, i.Notes, i.CreatedAt, i.UpdatedAt) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + } + + err = tx.Commit() + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} + +// ItemsByCategory получает элементы указанной категории. +func (s *Store) ItemsByCategory(ctx context.Context, category model.Category) ([]model.RawItem, error) { + const op = prefixOp + "Passwords" + + query := ` + SELECT guid, category_id, title, value, notes, created_at, updated_at + FROM items + WHERE category_id = $1; + ` + var rows []row + err := s.db.SelectContext(ctx, &rows, query, category) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, fmt.Errorf("%s: %w", op, err) + } + + return toModels(rows), nil +} + +func (s *Store) Items(ctx context.Context, limit, offset int64) ([]model.RawItem, error) { + const op = prefixOp + "Items" + + query := ` + SELECT guid, category_id, title, value, notes, created_at, updated_at + FROM items + LIMIT $1 OFFSET $2; + ` + + var rows []row + err := s.db.SelectContext(ctx, &rows, query, limit, offset) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, fmt.Errorf("%s: %w", op, err) + } + + return toModels(rows), nil +} + +func (s *Store) EraseItems(ctx context.Context) error { + const op = prefixOp + "EraseItems" + + query := `DELETE FROM items;` + + if _, err := s.db.ExecContext(ctx, query); err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} diff --git a/internal/infrastructure/store/client/option/model.go b/internal/infrastructure/store/client/option/model.go new file mode 100644 index 0000000..68df1db --- /dev/null +++ b/internal/infrastructure/store/client/option/model.go @@ -0,0 +1,24 @@ +package option + +import ( + model "github.com/bjlag/go-keeper/internal/domain/client" +) + +type row struct { + Slug string `db:"slug"` + Value string `db:"value"` +} + +func toRow(model model.Option) row { + return row{ + Slug: model.Slug, + Value: model.Value, + } +} + +func (r *row) toModel() model.Option { + return model.Option{ + Slug: r.Slug, + Value: r.Value, + } +} diff --git a/internal/infrastructure/store/client/option/store.go b/internal/infrastructure/store/client/option/store.go new file mode 100644 index 0000000..adcd9bf --- /dev/null +++ b/internal/infrastructure/store/client/option/store.go @@ -0,0 +1,69 @@ +// Package option отвечает за работу с опциями в базе данных на стороне клиента. +package option + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/jmoiron/sqlx" + + model "github.com/bjlag/go-keeper/internal/domain/client" +) + +const prefixOp = "store.option." + +type Store struct { + db *sqlx.DB +} + +func NewStore(db *sqlx.DB) *Store { + return &Store{ + db: db, + } +} + +// SaveOption сохраняет переданную модель опции в базе данных. +// Если элемента в базе нет, то создает. +func (s *Store) SaveOption(ctx context.Context, option model.Option) error { + const op = prefixOp + "SaveOption" + + query := ` + INSERT INTO options (slug, value) + VALUES (:slug, :value) + ON CONFLICT (slug) DO UPDATE + SET value = excluded.value + ` + + r := toRow(option) + _, err := s.db.NamedExecContext(ctx, query, r) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} + +// OptionBySlug получает опцию по ее слагу. +func (s *Store) OptionBySlug(ctx context.Context, slug string) (*model.Option, error) { + const op = prefixOp + "OptionBySlug" + + query := ` + SELECT slug, value + FROM options + WHERE slug = $1; + ` + var r row + err := s.db.GetContext(ctx, &r, query, slug) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, fmt.Errorf("%s: %w", op, err) + } + + m := r.toModel() + + return &m, nil +} diff --git a/internal/infrastructure/store/client/token/store.go b/internal/infrastructure/store/client/token/store.go new file mode 100644 index 0000000..7b436f4 --- /dev/null +++ b/internal/infrastructure/store/client/token/store.go @@ -0,0 +1,42 @@ +// Package token in-memory хранилище токенов и мастер ключа на стороне клиента. +package token + +type tokens struct { + accessToken string + refreshToken string + masterKey []byte +} + +type Store struct { + tokens tokens +} + +func NewStore() *Store { + return &Store{} +} + +// SaveTokens запомнить переданные токены. +func (s *Store) SaveTokens(accessToken, refreshToken string) { + s.tokens.accessToken = accessToken + s.tokens.refreshToken = refreshToken +} + +// SaveMasterKey запомнить мастер ключ. +func (s *Store) SaveMasterKey(key []byte) { + s.tokens.masterKey = key +} + +// AccessToken получить access токен. +func (s *Store) AccessToken() string { + return s.tokens.accessToken +} + +// RefreshToken получить refresh токен. +func (s *Store) RefreshToken() string { + return s.tokens.refreshToken +} + +// MasterKey получить мастер ключ. +func (s *Store) MasterKey() []byte { + return s.tokens.masterKey +} diff --git a/internal/infrastructure/store/server/item/model.go b/internal/infrastructure/store/server/item/model.go new file mode 100644 index 0000000..74b51ea --- /dev/null +++ b/internal/infrastructure/store/server/item/model.go @@ -0,0 +1,47 @@ +package item + +import ( + "time" + + "github.com/google/uuid" + + "github.com/bjlag/go-keeper/internal/domain/server/data" +) + +type row struct { + GUID uuid.UUID `db:"guid"` + UserGUID uuid.UUID `db:"user_guid"` + EncryptedData []byte `db:"encrypted_data"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func (r *row) convertToModel() data.Item { + return data.Item{ + GUID: r.GUID, + UserGUID: r.UserGUID, + EncryptedData: r.EncryptedData, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + } +} + +func convertToModels(rows []row) []data.Item { + result := make([]data.Item, 0, len(rows)) + for _, row := range rows { + result = append(result, row.convertToModel()) + } + return result +} + +type updated struct { + GUID uuid.UUID `db:"guid"` + UserGUID uuid.UUID `db:"user_guid"` + EncryptedData []byte `db:"encrypted_data"` + UpdatedAt time.Time `db:"updated_at"` +} + +type deleted struct { + GUID uuid.UUID `db:"guid"` + UserGUID uuid.UUID `db:"user_guid"` +} diff --git a/internal/infrastructure/store/server/item/store.go b/internal/infrastructure/store/server/item/store.go new file mode 100644 index 0000000..b405367 --- /dev/null +++ b/internal/infrastructure/store/server/item/store.go @@ -0,0 +1,186 @@ +// Package item отвечает за работу с элементами в базе данных на стороне сервера. +package item + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + + model "github.com/bjlag/go-keeper/internal/domain/server/data" +) + +var ( + // ErrNotAffectedRows ошибка в случае, если при обновлении не было задето ни одной записи. + ErrNotAffectedRows = errors.New("not affected") + // ErrNotFound ошибка в случае, если не было найдено ни одной записи. + ErrNotFound = errors.New("not found") +) + +type Store struct { + db *sqlx.DB +} + +func NewStore(db *sqlx.DB) *Store { + return &Store{ + db: db, + } +} + +// GetAllByUser получить все элементы пользователя. +func (s *Store) GetAllByUser(ctx context.Context, userGUID uuid.UUID, limit, offset uint32) ([]model.Item, error) { + const op = "store.item.GetAllByUser" + + query := ` + SELECT guid, user_guid, encrypted_data, created_at, updated_at + FROM items + WHERE user_guid = $1 + ORDER BY guid + LIMIT $2 + OFFSET $3 + ` + + var rows []row + if err := s.db.SelectContext(ctx, &rows, query, userGUID, limit, offset); err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + return convertToModels(rows), nil +} + +// ItemByGUID получить элемент по его GUID. +func (s *Store) ItemByGUID(ctx context.Context, guid uuid.UUID) (*model.Item, error) { + const op = "store.item.ItemByGUID" + + query := ` + SELECT guid, user_guid, encrypted_data, created_at, updated_at + FROM items + WHERE guid = $1 + ` + + var r row + if err := s.db.GetContext(ctx, &r, query, guid); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + return nil, fmt.Errorf("%s: %w", op, err) + } + + result := r.convertToModel() + + return &result, nil +} + +// UserItemByGUID получить элемент пользователя по его GUID. +func (s *Store) UserItemByGUID(ctx context.Context, userGUID, itemGUID uuid.UUID) (*model.Item, error) { + const op = "store.item.UserItemByGUID" + + query := ` + SELECT guid, user_guid, encrypted_data, created_at, updated_at + FROM items + WHERE guid = $1 AND user_guid = $2 + ` + + var r row + if err := s.db.GetContext(ctx, &r, query, itemGUID, userGUID); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + return nil, fmt.Errorf("%s: %w", op, err) + } + + result := r.convertToModel() + + return &result, nil +} + +// Create создать элемент по переданной модели. +func (s *Store) Create(ctx context.Context, item model.Item) error { + const op = "store.item.Create" + + query := ` + INSERT INTO items(guid, user_guid, encrypted_data, created_at, updated_at) + VALUES(:guid, :user_guid, :encrypted_data, :created_at, :updated_at) + ` + + arg := row{ + GUID: item.GUID, + UserGUID: item.UserGUID, + EncryptedData: item.EncryptedData, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + _, err := s.db.NamedExecContext(ctx, query, arg) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} + +// Update обновить элемент пользователя. +func (s *Store) Update(ctx context.Context, guid uuid.UUID, userGUID uuid.UUID, data model.UpdatedItem) error { + const op = "store.item.Update" + + query := ` + UPDATE items + SET encrypted_data = :encrypted_data, updated_at = :updated_at + WHERE guid = :guid AND user_guid = :user_guid + ` + + arg := updated{ + GUID: guid, + UserGUID: userGUID, + EncryptedData: data.EncryptedData, + UpdatedAt: data.UpdatedAt, + } + + result, err := s.db.NamedExecContext(ctx, query, arg) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + rows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + if rows == 0 { + return ErrNotAffectedRows + } + + return nil +} + +// Delete удалить элемент пользователя. +func (s *Store) Delete(ctx context.Context, guid uuid.UUID, userGUID uuid.UUID) error { + const op = "store.item.Delete" + + query := ` + DELETE FROM items + WHERE guid = :guid AND user_guid = :user_guid + ` + + arg := deleted{ + GUID: guid, + UserGUID: userGUID, + } + + result, err := s.db.NamedExecContext(ctx, query, arg) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + rows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + if rows == 0 { + return ErrNotAffectedRows + } + + return nil +} diff --git a/internal/infrastructure/store/server/user/model.go b/internal/infrastructure/store/server/user/model.go new file mode 100644 index 0000000..1007255 --- /dev/null +++ b/internal/infrastructure/store/server/user/model.go @@ -0,0 +1,37 @@ +package user + +import ( + "time" + + "github.com/google/uuid" + + model "github.com/bjlag/go-keeper/internal/domain/server/user" +) + +type row struct { + GUID uuid.UUID `db:"guid"` + Email string `db:"email"` + PasswordHash string `db:"password_hash"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func convertToRow(m model.User) row { + return row{ + GUID: m.GUID, + Email: m.Email, + PasswordHash: m.PasswordHash, + CreatedAt: m.CreatedAt, + UpdatedAt: m.UpdatedAt, + } +} + +func (r row) convertToModel() *model.User { + return &model.User{ + GUID: r.GUID, + Email: r.Email, + PasswordHash: r.PasswordHash, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + } +} diff --git a/internal/infrastructure/store/server/user/store.go b/internal/infrastructure/store/server/user/store.go new file mode 100644 index 0000000..98e7a48 --- /dev/null +++ b/internal/infrastructure/store/server/user/store.go @@ -0,0 +1,82 @@ +// Package user отвечает за работу с пользователями на стороне сервера. +package user + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + + model "github.com/bjlag/go-keeper/internal/domain/server/user" +) + +// ErrNotFound ошибка в случае, если пользователь не найден. +var ErrNotFound = errors.New("user not found") + +type Store struct { + db *sqlx.DB +} + +func NewStore(db *sqlx.DB) *Store { + return &Store{ + db: db, + } +} + +// GetByGUID получить пользователя по его GUID. +func (s Store) GetByGUID(ctx context.Context, guid uuid.UUID) (*model.User, error) { + const op = "store.user.GetByGUID" + + query := ` + SELECT guid, email, password_hash, created_at, updated_at + FROM users + WHERE guid = $1 + ` + + var user row + if err := s.db.GetContext(ctx, &user, query, guid); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + return nil, fmt.Errorf("%s: %w", op, err) + } + + return user.convertToModel(), nil +} + +// GetByEmail получить пользователя по его емейлу. +func (s Store) GetByEmail(ctx context.Context, email string) (*model.User, error) { + const op = "store.user.GetByEmail" + + query := ` + SELECT guid, email, password_hash, created_at, updated_at + FROM users + WHERE email = $1 + ` + + var user row + if err := s.db.GetContext(ctx, &user, query, email); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + return nil, fmt.Errorf("%s: %w", op, err) + } + + return user.convertToModel(), nil +} + +// Add добавить пользователя. +func (s Store) Add(ctx context.Context, user model.User) error { + const op = "store.user.Add" + + query := `INSERT INTO users (guid, email, password_hash) VALUES (:guid, :email, :password_hash)` + _, err := s.db.NamedExecContext(ctx, query, convertToRow(user)) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} diff --git a/internal/infrastructure/validator/email.go b/internal/infrastructure/validator/email.go new file mode 100644 index 0000000..b34a16f --- /dev/null +++ b/internal/infrastructure/validator/email.go @@ -0,0 +1,20 @@ +package validator + +import ( + "regexp" + "sync" +) + +var ( + once sync.Once + regexEmailPattern *regexp.Regexp +) + +// ValidateEmail валидация емейла. +func ValidateEmail(email string) bool { + once.Do(func() { + regexEmailPattern = regexp.MustCompile("^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$") + }) + + return regexEmailPattern.MatchString(email) +} diff --git a/internal/infrastructure/validator/password.go b/internal/infrastructure/validator/password.go new file mode 100644 index 0000000..4cfefa3 --- /dev/null +++ b/internal/infrastructure/validator/password.go @@ -0,0 +1,8 @@ +package validator + +const minPasswordLength = 8 + +// ValidatePassword валидация пароля. +func ValidatePassword(password string) bool { + return !(len(password) < minPasswordLength) +} diff --git a/internal/rpc/create_item/contract.go b/internal/rpc/create_item/contract.go new file mode 100644 index 0000000..72b1b0f --- /dev/null +++ b/internal/rpc/create_item/contract.go @@ -0,0 +1,11 @@ +package create_item + +import ( + "context" + + "github.com/bjlag/go-keeper/internal/usecase/server/item/create" +) + +type usecase interface { + Do(ctx context.Context, data create.In) error +} diff --git a/internal/rpc/create_item/handler.go b/internal/rpc/create_item/handler.go new file mode 100644 index 0000000..718344b --- /dev/null +++ b/internal/rpc/create_item/handler.go @@ -0,0 +1,58 @@ +package create_item + +import ( + "context" + + "github.com/google/uuid" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + pb "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/internal/infrastructure/auth" + "github.com/bjlag/go-keeper/internal/infrastructure/logger" + "github.com/bjlag/go-keeper/internal/usecase/server/item/create" +) + +type Handler struct { + usecase usecase +} + +func New(usecase usecase) *Handler { + return &Handler{ + usecase: usecase, + } +} + +// Handle метод создания элемента для аутентифицированного пользователя. +func (h *Handler) Handle(ctx context.Context, in *pb.CreateItemIn) (*emptypb.Empty, error) { + log := logger.FromCtx(ctx) + + userGUID := auth.UserGUIDFromCtx(ctx) + if userGUID == uuid.Nil { + return nil, status.Error(codes.PermissionDenied, "permission denied") + } + + if len(in.GetEncryptedData()) == 0 { + return nil, status.Error(codes.InvalidArgument, "encrypted data is empty") + } + + itemGUID, err := uuid.Parse(in.GetGuid()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "invalid item guid") + } + + err = h.usecase.Do(ctx, create.In{ + ItemGUID: itemGUID, + UserGUID: userGUID, + EncryptedData: in.GetEncryptedData(), + CreatedAt: in.GetCreatedAt().AsTime(), + }) + if err != nil { + log.Error("Failed to update item", zap.Error(err)) + return nil, status.Error(codes.Internal, "internal error") + } + + return &emptypb.Empty{}, nil +} diff --git a/internal/rpc/delete_item/contract.go b/internal/rpc/delete_item/contract.go new file mode 100644 index 0000000..52c8330 --- /dev/null +++ b/internal/rpc/delete_item/contract.go @@ -0,0 +1,11 @@ +package delete_item + +import ( + "context" + + "github.com/bjlag/go-keeper/internal/usecase/server/item/remove" +) + +type usecase interface { + Do(ctx context.Context, data remove.In) error +} diff --git a/internal/rpc/delete_item/handler.go b/internal/rpc/delete_item/handler.go new file mode 100644 index 0000000..a32722f --- /dev/null +++ b/internal/rpc/delete_item/handler.go @@ -0,0 +1,57 @@ +package delete_item + +import ( + "context" + "errors" + + "github.com/google/uuid" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + pb "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/internal/infrastructure/auth" + "github.com/bjlag/go-keeper/internal/infrastructure/logger" + "github.com/bjlag/go-keeper/internal/usecase/server/item/remove" +) + +type Handler struct { + usecase usecase +} + +func New(usecase usecase) *Handler { + return &Handler{ + usecase: usecase, + } +} + +// Handle метод удаления элемента у аутентифицированного пользователя. +func (h *Handler) Handle(ctx context.Context, in *pb.DeleteItemIn) (*emptypb.Empty, error) { + log := logger.FromCtx(ctx) + + userGUID := auth.UserGUIDFromCtx(ctx) + if userGUID == uuid.Nil { + return nil, status.Error(codes.PermissionDenied, "permission denied") + } + + itemGUID, err := uuid.Parse(in.GetGuid()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "invalid item guid") + } + + err = h.usecase.Do(ctx, remove.In{ + UserGUID: userGUID, + ItemGUID: itemGUID, + }) + if err != nil { + if errors.Is(err, remove.ErrNotFoundUpdatedData) { + return nil, status.Error(codes.NotFound, "item not found") + } + + log.Error("Failed to update item", zap.Error(err)) + return nil, status.Error(codes.Internal, "internal error") + } + + return &emptypb.Empty{}, nil +} diff --git a/internal/rpc/get_all_items/contract.go b/internal/rpc/get_all_items/contract.go new file mode 100644 index 0000000..0963a38 --- /dev/null +++ b/internal/rpc/get_all_items/contract.go @@ -0,0 +1,11 @@ +package get_all_items + +import ( + "context" + + "github.com/bjlag/go-keeper/internal/usecase/server/item/get_all" +) + +type usecase interface { + Do(ctx context.Context, data get_all.Data) (*get_all.Result, error) +} diff --git a/internal/rpc/get_all_items/handler.go b/internal/rpc/get_all_items/handler.go new file mode 100644 index 0000000..515957a --- /dev/null +++ b/internal/rpc/get_all_items/handler.go @@ -0,0 +1,80 @@ +package get_all_items + +import ( + "context" + "errors" + + "github.com/google/uuid" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + pb "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/internal/infrastructure/auth" + "github.com/bjlag/go-keeper/internal/infrastructure/logger" + "github.com/bjlag/go-keeper/internal/usecase/server/item/get_all" +) + +const ( + limitDefault = 40 + limitMax = 100 +) + +type Handler struct { + usecase usecase +} + +func New(usecase usecase) *Handler { + return &Handler{ + usecase: usecase, + } +} + +// Handle метод получения всех элементов аутентифицированного пользователя. +func (h *Handler) Handle(ctx context.Context, in *pb.GetAllItemsIn) (*pb.GetAllItemsOut, error) { + log := logger.FromCtx(ctx) + + userGUID := auth.UserGUIDFromCtx(ctx) + if userGUID == uuid.Nil { + return nil, status.Error(codes.PermissionDenied, "permission denied") + } + + if in.GetLimit() > limitMax { + in.Limit = limitMax + } + + if in.GetLimit() <= 0 { + in.Limit = limitDefault + } + + if in.GetOffset() <= 0 { + in.Offset = 0 + } + + result, err := h.usecase.Do(ctx, get_all.Data{ + UserGUID: userGUID, + Limit: in.GetLimit(), + Offset: in.GetOffset(), + }) + if err != nil { + if !errors.Is(err, get_all.ErrNoData) { + log.Error("Failed to get all item", zap.Error(err)) + return nil, status.Error(codes.Internal, "internal error") + } + } + + itemsOut := make([]*pb.Item, 0, len(result.Items)) + for _, item := range result.Items { + itemsOut = append(itemsOut, &pb.Item{ + Guid: item.GUID.String(), + EncryptedData: item.EncryptedData, + CreatedAt: timestamppb.New(item.CreatedAt), + UpdatedAt: timestamppb.New(item.UpdatedAt), + }) + } + + return &pb.GetAllItemsOut{ + Items: itemsOut, + }, nil +} diff --git a/internal/rpc/get_by_guid/contract.go b/internal/rpc/get_by_guid/contract.go new file mode 100644 index 0000000..7abb07a --- /dev/null +++ b/internal/rpc/get_by_guid/contract.go @@ -0,0 +1,12 @@ +package get_by_guid + +import ( + "context" + + "github.com/bjlag/go-keeper/internal/domain/server/data" + "github.com/bjlag/go-keeper/internal/usecase/server/item/get_by_guid" +) + +type usecase interface { + Do(ctx context.Context, data get_by_guid.Data) (*data.Item, error) +} diff --git a/internal/rpc/get_by_guid/handler.go b/internal/rpc/get_by_guid/handler.go new file mode 100644 index 0000000..6f58891 --- /dev/null +++ b/internal/rpc/get_by_guid/handler.go @@ -0,0 +1,62 @@ +package get_by_guid + +import ( + "context" + "errors" + + "github.com/google/uuid" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + pb "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/internal/infrastructure/auth" + "github.com/bjlag/go-keeper/internal/infrastructure/logger" + "github.com/bjlag/go-keeper/internal/usecase/server/item/get_by_guid" +) + +type Handler struct { + usecase usecase +} + +func New(usecase usecase) *Handler { + return &Handler{ + usecase: usecase, + } +} + +// Handle метод получение элемента по его GUID аутентифицированного пользователя. +func (h *Handler) Handle(ctx context.Context, in *pb.GetByGuidIn) (*pb.GetByGuidOut, error) { + log := logger.FromCtx(ctx) + + userGUID := auth.UserGUIDFromCtx(ctx) + if userGUID == uuid.Nil { + return nil, status.Error(codes.PermissionDenied, "permission denied") + } + + itemGUID, err := uuid.Parse(in.GetGuid()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + result, err := h.usecase.Do(ctx, get_by_guid.Data{ + GUID: itemGUID, + UserGUID: userGUID, + }) + if err != nil { + if !errors.Is(err, get_by_guid.ErrNoData) { + log.Error("Failed to get user item by guid", zap.Error(err)) + return nil, status.Error(codes.Internal, "internal error") + } + + return nil, status.Error(codes.NotFound, "item not found") + } + + return &pb.GetByGuidOut{ + Guid: result.GUID.String(), + EncryptedData: result.EncryptedData, + CreatedAt: timestamppb.New(result.CreatedAt), + UpdatedAt: timestamppb.New(result.UpdatedAt), + }, nil +} diff --git a/internal/rpc/login/contract.go b/internal/rpc/login/contract.go new file mode 100644 index 0000000..11af932 --- /dev/null +++ b/internal/rpc/login/contract.go @@ -0,0 +1,11 @@ +package login + +import ( + "context" + + "github.com/bjlag/go-keeper/internal/usecase/server/user/login" +) + +type usecase interface { + Do(ctx context.Context, data login.Data) (*login.Result, error) +} diff --git a/internal/rpc/login/handler.go b/internal/rpc/login/handler.go new file mode 100644 index 0000000..e381d43 --- /dev/null +++ b/internal/rpc/login/handler.go @@ -0,0 +1,59 @@ +package login + +import ( + "context" + "errors" + + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + pb "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/internal/infrastructure/logger" + "github.com/bjlag/go-keeper/internal/infrastructure/validator" + "github.com/bjlag/go-keeper/internal/usecase/server/user/login" +) + +type Handler struct { + usecase usecase +} + +func New(usecase usecase) *Handler { + return &Handler{ + usecase: usecase, + } +} + +// Handle метод для аутентификации пользователя. +func (h *Handler) Handle(ctx context.Context, in *pb.LoginIn) (*pb.LoginOut, error) { + log := logger.FromCtx(ctx) + + if !validator.ValidateEmail(in.GetEmail()) { + return nil, status.Error(codes.InvalidArgument, "email is invalid") + } + + if len(in.GetPassword()) == 0 { + return nil, status.Error(codes.InvalidArgument, "password is empty") + } + + result, err := h.usecase.Do(ctx, login.Data{ + Email: in.GetEmail(), + Password: in.GetPassword(), + }) + if err != nil { + switch { + case errors.Is(err, login.ErrUserNotFound): + return nil, status.Error(codes.Unauthenticated, "credentials incorrect") + case errors.Is(err, login.ErrPasswordIncorrect): + return nil, status.Error(codes.Unauthenticated, "credentials incorrect") + default: + log.Error("Failed to login user", zap.Error(err)) + return nil, status.Error(codes.Internal, "internal error") + } + } + + return &pb.LoginOut{ + AccessToken: result.AccessToken, + RefreshToken: result.RefreshToken, + }, nil +} diff --git a/internal/rpc/refresh_tokens/contract.go b/internal/rpc/refresh_tokens/contract.go new file mode 100644 index 0000000..ca372ea --- /dev/null +++ b/internal/rpc/refresh_tokens/contract.go @@ -0,0 +1,11 @@ +package refresh_tokens + +import ( + "context" + + rt "github.com/bjlag/go-keeper/internal/usecase/server/user/refresh_tokens" +) + +type usecase interface { + Do(ctx context.Context, data rt.Data) (*rt.Result, error) +} diff --git a/internal/rpc/refresh_tokens/handler.go b/internal/rpc/refresh_tokens/handler.go new file mode 100644 index 0000000..38615d6 --- /dev/null +++ b/internal/rpc/refresh_tokens/handler.go @@ -0,0 +1,50 @@ +package refresh_tokens + +import ( + "context" + "errors" + + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + pb "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/internal/infrastructure/logger" + "github.com/bjlag/go-keeper/internal/usecase/server/user/refresh_tokens" +) + +const lenRefreshToken = 24 + +type Handler struct { + usecase usecase +} + +func New(usecase usecase) *Handler { + return &Handler{ + usecase: usecase, + } +} + +// Handle метод для обновления токенов по refresh токену. +func (h *Handler) Handle(ctx context.Context, in *pb.RefreshTokensIn) (*pb.RefreshTokensOut, error) { + log := logger.FromCtx(ctx) + + if len(in.GetRefreshToken()) < lenRefreshToken { + return nil, status.Error(codes.InvalidArgument, "refresh token too short") + } + + result, err := h.usecase.Do(ctx, refresh_tokens.Data{RefreshToken: in.GetRefreshToken()}) + if err != nil { + if errors.Is(err, refresh_tokens.ErrInvalidRefreshToken) { + return nil, status.Error(codes.FailedPrecondition, "invalid refresh token") + } + + log.Error("Failed to refresh tokens", zap.Error(err)) + return nil, status.Error(codes.Internal, "internal server error") + } + + return &pb.RefreshTokensOut{ + AccessToken: result.AccessToken, + RefreshToken: result.RefreshToken, + }, nil +} diff --git a/internal/rpc/register/contract.go b/internal/rpc/register/contract.go new file mode 100644 index 0000000..0fc6b5e --- /dev/null +++ b/internal/rpc/register/contract.go @@ -0,0 +1,11 @@ +package register + +import ( + "context" + + "github.com/bjlag/go-keeper/internal/usecase/server/user/register" +) + +type usecase interface { + Do(ctx context.Context, data register.Data) (*register.Result, error) +} diff --git a/internal/rpc/register/handler.go b/internal/rpc/register/handler.go new file mode 100644 index 0000000..990dc59 --- /dev/null +++ b/internal/rpc/register/handler.go @@ -0,0 +1,56 @@ +package register + +import ( + "context" + "errors" + + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + pb "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/internal/infrastructure/logger" + "github.com/bjlag/go-keeper/internal/infrastructure/validator" + "github.com/bjlag/go-keeper/internal/usecase/server/user/register" +) + +type Handler struct { + usecase usecase +} + +func New(usecase usecase) *Handler { + return &Handler{ + usecase: usecase, + } +} + +// Handle метод для регистрации пользователя. +func (h *Handler) Handle(ctx context.Context, in *pb.RegisterIn) (*pb.RegisterOut, error) { + log := logger.FromCtx(ctx) + + if !validator.ValidateEmail(in.GetEmail()) { + return nil, status.Error(codes.InvalidArgument, "email is invalid") + } + + if !validator.ValidatePassword(in.GetPassword()) { + return nil, status.Error(codes.InvalidArgument, "password is invalid (min. length 8 characters)") + } + + result, err := h.usecase.Do(ctx, register.Data{ + Email: in.GetEmail(), + Password: in.GetPassword(), + }) + if err != nil { + if errors.Is(err, register.ErrUserAlreadyExists) { + return nil, status.Error(codes.AlreadyExists, "user with this email already exists") + } + + log.Error("failed to register user", zap.Error(err)) + return nil, status.Error(codes.Internal, "internal server error") + } + + return &pb.RegisterOut{ + AccessToken: result.AccessToken, + RefreshToken: result.RefreshToken, + }, nil +} diff --git a/internal/rpc/update_item/contract.go b/internal/rpc/update_item/contract.go new file mode 100644 index 0000000..35dd013 --- /dev/null +++ b/internal/rpc/update_item/contract.go @@ -0,0 +1,11 @@ +package update_item + +import ( + "context" + + "github.com/bjlag/go-keeper/internal/usecase/server/item/update" +) + +type usecase interface { + Do(ctx context.Context, data update.In) (newVersion int64, err error) +} diff --git a/internal/rpc/update_item/handler.go b/internal/rpc/update_item/handler.go new file mode 100644 index 0000000..50ea300 --- /dev/null +++ b/internal/rpc/update_item/handler.go @@ -0,0 +1,68 @@ +package update_item + +import ( + "context" + "errors" + + "github.com/google/uuid" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + pb "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/internal/infrastructure/auth" + "github.com/bjlag/go-keeper/internal/infrastructure/logger" + "github.com/bjlag/go-keeper/internal/usecase/server/item/update" +) + +type Handler struct { + usecase usecase +} + +func New(usecase usecase) *Handler { + return &Handler{ + usecase: usecase, + } +} + +// Handle метод для обновления элемента аутентифицированного пользователя. +func (h *Handler) Handle(ctx context.Context, in *pb.UpdateItemIn) (*pb.UpdateItemOut, error) { + log := logger.FromCtx(ctx) + + userGUID := auth.UserGUIDFromCtx(ctx) + if userGUID == uuid.Nil { + return nil, status.Error(codes.PermissionDenied, "permission denied") + } + + if len(in.GetEncryptedData()) == 0 { + return nil, status.Error(codes.InvalidArgument, "encrypted data is empty") + } + + itemGUID, err := uuid.Parse(in.GetGuid()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "invalid item guid") + } + + newVersion, err := h.usecase.Do(ctx, update.In{ + UserGUID: userGUID, + ItemGUID: itemGUID, + EncryptedData: in.GetEncryptedData(), + Version: in.GetVersion(), + }) + if err != nil { + if errors.Is(err, update.ErrConflict) { + return nil, status.Error(codes.FailedPrecondition, "item is outdated") + } + + if errors.Is(err, update.ErrItemNotFound) { + return nil, status.Error(codes.NotFound, "item not found") + } + + log.Error("Failed to update item", zap.Error(err)) + return nil, status.Error(codes.Internal, "internal error") + } + + return &pb.UpdateItemOut{ + NewVersion: newVersion, + }, nil +} diff --git a/internal/usecase/client/backup/contract.go b/internal/usecase/client/backup/contract.go new file mode 100644 index 0000000..841b5e1 --- /dev/null +++ b/internal/usecase/client/backup/contract.go @@ -0,0 +1,24 @@ +package backup + +import ( + "context" + + model "github.com/bjlag/go-keeper/internal/domain/client" +) + +type items interface { + Items(ctx context.Context, limit, offset int64) ([]model.RawItem, error) + EraseItems(ctx context.Context) error +} + +type tokens interface { + MasterKey() []byte +} + +type backup interface { + Save(ctx context.Context, items []model.Backup) error +} + +type cipher interface { + Encrypt(data, key []byte) ([]byte, error) +} diff --git a/internal/usecase/client/backup/usecase.go b/internal/usecase/client/backup/usecase.go new file mode 100644 index 0000000..6bf34c3 --- /dev/null +++ b/internal/usecase/client/backup/usecase.go @@ -0,0 +1,77 @@ +// Package backup отвечает за сброс локальных данных в бекап в зашифрованном виде. +package backup + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/bjlag/go-keeper/internal/domain/client" +) + +type Usecase struct { + items items + tokens tokens + backup backup + cipher cipher +} + +func NewUsecase(items items, tokens tokens, backups backup, cipher cipher) *Usecase { + return &Usecase{ + items: items, + tokens: tokens, + backup: backups, + cipher: cipher, + } +} + +func (u *Usecase) Do(ctx context.Context) error { + const op = "usecase.backup.Do" + + itemModels, err := u.items.Items(ctx, 100, 0) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + if len(itemModels) == 0 { + return nil + } + + key := u.tokens.MasterKey() + backupItems := make([]client.Backup, 0, len(itemModels)) + for _, i := range itemModels { + v := &client.BackupValue{ + Category: i.Category, + Title: i.Title, + Value: i.Value, + Notes: i.Notes, + CreatedAt: i.CreatedAt, + UpdatedAt: i.UpdatedAt, + } + + marshaledValue, err := json.Marshal(v) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + encrypted, err := u.cipher.Encrypt(marshaledValue, key) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + backupItems = append(backupItems, client.Backup{ + GUID: i.GUID, + Value: encrypted, + }) + } + + if err = u.backup.Save(ctx, backupItems); err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + if err = u.items.EraseItems(ctx); err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} diff --git a/internal/usecase/client/item/create/contract.go b/internal/usecase/client/item/create/contract.go new file mode 100644 index 0000000..f7a51b9 --- /dev/null +++ b/internal/usecase/client/item/create/contract.go @@ -0,0 +1,24 @@ +package create + +import ( + "context" + + model "github.com/bjlag/go-keeper/internal/domain/client" + dto "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +type rpc interface { + CreateItem(ctx context.Context, in *dto.CreateItemIn) error +} + +type itemStore interface { + CreateItem(ctx context.Context, item model.Item) error +} + +type keyStore interface { + MasterKey() []byte +} + +type cipher interface { + Encrypt(data, key []byte) ([]byte, error) +} diff --git a/internal/usecase/client/item/create/usecase.go b/internal/usecase/client/item/create/usecase.go new file mode 100644 index 0000000..b83f807 --- /dev/null +++ b/internal/usecase/client/item/create/usecase.go @@ -0,0 +1,74 @@ +// Package create отвечает за сценарий создания элемента на стороне клиента. +package create + +import ( + "context" + "encoding/json" + "fmt" + + model "github.com/bjlag/go-keeper/internal/domain/client" + dto "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +type Usecase struct { + rpc rpc + itemStore itemStore + keyStore keyStore + cipher cipher +} + +func NewUsecase(rpc rpc, itemStore itemStore, keyStore keyStore, cipher cipher) *Usecase { + return &Usecase{ + rpc: rpc, + itemStore: itemStore, + keyStore: keyStore, + cipher: cipher, + } +} + +func (u *Usecase) Do(ctx context.Context, item model.Item) error { + const op = "usecase.item.create.Do" + + var value *[]byte + if item.Value != nil { + v, err := json.Marshal(item.Value) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + value = &v + } + + data := model.EncryptedData{ + Title: item.Title, + Category: item.Category, + Value: value, + Notes: item.Notes, + } + + plainText, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + encryptedData, err := u.cipher.Encrypt(plainText, u.keyStore.MasterKey()) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + err = u.rpc.CreateItem(ctx, &dto.CreateItemIn{ + GUID: item.GUID, + EncryptedData: encryptedData, + CreatedAt: item.CreatedAt, + }) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + err = u.itemStore.CreateItem(ctx, item) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} diff --git a/internal/usecase/client/item/edit/contract.go b/internal/usecase/client/item/edit/contract.go new file mode 100644 index 0000000..0146a44 --- /dev/null +++ b/internal/usecase/client/item/edit/contract.go @@ -0,0 +1,24 @@ +package edit + +import ( + "context" + + model "github.com/bjlag/go-keeper/internal/domain/client" + dto "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +type rpc interface { + UpdateItem(ctx context.Context, in *dto.UpdateItemIn) (int64, error) +} + +type itemStore interface { + SaveItem(ctx context.Context, item model.Item) error +} + +type keyStore interface { + MasterKey() []byte +} + +type cipher interface { + Encrypt(data, key []byte) ([]byte, error) +} diff --git a/internal/usecase/client/item/edit/usecase.go b/internal/usecase/client/item/edit/usecase.go new file mode 100644 index 0000000..de99c04 --- /dev/null +++ b/internal/usecase/client/item/edit/usecase.go @@ -0,0 +1,88 @@ +// Package edit отвечает за сценарий изменения элемента на стороне клиента. +package edit + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + model "github.com/bjlag/go-keeper/internal/domain/client" + dto "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +var ErrConflict = errors.New("conflict") + +type Usecase struct { + rpc rpc + itemStore itemStore + keyStore keyStore + cipher cipher +} + +func NewUsecase(rpc rpc, itemStore itemStore, keyStore keyStore, cipher cipher) *Usecase { + return &Usecase{ + rpc: rpc, + itemStore: itemStore, + keyStore: keyStore, + cipher: cipher, + } +} + +func (u *Usecase) Do(ctx context.Context, item model.Item) error { + const op = "usecase.item.edit.Do" + + var value *[]byte + if item.Value != nil { + v, err := json.Marshal(item.Value) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + value = &v + } + + data := model.EncryptedData{ + Title: item.Title, + Category: item.Category, + Value: value, + Notes: item.Notes, + } + + plainText, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + encryptedData, err := u.cipher.Encrypt(plainText, u.keyStore.MasterKey()) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + newVersion, err := u.rpc.UpdateItem(ctx, &dto.UpdateItemIn{ + GUID: item.GUID, + EncryptedData: encryptedData, + Version: item.UpdatedAt.UTC().UnixMicro(), + }) + if err != nil { + if s, ok := status.FromError(err); ok && s.Code() == codes.FailedPrecondition { + return ErrConflict + } + return fmt.Errorf("%s: %w", op, err) + } + + sec := newVersion / 1000000 + nsec := (newVersion % 1000000) * time.Microsecond.Nanoseconds() + item.UpdatedAt = time.Unix(sec, nsec) + + err = u.itemStore.SaveItem(ctx, item) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} diff --git a/internal/usecase/client/item/remove/contract.go b/internal/usecase/client/item/remove/contract.go new file mode 100644 index 0000000..bf9d2a1 --- /dev/null +++ b/internal/usecase/client/item/remove/contract.go @@ -0,0 +1,17 @@ +package remove + +import ( + "context" + + "github.com/google/uuid" + + dto "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +type rpc interface { + DeleteItem(ctx context.Context, in *dto.DeleteItemIn) error +} + +type store interface { + DeleteItem(ctx context.Context, guid uuid.UUID) error +} diff --git a/internal/usecase/client/item/remove/usecase.go b/internal/usecase/client/item/remove/usecase.go new file mode 100644 index 0000000..279e726 --- /dev/null +++ b/internal/usecase/client/item/remove/usecase.go @@ -0,0 +1,41 @@ +// Package remove отвечает за сценарий удаления элемента на стороне клиента. +package remove + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + dto "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +type Usecase struct { + rpc rpc + store store +} + +func NewUsecase(rpc rpc, store store) *Usecase { + return &Usecase{ + rpc: rpc, + store: store, + } +} + +func (u *Usecase) Do(ctx context.Context, guid uuid.UUID) error { + const op = "usecase.item.remove.Do" + + err := u.rpc.DeleteItem(ctx, &dto.DeleteItemIn{ + GUID: guid, + }) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + err = u.store.DeleteItem(ctx, guid) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} diff --git a/internal/usecase/client/item/sync/contract.go b/internal/usecase/client/item/sync/contract.go new file mode 100644 index 0000000..aaae52c --- /dev/null +++ b/internal/usecase/client/item/sync/contract.go @@ -0,0 +1,24 @@ +package sync + +import ( + "context" + + model "github.com/bjlag/go-keeper/internal/domain/client" + rpc "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +type client interface { + GetByGUID(ctx context.Context, in *rpc.GetByGUIDIn) (*rpc.GetByGUIDOut, error) +} + +type itemStore interface { + SaveItem(ctx context.Context, item model.Item) error +} + +type keyStore interface { + MasterKey() []byte +} + +type cipher interface { + Decrypt(data, key []byte) ([]byte, error) +} diff --git a/internal/usecase/client/item/sync/usecase.go b/internal/usecase/client/item/sync/usecase.go new file mode 100644 index 0000000..65012c8 --- /dev/null +++ b/internal/usecase/client/item/sync/usecase.go @@ -0,0 +1,97 @@ +// Package sync отвечает за сценарий синхронизации элемента с сервером на стороне клиента. +package sync + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/google/uuid" + + model "github.com/bjlag/go-keeper/internal/domain/client" + rpc "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +const prefixOp = "usecase.item.sync." + +var ErrUnknownCategory = errors.New("unknown category") + +type Usecase struct { + client client + itemStore itemStore + keyStore keyStore + cipher cipher +} + +func NewUsecase(client client, itemStore itemStore, keyStore keyStore, cipher cipher) *Usecase { + return &Usecase{ + client: client, + itemStore: itemStore, + keyStore: keyStore, + cipher: cipher, + } +} + +func (u *Usecase) Do(ctx context.Context, guid uuid.UUID) (*model.Item, error) { + const op = prefixOp + "Do" + + key := u.keyStore.MasterKey() + + in := &rpc.GetByGUIDIn{ + GUID: guid, + } + item, err := u.client.GetByGUID(ctx, in) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + decrypted, err := u.cipher.Decrypt(item.EncryptedData, key) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + var data model.EncryptedData + err = json.Unmarshal(decrypted, &data) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + var value interface{} + if data.Value != nil { + switch data.Category { + case model.CategoryPassword: + value = &model.Password{} + case model.CategoryText: + break + case model.CategoryFile: + value = &model.File{} + case model.CategoryBankCard: + value = &model.BankCard{} + default: + return nil, fmt.Errorf("%w: %d", ErrUnknownCategory, data.Category) + } + + err = json.Unmarshal(*data.Value, &value) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + } + + itemModel := model.Item{ + GUID: item.GUID, + Category: data.Category, + Title: data.Title, + Value: value, + Notes: data.Notes, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + err = u.itemStore.SaveItem(ctx, itemModel) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + return &itemModel, nil +} diff --git a/internal/usecase/client/login/contract.go b/internal/usecase/client/login/contract.go new file mode 100644 index 0000000..eeee22a --- /dev/null +++ b/internal/usecase/client/login/contract.go @@ -0,0 +1,15 @@ +package login + +import ( + "context" + + rpc "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +type client interface { + Login(ctx context.Context, in rpc.LoginIn) (*rpc.LoginOut, error) +} + +type tokens interface { + SaveTokens(accessToken, refreshToken string) +} diff --git a/internal/usecase/client/login/model.go b/internal/usecase/client/login/model.go new file mode 100644 index 0000000..4efa94d --- /dev/null +++ b/internal/usecase/client/login/model.go @@ -0,0 +1,6 @@ +package login + +type Data struct { + Email string + Password string +} diff --git a/internal/usecase/client/login/usecase.go b/internal/usecase/client/login/usecase.go new file mode 100644 index 0000000..946aad9 --- /dev/null +++ b/internal/usecase/client/login/usecase.go @@ -0,0 +1,37 @@ +// Package login отвечает за сценарий аутентификации пользователя на стороне клиента. +package login + +import ( + "context" + "fmt" + + rpc "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +type Usecase struct { + client client + tokens tokens +} + +func NewUsecase(client client, tokens tokens) *Usecase { + return &Usecase{ + client: client, + tokens: tokens, + } +} + +func (u *Usecase) Do(ctx context.Context, data Data) error { + const op = "usecase.login.Do" + + out, err := u.client.Login(ctx, rpc.LoginIn{ + Email: data.Email, + Password: data.Password, + }) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + u.tokens.SaveTokens(out.AccessToken, out.RefreshToken) + + return nil +} diff --git a/internal/usecase/client/master_key/contract.go b/internal/usecase/client/master_key/contract.go new file mode 100644 index 0000000..b7df1db --- /dev/null +++ b/internal/usecase/client/master_key/contract.go @@ -0,0 +1,25 @@ +package master_key + +import ( + "context" + + model "github.com/bjlag/go-keeper/internal/domain/client" + "github.com/bjlag/go-keeper/internal/infrastructure/crypt/master_key" +) + +type tokens interface { + SaveMasterKey(key []byte) +} + +type options interface { + OptionBySlug(ctx context.Context, slug string) (*model.Option, error) + SaveOption(ctx context.Context, option model.Option) error +} + +type salter interface { + GenerateSalt() (*master_key.Salt, error) +} + +type keymaker interface { + GenerateMasterKey(password, salt []byte) []byte +} diff --git a/internal/usecase/client/master_key/model.go b/internal/usecase/client/master_key/model.go new file mode 100644 index 0000000..7743b80 --- /dev/null +++ b/internal/usecase/client/master_key/model.go @@ -0,0 +1,5 @@ +package master_key + +type Data struct { + Password string +} diff --git a/internal/usecase/client/master_key/usecase.go b/internal/usecase/client/master_key/usecase.go new file mode 100644 index 0000000..3404d07 --- /dev/null +++ b/internal/usecase/client/master_key/usecase.go @@ -0,0 +1,63 @@ +// Package master_key отвечает за сценарий генерации и сохранения мастер ключа на стороне клиента. +package master_key + +import ( + "context" + "fmt" + + model "github.com/bjlag/go-keeper/internal/domain/client" + "github.com/bjlag/go-keeper/internal/infrastructure/crypt/master_key" +) + +type Usecase struct { + tokens tokens + options options + salter salter + keymaker keymaker +} + +func NewUsecase(tokens tokens, options options, salter salter, keymaker keymaker) *Usecase { + return &Usecase{ + tokens: tokens, + options: options, + salter: salter, + keymaker: keymaker, + } +} + +func (u *Usecase) Do(ctx context.Context, data Data) error { + const op = "usecase.master_key.Do" + + option, err := u.options.OptionBySlug(ctx, model.OptSaltKey) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + var salt *master_key.Salt + + if option == nil { + salt, err = u.salter.GenerateSalt() + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + err = u.options.SaveOption(ctx, model.Option{ + Slug: model.OptSaltKey, + Value: salt.ToString(), + }) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + } else { + salt, err = master_key.ParseString(option.Value) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + } + + masterKey := u.keymaker.GenerateMasterKey([]byte(data.Password), salt.Value()) + + u.tokens.SaveMasterKey(masterKey) + + return nil +} diff --git a/internal/usecase/client/register/contract.go b/internal/usecase/client/register/contract.go new file mode 100644 index 0000000..67c4d20 --- /dev/null +++ b/internal/usecase/client/register/contract.go @@ -0,0 +1,16 @@ +package register + +import ( + "context" + + rpc "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +type client interface { + Register(ctx context.Context, in rpc.RegisterIn) (*rpc.RegisterOut, error) +} + +type tokens interface { + SaveTokens(accessToken, refreshToken string) + SaveMasterKey(key []byte) +} diff --git a/internal/usecase/client/register/model.go b/internal/usecase/client/register/model.go new file mode 100644 index 0000000..6dca93a --- /dev/null +++ b/internal/usecase/client/register/model.go @@ -0,0 +1,11 @@ +package register + +type Data struct { + Email string + Password string +} + +type Result struct { + AccessToken string + RefreshToken string +} diff --git a/internal/usecase/client/register/usecase.go b/internal/usecase/client/register/usecase.go new file mode 100644 index 0000000..c2547cc --- /dev/null +++ b/internal/usecase/client/register/usecase.go @@ -0,0 +1,37 @@ +// Package register отвечает за сценарий регистрации пользователя на стороне клиента. +package register + +import ( + "context" + "fmt" + + rpc "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +type Usecase struct { + client client + tokens tokens +} + +func NewUsecase(client client, tokens tokens) *Usecase { + return &Usecase{ + client: client, + tokens: tokens, + } +} + +func (u *Usecase) Do(ctx context.Context, data Data) error { + const op = "usecase.register.Do" + + out, err := u.client.Register(ctx, rpc.RegisterIn{ + Email: data.Email, + Password: data.Password, + }) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + u.tokens.SaveTokens(out.AccessToken, out.RefreshToken) + + return nil +} diff --git a/internal/usecase/client/restore/contract.go b/internal/usecase/client/restore/contract.go new file mode 100644 index 0000000..9d283cb --- /dev/null +++ b/internal/usecase/client/restore/contract.go @@ -0,0 +1,24 @@ +package restore + +import ( + "context" + + model "github.com/bjlag/go-keeper/internal/domain/client" +) + +type items interface { + SaveItems(ctx context.Context, items []model.RawItem) error +} + +type tokens interface { + MasterKey() []byte +} + +type backup interface { + Get(ctx context.Context) ([]model.Backup, error) + Erase(ctx context.Context) error +} + +type cipher interface { + Decrypt(encryptedData, key []byte) ([]byte, error) +} diff --git a/internal/usecase/client/restore/usecase.go b/internal/usecase/client/restore/usecase.go new file mode 100644 index 0000000..de083b2 --- /dev/null +++ b/internal/usecase/client/restore/usecase.go @@ -0,0 +1,75 @@ +// Package restore отвечает за восстановление локальных данных из бекапа. +package restore + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/bjlag/go-keeper/internal/domain/client" +) + +type Usecase struct { + items items + tokens tokens + backup backup + cipher cipher +} + +func NewUsecase(items items, tokens tokens, backups backup, cipher cipher) *Usecase { + return &Usecase{ + items: items, + tokens: tokens, + backup: backups, + cipher: cipher, + } +} + +func (u *Usecase) Do(ctx context.Context) error { + const op = "usecase.backup.Do" + + backupItems, err := u.backup.Get(ctx) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + if len(backupItems) == 0 { + return nil + } + + var v client.BackupValue + + key := u.tokens.MasterKey() + itemModels := make([]client.RawItem, 0, len(backupItems)) + for _, bi := range backupItems { + decrypted, err := u.cipher.Decrypt(bi.Value, key) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + err = json.Unmarshal(decrypted, &v) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + itemModels = append(itemModels, client.RawItem{ + GUID: bi.GUID, + Category: v.Category, + Title: v.Title, + Value: v.Value, + Notes: v.Notes, + CreatedAt: v.CreatedAt, + UpdatedAt: v.UpdatedAt, + }) + } + + if err = u.items.SaveItems(ctx, itemModels); err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + if err = u.backup.Erase(ctx); err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} diff --git a/internal/usecase/client/sync/contract.go b/internal/usecase/client/sync/contract.go new file mode 100644 index 0000000..ba04aa3 --- /dev/null +++ b/internal/usecase/client/sync/contract.go @@ -0,0 +1,24 @@ +package sync + +import ( + "context" + + model "github.com/bjlag/go-keeper/internal/domain/client" + rpc "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +type client interface { + GetAllItems(ctx context.Context, in *rpc.GetAllItemsIn) (*rpc.GetAllItemsOut, error) +} + +type itemStore interface { + SaveItems(ctx context.Context, items []model.RawItem) error +} + +type keyStore interface { + MasterKey() []byte +} + +type cipher interface { + Decrypt(data, key []byte) ([]byte, error) +} diff --git a/internal/usecase/client/sync/usecase.go b/internal/usecase/client/sync/usecase.go new file mode 100644 index 0000000..7b16b8b --- /dev/null +++ b/internal/usecase/client/sync/usecase.go @@ -0,0 +1,88 @@ +// Package sync отвечает за сценарий синхронизации клиента с сервером. +package sync + +import ( + "context" + "encoding/json" + "fmt" + + model "github.com/bjlag/go-keeper/internal/domain/client" + rpc "github.com/bjlag/go-keeper/internal/infrastructure/rpc/client" +) + +const ( + prefixOp = "usecase.sync." + + limit = 40 +) + +type Usecase struct { + client client + itemStore itemStore + keyStore keyStore + cipher cipher +} + +func NewUsecase(client client, itemStore itemStore, keyStore keyStore, cipher cipher) *Usecase { + return &Usecase{ + client: client, + itemStore: itemStore, + keyStore: keyStore, + cipher: cipher, + } +} + +func (u *Usecase) Do(ctx context.Context) error { + const op = prefixOp + "Do" + + var offset uint32 + + key := u.keyStore.MasterKey() + + for { + in := &rpc.GetAllItemsIn{ + Limit: limit, + Offset: offset, + } + out, err := u.client.GetAllItems(ctx, in) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + if out == nil || len(out.Items) == 0 { + break + } + + items := make([]model.RawItem, 0, len(out.Items)) + for _, item := range out.Items { + decrypted, err := u.cipher.Decrypt(item.EncryptedData, key) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + var data model.EncryptedData + err = json.Unmarshal(decrypted, &data) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + items = append(items, model.RawItem{ + GUID: item.GUID, + Category: data.Category, + Title: data.Title, + Value: data.Value, + Notes: data.Notes, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + }) + } + + err = u.itemStore.SaveItems(ctx, items) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + offset += limit + } + + return nil +} diff --git a/internal/usecase/server/item/create/contract.go b/internal/usecase/server/item/create/contract.go new file mode 100644 index 0000000..2ac92cc --- /dev/null +++ b/internal/usecase/server/item/create/contract.go @@ -0,0 +1,11 @@ +package create + +import ( + "context" + + model "github.com/bjlag/go-keeper/internal/domain/server/data" +) + +type store interface { + Create(ctx context.Context, item model.Item) error +} diff --git a/internal/usecase/server/item/create/model.go b/internal/usecase/server/item/create/model.go new file mode 100644 index 0000000..3eed4ad --- /dev/null +++ b/internal/usecase/server/item/create/model.go @@ -0,0 +1,14 @@ +package create + +import ( + "time" + + "github.com/google/uuid" +) + +type In struct { + ItemGUID uuid.UUID + UserGUID uuid.UUID + EncryptedData []byte + CreatedAt time.Time +} diff --git a/internal/usecase/server/item/create/usecase.go b/internal/usecase/server/item/create/usecase.go new file mode 100644 index 0000000..f5efdf1 --- /dev/null +++ b/internal/usecase/server/item/create/usecase.go @@ -0,0 +1,38 @@ +// Package create отвечает за сценарий создания элемента на стороне сервера. +package create + +import ( + "context" + "fmt" + + model "github.com/bjlag/go-keeper/internal/domain/server/data" +) + +type Usecase struct { + store store +} + +func NewUsecase(store store) *Usecase { + return &Usecase{ + store: store, + } +} + +func (u Usecase) Do(ctx context.Context, in In) error { + const op = "usecase.item.create.Do" + + data := model.Item{ + GUID: in.ItemGUID, + UserGUID: in.UserGUID, + EncryptedData: in.EncryptedData, + CreatedAt: in.CreatedAt, + UpdatedAt: in.CreatedAt, + } + + err := u.store.Create(ctx, data) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} diff --git a/internal/usecase/server/item/get_all/contract.go b/internal/usecase/server/item/get_all/contract.go new file mode 100644 index 0000000..b668a6a --- /dev/null +++ b/internal/usecase/server/item/get_all/contract.go @@ -0,0 +1,13 @@ +package get_all + +import ( + "context" + + "github.com/google/uuid" + + model "github.com/bjlag/go-keeper/internal/domain/server/data" +) + +type dataStore interface { + GetAllByUser(ctx context.Context, userGUID uuid.UUID, limit, offset uint32) ([]model.Item, error) +} diff --git a/internal/usecase/server/item/get_all/model.go b/internal/usecase/server/item/get_all/model.go new file mode 100644 index 0000000..0b2c544 --- /dev/null +++ b/internal/usecase/server/item/get_all/model.go @@ -0,0 +1,24 @@ +package get_all + +import ( + "time" + + "github.com/google/uuid" +) + +type Data struct { + UserGUID uuid.UUID + Limit uint32 + Offset uint32 +} + +type Result struct { + Items []Item +} + +type Item struct { + GUID uuid.UUID + EncryptedData []byte + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/usecase/server/item/get_all/usecase.go b/internal/usecase/server/item/get_all/usecase.go new file mode 100644 index 0000000..d719cd4 --- /dev/null +++ b/internal/usecase/server/item/get_all/usecase.go @@ -0,0 +1,49 @@ +// Package get_all отвечает за получение всех элементов на стороне сервера. +package get_all + +import ( + "context" + "errors" + "fmt" + + storeUser "github.com/bjlag/go-keeper/internal/infrastructure/store/server/user" +) + +var ErrNoData = errors.New("no data") + +type Usecase struct { + dataStore dataStore +} + +func NewUsecase(dataStore dataStore) *Usecase { + return &Usecase{ + dataStore: dataStore, + } +} + +func (u Usecase) Do(ctx context.Context, data Data) (*Result, error) { + const op = "usecase.item.getAll.Do" + + rows, err := u.dataStore.GetAllByUser(ctx, data.UserGUID, data.Limit, data.Offset) + if err != nil { + if errors.Is(err, storeUser.ErrNotFound) { + return nil, ErrNoData + } + + return nil, fmt.Errorf("%s: %w", op, err) + } + + items := make([]Item, 0, len(rows)) + for _, r := range rows { + items = append(items, Item{ + GUID: r.GUID, + EncryptedData: r.EncryptedData, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + }) + } + + return &Result{ + Items: items, + }, nil +} diff --git a/internal/usecase/server/item/get_by_guid/contract.go b/internal/usecase/server/item/get_by_guid/contract.go new file mode 100644 index 0000000..69a4d15 --- /dev/null +++ b/internal/usecase/server/item/get_by_guid/contract.go @@ -0,0 +1,13 @@ +package get_by_guid + +import ( + "context" + + "github.com/google/uuid" + + model "github.com/bjlag/go-keeper/internal/domain/server/data" +) + +type dataStore interface { + UserItemByGUID(ctx context.Context, userGUID, itemGUID uuid.UUID) (*model.Item, error) +} diff --git a/internal/usecase/server/item/get_by_guid/model.go b/internal/usecase/server/item/get_by_guid/model.go new file mode 100644 index 0000000..6da0f29 --- /dev/null +++ b/internal/usecase/server/item/get_by_guid/model.go @@ -0,0 +1,19 @@ +package get_by_guid + +import ( + "time" + + "github.com/google/uuid" +) + +type Data struct { + GUID uuid.UUID + UserGUID uuid.UUID +} + +type Result struct { + GUID uuid.UUID + EncryptedData []byte + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/usecase/server/item/get_by_guid/usecase.go b/internal/usecase/server/item/get_by_guid/usecase.go new file mode 100644 index 0000000..539bce0 --- /dev/null +++ b/internal/usecase/server/item/get_by_guid/usecase.go @@ -0,0 +1,38 @@ +// Package get_by_guid отвечает за получение элемента на стороне сервера. +package get_by_guid + +import ( + "context" + "errors" + "fmt" + + "github.com/bjlag/go-keeper/internal/domain/server/data" + "github.com/bjlag/go-keeper/internal/infrastructure/store/server/item" +) + +var ErrNoData = errors.New("no data") + +type Usecase struct { + dataStore dataStore +} + +func NewUsecase(dataStore dataStore) *Usecase { + return &Usecase{ + dataStore: dataStore, + } +} + +func (u Usecase) Do(ctx context.Context, data Data) (*data.Item, error) { + const op = "usecase.item.getByGuid.Do" + + model, err := u.dataStore.UserItemByGUID(ctx, data.UserGUID, data.GUID) + if err != nil { + if errors.Is(err, item.ErrNotFound) { + return nil, ErrNoData + } + + return nil, fmt.Errorf("%s: %w", op, err) + } + + return model, nil +} diff --git a/internal/usecase/server/item/remove/contract.go b/internal/usecase/server/item/remove/contract.go new file mode 100644 index 0000000..89e477c --- /dev/null +++ b/internal/usecase/server/item/remove/contract.go @@ -0,0 +1,11 @@ +package remove + +import ( + "context" + + "github.com/google/uuid" +) + +type store interface { + Delete(ctx context.Context, guid uuid.UUID, userGUID uuid.UUID) error +} diff --git a/internal/usecase/server/item/remove/model.go b/internal/usecase/server/item/remove/model.go new file mode 100644 index 0000000..95a90cd --- /dev/null +++ b/internal/usecase/server/item/remove/model.go @@ -0,0 +1,10 @@ +package remove + +import ( + "github.com/google/uuid" +) + +type In struct { + UserGUID uuid.UUID + ItemGUID uuid.UUID +} diff --git a/internal/usecase/server/item/remove/usecase.go b/internal/usecase/server/item/remove/usecase.go new file mode 100644 index 0000000..06e5fd1 --- /dev/null +++ b/internal/usecase/server/item/remove/usecase.go @@ -0,0 +1,36 @@ +// Package remove отвечает за удаление элемента на стороне сервера. +package remove + +import ( + "context" + "errors" + "fmt" + + "github.com/bjlag/go-keeper/internal/infrastructure/store/server/item" +) + +var ErrNotFoundUpdatedData = errors.New("deleted data is not found ") + +type Usecase struct { + store store +} + +func NewUsecase(store store) *Usecase { + return &Usecase{ + store: store, + } +} + +func (u Usecase) Do(ctx context.Context, in In) error { + const op = "usecase.item.remove.Do" + + err := u.store.Delete(ctx, in.ItemGUID, in.UserGUID) + if err != nil { + if errors.Is(err, item.ErrNotAffectedRows) { + return ErrNotFoundUpdatedData + } + return fmt.Errorf("%s: %w", op, err) + } + + return nil +} diff --git a/internal/usecase/server/item/update/contract.go b/internal/usecase/server/item/update/contract.go new file mode 100644 index 0000000..7b1b6d7 --- /dev/null +++ b/internal/usecase/server/item/update/contract.go @@ -0,0 +1,14 @@ +package update + +import ( + "context" + + "github.com/google/uuid" + + model "github.com/bjlag/go-keeper/internal/domain/server/data" +) + +type store interface { + UserItemByGUID(ctx context.Context, userGUID, itemGUID uuid.UUID) (*model.Item, error) + Update(ctx context.Context, guid uuid.UUID, userGUID uuid.UUID, updatedData model.UpdatedItem) error +} diff --git a/internal/usecase/server/item/update/model.go b/internal/usecase/server/item/update/model.go new file mode 100644 index 0000000..db7b92c --- /dev/null +++ b/internal/usecase/server/item/update/model.go @@ -0,0 +1,12 @@ +package update + +import ( + "github.com/google/uuid" +) + +type In struct { + UserGUID uuid.UUID + ItemGUID uuid.UUID + EncryptedData []byte + Version int64 +} diff --git a/internal/usecase/server/item/update/usecase.go b/internal/usecase/server/item/update/usecase.go new file mode 100644 index 0000000..dcdf3e6 --- /dev/null +++ b/internal/usecase/server/item/update/usecase.go @@ -0,0 +1,58 @@ +// Package update отвечает за обновление элемента на стороне сервера. +package update + +import ( + "context" + "errors" + "fmt" + "time" + + model "github.com/bjlag/go-keeper/internal/domain/server/data" + "github.com/bjlag/go-keeper/internal/infrastructure/store/server/item" +) + +var ( + ErrItemNotFound = errors.New("item not found") + ErrConflict = errors.New("conflict") +) + +type Usecase struct { + store store +} + +func NewUsecase(store store) *Usecase { + return &Usecase{ + store: store, + } +} + +func (u Usecase) Do(ctx context.Context, in In) (newVersion int64, err error) { + const op = "usecase.item.update.Do" + + currentItem, err := u.store.UserItemByGUID(ctx, in.UserGUID, in.ItemGUID) + if err != nil { + if errors.Is(err, item.ErrNotFound) { + return 0, ErrItemNotFound + } + } + + if in.Version != currentItem.UpdatedAt.UTC().UnixMicro() { + return 0, ErrConflict + } + + updatedAt := time.Now() + data := model.UpdatedItem{ + EncryptedData: in.EncryptedData, + UpdatedAt: updatedAt, + } + + err = u.store.Update(ctx, in.ItemGUID, in.UserGUID, data) + if err != nil { + if errors.Is(err, item.ErrNotAffectedRows) { + return 0, ErrItemNotFound + } + return 0, fmt.Errorf("%s: %w", op, err) + } + + return updatedAt.UnixMicro(), nil +} diff --git a/internal/usecase/server/user/login/contract.go b/internal/usecase/server/user/login/contract.go new file mode 100644 index 0000000..b39223b --- /dev/null +++ b/internal/usecase/server/user/login/contract.go @@ -0,0 +1,15 @@ +package login + +import ( + "context" + + model "github.com/bjlag/go-keeper/internal/domain/server/user" +) + +type userStore interface { + GetByEmail(ctx context.Context, email string) (*model.User, error) +} + +type tokenGenerator interface { + GenerateTokens(guid string) (accessToken, refreshToken string, err error) +} diff --git a/internal/usecase/server/user/login/model.go b/internal/usecase/server/user/login/model.go new file mode 100644 index 0000000..17dac34 --- /dev/null +++ b/internal/usecase/server/user/login/model.go @@ -0,0 +1,11 @@ +package login + +type Data struct { + Email string + Password string +} + +type Result struct { + AccessToken string + RefreshToken string +} diff --git a/internal/usecase/server/user/login/usecase.go b/internal/usecase/server/user/login/usecase.go new file mode 100644 index 0000000..5d3e63e --- /dev/null +++ b/internal/usecase/server/user/login/usecase.go @@ -0,0 +1,57 @@ +// Package login отвечает за аутентификацию пользователя на стороне сервера. +package login + +import ( + "context" + "errors" + "fmt" + + "golang.org/x/crypto/bcrypt" + + storeUser "github.com/bjlag/go-keeper/internal/infrastructure/store/server/user" +) + +var ( + ErrUserNotFound = errors.New("user not found") + ErrPasswordIncorrect = errors.New("password incorrect") +) + +type Usecase struct { + userStore userStore + tokenGenerator tokenGenerator +} + +func NewUsecase(userStore userStore, tokenGenerator tokenGenerator) *Usecase { + return &Usecase{ + userStore: userStore, + tokenGenerator: tokenGenerator, + } +} + +func (u Usecase) Do(ctx context.Context, data Data) (*Result, error) { + const op = "usecase.user.login.Do" + + user, err := u.userStore.GetByEmail(ctx, data.Email) + if err != nil { + if errors.Is(err, storeUser.ErrNotFound) { + return nil, ErrUserNotFound + } + + return nil, fmt.Errorf("%s: %w", op, err) + } + + err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(data.Password)) + if err != nil { + return nil, ErrPasswordIncorrect + } + + accessToken, refreshToken, err := u.tokenGenerator.GenerateTokens(user.GUID.String()) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + return &Result{ + AccessToken: accessToken, + RefreshToken: refreshToken, + }, nil +} diff --git a/internal/usecase/server/user/refresh_tokens/contract.go b/internal/usecase/server/user/refresh_tokens/contract.go new file mode 100644 index 0000000..dc357f9 --- /dev/null +++ b/internal/usecase/server/user/refresh_tokens/contract.go @@ -0,0 +1,18 @@ +package refresh_tokens + +import ( + "context" + + "github.com/google/uuid" + + model "github.com/bjlag/go-keeper/internal/domain/server/user" +) + +type userStore interface { + GetByGUID(ctx context.Context, guid uuid.UUID) (*model.User, error) +} + +type tokenGenerator interface { + GetUserGUIDFromRefreshToken(tokenString string) (uuid.UUID, error) + GenerateTokens(guid string) (accessToken, refreshToken string, err error) +} diff --git a/internal/usecase/server/user/refresh_tokens/model.go b/internal/usecase/server/user/refresh_tokens/model.go new file mode 100644 index 0000000..bb45586 --- /dev/null +++ b/internal/usecase/server/user/refresh_tokens/model.go @@ -0,0 +1,10 @@ +package refresh_tokens + +type Data struct { + RefreshToken string +} + +type Result struct { + AccessToken string + RefreshToken string +} diff --git a/internal/usecase/server/user/refresh_tokens/usecase.go b/internal/usecase/server/user/refresh_tokens/usecase.go new file mode 100644 index 0000000..1146c82 --- /dev/null +++ b/internal/usecase/server/user/refresh_tokens/usecase.go @@ -0,0 +1,59 @@ +// Package refresh_tokens отвечает за обновление токенов на стороне сервера. +package refresh_tokens + +import ( + "context" + "errors" + "fmt" + + "github.com/bjlag/go-keeper/internal/infrastructure/auth" + storeUser "github.com/bjlag/go-keeper/internal/infrastructure/store/server/user" +) + +var ( + ErrInvalidRefreshToken = errors.New("invalid refresh token") + ErrUserNotFound = errors.New("user not found") +) + +type Usecase struct { + userStore userStore + tokenGenerator tokenGenerator +} + +func NewUsecase(userStore userStore, tokenGenerator tokenGenerator) *Usecase { + return &Usecase{ + userStore: userStore, + tokenGenerator: tokenGenerator, + } +} + +func (u Usecase) Do(ctx context.Context, data Data) (*Result, error) { + const op = "usecase.user.refreshTokens.Do" + + guid, err := u.tokenGenerator.GetUserGUIDFromRefreshToken(data.RefreshToken) + if err != nil { + if errors.Is(err, auth.ErrInvalidToken) { + return nil, ErrInvalidRefreshToken + } + return nil, fmt.Errorf("%s: %w", op, err) + } + + user, err := u.userStore.GetByGUID(ctx, guid) + if err != nil { + if errors.Is(err, storeUser.ErrNotFound) { + return nil, ErrUserNotFound + } + + return nil, fmt.Errorf("%s: %w", op, err) + } + + accessToken, refreshToken, err := u.tokenGenerator.GenerateTokens(user.GUID.String()) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + return &Result{ + AccessToken: accessToken, + RefreshToken: refreshToken, + }, nil +} diff --git a/internal/usecase/server/user/register/contract.go b/internal/usecase/server/user/register/contract.go new file mode 100644 index 0000000..ac20c54 --- /dev/null +++ b/internal/usecase/server/user/register/contract.go @@ -0,0 +1,16 @@ +package register + +import ( + "context" + + model "github.com/bjlag/go-keeper/internal/domain/server/user" +) + +type userStore interface { + GetByEmail(ctx context.Context, email string) (*model.User, error) + Add(ctx context.Context, user model.User) error +} + +type tokenGenerator interface { + GenerateTokens(guid string) (accessToken, refreshToken string, err error) +} diff --git a/internal/usecase/server/user/register/model.go b/internal/usecase/server/user/register/model.go new file mode 100644 index 0000000..6dca93a --- /dev/null +++ b/internal/usecase/server/user/register/model.go @@ -0,0 +1,11 @@ +package register + +type Data struct { + Email string + Password string +} + +type Result struct { + AccessToken string + RefreshToken string +} diff --git a/internal/usecase/server/user/register/usecase.go b/internal/usecase/server/user/register/usecase.go new file mode 100644 index 0000000..5870feb --- /dev/null +++ b/internal/usecase/server/user/register/usecase.go @@ -0,0 +1,81 @@ +// Package register отвечает за регистрацию пользователя на стороне сервера. +package register + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" + + model "github.com/bjlag/go-keeper/internal/domain/server/user" + storeUser "github.com/bjlag/go-keeper/internal/infrastructure/store/server/user" +) + +var ErrUserAlreadyExists = errors.New("user already exists") + +type Usecase struct { + userStore userStore + tokenGenerator tokenGenerator +} + +func NewUsecase(userStore userStore, tokenGenerator tokenGenerator) *Usecase { + return &Usecase{ + userStore: userStore, + tokenGenerator: tokenGenerator, + } +} + +func (u Usecase) Do(ctx context.Context, data Data) (*Result, error) { + const op = "usecase.user.register.Do" + + exist, err := u.isUserExists(ctx, data.Email) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + if exist { + return nil, ErrUserAlreadyExists + } + + passwordHash, err := bcrypt.GenerateFromPassword([]byte(data.Password), bcrypt.MinCost) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + user := model.User{ + GUID: uuid.New(), + Email: data.Email, + PasswordHash: string(passwordHash), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + err = u.userStore.Add(ctx, user) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + accessToken, refreshToken, err := u.tokenGenerator.GenerateTokens(user.GUID.String()) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + + return &Result{ + AccessToken: accessToken, + RefreshToken: refreshToken, + }, nil +} + +func (u Usecase) isUserExists(ctx context.Context, email string) (bool, error) { + m, err := u.userStore.GetByEmail(ctx, email) + if err != nil { + if errors.Is(err, storeUser.ErrNotFound) { + return false, nil + } + return false, fmt.Errorf("failed to get user by email: %w", err) + } + + return m != nil, nil +} diff --git a/migrations/client/001_create_data_table.up.sql b/migrations/client/001_create_data_table.up.sql new file mode 100644 index 0000000..fce931e --- /dev/null +++ b/migrations/client/001_create_data_table.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS items ( + guid text PRIMARY KEY NOT NULL, + category_id INT NOT NULL, + title text NOT NULL, + value text, + notes text NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL +); diff --git a/migrations/client/002_create_options_table.up.sql b/migrations/client/002_create_options_table.up.sql new file mode 100644 index 0000000..2f9aaa5 --- /dev/null +++ b/migrations/client/002_create_options_table.up.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS options ( + slug text PRIMARY KEY NOT NULL, + value text NOT NULL +); diff --git a/migrations/client/003_create_backup_table.up.sql b/migrations/client/003_create_backup_table.up.sql new file mode 100644 index 0000000..8975ace --- /dev/null +++ b/migrations/client/003_create_backup_table.up.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS backup ( + guid text PRIMARY KEY NOT NULL, + value text NOT NULL +); diff --git a/migrations/server/001_create_users_table.up.sql b/migrations/server/001_create_users_table.up.sql new file mode 100644 index 0000000..93bce7b --- /dev/null +++ b/migrations/server/001_create_users_table.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS users ( + guid uuid PRIMARY KEY NOT NULL, + email varchar(20) NOT NULL, + password_hash varchar(60) NOT NULL, + created_at timestamptz NOT NULL DEFAULT NOW(), + updated_at timestamptz NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX users_email_uniq_idx ON users (email); + +COMMENT ON TABLE users IS 'Пользователи'; +COMMENT ON COLUMN users.guid IS 'GUID'; +COMMENT ON COLUMN users.email IS 'Email'; +COMMENT ON COLUMN users.created_at IS 'Дата создания пользователя'; +COMMENT ON COLUMN users.updated_at IS 'Дата изменения пользователя'; \ No newline at end of file diff --git a/migrations/server/002_create_data_table.up.sql b/migrations/server/002_create_data_table.up.sql new file mode 100644 index 0000000..698db74 --- /dev/null +++ b/migrations/server/002_create_data_table.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS items ( + guid uuid PRIMARY KEY NOT NULL, + user_guid uuid NOT NULL REFERENCES users (guid) ON DELETE RESTRICT, + encrypted_data bytea NOT NULL, + created_at timestamptz NOT NULL DEFAULT NOW(), + updated_at timestamptz NOT NULL DEFAULT NOW() +); + +CREATE INDEX items_user_guid_idx ON items (user_guid); + +COMMENT ON TABLE items IS 'Данные (пароли, логины, тексты и пр.)'; +COMMENT ON COLUMN items.guid IS 'GUID'; +COMMENT ON COLUMN items.user_guid IS 'Владелец'; +COMMENT ON COLUMN items.encrypted_data IS 'Сами данные в зашифрованном виде'; +COMMENT ON COLUMN items.created_at IS 'Дата создания записи'; +COMMENT ON COLUMN items.updated_at IS 'Дата изменения записи'; \ No newline at end of file diff --git a/proto/keeper.proto b/proto/keeper.proto new file mode 100644 index 0000000..e48adcf --- /dev/null +++ b/proto/keeper.proto @@ -0,0 +1,100 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; + +package keeper; + +option go_package = "internal/generated/rpc"; + +service Keeper { + // auth + rpc Register(RegisterIn) returns (RegisterOut); + rpc Login(LoginIn) returns (LoginOut); + rpc RefreshTokens(RefreshTokensIn) returns (RefreshTokensOut); + + // data + rpc GetByGuid(GetByGuidIn) returns (GetByGuidOut); + rpc GetAllItems(GetAllItemsIn) returns (GetAllItemsOut); + rpc CreateItem(CreateItemIn) returns (google.protobuf.Empty); + rpc UpdateItem(UpdateItemIn) returns (UpdateItemOut); + rpc DeleteItem(DeleteItemIn) returns (google.protobuf.Empty); +} + +// auth +message RegisterIn { + string email = 1; + string password = 2; +} + +message RegisterOut { + string access_token = 1; + string refresh_token = 2; +} + +message LoginIn { + string email = 1; + string password = 2; +} + +message LoginOut { + string access_token = 1; + string refresh_token = 2; +} + +message RefreshTokensIn { + string refresh_token = 1; +} + +message RefreshTokensOut { + string access_token = 1; + string refresh_token = 2; +} + +// data +message GetByGuidIn { + string guid = 1; +} + +message GetByGuidOut { + string guid = 1; + bytes encryptedData = 2; + google.protobuf.Timestamp CreatedAt = 3; + google.protobuf.Timestamp UpdatedAt = 4; +} + +message GetAllItemsIn { + uint32 limit = 1; + uint32 offset = 2; +} + +message GetAllItemsOut { + repeated Item items = 1; +} + +message Item { + string guid = 1; + bytes encryptedData = 2; + google.protobuf.Timestamp CreatedAt = 3; + google.protobuf.Timestamp UpdatedAt = 4; +} + +message CreateItemIn { + string guid = 1; + bytes encryptedData = 2; + google.protobuf.Timestamp CreatedAt = 3; +} + +message UpdateItemIn { + string guid = 1; + bytes encryptedData = 2; + int64 version = 3; +} + +message UpdateItemOut { + int64 newVersion = 1; +} + +message DeleteItemIn { + string guid = 1; +} \ No newline at end of file diff --git a/test/fixture/server/items.yaml b/test/fixture/server/items.yaml new file mode 100644 index 0000000..f6e48be --- /dev/null +++ b/test/fixture/server/items.yaml @@ -0,0 +1,34 @@ +# password +- guid: 60308368-7729-4d2d-a510-67926f5a159b + user_guid: bf4e6232-f1ae-41da-8535-73048891b1e3 + encrypted_data: 0x4CA6423E2DACC0ACD0AA60620930AF7FC3E30F8692D716FBC637C9972B76CAEC09D5643205EFFEE95567204FBE66E8E5683DFB1E7319BA25E09C4F719810985C1CFC80E5FE8A55AC142716357023DE30BF4892DA3E481055290F3B90F8EB93AFBC9CE94DEDF2B73B734EB8ED0FF10EF6A925711CA83AD7C53744 + created_at: 2025-03-15 13:00:00 + updated_at: 2025-03-15 13:00:00 + +# text +- guid: 6e7fc4fa-31aa-4d75-8b6e-0479122e0147 + user_guid: bf4e6232-f1ae-41da-8535-73048891b1e3 + encrypted_data: 0x104501B790B26B92AFB9648067356F6ADA2EE513C1CE48F93789F1B8ED23E73054998D9E3D40AB4D2D790EAC919BC558D329A0134485D2C26F8A23C27A46CF06D99B80ECCA6EE739BFFA + created_at: 2025-03-15 13:10:00 + updated_at: 2025-03-15 13:10:00 + +# file +- guid: b2bd09eb-2c84-4149-b2b8-29040472264a + user_guid: bf4e6232-f1ae-41da-8535-73048891b1e3 + encrypted_data: 0xDE8B088675B88D6922E7B789468BBBD9707A7492F65669DFF15A7EBD25F8DC489B2D20F47F461AD288AF1D66135711837D425BB66E459E271C09D7171D83F6F7B346ACA35C45A9BA04CF39CF4F656055F89A9DD9A28F5933C6B6BB1FB8166DAC946F48BA14AB9A2AE8C589ABE62464E44B4FF0656F1FF66EBBABAA47 + created_at: 2025-03-15 13:20:00 + updated_at: 2025-03-15 13:20:00 + +# bank card +- guid: 127e1a2d-1943-4fb1-ba60-7dc4fc820ed4 + user_guid: bf4e6232-f1ae-41da-8535-73048891b1e3 + encrypted_data: 0x460D40A892881F1049226D48E3FD6B9C1271C570379A1659AD366E5616A81AD7D313358AC4752C49F420C414336DBD4FF25C39E5A7B49B8B2CD765B885502A207CB6E1672E8EBD57FFC0D79A608A44211AD1F915010094B3B8FC1CDC11ED992EE30B404DF0766843388434034EE640BCCB50374CD33CE63E4843A75934693BE2CF19F6AD05E132E2160B0443187DDC + created_at: 2025-03-15 13:30:00 + updated_at: 2025-03-15 13:30:00 + +# password other@user.ru +- guid: d07f9605-0b8e-42f6-a07b-3e3f839a7bee + user_guid: 67ff83b7-d199-4014-9c59-ad1e77340d9f + encrypted_data: 0x4CA6423E2DACC0ACD0AA60620930AF7FC3E30F8692D716FBC637C9972B76CAEC09D5643205EFFEE95567204FBE66E8E5683DFB1E7319BA25E09C4F719810985C1CFC80E5FE8A55AC142716357023DE30BF4892DA3E481055290F3B90F8EB93AFBC9CE94DEDF2B73B734EB8ED0FF10EF6A925711CA83AD7C53744 + created_at: 2025-03-15 13:00:00 + updated_at: 2025-03-15 13:00:00 \ No newline at end of file diff --git a/test/fixture/server/users.yaml b/test/fixture/server/users.yaml new file mode 100644 index 0000000..004b453 --- /dev/null +++ b/test/fixture/server/users.yaml @@ -0,0 +1,11 @@ +- guid: bf4e6232-f1ae-41da-8535-73048891b1e3 + email: test@test.ru + password_hash: $2a$04$OxrD4jeA4SrmsnQOdYH4iu1dfsydv36kbBsmQSXZBDtDTca6Vef2K # 12345678 + created_at: 2025-03-15 13:00:00 + updated_at: 2025-03-15 13:00:00 + +- guid: 67ff83b7-d199-4014-9c59-ad1e77340d9f + email: other@user.ru + password_hash: $2a$04$OxrD4jeA4SrmsnQOdYH4iu1dfsydv36kbBsmQSXZBDtDTca6Vef2K # 12345678 + created_at: 2025-03-13 13:00:00 + updated_at: 2025-03-13 13:00:00 \ No newline at end of file diff --git a/test/functional/server/create_item_test.go b/test/functional/server/create_item_test.go new file mode 100644 index 0000000..cf80009 --- /dev/null +++ b/test/functional/server/create_item_test.go @@ -0,0 +1,81 @@ +package server_test + +import ( + "context" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/test/infrastructure/fixture" + _ "github.com/bjlag/go-keeper/test/infrastructure/init" +) + +func (s *TestSuite) TestCreateItem() { + err := fixture.Load(s.db, "test/fixture/server") + s.Require().NoError(err) + + s.Run("success", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + _, err := s.client.CreateItem(ctx, &rpc.CreateItemIn{ + Guid: "c904fe47-4ec8-4ff4-a642-774a7bf4e351", + EncryptedData: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + CreatedAt: timestamppb.New(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)), + }) + s.Require().NoError(err) + + item := s.getFromDBByGUID(ctx, "c904fe47-4ec8-4ff4-a642-774a7bf4e351") + + s.Equal("c904fe47-4ec8-4ff4-a642-774a7bf4e351", item.GUID) + s.Equal([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, item.EncryptedData) + s.Equal("bf4e6232-f1ae-41da-8535-73048891b1e3", item.UserGUID) + s.True(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC).Equal(item.CreatedAt.UTC())) + s.True(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC).Equal(item.UpdatedAt.UTC())) + }) + + s.Run("permission denied", func() { + _, err := s.client.CreateItem(context.Background(), &rpc.CreateItemIn{ + Guid: "c904fe47-4ec8-4ff4-a642-774a7bf4e351", + EncryptedData: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + CreatedAt: timestamppb.New(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)), + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.PermissionDenied, st.Code()) + s.Equal("permission denied", st.Message()) + }) + + s.Run("invalid item guid", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + _, err := s.client.CreateItem(ctx, &rpc.CreateItemIn{ + Guid: "invalid guid", + EncryptedData: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + CreatedAt: timestamppb.New(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)), + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.InvalidArgument, st.Code()) + s.Equal("invalid item guid", st.Message()) + }) + + s.Run("encrypted data is empty", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + _, err := s.client.CreateItem(ctx, &rpc.CreateItemIn{ + Guid: "c904fe47-4ec8-4ff4-a642-774a7bf4e351", + EncryptedData: []byte{}, + CreatedAt: timestamppb.New(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)), + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.InvalidArgument, st.Code()) + s.Equal("encrypted data is empty", st.Message()) + }) +} diff --git a/test/functional/server/delete_item_test.go b/test/functional/server/delete_item_test.go new file mode 100644 index 0000000..8040a60 --- /dev/null +++ b/test/functional/server/delete_item_test.go @@ -0,0 +1,76 @@ +package server_test + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/test/infrastructure/fixture" + _ "github.com/bjlag/go-keeper/test/infrastructure/init" +) + +func (s *TestSuite) TestDeleteItem() { + err := fixture.Load(s.db, "test/fixture/server") + s.Require().NoError(err) + + s.Run("success", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + _, err := s.client.DeleteItem(ctx, &rpc.DeleteItemIn{ + Guid: "60308368-7729-4d2d-a510-67926f5a159b", + }) + s.Require().NoError(err) + + items := s.getAllFromDBByUserGUID(ctx, "bf4e6232-f1ae-41da-8535-73048891b1e3") + + s.Len(items, 3) + s.Condition(func() (success bool) { + success = true + for _, item := range items { + if item.GUID == "60308368-7729-4d2d-a510-67926f5a159b" { + success = false + } + } + return + }, "Не должно быть элемента с GUID 60308368-7729-4d2d-a510-67926f5a159b") + }) + + s.Run("permission denied", func() { + _, err := s.client.DeleteItem(context.Background(), &rpc.DeleteItemIn{ + Guid: "60308368-7729-4d2d-a510-67926f5a159b", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.PermissionDenied, st.Code()) + s.Equal("permission denied", st.Message()) + }) + + s.Run("not found", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + _, err := s.client.DeleteItem(ctx, &rpc.DeleteItemIn{ + Guid: "00000000-0000-0000-0000-000000000000", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.NotFound, st.Code()) + s.Equal("item not found", st.Message()) + }) + + s.Run("not found if item belongs to other user", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + _, err := s.client.DeleteItem(ctx, &rpc.DeleteItemIn{ + Guid: "d07f9605-0b8e-42f6-a07b-3e3f839a7bee", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.NotFound, st.Code()) + s.Equal("item not found", st.Message()) + }) +} diff --git a/test/functional/server/get_all_items_test.go b/test/functional/server/get_all_items_test.go new file mode 100644 index 0000000..7224d1d --- /dev/null +++ b/test/functional/server/get_all_items_test.go @@ -0,0 +1,59 @@ +package server_test + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/test/infrastructure/fixture" + _ "github.com/bjlag/go-keeper/test/infrastructure/init" +) + +func (s *TestSuite) TestGetAllItems() { + err := fixture.Load(s.db, "test/fixture/server") + s.Require().NoError(err) + + s.Run("success", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + getAllItemsOut, err := s.client.GetAllItems(ctx, &rpc.GetAllItemsIn{}) + s.Require().NoError(err) + s.Len(getAllItemsOut.GetItems(), 4) + }) + + s.Run("success limit offset", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + getAllItemsOut1, err := s.client.GetAllItems(ctx, &rpc.GetAllItemsIn{ + Offset: 0, + Limit: 2, + }) + s.Require().NoError(err) + s.Len(getAllItemsOut1.GetItems(), 2) + s.Equal(getAllItemsOut1.GetItems()[0].GetGuid(), "127e1a2d-1943-4fb1-ba60-7dc4fc820ed4") + s.Equal(getAllItemsOut1.GetItems()[1].GetGuid(), "60308368-7729-4d2d-a510-67926f5a159b") + + getAllItemsOut2, err := s.client.GetAllItems(ctx, &rpc.GetAllItemsIn{ + Offset: 2, + Limit: 2, + }) + s.Require().NoError(err) + s.Len(getAllItemsOut2.GetItems(), 2) + s.Equal(getAllItemsOut2.GetItems()[0].GetGuid(), "6e7fc4fa-31aa-4d75-8b6e-0479122e0147") + s.Equal(getAllItemsOut2.GetItems()[1].GetGuid(), "b2bd09eb-2c84-4149-b2b8-29040472264a") + }) + + s.Run("permission denied", func() { + ctx := context.Background() + out, err := s.client.GetAllItems(ctx, &rpc.GetAllItemsIn{}) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.PermissionDenied, st.Code()) + s.Equal("permission denied", st.Message()) + + s.Empty(out.GetItems()) + }) +} diff --git a/test/functional/server/get_by_guid_test.go b/test/functional/server/get_by_guid_test.go new file mode 100644 index 0000000..a5cf1d0 --- /dev/null +++ b/test/functional/server/get_by_guid_test.go @@ -0,0 +1,68 @@ +package server_test + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/test/infrastructure/fixture" + _ "github.com/bjlag/go-keeper/test/infrastructure/init" +) + +func (s *TestSuite) TestGetByGUID() { + err := fixture.Load(s.db, "test/fixture/server") + s.Require().NoError(err) + + s.Run("success", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + getByGUIDOut, err := s.client.GetByGuid(ctx, &rpc.GetByGuidIn{ + Guid: "60308368-7729-4d2d-a510-67926f5a159b", + }) + s.Require().NoError(err) + s.Equal("60308368-7729-4d2d-a510-67926f5a159b", getByGUIDOut.GetGuid()) + }) + + s.Run("not found", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + getByGUIDOut, err := s.client.GetByGuid(ctx, &rpc.GetByGuidIn{ + Guid: "00000000-0000-0000-0000-000000000000", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.NotFound, st.Code()) + s.Equal("item not found", st.Message()) + s.Nil(getByGUIDOut) + }) + + s.Run("not found if item belongs to other user", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + getByGUIDOut, err := s.client.GetByGuid(ctx, &rpc.GetByGuidIn{ + Guid: "d07f9605-0b8e-42f6-a07b-3e3f839a7bee", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.NotFound, st.Code()) + s.Equal("item not found", st.Message()) + s.Nil(getByGUIDOut) + }) + + s.Run("permission denied", func() { + ctx := context.Background() + out, err := s.client.GetByGuid(ctx, &rpc.GetByGuidIn{ + Guid: "60308368-7729-4d2d-a510-67926f5a159b", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.PermissionDenied, st.Code()) + s.Equal("permission denied", st.Message()) + s.Nil(out) + }) +} diff --git a/test/functional/server/helper_test.go b/test/functional/server/helper_test.go new file mode 100644 index 0000000..802c6f3 --- /dev/null +++ b/test/functional/server/helper_test.go @@ -0,0 +1,56 @@ +package server_test + +import ( + "context" + "fmt" + + "google.golang.org/grpc/metadata" + + "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/test/functional/server" +) + +//nolint:unparam +func (s *TestSuite) login(ctx context.Context, email, password string) context.Context { + out, err := s.client.Login(ctx, &rpc.LoginIn{ + Email: email, + Password: password, + }) + s.Require().NoError(err) + + md, ok := metadata.FromOutgoingContext(ctx) + if !ok { + md = metadata.New(nil) + } + md.Set("authorization", fmt.Sprintf("%s %s", "Bearer", out.GetAccessToken())) + + return metadata.NewOutgoingContext(ctx, md) +} + +func (s *TestSuite) getFromDBByGUID(ctx context.Context, guid string) server.Item { + query := ` + SELECT guid, user_guid, encrypted_data, created_at, updated_at + FROM items + WHERE guid = $1 + ` + + var row server.Item + err := s.db.GetContext(ctx, &row, query, guid) + s.Require().NoError(err) + + return row +} + +func (s *TestSuite) getAllFromDBByUserGUID(ctx context.Context, guid string) []server.Item { + query := ` + SELECT guid, user_guid, encrypted_data, created_at, updated_at + FROM items + WHERE user_guid = $1 + ` + + var rows []server.Item + err := s.db.SelectContext(ctx, &rows, query, guid) + s.Require().NoError(err) + + return rows +} diff --git a/test/functional/server/login_test.go b/test/functional/server/login_test.go new file mode 100644 index 0000000..eee4dc6 --- /dev/null +++ b/test/functional/server/login_test.go @@ -0,0 +1,90 @@ +package server_test + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/test/infrastructure/fixture" + _ "github.com/bjlag/go-keeper/test/infrastructure/init" +) + +func (s *TestSuite) TestLogin() { + err := fixture.Load(s.db, "test/fixture/server") + s.Require().NoError(err) + + s.Run("success", func() { + ctx := context.Background() + out, err := s.client.Login(ctx, &rpc.LoginIn{ + Email: "test@test.ru", + Password: "12345678", + }) + + s.NoError(err) + s.NotEmpty(out.GetAccessToken()) + s.NotEmpty(out.GetRefreshToken()) + }) + + s.Run("wrong credentials", func() { + ctx := context.Background() + out, err := s.client.Login(ctx, &rpc.LoginIn{ + Email: "test@test.ru", + Password: "1111111", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.Unauthenticated, st.Code()) + s.Equal("credentials incorrect", st.Message()) + + s.Empty(out.GetAccessToken()) + s.Empty(out.GetRefreshToken()) + }) + + s.Run("empty body", func() { + ctx := context.Background() + out, err := s.client.Login(ctx, &rpc.LoginIn{}) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.InvalidArgument, st.Code()) + s.Equal("email is invalid", st.Message()) + + s.Empty(out.GetAccessToken()) + s.Empty(out.GetRefreshToken()) + }) + + s.Run("wrong email", func() { + ctx := context.Background() + out, err := s.client.Login(ctx, &rpc.LoginIn{ + Email: "test@test", + Password: "12345678", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.InvalidArgument, st.Code()) + s.Equal(st.Message(), "email is invalid") + + s.Empty(out.GetAccessToken()) + s.Empty(out.GetRefreshToken()) + }) + + s.Run("wrong password", func() { + ctx := context.Background() + out, err := s.client.Login(ctx, &rpc.LoginIn{ + Email: "test@test.ru", + Password: "", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.InvalidArgument, st.Code()) + s.Equal("password is empty", st.Message()) + + s.Empty(out.GetAccessToken()) + s.Empty(out.GetRefreshToken()) + }) +} diff --git a/test/functional/server/model.go b/test/functional/server/model.go new file mode 100644 index 0000000..3ca3dc5 --- /dev/null +++ b/test/functional/server/model.go @@ -0,0 +1,11 @@ +package server + +import "time" + +type Item struct { + GUID string `db:"guid"` + UserGUID string `db:"user_guid"` + EncryptedData []byte `db:"encrypted_data"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} diff --git a/test/functional/server/refresh_tokens_test.go b/test/functional/server/refresh_tokens_test.go new file mode 100644 index 0000000..31ef5f3 --- /dev/null +++ b/test/functional/server/refresh_tokens_test.go @@ -0,0 +1,103 @@ +package server_test + +import ( + "context" + "fmt" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/test/infrastructure/fixture" + _ "github.com/bjlag/go-keeper/test/infrastructure/init" +) + +func (s *TestSuite) TestRefreshTokens() { + err := fixture.Load(s.db, "test/fixture/server") + s.Require().NoError(err) + + s.Run("success", func() { + ctx := context.Background() + loginOut, err := s.client.Login(ctx, &rpc.LoginIn{ + Email: "test@test.ru", + Password: "12345678", + }) + s.Require().NoError(err) + + refreshTokensOut, err := s.client.RefreshTokens(ctx, &rpc.RefreshTokensIn{ + RefreshToken: loginOut.GetRefreshToken(), + }) + s.Require().NoError(err) + s.NotEmpty(refreshTokensOut.GetAccessToken()) + s.NotEmpty(refreshTokensOut.GetRefreshToken()) + + md, ok := metadata.FromOutgoingContext(ctx) + if !ok { + md = metadata.New(nil) + } + md.Set("authorization", fmt.Sprintf("%s %s", "Bearer", refreshTokensOut.GetAccessToken())) + + getAllItemsOut, err := s.client.GetAllItems(metadata.NewOutgoingContext(ctx, md), &rpc.GetAllItemsIn{}) + s.Require().NoError(err) + s.Len(getAllItemsOut.GetItems(), 4) + }) + + s.Run("sent access token", func() { + ctx := context.Background() + loginOut, err := s.client.Login(ctx, &rpc.LoginIn{ + Email: "test@test.ru", + Password: "12345678", + }) + s.Require().NoError(err) + + refreshTokensOut, err := s.client.RefreshTokens(ctx, &rpc.RefreshTokensIn{ + RefreshToken: loginOut.GetAccessToken(), + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.FailedPrecondition, st.Code()) + s.Equal("invalid refresh token", st.Message()) + s.Nil(refreshTokensOut) + }) + + s.Run("refresh token too short", func() { + ctx := context.Background() + refreshTokensOut, err := s.client.RefreshTokens(ctx, &rpc.RefreshTokensIn{ + RefreshToken: "refresh token too short", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.InvalidArgument, st.Code()) + s.Equal("refresh token too short", st.Message()) + s.Nil(refreshTokensOut) + }) + + s.Run("refresh token is wrong", func() { + ctx := context.Background() + refreshTokensOut, err := s.client.RefreshTokens(ctx, &rpc.RefreshTokensIn{ + RefreshToken: "wrong_token_eyJhbGciOiJIUzI1NiIsInR5.eyJpc3MiOiJiZjRlNjIzMi1mMWFlLTQxZGEtODUzNS03MzA0ODg5MWIxZTMiLCJzdWIiOiJyZWZyZXNoX3Rva2VuIiwiZXhwIjoxNzQyMzE2MDQ1LCJpYXQiOjE3NDIzMDg4NDV9.YO1nbCZ-1r4u4BWBvMSPOAda5m9R_IvoTJNqsziU-ik", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.FailedPrecondition, st.Code()) + s.Equal("invalid refresh token", st.Message()) + s.Nil(refreshTokensOut) + }) + + s.Run("refresh token is expired", func() { + ctx := context.Background() + refreshTokensOut, err := s.client.RefreshTokens(ctx, &rpc.RefreshTokensIn{ + RefreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiZjRlNjIzMi1mMWFlLTQxZGEtODUzNS03MzA0ODg5MWIxZTMiLCJzdWIiOiJyZWZyZXNoX3Rva2VuIiwiZXhwIjoxNzQyMzA5MTQwLCJpYXQiOjE3NDIzMDkxMzB9.zd-cbzbhry50DmC94xjxeBxXpX6HkHurgLpVorA9lg0", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.FailedPrecondition, st.Code()) + s.Equal("invalid refresh token", st.Message()) + s.Nil(refreshTokensOut) + }) +} diff --git a/test/functional/server/register_test.go b/test/functional/server/register_test.go new file mode 100644 index 0000000..7c526e9 --- /dev/null +++ b/test/functional/server/register_test.go @@ -0,0 +1,77 @@ +package server_test + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/test/infrastructure/fixture" + _ "github.com/bjlag/go-keeper/test/infrastructure/init" +) + +func (s *TestSuite) TestRegister() { + err := fixture.Load(s.db, "test/fixture/server") + s.Require().NoError(err) + + s.Run("success", func() { + ctx := context.Background() + out, err := s.client.Register(ctx, &rpc.RegisterIn{ + Email: "new@test.ru", + Password: "12345678", + }) + + s.NoError(err) + s.NotEmpty(out.GetAccessToken()) + s.NotEmpty(out.GetRefreshToken()) + }) + + s.Run("already exists", func() { + ctx := context.Background() + out, err := s.client.Register(ctx, &rpc.RegisterIn{ + Email: "test@test.ru", + Password: "12345678", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.AlreadyExists, st.Code()) + s.Equal("user with this email already exists", st.Message()) + + s.Empty(out.GetAccessToken()) + s.Empty(out.GetRefreshToken()) + }) + + s.Run("invalid email", func() { + ctx := context.Background() + out, err := s.client.Register(ctx, &rpc.RegisterIn{ + Email: "test@test", + Password: "12345678", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.InvalidArgument, st.Code()) + s.Equal("email is invalid", st.Message()) + + s.Empty(out.GetAccessToken()) + s.Empty(out.GetRefreshToken()) + }) + + s.Run("invalid password", func() { + ctx := context.Background() + out, err := s.client.Register(ctx, &rpc.RegisterIn{ + Email: "test@test.ru", + Password: "1234567", + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.InvalidArgument, st.Code()) + s.Equal("password is invalid (min. length 8 characters)", st.Message()) + + s.Empty(out.GetAccessToken()) + s.Empty(out.GetRefreshToken()) + }) +} diff --git a/test/functional/server/setup_test.go b/test/functional/server/setup_test.go new file mode 100644 index 0000000..1f1afa4 --- /dev/null +++ b/test/functional/server/setup_test.go @@ -0,0 +1,98 @@ +package server_test + +import ( + "context" + "testing" + "time" + + "github.com/ilyakaznacheev/cleanenv" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/suite" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/internal/infrastructure/auth" + "github.com/bjlag/go-keeper/internal/infrastructure/db/pg" + "github.com/bjlag/go-keeper/internal/infrastructure/logger" + "github.com/bjlag/go-keeper/internal/infrastructure/migrator" + "github.com/bjlag/go-keeper/test/infrastructure/config" + "github.com/bjlag/go-keeper/test/infrastructure/container" + _ "github.com/bjlag/go-keeper/test/infrastructure/init" + "github.com/bjlag/go-keeper/test/infrastructure/server" +) + +type TestSuite struct { + suite.Suite + pgContainer *container.PostgreSQLContainer + db *sqlx.DB + conn *grpc.ClientConn + client rpc.KeeperClient +} + +func TestSuite_Run(t *testing.T) { + suite.Run(t, new(TestSuite)) +} + +func (s *TestSuite) SetupSuite() { + const pathToConfig = "./config/server_test.yaml" + + ctx, ctxCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer ctxCancel() + + // load config + var cfg config.Config + err := cleanenv.ReadConfig(pathToConfig, &cfg) + s.Require().NoError(err) + + // crate db container + pgContainer, err := container.NewPostgreSQLContainer(ctx, container.PostgreSQLConfig{ + Database: cfg.Container.PG.DBName, + Username: cfg.Container.PG.DBUser, + Password: cfg.Container.PG.DBPassword, + ImageTag: cfg.Container.PG.Tag, + }) + s.Require().NoError(err) + + s.pgContainer = pgContainer + + // get db connection + db, err := pg.New(pg.GetDSN(pgContainer.Host, pgContainer.Port, pgContainer.Database, pgContainer.Username, pgContainer.Password)).Connect() + s.Require().NoError(err) + + s.db = db + + // apply migrations + m, err := migrator.Get(db, migrator.TypePG, pgContainer.Database, cfg.Migration.SourcePath, cfg.Migration.Table) + s.Require().NoError(err) + + err = m.Up() + s.Require().NoError(err) + + // create grpc client + jwt := auth.NewJWT(cfg.Auth.SecretKey, cfg.Auth.AccessTokenExp, cfg.Auth.RefreshTokenExp) + log := logger.Get(cfg.Env) + + conn, err := grpc.NewClient( + "passthrough://bufnet", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(server.Start(context.Background(), db, jwt, log)), + ) + s.Require().NoError(err) + + s.conn = conn + s.client = rpc.NewKeeperClient(conn) +} + +func (s *TestSuite) TearDownSuite() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + s.Require().NoError(s.pgContainer.Terminate(ctx)) + + err := s.conn.Close() + s.Require().NoError(err) + + err = s.db.Close() + s.Require().NoError(err) +} diff --git a/test/functional/server/update_item_test.go b/test/functional/server/update_item_test.go new file mode 100644 index 0000000..fac8642 --- /dev/null +++ b/test/functional/server/update_item_test.go @@ -0,0 +1,110 @@ +package server_test + +import ( + "context" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/bjlag/go-keeper/internal/generated/rpc" + "github.com/bjlag/go-keeper/test/infrastructure/fixture" + _ "github.com/bjlag/go-keeper/test/infrastructure/init" +) + +func (s *TestSuite) TestUpdateItem() { + err := fixture.Load(s.db, "test/fixture/server") + s.Require().NoError(err) + + s.Run("success", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + _, err := s.client.UpdateItem(ctx, &rpc.UpdateItemIn{ + Guid: "60308368-7729-4d2d-a510-67926f5a159b", + EncryptedData: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + Version: time.Date(2025, time.March, 15, 13, 0, 0, 0, time.UTC).UnixMicro(), + }) + s.Require().NoError(err) + + item := s.getFromDBByGUID(ctx, "60308368-7729-4d2d-a510-67926f5a159b") + + s.Equal("60308368-7729-4d2d-a510-67926f5a159b", item.GUID) + s.Equal([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, item.EncryptedData) + s.Equal("bf4e6232-f1ae-41da-8535-73048891b1e3", item.UserGUID) + s.True(time.Date(2025, time.March, 15, 13, 0, 0, 0, time.UTC).Equal(item.CreatedAt.UTC())) + s.InDelta(time.Now().UTC().Unix(), item.UpdatedAt.UTC().Unix(), 2) + }) + + s.Run("permission denied", func() { + _, err := s.client.UpdateItem(context.Background(), &rpc.UpdateItemIn{ + Guid: "60308368-7729-4d2d-a510-67926f5a159b", + EncryptedData: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + Version: time.Date(2025, time.March, 15, 13, 0, 0, 0, time.UTC).UnixMicro(), + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.PermissionDenied, st.Code()) + s.Equal("permission denied", st.Message()) + }) + + s.Run("invalid item guid", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + _, err := s.client.UpdateItem(ctx, &rpc.UpdateItemIn{ + Guid: "invalid guid", + EncryptedData: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + Version: time.Date(2025, time.March, 15, 13, 0, 0, 0, time.UTC).UnixMicro(), + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.InvalidArgument, st.Code()) + s.Equal("invalid item guid", st.Message()) + }) + + s.Run("encrypted data is empty", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + _, err := s.client.UpdateItem(ctx, &rpc.UpdateItemIn{ + Guid: "60308368-7729-4d2d-a510-67926f5a159b", + EncryptedData: []byte{}, + Version: time.Date(2025, time.March, 15, 13, 0, 0, 0, time.UTC).UnixMicro(), + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.InvalidArgument, st.Code()) + s.Equal("encrypted data is empty", st.Message()) + }) + + s.Run("item belongs to other user", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + _, err := s.client.UpdateItem(ctx, &rpc.UpdateItemIn{ + Guid: "d07f9605-0b8e-42f6-a07b-3e3f839a7bee", + EncryptedData: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + Version: time.Date(2025, time.March, 15, 12, 0, 0, 0, time.UTC).UnixMicro(), + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.NotFound, st.Code()) + s.Equal("item not found", st.Message()) + }) + + s.Run("item is outdated", func() { + ctx := s.login(context.Background(), "test@test.ru", "12345678") + + _, err := s.client.UpdateItem(ctx, &rpc.UpdateItemIn{ + Guid: "60308368-7729-4d2d-a510-67926f5a159b", + EncryptedData: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + Version: time.Date(2025, time.March, 15, 12, 0, 0, 0, time.UTC).UnixMicro(), + }) + + st, ok := status.FromError(err) + s.True(ok) + s.Equal(codes.FailedPrecondition, st.Code()) + s.Equal("item is outdated", st.Message()) + }) +} diff --git a/test/infrastructure/config/config.go b/test/infrastructure/config/config.go new file mode 100644 index 0000000..cd5fbe2 --- /dev/null +++ b/test/infrastructure/config/config.go @@ -0,0 +1,42 @@ +package config + +import "time" + +// Config хранит конфигурацию тестового окружения. +type Config struct { + // Env окружение. + Env string `yaml:"env" env:"ENV" env-default:"dev" env-description:"Environment" json:"env"` + + // Migration настройки магратора. + Migration struct { + // SourcePath путь до файлов миграций. + SourcePath string `yaml:"sourcePath" env:"MIGRATION_SOURCE_PATH" env-description:"Path to migration source" json:"source_path"` + // Table название таблицы с примененными миграциями. + Table string `yaml:"table" env:"MIGRATION_TABLE" env-description:"Migration table" json:"table"` + } `yaml:"migration" json:"migration"` + + // Auth настройки авторизации. + Auth struct { + // AccessTokenExp время жизни access токена. + AccessTokenExp time.Duration `yaml:"accessTokenExp" env:"ACCESS_TOKEN_EXP" env-description:"Access token expiration" json:"access_token_exp"` + // RefreshTokenExp время жизни refresh токена. + RefreshTokenExp time.Duration `yaml:"refreshTokenExp" env:"REFRESH_TOKEN_EXP" env-description:"Refresh token expiration" json:"refresh_token_exp"` + // SecretKey секретный ключ. + SecretKey string `yaml:"secretKey" env:"SECRET_KEY" env-description:"Secret key" json:"secret_key"` + } `yaml:"auth" json:"auth"` + + // Container настройки Docker контейнеров. + Container struct { + // PG настройки для контейнера под PostgreSQL. + PG struct { + // Tag контейнера + Tag string `yaml:"tag" env:"CONTAINER_PG_TAG" env-description:"Container image tag" json:"tag"` + // DBName значение для переменной окружения POSTGRES_DB + DBName string `yaml:"dbName" env:"CONTAINER_PG_DB_NAME" env-description:"DB name" json:"db_name"` + // DBUser значение для переменной окружения POSTGRES_USER + DBUser string `yaml:"dbUser" env:"CONTAINER_PG_DB_USER" env-description:"DB user" json:"db_user"` + // DBPassword значение для переменной окружения POSTGRES_PASSWORD + DBPassword string `yaml:"dbPassword" env:"CONTAINER_PG_DB_PASSWORD" env-description:"DB password" json:"db_password"` + } `yaml:"pg" json:"pg"` + } `yaml:"container" json:"container"` +} diff --git a/test/infrastructure/container/container.go b/test/infrastructure/container/container.go new file mode 100644 index 0000000..17cc97b --- /dev/null +++ b/test/infrastructure/container/container.go @@ -0,0 +1,85 @@ +package container + +import ( + "context" + "fmt" + "time" + + "github.com/docker/go-connections/nat" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +type ( + // PostgreSQLContainer хранит информацию о готовом контейнере. + PostgreSQLContainer struct { + testcontainers.Container + Port string // Port порт по которому можно подключиться к БД. + Host string // Host хост на котором будет доступна БД. + Database string // Database название БД. + Username string // Username пользователь БД. + Password string // Password пароль пользователя БД. + } + + // PostgreSQLConfig хранит конфигурацию контейнера. + PostgreSQLConfig struct { + Database string // Database название базы данных для переменной окружения контейнера POSTGRES_DB. + Username string // Username пользователь для переменной окружения контейнера POSTGRES_USER. + Password string // Password пароль для переменной окружения контейнера POSTGRES_PASSWORD. + ImageTag string // ImageTag тег докер образа. + } +) + +// NewPostgreSQLContainer создает контейнера для базы данных PostgreSQL по переданной конфигурации. +func NewPostgreSQLContainer(ctx context.Context, cfg PostgreSQLConfig) (*PostgreSQLContainer, error) { + const ( + image = "postgres" + port = "5432" + ) + + containerPort := fmt.Sprintf("%s/tcp", port) + + req := testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Env: map[string]string{ + "POSTGRES_USER": cfg.Username, + "POSTGRES_PASSWORD": cfg.Password, + "POSTGRES_DB": cfg.Database, + }, + ExposedPorts: []string{ + containerPort, + }, + Image: fmt.Sprintf("%s:%s", image, cfg.ImageTag), + WaitingFor: wait.ForExec([]string{"pg_isready"}). + WithPollInterval(2 * time.Second). + WithExitCodeMatcher(func(exitCode int) bool { + return exitCode == 0 + }), + }, + Started: true, + } + + container, err := testcontainers.GenericContainer(ctx, req) + if err != nil { + return nil, err + } + + host, err := container.Host(ctx) + if err != nil { + return nil, err + } + + mappedPort, err := container.MappedPort(ctx, nat.Port(containerPort)) + if err != nil { + return nil, err + } + + return &PostgreSQLContainer{ + Container: container, + Host: host, + Port: mappedPort.Port(), + Database: cfg.Database, + Username: cfg.Username, + Password: cfg.Password, + }, nil +} diff --git a/test/infrastructure/fixture/fixture.go b/test/infrastructure/fixture/fixture.go new file mode 100644 index 0000000..a936af5 --- /dev/null +++ b/test/infrastructure/fixture/fixture.go @@ -0,0 +1,24 @@ +package fixture + +import ( + "github.com/go-testfixtures/testfixtures/v3" + "github.com/jmoiron/sqlx" +) + +// Load загружает в БД по переданному подключению db фикстуры из директории dir. +func Load(db *sqlx.DB, dir string) error { + fixtures, err := testfixtures.New( + testfixtures.Database(db.DB), + testfixtures.Dialect("postgres"), + testfixtures.Directory(dir), + ) + if err != nil { + return err + } + + if err := fixtures.Load(); err != nil { + return err + } + + return nil +} diff --git a/test/infrastructure/init/init.go b/test/infrastructure/init/init.go new file mode 100644 index 0000000..11e96d7 --- /dev/null +++ b/test/infrastructure/init/init.go @@ -0,0 +1,16 @@ +package init + +import ( + "os" + "path" + "runtime" +) + +func init() { + _, filename, _, _ := runtime.Caller(0) //nolint:dogsled + dir := path.Join(path.Dir(filename), "..", "..", "..") + err := os.Chdir(dir) + if err != nil { + panic(err) + } +} diff --git a/test/infrastructure/server/server.go b/test/infrastructure/server/server.go new file mode 100644 index 0000000..bd01ba4 --- /dev/null +++ b/test/infrastructure/server/server.go @@ -0,0 +1,29 @@ +package server + +import ( + "context" + "net" + + "github.com/jmoiron/sqlx" + "go.uber.org/zap" + "google.golang.org/grpc/test/bufconn" + + "github.com/bjlag/go-keeper/internal/app/server" + "github.com/bjlag/go-keeper/internal/infrastructure/auth" +) + +const bufSize = 1024 * 1024 + +func Start(ctx context.Context, db *sqlx.DB, jwt *auth.JWT, log *zap.Logger) func(context.Context, string) (net.Conn, error) { + listener := bufconn.Listen(bufSize) + + go func() { + if err := server.NewApp(db, jwt, listener, log).Run(ctx); err != nil { + panic(err) + } + }() + + return func(context.Context, string) (net.Conn, error) { + return listener.Dial() + } +}