From 6537aded89d6fcfd00d603b7352732c15262d42d Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Wed, 17 Dec 2025 10:40:23 +0300 Subject: [PATCH 1/3] add task service --- cmd/task/main.go | 32 ++++ go.mod | 7 +- go.sum | 29 +++- internal/task/config/config.go | 74 +++++++++ internal/task/handler/grpc/handler.go | 50 +++++++ .../task/repository/postgres/repository.go | 79 ++++++++++ internal/task/server/server.go | 96 ++++++++++++ .../task/service/mocks/mock_repository.go | 131 ++++++++++++++++ internal/task/service/service.go | 62 ++++++++ internal/task/service/service_test.go | 140 ++++++++++++++++++ 10 files changed, 697 insertions(+), 3 deletions(-) create mode 100644 cmd/task/main.go create mode 100644 internal/task/config/config.go create mode 100644 internal/task/handler/grpc/handler.go create mode 100644 internal/task/repository/postgres/repository.go create mode 100644 internal/task/server/server.go create mode 100644 internal/task/service/mocks/mock_repository.go create mode 100644 internal/task/service/service.go create mode 100644 internal/task/service/service_test.go diff --git a/cmd/task/main.go b/cmd/task/main.go new file mode 100644 index 0000000..5feae5b --- /dev/null +++ b/cmd/task/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "log" + "os" + "os/signal" + "syscall" + + "datarush/internal/task/config" + "datarush/internal/task/server" +) + +func main() { + cfg, err := config.Load() + if err != nil { + log.Fatalf("failed to load config: %v", err) + } + + srv := server.New(cfg) + + if err := srv.Start(); err != nil { + log.Fatalf("failed to start server: %v", err) + } + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("shutting down task server...") + srv.Stop() + log.Println("task server stopped") +} \ No newline at end of file diff --git a/go.mod b/go.mod index 9d96342..071759d 100644 --- a/go.mod +++ b/go.mod @@ -6,16 +6,18 @@ toolchain go1.24.9 require ( github.com/Masterminds/squirrel v1.5.4 - github.com/gin-gonic/gin v1.10.0 github.com/go-playground/validator/v10 v10.28.0 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-migrate/migrate/v4 v4.19.0 + github.com/golang/mock v1.6.0 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 github.com/redis/go-redis/v9 v9.16.0 + github.com/stretchr/testify v1.10.0 + go.uber.org/mock v0.6.0 golang.org/x/crypto v0.42.0 google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.10 @@ -23,6 +25,7 @@ require ( require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -33,6 +36,7 @@ require ( github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.43.0 // indirect @@ -41,6 +45,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) tool ( diff --git a/go.sum b/go.sum index af41754..b254ebc 100644 --- a/go.sum +++ b/go.sum @@ -35,7 +35,6 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -52,11 +51,12 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 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/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -109,6 +109,7 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= @@ -123,16 +124,40 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU= diff --git a/internal/task/config/config.go b/internal/task/config/config.go new file mode 100644 index 0000000..983d00e --- /dev/null +++ b/internal/task/config/config.go @@ -0,0 +1,74 @@ +package config + +import ( + "fmt" + "log" + "net" + "os" + "strconv" + + "github.com/joho/godotenv" +) + +type Config struct { + GRPCPort int + GRPCEnableReflection bool + HTTPPort int + LogLevel string + DBHost string + DBPort int + DBUser string + DBPassword string + DBName string +} + +func Load() (*Config, error) { + _ = godotenv.Load() + + return &Config{ + GRPCPort: mustGetInt("TASK_GRPC_PORT", 50053), + GRPCEnableReflection: mustGetBool("TASK_GRPC_ENABLE_REFLECTION", false), + HTTPPort: mustGetInt("TASK_HTTP_PORT", 8082), + LogLevel: getEnv("LOG_LEVEL", "info"), + DBHost: getEnv("POSTGRES_HOST", "localhost"), + DBPort: mustGetInt("POSTGRES_PORT", 5432), + DBUser: getEnv("POSTGRES_USERNAME", "postgres"), + DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"), + DBName: getEnv("POSTGRES_DATABASE", "postgres"), + }, nil +} + +func getEnv(key, def string) string { + if val := os.Getenv(key); val != "" { + return val + } + return def +} + +func mustGetInt(key string, def int) int { + val := getEnv(key, strconv.Itoa(def)) + n, err := strconv.Atoi(val) + if err != nil { + log.Fatalf("invalid int for %s: %v", key, err) + } + return n +} + +func mustGetBool(key string, def bool) bool { + val := getEnv(key, strconv.FormatBool(def)) + b, err := strconv.ParseBool(val) + if err != nil { + log.Fatalf("invalid bool for %s: %v", key, err) + } + return b +} + +func (c Config) BuildPostgresConnStr() string { + return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + c.DBHost, c.DBPort, c.DBUser, c.DBPassword, c.DBName) +} + +func (c Config) BuildPostgresDSN() string { + return fmt.Sprintf("postgresql://%s:%s@%s/%s?sslmode=disable", + c.DBUser, c.DBPassword, net.JoinHostPort(c.DBHost, strconv.Itoa(c.DBPort)), c.DBName) +} \ No newline at end of file diff --git a/internal/task/handler/grpc/handler.go b/internal/task/handler/grpc/handler.go new file mode 100644 index 0000000..de6ff88 --- /dev/null +++ b/internal/task/handler/grpc/handler.go @@ -0,0 +1,50 @@ +package grpc + +import ( + "context" + + pb "datarush/pkg/api/task" + "google.golang.org/protobuf/types/known/emptypb" +) + +type TaskService interface { + CreateTask(ctx context.Context, req *pb.Task) (*pb.Task, error) + GetTask(ctx context.Context, req *pb.GetTaskRequest) (*pb.Task, error) + EditTask(ctx context.Context, req *pb.Task) (*pb.Task, error) + DeleteTask(ctx context.Context, req *pb.DeleteTaskRequest) (*emptypb.Empty, error) + ListCompetitionTasks(ctx context.Context, req *pb.ListCompetitionTasksRequest) (*pb.ListCompetitionTasksResponse, error) + GetTaskAttachments(ctx context.Context, req *pb.GetTaskAttachmentsRequest) (*pb.GetTaskAttachmentsResponse, error) +} + +type TaskHandler struct { + pb.UnimplementedTaskServiceServer + service TaskService +} + +func NewTaskHandler(service TaskService) *TaskHandler { + return &TaskHandler{service: service} +} + +func (h *TaskHandler) CreateTask(ctx context.Context, req *pb.Task) (*pb.Task, error) { + return h.service.CreateTask(ctx, req) +} + +func (h *TaskHandler) GetTask(ctx context.Context, req *pb.GetTaskRequest) (*pb.Task, error) { + return h.service.GetTask(ctx, req) +} + +func (h *TaskHandler) EditTask(ctx context.Context, req *pb.Task) (*pb.Task, error) { + return h.service.EditTask(ctx, req) +} + +func (h *TaskHandler) DeleteTask(ctx context.Context, req *pb.DeleteTaskRequest) (*emptypb.Empty, error) { + return h.service.DeleteTask(ctx, req) +} + +func (h *TaskHandler) ListCompetitionTasks(ctx context.Context, req *pb.ListCompetitionTasksRequest) (*pb.ListCompetitionTasksResponse, error) { + return h.service.ListCompetitionTasks(ctx, req) +} + +func (h *TaskHandler) GetTaskAttachments(ctx context.Context, req *pb.GetTaskAttachmentsRequest) (*pb.GetTaskAttachmentsResponse, error) { + return h.service.GetTaskAttachments(ctx, req) +} \ No newline at end of file diff --git a/internal/task/repository/postgres/repository.go b/internal/task/repository/postgres/repository.go new file mode 100644 index 0000000..6fa0b10 --- /dev/null +++ b/internal/task/repository/postgres/repository.go @@ -0,0 +1,79 @@ +package postgres + +import ( + "context" + + "github.com/jmoiron/sqlx" + + "datarush/pkg/api/task" +) + +type TaskRepository struct { + db *sqlx.DB +} + +func NewTaskRepository(db *sqlx.DB) *TaskRepository { + return &TaskRepository{db: db} +} + +func (r *TaskRepository) CreateTask(ctx context.Context, t *task.Task) (*task.Task, error) { + query := `INSERT INTO tasks (competition_id, title, description, in_competition_position, max_points, max_attempts, type) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, updated_at` + + var createdTask task.Task + err := r.db.QueryRowxContext(ctx, query, t.CompetitionId, t.Title, t.Description, t.InCompetitionPosition, t.MaxPoints, t.MaxAttempts, t.Type).StructScan(&createdTask) + if err != nil { + return nil, err + } + createdTask.CompetitionId = t.CompetitionId + createdTask.Title = t.Title + createdTask.Description = t.Description + createdTask.InCompetitionPosition = t.InCompetitionPosition + createdTask.MaxPoints = t.MaxPoints + createdTask.MaxAttempts = t.MaxAttempts + createdTask.Type = t.Type + return &createdTask, nil +} + +func (r *TaskRepository) GetTask(ctx context.Context, id string) (*task.Task, error) { + var t task.Task + err := r.db.GetContext(ctx, &t, "SELECT * FROM tasks WHERE id = $1", id) + return &t, err +} + +func (r *TaskRepository) EditTask(ctx context.Context, t *task.Task) (*task.Task, error) { + query := `UPDATE tasks SET title = $1, description = $2, in_competition_position = $3, max_points = $4, max_attempts = $5, type = $6, updated_at = now() + WHERE id = $7 RETURNING updated_at` + + var updatedTask task.Task + err := r.db.QueryRowxContext(ctx, query, t.Title, t.Description, t.InCompetitionPosition, t.MaxPoints, t.MaxAttempts, t.Type, t.Id).StructScan(&updatedTask) + if err != nil { + return nil, err + } + t.UpdatedAt = updatedTask.UpdatedAt + return t, nil +} + +func (r *TaskRepository) DeleteTask(ctx context.Context, id string) error { + _, err := r.db.ExecContext(ctx, "DELETE FROM tasks WHERE id = $1", id) + return err +} + +func (r *TaskRepository) ListCompetitionTasks(ctx context.Context, competitionID string) ([]*task.Task, error) { + var tasks []*task.Task + err := r.db.SelectContext(ctx, &tasks, "SELECT * FROM tasks WHERE competition_id = $1", competitionID) + return tasks, err +} + +func (r *TaskRepository) GetTaskAttachments(ctx context.Context, taskID string, showPrivate bool) ([]*task.TaskAttachment, error) { + var attachments []*task.TaskAttachment + query := "SELECT * FROM task_attachments WHERE task_id = $1" + args := []interface{}{taskID} + + if !showPrivate { + query += " AND is_public = true" + } + + err := r.db.SelectContext(ctx, &attachments, query, args...) + return attachments, err +} \ No newline at end of file diff --git a/internal/task/server/server.go b/internal/task/server/server.go new file mode 100644 index 0000000..5926882 --- /dev/null +++ b/internal/task/server/server.go @@ -0,0 +1,96 @@ +package server + +import ( + "fmt" + "log" + "net" + "time" + + "datarush/internal/task/config" + grpcHandlers "datarush/internal/task/handler/grpc" + taskPostgresRepo "datarush/internal/task/repository/postgres" + "datarush/internal/task/service" + pb "datarush/pkg/api/task" + + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +const ( + httpReadTimeout = 10 * time.Second + httpWriteTimeout = 10 * time.Second + httpIdleTimeout = 60 * time.Second +) + +type Server struct { + grpcServer *grpc.Server + config *config.Config + db *sqlx.DB +} + +func New(cfg *config.Config) *Server { + return &Server{ + config: cfg, + } +} + +func (s *Server) Start() error { + db, err := sqlx.Connect("postgres", s.config.BuildPostgresConnStr()) + if err != nil { + return fmt.Errorf("failed to connect to postgres: %w", err) + } + s.db = db + + if err := s.registerGRPCServices(); err != nil { + return fmt.Errorf("failed to register gRPC services: %w", err) + } + + go func() { + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.config.GRPCPort)) + if err != nil { + log.Fatalf("failed to listen on grpc port: %v", err) + } + + log.Printf("starting gRPC server on port %d", s.config.GRPCPort) + if err := s.grpcServer.Serve(lis); err != nil { + log.Fatalf("failed to serve gRPC: %v", err) + } + }() + + return nil +} + +func (s *Server) registerGRPCServices() error { + s.grpcServer = grpc.NewServer() + + taskRepo := taskPostgresRepo.NewTaskRepository(s.db) + + taskService := service.NewTaskService(taskRepo) + + taskHandler := grpcHandlers.NewTaskHandler(taskService) + pb.RegisterTaskServiceServer(s.grpcServer, taskHandler) + + if s.config.GRPCEnableReflection { + reflection.Register(s.grpcServer) + } + + return nil +} + +func (s *Server) Stop() { + log.Println("shutting down task server...") + + if s.grpcServer != nil { + s.grpcServer.GracefulStop() + } + + if s.db != nil { + if err := s.db.Close(); err != nil { + log.Printf("failed to close database: %v", err) + } + } + + log.Println("task server stopped") +} \ No newline at end of file diff --git a/internal/task/service/mocks/mock_repository.go b/internal/task/service/mocks/mock_repository.go new file mode 100644 index 0000000..51899c9 --- /dev/null +++ b/internal/task/service/mocks/mock_repository.go @@ -0,0 +1,131 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/task/service/service.go +// +// Generated by this command: +// +// mockgen -source=internal/task/service/service.go -destination=internal/task/service/mocks/mock_repository.go -package=mocks +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + task "datarush/pkg/api/task" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockTaskRepository is a mock of TaskRepository interface. +type MockTaskRepository struct { + ctrl *gomock.Controller + recorder *MockTaskRepositoryMockRecorder + isgomock struct{} +} + +// MockTaskRepositoryMockRecorder is the mock recorder for MockTaskRepository. +type MockTaskRepositoryMockRecorder struct { + mock *MockTaskRepository +} + +// NewMockTaskRepository creates a new mock instance. +func NewMockTaskRepository(ctrl *gomock.Controller) *MockTaskRepository { + mock := &MockTaskRepository{ctrl: ctrl} + mock.recorder = &MockTaskRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTaskRepository) EXPECT() *MockTaskRepositoryMockRecorder { + return m.recorder +} + +// CreateTask mocks base method. +func (m *MockTaskRepository) CreateTask(ctx context.Context, t *task.Task) (*task.Task, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateTask", ctx, t) + ret0, _ := ret[0].(*task.Task) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateTask indicates an expected call of CreateTask. +func (mr *MockTaskRepositoryMockRecorder) CreateTask(ctx, t any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTask", reflect.TypeOf((*MockTaskRepository)(nil).CreateTask), ctx, t) +} + +// DeleteTask mocks base method. +func (m *MockTaskRepository) DeleteTask(ctx context.Context, id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteTask", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteTask indicates an expected call of DeleteTask. +func (mr *MockTaskRepositoryMockRecorder) DeleteTask(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTask", reflect.TypeOf((*MockTaskRepository)(nil).DeleteTask), ctx, id) +} + +// EditTask mocks base method. +func (m *MockTaskRepository) EditTask(ctx context.Context, t *task.Task) (*task.Task, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EditTask", ctx, t) + ret0, _ := ret[0].(*task.Task) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EditTask indicates an expected call of EditTask. +func (mr *MockTaskRepositoryMockRecorder) EditTask(ctx, t any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditTask", reflect.TypeOf((*MockTaskRepository)(nil).EditTask), ctx, t) +} + +// GetTask mocks base method. +func (m *MockTaskRepository) GetTask(ctx context.Context, id string) (*task.Task, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTask", ctx, id) + ret0, _ := ret[0].(*task.Task) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTask indicates an expected call of GetTask. +func (mr *MockTaskRepositoryMockRecorder) GetTask(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTask", reflect.TypeOf((*MockTaskRepository)(nil).GetTask), ctx, id) +} + +// GetTaskAttachments mocks base method. +func (m *MockTaskRepository) GetTaskAttachments(ctx context.Context, taskID string, showPrivate bool) ([]*task.TaskAttachment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTaskAttachments", ctx, taskID, showPrivate) + ret0, _ := ret[0].([]*task.TaskAttachment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTaskAttachments indicates an expected call of GetTaskAttachments. +func (mr *MockTaskRepositoryMockRecorder) GetTaskAttachments(ctx, taskID, showPrivate any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTaskAttachments", reflect.TypeOf((*MockTaskRepository)(nil).GetTaskAttachments), ctx, taskID, showPrivate) +} + +// ListCompetitionTasks mocks base method. +func (m *MockTaskRepository) ListCompetitionTasks(ctx context.Context, competitionID string) ([]*task.Task, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListCompetitionTasks", ctx, competitionID) + ret0, _ := ret[0].([]*task.Task) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListCompetitionTasks indicates an expected call of ListCompetitionTasks. +func (mr *MockTaskRepositoryMockRecorder) ListCompetitionTasks(ctx, competitionID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCompetitionTasks", reflect.TypeOf((*MockTaskRepository)(nil).ListCompetitionTasks), ctx, competitionID) +} diff --git a/internal/task/service/service.go b/internal/task/service/service.go new file mode 100644 index 0000000..23c39d7 --- /dev/null +++ b/internal/task/service/service.go @@ -0,0 +1,62 @@ +package service + +import ( + "context" + pb "datarush/pkg/api/task" + + "google.golang.org/protobuf/types/known/emptypb" +) + +type TaskRepository interface { + CreateTask(ctx context.Context, t *pb.Task) (*pb.Task, error) + GetTask(ctx context.Context, id string) (*pb.Task, error) + EditTask(ctx context.Context, t *pb.Task) (*pb.Task, error) + DeleteTask(ctx context.Context, id string) error + ListCompetitionTasks(ctx context.Context, competitionID string) ([]*pb.Task, error) + GetTaskAttachments(ctx context.Context, taskID string, showPrivate bool) ([]*pb.TaskAttachment, error) +} + +type TaskService struct { + repo TaskRepository +} + +func NewTaskService(repo TaskRepository) *TaskService { + return &TaskService{repo: repo} +} + +func (s *TaskService) CreateTask(ctx context.Context, req *pb.Task) (*pb.Task, error) { + return s.repo.CreateTask(ctx, req) +} + +func (s *TaskService) GetTask(ctx context.Context, req *pb.GetTaskRequest) (*pb.Task, error) { + return s.repo.GetTask(ctx, req.TaskId) +} + +func (s *TaskService) EditTask(ctx context.Context, req *pb.Task) (*pb.Task, error) { + return s.repo.EditTask(ctx, req) +} + +func (s *TaskService) DeleteTask(ctx context.Context, req *pb.DeleteTaskRequest) (*emptypb.Empty, error) { + err := s.repo.DeleteTask(ctx, req.TaskId) + return &emptypb.Empty{}, err +} + +func (s *TaskService) ListCompetitionTasks(ctx context.Context, req *pb.ListCompetitionTasksRequest) (*pb.ListCompetitionTasksResponse, error) { + tasks, err := s.repo.ListCompetitionTasks(ctx, req.CompetitionId) + if err != nil { + return nil, err + } + return &pb.ListCompetitionTasksResponse{Tasks: tasks}, nil +} + +func (s *TaskService) GetTaskAttachments(ctx context.Context, req *pb.GetTaskAttachmentsRequest) (*pb.GetTaskAttachmentsResponse, error) { + showPrivate := false + if req.ShowPrivate != nil { + showPrivate = *req.ShowPrivate + } + attachments, err := s.repo.GetTaskAttachments(ctx, req.TaskId, showPrivate) + if err != nil { + return nil, err + } + return &pb.GetTaskAttachmentsResponse{Attachments: attachments}, nil +} \ No newline at end of file diff --git a/internal/task/service/service_test.go b/internal/task/service/service_test.go new file mode 100644 index 0000000..0035e8c --- /dev/null +++ b/internal/task/service/service_test.go @@ -0,0 +1,140 @@ +package service + +import ( + "context" + "testing" + + "datarush/internal/task/service/mocks" + pb "datarush/pkg/api/task" + + "go.uber.org/mock/gomock" + "github.com/stretchr/testify/assert" + + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestTaskService(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockRepo := mocks.NewMockTaskRepository(ctrl) + service := NewTaskService(mockRepo) + + ctx := context.Background() + + t.Run("CreateTask", func(t *testing.T) { + task := &pb.Task{ + CompetitionId: "comp1", + Title: "Test Task", + Description: "This is a test task", + InCompetitionPosition: 1, + MaxPoints: 100, + MaxAttempts: 10, + Type: pb.TaskType_TASK_TYPE_INPUT, + } + + mockRepo.EXPECT().CreateTask(ctx, task).Return(task, nil) + + createdTask, err := service.CreateTask(ctx, task) + assert.NoError(t, err) + assert.Equal(t, task, createdTask) + }) + + t.Run("GetTask", func(t *testing.T) { + taskID := "task1" + req := &pb.GetTaskRequest{TaskId: taskID} + expectedTask := &pb.Task{ + Id: taskID, + CompetitionId: "comp1", + Title: "Test Task", + Description: "This is a test task", + InCompetitionPosition: 1, + MaxPoints: 100, + MaxAttempts: 10, + Type: pb.TaskType_TASK_TYPE_INPUT, + CreatedAt: timestamppb.Now(), + UpdatedAt: timestamppb.Now(), + } + + mockRepo.EXPECT().GetTask(ctx, taskID).Return(expectedTask, nil) + + task, err := service.GetTask(ctx, req) + assert.NoError(t, err) + assert.Equal(t, expectedTask, task) + }) + + t.Run("EditTask", func(t *testing.T) { + task := &pb.Task{ + Id: "task1", + CompetitionId: "comp1", + Title: "Updated Test Task", + Description: "This is an updated test task", + InCompetitionPosition: 1, + MaxPoints: 150, + MaxAttempts: 5, + Type: pb.TaskType_TASK_TYPE_CHECKER, + } + + mockRepo.EXPECT().EditTask(ctx, task).Return(task, nil) + + updatedTask, err := service.EditTask(ctx, task) + assert.NoError(t, err) + assert.Equal(t, task, updatedTask) + }) + + t.Run("DeleteTask", func(t *testing.T) { + taskID := "task1" + req := &pb.DeleteTaskRequest{TaskId: taskID} + + mockRepo.EXPECT().DeleteTask(ctx, taskID).Return(nil) + + _, err := service.DeleteTask(ctx, req) + assert.NoError(t, err) + }) + + t.Run("ListCompetitionTasks", func(t *testing.T) { + competitionID := "comp1" + req := &pb.ListCompetitionTasksRequest{CompetitionId: competitionID} + expectedTasks := []*pb.Task{ + {Id: "task1", CompetitionId: competitionID, Title: "Task 1"}, + {Id: "task2", CompetitionId: competitionID, Title: "Task 2"}, + } + + mockRepo.EXPECT().ListCompetitionTasks(ctx, competitionID).Return(expectedTasks, nil) + + resp, err := service.ListCompetitionTasks(ctx, req) + assert.NoError(t, err) + assert.Equal(t, expectedTasks, resp.Tasks) + }) + + t.Run("GetTaskAttachments", func(t *testing.T) { + taskID := "task1" + showPrivate := true + req := &pb.GetTaskAttachmentsRequest{TaskId: taskID, ShowPrivate: &showPrivate} + expectedAttachments := []*pb.TaskAttachment{ + {Id: "att1", FileUrl: "url1", IsPublic: true}, + {Id: "att2", FileUrl: "url2", IsPublic: false}, + } + + mockRepo.EXPECT().GetTaskAttachments(ctx, taskID, showPrivate).Return(expectedAttachments, nil) + + resp, err := service.GetTaskAttachments(ctx, req) + assert.NoError(t, err) + assert.Equal(t, expectedAttachments, resp.Attachments) + }) + + t.Run("GetTaskAttachments - show public only", func(t *testing.T) { + taskID := "task1" + showPrivate := false + req := &pb.GetTaskAttachmentsRequest{TaskId: taskID, ShowPrivate: &showPrivate} + expectedAttachments := []*pb.TaskAttachment{ + {Id: "att1", FileUrl: "url1", IsPublic: true}, + } + + mockRepo.EXPECT().GetTaskAttachments(ctx, taskID, showPrivate).Return(expectedAttachments, nil) + + resp, err := service.GetTaskAttachments(ctx, req) + assert.NoError(t, err) + assert.Equal(t, expectedAttachments, resp.Attachments) + }) +} \ No newline at end of file From 91a72f25fd17d49a098421bc5e1c6d1cd0bb84a9 Mon Sep 17 00:00:00 2001 From: timka Date: Wed, 17 Dec 2025 17:42:16 +0300 Subject: [PATCH 2/3] setup tasks auth middleware and compose --- internal/competition/middleware/auth.go | 67 ------------------------- internal/competition/server/server.go | 4 +- internal/task/config/config.go | 4 +- internal/task/server/server.go | 8 ++- 4 files changed, 12 insertions(+), 71 deletions(-) delete mode 100644 internal/competition/middleware/auth.go diff --git a/internal/competition/middleware/auth.go b/internal/competition/middleware/auth.go deleted file mode 100644 index 50856cc..0000000 --- a/internal/competition/middleware/auth.go +++ /dev/null @@ -1,67 +0,0 @@ -package middleware - -import ( - "context" - "strings" - - authpb "datarush/pkg/api/auth" - - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" -) - -type contextKey string - -const ( - UserIDKey contextKey = "user_id" - authHeader = "authorization" - bearerScheme = "bearer" -) - -type AuthInterceptor struct { - authClient authpb.AuthServiceClient -} - -func NewAuthInterceptor(authClient authpb.AuthServiceClient) *AuthInterceptor { - return &AuthInterceptor{authClient: authClient} -} - -func (i *AuthInterceptor) Unary() grpc.UnaryServerInterceptor { - return func( - ctx context.Context, - req interface{}, - info *grpc.UnaryServerInfo, - handler grpc.UnaryHandler, - ) (interface{}, error) { - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - return nil, status.Error(codes.Unauthenticated, "metadata is not provided") - } - - authHeaders := md.Get(authHeader) - if len(authHeaders) == 0 { - return nil, status.Error(codes.Unauthenticated, "authorization token is not provided") - } - - header := authHeaders[0] - parts := strings.Split(header, " ") - if len(parts) != 2 || !strings.EqualFold(parts[0], bearerScheme) { - return nil, status.Errorf(codes.Unauthenticated, "invalid authorization header format") - } - - token := parts[1] - - validateResp, err := i.authClient.ValidateToken(ctx, &authpb.ValidateTokenRequest{ - Token: token, - }) - if err != nil { - return nil, status.Errorf(codes.Unauthenticated, "failed to validate token: %v", err) - } - - newCtx := context.WithValue(ctx, UserIDKey, validateResp.GetUserId()) - - return handler(newCtx, req) - } -} diff --git a/internal/competition/server/server.go b/internal/competition/server/server.go index 528ab3a..a0d821a 100644 --- a/internal/competition/server/server.go +++ b/internal/competition/server/server.go @@ -8,11 +8,11 @@ import ( "datarush/internal/competition/config" grpcHandlers "datarush/internal/competition/handler/grpc" - "datarush/internal/competition/middleware" "datarush/internal/competition/repository/postgres" "datarush/internal/competition/service" authpb "datarush/pkg/api/auth" pb "datarush/pkg/api/competition" + "datarush/pkg/interceptor" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" @@ -82,7 +82,7 @@ func (s *Server) Start() error { func (s *Server) registerGRPCServices() error { authClient := authpb.NewAuthServiceClient(s.authConn) - authInterceptor := middleware.NewAuthInterceptor(authClient) + authInterceptor := interceptor.NewAuthInterceptor(authClient) s.grpcServer = grpc.NewServer( grpc.UnaryInterceptor(authInterceptor.Unary()), diff --git a/internal/task/config/config.go b/internal/task/config/config.go index 983d00e..df5206f 100644 --- a/internal/task/config/config.go +++ b/internal/task/config/config.go @@ -20,6 +20,7 @@ type Config struct { DBUser string DBPassword string DBName string + AuthSvcAddr string } func Load() (*Config, error) { @@ -35,6 +36,7 @@ func Load() (*Config, error) { DBUser: getEnv("POSTGRES_USERNAME", "postgres"), DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"), DBName: getEnv("POSTGRES_DATABASE", "postgres"), + AuthSvcAddr: getEnv("AUTH_SVC_ADDR", "localhost:50051"), }, nil } @@ -71,4 +73,4 @@ func (c Config) BuildPostgresConnStr() string { func (c Config) BuildPostgresDSN() string { return fmt.Sprintf("postgresql://%s:%s@%s/%s?sslmode=disable", c.DBUser, c.DBPassword, net.JoinHostPort(c.DBHost, strconv.Itoa(c.DBPort)), c.DBName) -} \ No newline at end of file +} diff --git a/internal/task/server/server.go b/internal/task/server/server.go index 5926882..b2f94bf 100644 --- a/internal/task/server/server.go +++ b/internal/task/server/server.go @@ -10,7 +10,9 @@ import ( grpcHandlers "datarush/internal/task/handler/grpc" taskPostgresRepo "datarush/internal/task/repository/postgres" "datarush/internal/task/service" + authpb "datarush/pkg/api/auth" pb "datarush/pkg/api/task" + "datarush/pkg/interceptor" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" @@ -28,6 +30,7 @@ type Server struct { grpcServer *grpc.Server config *config.Config db *sqlx.DB + authConn *grpc.ClientConn } func New(cfg *config.Config) *Server { @@ -63,6 +66,9 @@ func (s *Server) Start() error { } func (s *Server) registerGRPCServices() error { + authClient := authpb.NewAuthServiceClient(s.authConn) + authInterceptor := interceptor.NewAuthInterceptor(authClient) + s.grpcServer = grpc.NewServer() taskRepo := taskPostgresRepo.NewTaskRepository(s.db) @@ -93,4 +99,4 @@ func (s *Server) Stop() { } log.Println("task server stopped") -} \ No newline at end of file +} From 49f4c0045f6222c7ede7e88832ca10d0667895b4 Mon Sep 17 00:00:00 2001 From: timka Date: Wed, 17 Dec 2025 17:44:50 +0300 Subject: [PATCH 3/3] refactor add auth interceptor --- compose.yaml | 49 +++++++++++++++++++++++++++--- go.mod | 10 +++--- go.sum | 39 ------------------------ pkg/interceptor/auth.go | 67 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 49 deletions(-) create mode 100644 pkg/interceptor/auth.go diff --git a/compose.yaml b/compose.yaml index 2afbdcf..c5e3dc0 100644 --- a/compose.yaml +++ b/compose.yaml @@ -73,6 +73,46 @@ services: restart: unless-stopped shm_size: 4mb + task: + build: + context: . + dockerfile: Containerfile + depends_on: + postgres: + restart: false + condition: service_healthy + required: true + redis: + restart: false + condition: service_healthy + required: true + auth: + restart: false + condition: service_started + required: true + env_file: + - path: ./infrastructure/task/.env.template + required: true + - path: ./infrastructure/task/.env + required: false + ports: + - name: http + target: 8083 + published: 13447 + host_ip: 127.0.0.1 + protocol: tcp + app_protocol: http + - name: grpc + target: 50054 + published: 13448 + host_ip: 127.0.0.1 + protocol: tcp + app_protocol: http + networks: + - default + restart: unless-stopped + shm_size: 4mb + core: build: context: . @@ -98,6 +138,10 @@ services: restart: false condition: service_started required: true + task: + restart: false + condition: service_started + required: true env_file: - path: ./infrastructure/core/.env.template required: true @@ -149,7 +193,7 @@ services: - path: ./infrastructure/postgres/.env required: false healthcheck: - test: [ "CMD", "pg_isready", "--dbname=postgres" ] + test: ["CMD", "pg_isready", "--dbname=postgres"] interval: 1m30s timeout: 5s start_period: 5s @@ -234,7 +278,6 @@ services: target: /data read_only: false - networks: default: driver: bridge @@ -244,13 +287,11 @@ networks: enable_ipv6: true internal: false - volumes: postgres_data: pgadmin_data: redis_data: - configs: postgres_config: file: ./infrastructure/postgres/postgresql.conf diff --git a/go.mod b/go.mod index 6dd554c..7f091f2 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,12 @@ toolchain go1.24.9 require ( github.com/Masterminds/squirrel v1.5.4 - github.com/go-playground/validator/v10 v10.28.0 + github.com/aws/aws-sdk-go-v2 v1.41.0 + github.com/aws/aws-sdk-go-v2/config v1.32.6 + github.com/aws/aws-sdk-go-v2/credentials v1.19.6 + github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-migrate/migrate/v4 v4.19.0 - github.com/golang/mock v1.6.0 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.7.4 github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 @@ -43,15 +45,11 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect diff --git a/go.sum b/go.sum index 371f8e4..3003467 100644 --- a/go.sum +++ b/go.sum @@ -78,20 +78,10 @@ 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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= -github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= 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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -100,8 +90,6 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -132,8 +120,6 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= @@ -154,15 +140,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= -github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= -github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= @@ -181,36 +164,14 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU= diff --git a/pkg/interceptor/auth.go b/pkg/interceptor/auth.go new file mode 100644 index 0000000..dc097c3 --- /dev/null +++ b/pkg/interceptor/auth.go @@ -0,0 +1,67 @@ +package interceptor + +import ( + "context" + "strings" + + authpb "datarush/pkg/api/auth" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +type contextKey string + +const ( + UserIDKey contextKey = "user_id" + authHeader = "authorization" + bearerScheme = "bearer" +) + +type AuthInterceptor struct { + authClient authpb.AuthServiceClient +} + +func NewAuthInterceptor(authClient authpb.AuthServiceClient) *AuthInterceptor { + return &AuthInterceptor{authClient: authClient} +} + +func (i *AuthInterceptor) Unary() grpc.UnaryServerInterceptor { + return func( + ctx context.Context, + req interface{}, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, + ) (interface{}, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Error(codes.Unauthenticated, "metadata is not provided") + } + + authHeaders := md.Get(authHeader) + if len(authHeaders) == 0 { + return nil, status.Error(codes.Unauthenticated, "authorization token is not provided") + } + + header := authHeaders[0] + parts := strings.Split(header, " ") + if len(parts) != 2 || !strings.EqualFold(parts[0], bearerScheme) { + return nil, status.Errorf(codes.Unauthenticated, "invalid authorization header format") + } + + token := parts[1] + + validateResp, err := i.authClient.ValidateToken(ctx, &authpb.ValidateTokenRequest{ + Token: token, + }) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "failed to validate token: %v", err) + } + + newCtx := context.WithValue(ctx, UserIDKey, validateResp.GetUserId()) + + return handler(newCtx, req) + } +}