diff --git a/.env.template b/.env.template deleted file mode 100644 index 07506a0..0000000 --- a/.env.template +++ /dev/null @@ -1,14 +0,0 @@ -# Change all vars before going to production and remove all comments (!) -# Below all environment variables and default values - -GRPC_PORT=50051 -GRPC_ENABLE_REFLECTION=false -HTTP_HANDLER_ENABLE=false -HTTP_PORT=8080 -LOG_LEVEL=info -POSTGRES_HOST=localhost -POSTGRES_PORT=5432 -POSTGRES_USERNAME=postgres -POSTGRES_PASSWORD=postgres -POSTGRES_DATABASE=postgres -REDIS_URI=redis://localhost:6379 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7d0eff6..92686ce 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -185,14 +185,6 @@ cache: when: manual allow_failure: true -build-auth: - <<: *build-config - variables: - IMAGE_NAME: $BASE_IMAGE_NAME/auth - CONTAINERFILE: Containerfile - BUILDTARGET: runtime - SERVICE_NAME: auth - build-migrate: <<: *build-config variables: @@ -201,6 +193,22 @@ build-migrate: BUILDTARGET: runtime SERVICE_NAME: migrate +build-gw: + <<: *build-config + variables: + IMAGE_NAME: $BASE_IMAGE_NAME/gw + CONTAINERFILE: Containerfile + BUILDTARGET: runtime + SERVICE_NAME: gw + +build-auth: + <<: *build-config + variables: + IMAGE_NAME: $BASE_IMAGE_NAME/auth + CONTAINERFILE: Containerfile + BUILDTARGET: runtime + SERVICE_NAME: auth + golangci-lint: stage: lint image: docker.io/golangci/golangci-lint:latest-alpine @@ -249,14 +257,6 @@ go-test: sast-filesystem: <<: *trivy-fs-scan -sast-image-auth: - <<: *trivy-image-scan - variables: - IMAGE_NAME: $BASE_IMAGE_NAME/auth - IMAGE_TYPE: auth - dependencies: - - build-auth - sast-image-migrate: <<: *trivy-image-scan variables: @@ -265,16 +265,37 @@ sast-image-migrate: dependencies: - build-migrate -tag-auth: - <<: *tag-config +sast-image-gw: + <<: *trivy-image-scan + variables: + IMAGE_NAME: $BASE_IMAGE_NAME/gw + IMAGE_TYPE: gw + dependencies: + - build-gw + +sast-image-auth: + <<: *trivy-image-scan variables: IMAGE_NAME: $BASE_IMAGE_NAME/auth + IMAGE_TYPE: auth + dependencies: + - build-auth tag-migrate: <<: *tag-config variables: IMAGE_NAME: $BASE_IMAGE_NAME/migrate +tag-gw: + <<: *tag-config + variables: + IMAGE_NAME: $BASE_IMAGE_NAME/gw + +tag-auth: + <<: *tag-config + variables: + IMAGE_NAME: $BASE_IMAGE_NAME/auth + # webhook-backend-deploy: # <<: *webhook-config # stage: deploy diff --git a/Containerfile b/Containerfile index 454012f..e66ba23 100644 --- a/Containerfile +++ b/Containerfile @@ -44,7 +44,7 @@ ARG SERVICE WORKDIR /app -COPY --from=build --chown=1000:1000 /out/${SERVICE} /app/bin/${SERVICE} +COPY --from=build --chown=1000:1000 /out/${SERVICE} /app/bin EXPOSE 8080 50051 @@ -54,6 +54,6 @@ LABEL org.opencontainers.image.created="${BUILD_TIME}" \ USER 1000 -ENTRYPOINT ["/app/bin/${SERVICE}"] +ENTRYPOINT ["/app/bin"] CMD [] diff --git a/Makefile b/Makefile index f1e2420..633f5cd 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,24 @@ -# Go parameters GOCMD=go GOBUILD=$(GOCMD) build -trimpath -ldflags="-s -w" GOTEST=$(GOCMD) test -GODOWNLOAD=$(GOCMD) mod download -BINARY_NAME=datarush -MIGRATE_BINARY_NAME=datarush-migrate + +BASE_BINARY_NAME=datarush +GW_BINARY_NAME=$(BASE_BINARY_NAME)-gw +MIGRATE_BINARY_NAME=$(BASE_BINARY_NAME)-migrate +AUTH_BINARY_NAME=$(BASE_BINARY_NAME)-auth + BINARY_DIR=bin -# Protobuf parameters PROTOC=protoc PROTO_DIR=api/proto -PROTO_FILE=$(PROTO_DIR)/auth.proto $(PROTO_DIR)/competition.proto $(PROTO_DIR)/results.proto $(PROTO_DIR)/review.proto $(PROTO_DIR)/submission.proto $(PROTO_DIR)/task.proto $(PROTO_DIR)/user.proto +PROTO_FILE=${PROTO_DIR}/achievements.proto $(PROTO_DIR)/auth.proto $(PROTO_DIR)/competition.proto $(PROTO_DIR)/results.proto $(PROTO_DIR)/review.proto $(PROTO_DIR)/submission.proto $(PROTO_DIR)/task.proto $(PROTO_DIR)/user.proto PROTO_OUT=. .PHONY: install i generate gen generate-gw test build run migrate lint fmt format clean help codegen examples install: - $(GODOWNLOAD) + $(GOCMD) mod download + $(GOCMD) mod tidy i: install @@ -35,16 +37,23 @@ generate-gw: test: $(GOTEST) ./... -build: - $(GOBUILD) -o ./$(BINARY_DIR)/$(BINARY_NAME) ./cmd/server - chmod +x ./$(BINARY_DIR)/$(BINARY_NAME) +build-gw: + $(GOBUILD) -o ./$(BINARY_DIR)/$(GW_BINARY_NAME) ./cmd/gw + chmod +x ./$(BINARY_DIR)/$(GW_BINARY_NAME) build-migrate: $(GOBUILD) -o ./$(BINARY_DIR)/$(MIGRATE_BINARY_NAME) ./cmd/migrate chmod +x ./$(BINARY_DIR)/$(MIGRATE_BINARY_NAME) -run: build - ./$(BINARY_DIR)/$(BINARY_NAME) +build-auth: + $(GOBUILD) -o ./$(BINARY_DIR)/$(AUTH_BINARY_NAME) ./cmd/auth + chmod +x ./$(BINARY_DIR)/$(AUTH_BINARY_NAME) + +build: build-gw build-migrate build-auth + +run: + ./$(BINARY_DIR)/$(AUTH_BINARY_NAME) & + ./$(BINARY_DIR)/$(GW_BINARY_NAME) migrate: build-migrate @cmd=$(word 2,$(MAKECMDGOALS)); \ @@ -65,28 +74,15 @@ format: fmt clean: rm -rf bin/* -codegen: - @if [ -z "$(SERVICE)" ]; then \ - echo "Usage: make codegen SERVICE="; \ - echo "Example: make codegen SERVICE=auth"; \ - exit 1; \ - fi - $(GOBUILD) -o ./$(BINARY_DIR)/codegen ./cmd/codegen - ./$(BINARY_DIR)/codegen $(SERVICE) ./ - - help: - @echo "Available commands:" + @echo "Help:" @echo " install - Install all deps using go mod download" @echo " i" @echo " generate - Generate gRPC code" @echo " gen" @echo " protoc" - @echo " codegen - Generate service template" - @echo " Usage: make codegen SERVICE=" - @echo " examples - Show integration examples" @echo " test - Run tests" - @echo " build - Build the binary" + @echo " build - Build all binaries" @echo " run - Run the application" @echo " lint - Run golangci-lint linter" @echo " format - Run golangci-lint formatter" diff --git a/api/proto/results.proto b/api/proto/results.proto index 91b2265..c14a8de 100644 --- a/api/proto/results.proto +++ b/api/proto/results.proto @@ -35,9 +35,10 @@ message GetCompetitionResultsRequest { string competition_id = 3; } message GetCompetitionResultsResponse { - repeated UserResult results = 1; - int32 total_count = 2; - int32 next_page_token = 3; + int32 total_count = 1; + int32 next_page_token = 2; + + repeated UserResult results = 3; } message GetUserCompetitionResultsRequest { diff --git a/api/proto/review.proto b/api/proto/review.proto index bcf48c1..b799c20 100644 --- a/api/proto/review.proto +++ b/api/proto/review.proto @@ -8,6 +8,7 @@ import "google/protobuf/empty.proto"; option go_package = "pkg/api/review"; service ReviewService { + rpc ValidateReviewToken(ValidateReviewTokenRequest) returns (ValidateReviewTokenResponse); rpc ListSubmissionsForReview(ListSubmissionsForReviewRequest) returns (ListSubmissionsForReviewResponse); rpc GetSubmissionForReview(GetSubmissionForReviewRequest) returns (SubmissionForReview); rpc EvaluateSubmission(EvaluateSubmissionRequest) returns (EvaluateSubmissionResponse); @@ -48,11 +49,21 @@ message SubmissionForReview { optional google.protobuf.Timestamp checked_at = 8; } +message ValidateReviewTokenRequest { + string token = 1; +} +message ValidateReviewTokenResponse { + bool is_valid = 1; + string reviewer_id = 2; + string competition_id = 3; +} + message ListSubmissionsForReviewRequest { int32 page_size = 1; int32 page_token = 2; - optional ReviewStatus status = 3; + string token = 3; + optional ReviewStatus status = 4; } message ListSubmissionsForReviewResponse { int32 total_count = 1; @@ -62,14 +73,16 @@ message ListSubmissionsForReviewResponse { } message GetSubmissionForReviewRequest { - string submission_id = 1; + string token = 1; + string submission_id = 2; } message EvaluateSubmissionRequest { - string submission_id = 1; - int32 earned_points = 2; - string reviewer_comment = 3; - repeated CriteriaMark marks = 4; + string token = 1; + string submission_id = 2; + int32 earned_points = 3; + string reviewer_comment = 4; + repeated CriteriaMark marks = 5; } message EvaluateSubmissionResponse { string submission_id = 1; @@ -78,5 +91,6 @@ message EvaluateSubmissionResponse { } message ReleaseSubmissionRequest { - string submission_id = 1; + string token = 1; + string submission_id = 2; } diff --git a/api/proto/submission.proto b/api/proto/submission.proto index aae0429..62181c3 100644 --- a/api/proto/submission.proto +++ b/api/proto/submission.proto @@ -35,14 +35,16 @@ message Submission { } message SubmitTaskRequest { - string competition_id = 1; - string task_id = 2; - string file_url = 3; + string user_id = 1; + string competition_id = 2; + string task_id = 3; + string file_url = 4; } message GetSubmissionsHistoryRequest { - string competition_id = 1; - string task_id = 2; + string user_id = 1; + string competition_id = 2; + string task_id = 3; } message GetSubmissionsHistoryResponse { repeated Submission submissions = 1; @@ -62,8 +64,8 @@ message ListSubmissionsRequest { SubmissionStatus status = 6; } message ListSubmissionsResponse { - int32 total_count = 2; - int32 next_page_token = 3; + int32 total_count = 1; + int32 next_page_token = 2; - repeated Submission submissions = 1; + repeated Submission submissions = 3; } diff --git a/cmd/competition/main.go b/cmd/competition/main.go new file mode 100644 index 0000000..9cb8526 --- /dev/null +++ b/cmd/competition/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "log" + "os" + "os/signal" + "syscall" + + "datarush/internal/competition/config" + "datarush/internal/competition/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 competition server...") + srv.Stop() + log.Println("competition server stopped") +} diff --git a/cmd/gw/main.go b/cmd/gw/main.go new file mode 100644 index 0000000..9cd7d3c --- /dev/null +++ b/cmd/gw/main.go @@ -0,0 +1,191 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "datarush/internal/gw/config" + "datarush/internal/gw/grpc_client" + "datarush/internal/gw/handler" + "datarush/internal/gw/middleware" + "datarush/internal/gw/router" + "datarush/internal/gw/storage" +) + +func main() { + cfg, err := config.Load() + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + ctx := context.Background() + + clientFactory := grpc_client.NewClientFactory() + defer clientFactory.Close() + + authClient, err := grpc_client.NewAuthClient(ctx, cfg.GRPC.AuthServiceAddr, clientFactory) + if err != nil { + log.Printf("Warning: Failed to create auth client: %v", err) + authClient = nil + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + userClient, err := grpc_client.NewUserClient(ctx, cfg.GRPC.UserServiceAddr, clientFactory) + if err != nil { + log.Printf("Warning: Failed to create user client: %v", err) + userClient = nil + } + + competitionClient, err := grpc_client.NewCompetitionClient(ctx, cfg.GRPC.CompetitionServiceAddr, clientFactory) + if err != nil { + log.Printf("Warning: Failed to create competition client: %v", err) + competitionClient = nil + } + + taskClient, err := grpc_client.NewTaskClient(ctx, cfg.GRPC.TaskServiceAddr, clientFactory) + if err != nil { + log.Printf("Warning: Failed to create task client: %v", err) + taskClient = nil + } + + submissionClient, err := grpc_client.NewSubmissionClient(ctx, cfg.GRPC.SubmissionServiceAddr, clientFactory) + if err != nil { + log.Printf("Warning: Failed to create submission client: %v", err) + submissionClient = nil + } + + resultsClient, err := grpc_client.NewResultsClient(ctx, cfg.GRPC.ResultsServiceAddr, clientFactory) + if err != nil { + log.Printf("Warning: Failed to create results client: %v", err) + resultsClient = nil + } + + reviewClient, err := grpc_client.NewReviewClient(ctx, cfg.GRPC.ReviewServiceAddr, clientFactory) + if err != nil { + log.Printf("Warning: Failed to create review client: %v", err) + reviewClient = nil + } + + achievementsClient, err := grpc_client.NewAchievementsClient(ctx, cfg.GRPC.AchievementsServiceAddr, clientFactory) + if err != nil { + log.Printf("Warning: Failed to create achievements client: %v", err) + achievementsClient = nil + } + + s3Storage, err := storage.NewS3Storage(storage.S3Config{ + AccessKeyID: cfg.S3.AccessKeyID, + SecretAccessKey: cfg.S3.SecretAccessKey, + Region: cfg.S3.Region, + Bucket: cfg.S3.Bucket, + Endpoint: cfg.S3.Endpoint, + }) + if err != nil { + log.Fatalf("Failed to create S3 storage: %v", err) + } + + authMiddleware := middleware.NewAuthMiddleware(authClient) + + var authHandler handler.AuthHandler + if authClient != nil { + authHandler = *handler.NewAuthHandler(authClient, userClient) + } else { + log.Printf("Warning: AuthHandler not initialized due to missing authClient") + } + + var competitionHandler handler.CompetitionHandler + if competitionClient != nil { + competitionHandler = *handler.NewCompetitionHandler(competitionClient, userClient) + } else { + log.Printf("Warning: CompetitionHandler not initialized due to missing competitionClient") + } + + var taskHandler handler.TaskHandler + if taskClient != nil { + taskHandler = *handler.NewTaskHandler(taskClient) + } else { + log.Printf("Warning: TaskHandler not initialized due to missing taskClient") + } + + var submissionHandler handler.SubmissionHandler + if submissionClient != nil { + submissionHandler = *handler.NewSubmissionHandler(submissionClient, s3Storage) + } else { + log.Printf("Warning: SubmissionHandler not initialized due to missing submissionClient") + } + + var resultsHandler handler.ResultsHandler + if resultsClient != nil { + resultsHandler = *handler.NewResultsHandler(resultsClient) + } else { + log.Printf("Warning: ResultsHandler not initialized due to missing resultsClient") + } + + var reviewHandler handler.ReviewHandler + if reviewClient != nil { + reviewHandler = *handler.NewReviewHandler(reviewClient) + } else { + log.Printf("Warning: ReviewHandler not initialized due to missing reviewClient") + } + + var achievementsHandler handler.AchievementsHandler + if achievementsClient != nil { + achievementsHandler = *handler.NewAchievementsHandler(achievementsClient) + } else { + log.Printf("Warning: AchievementsHandler not initialized due to missing achievementsClient") + } + + pingHandler := handler.NewPingHandler() + + rt := router.NewRouter( + &authHandler, + &competitionHandler, + &taskHandler, + &submissionHandler, + &resultsHandler, + &reviewHandler, + &achievementsHandler, + pingHandler, + authMiddleware, + ) + + httpHandler := rt.Setup() + + addr := fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port) + server := &http.Server{ + Addr: addr, + Handler: httpHandler, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + go func() { + log.Printf("Starting API Gateway on %s", addr) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + 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 server...") + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := server.Shutdown(shutdownCtx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + + log.Println("Server exited") +} diff --git a/cmd/migrate/main.go b/cmd/migrate/main.go index 83eab80..3fe33b2 100644 --- a/cmd/migrate/main.go +++ b/cmd/migrate/main.go @@ -7,7 +7,7 @@ import ( "flag" "log" - "datarush/internal/lms/config" + "datarush/internal/migrate/config" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database/postgres" diff --git a/cmd/server/main.go b/cmd/server/main.go deleted file mode 100644 index 37ff221..0000000 --- a/cmd/server/main.go +++ /dev/null @@ -1,6 +0,0 @@ -package main - - -func main() { - -} 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/compose.yaml b/compose.yaml index e40a5eb..48956cb 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,42 +1,12 @@ name: datarush services: - auth: - build: - context: . - dockerfile: Containerfile - depends_on: - postgres: - restart: false - condition: service_healthy - required: true - env_file: - - path: ./infrastructure/auth/.env.template - required: true - - path: ./infrastructure/auth/.env - required: false - ports: - - name: http - target: 8081 - published: 13443 - host_ip: 127.0.0.1 - protocol: tcp - app_protocol: http - - name: grpc - target: 50052 - published: 13444 - host_ip: 127.0.0.1 - protocol: tcp - app_protocol: http - networks: - - default - restart: unless-stopped - shm_size: 4mb - - core: + gw: build: context: . dockerfile: Containerfile + args: + SERVICE: gw depends_on: migrate: restart: false @@ -54,23 +24,19 @@ services: restart: false condition: service_started required: true - env_file: - - path: ./infrastructure/core/.env.template + competition: + restart: false + condition: service_started required: true - - path: ./infrastructure/core/.env - ports: - - name: http - target: 8080 - published: 13440 - host_ip: 127.0.0.1 - protocol: tcp - app_protocol: http - - name: grpc - target: 50051 - published: 13441 - host_ip: 127.0.0.1 - protocol: tcp - app_protocol: http + task: + restart: false + condition: service_started + required: true + env_file: + - path: ./infrastructure/gw/.env.template + required: true + - path: ./infrastructure/gw/.env + required: false networks: - default restart: unless-stopped @@ -80,20 +46,131 @@ services: build: context: . dockerfile: Containerfile + args: + SERVICE: migrate depends_on: postgres: restart: false condition: service_healthy required: true env_file: - - path: ./infrastructure/core/.env.template + - path: ./infrastructure/migrate/.env.template required: true - - path: ./infrastructure/core/.env + - path: ./infrastructure/migrate/.env + required: false networks: - default restart: no shm_size: 4mb + auth: + build: + context: . + dockerfile: Containerfile + args: + SERVICE: auth + depends_on: + postgres: + restart: false + condition: service_healthy + required: true + env_file: + - path: ./infrastructure/auth/.env.template + required: true + - path: ./infrastructure/auth/.env + required: false + networks: + - default + restart: unless-stopped + shm_size: 4mb + + competition: + build: + context: . + dockerfile: Containerfile + args: + SERVICE: competition + 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/competition/.env.template + required: true + - path: ./infrastructure/competition/.env + required: false + networks: + - default + restart: unless-stopped + shm_size: 4mb + + task: + build: + context: . + dockerfile: Containerfile + args: + SERVICE: task + 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 + networks: + - default + restart: unless-stopped + shm_size: 4mb + + nginx: + image: docker.io/nginx:1.29-alpine + configs: + - source: nginx_config + target: /etc/nginx/nginx.conf + depends_on: + gw: + restart: false + condition: service_started + required: true + healthcheck: + test: ["CMD", "wget", "-O", "-", "http://localhost:80/healthz"] + interval: 1m30s + timeout: 5s + start_period: 5s + start_interval: 2s + retries: 5 + ports: + - name: web + target: 80 + published: 8080 + host_ip: 0.0.0.0 + protocol: tcp + app_protocol: http + networks: + - default + restart: unless-stopped + shm_size: 4mb + postgres: image: docker.io/postgres:17-alpine configs: @@ -105,7 +182,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 @@ -190,6 +267,59 @@ services: target: /data read_only: false + minio-init: + image: docker.io/minio/minio:RELEASE.2025-09-07T16-13-09Z + entrypoint: > + /bin/sh -c " + sleep 5; + /usr/bin/mc alias set minio http://minio:9000 admin password; + /usr/bin/mc mb minio/datarush; + exit 0; + " + env_file: + - path: ./infrastructure/minio/.env.template + required: true + - path: ./infrastructure/minio/.env + required: false + restart: no + volumes: + - type: volume + source: minio_data + target: /data + + minio: + image: docker.io/minio/minio:RELEASE.2025-09-07T16-13-09Z + command: server --console-address ":9001" + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 1m30s + timeout: 5s + start_period: 5s + start_interval: 2s + retries: 5 + env_file: + - path: ./infrastructure/minio/.env.template + required: true + - path: ./infrastructure/minio/.env + required: false + ports: + - name: api + target: 9000 + published: 8005 + host_ip: 127.0.0.1 + protocol: tcp + app_protocol: http + - name: console + target: 9001 + published: 8006 + host_ip: 127.0.0.1 + protocol: tcp + app_protocol: http + restart: unless-stopped + volumes: + - type: volume + source: minio_data + target: /data networks: default: @@ -200,14 +330,15 @@ networks: enable_ipv6: true internal: false - volumes: postgres_data: pgadmin_data: redis_data: - + minio_data: configs: + nginx_config: + file: ./infrastructure/nginx/nginx.conf postgres_config: file: ./infrastructure/postgres/postgresql.conf pgadmin_servers_config: diff --git a/go.mod b/go.mod index 9d96342..7f091f2 100644 --- a/go.mod +++ b/go.mod @@ -6,33 +6,51 @@ 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/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/google/uuid v1.6.0 + github.com/gorilla/mux v1.7.4 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 ) require ( + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.18 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.17 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect + github.com/aws/smithy-go v1.24.0 // indirect 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 golang.org/x/net v0.43.0 // indirect @@ -41,6 +59,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..b25dc5c 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,50 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/aws/aws-sdk-go-v2 v1.16.16/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k= +github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= +github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8 h1:tcFliCWne+zOuUfKNRn8JdFBuWPDuISDH08wD2ULkhk= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8/go.mod h1:JTnlBSot91steJeti4ryyu/tLd4Sk84O5W22L7O2EQU= +github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8= +github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI= +github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23/go.mod h1:2DFxAQ9pfIRy0imBCJv+vZ2X6RKxves6fbnEuSry6b4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17/go.mod h1:pRwaTYCJemADaqCbUAxltMoHKata7hmB5PjEXeu0kfg= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14 h1:ZSIPAkAsCCjYrhqfw2+lNzWDzxzHXEckFkTePL5RSWQ= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14/go.mod h1:AyGgqiKv9ECM6IZeNQtdT8NnMvUb3/2wokeq2Fgryto= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.9/go.mod h1:a9j48l6yL5XINLHLcOKInjdvknN+vWqPBxqeIDw7ktw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.18 h1:BBYoNQt2kUZUUK4bIPsKrCcjVPUMNsgQpNAwhznK/zo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.18/go.mod h1:NS55eQ4YixUJPTC+INxi2/jCqe1y2Uw3rnh9wEOVJxY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17/go.mod h1:4nYOrY41Lrbk2170/BGkcJKBhws9Pfn8MG3aGqjjeFI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.17 h1:HfVVR1vItaG6le+Bpw6P4midjBDMKnjMyZnw9MXYUcE= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.17/go.mod h1:YqMdV+gEKCQ59NrB7rzrJdALeBIsYiVi8Inj3+KcqHI= +github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11 h1:3/gm/JTX9bX8CpzTgIlrtYpB3EVBDxyg/GY/QdcIEZw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11/go.mod h1:fmgDANqTUCxciViKl9hb/zD5LFbvPINFRgWhDbR+vZo= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= +github.com/aws/smithy-go v1.13.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -17,6 +61,7 @@ github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= @@ -33,36 +78,27 @@ 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/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= 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= 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/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= 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -70,6 +106,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY 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/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 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= @@ -82,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= @@ -102,10 +138,16 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +<<<<<<< HEAD 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= +>>>>>>> origin/feature/task 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= @@ -123,6 +165,8 @@ 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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= @@ -148,5 +192,6 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/infrastructure/auth/.env.template b/infrastructure/auth/.env.template index 1607dd9..284b31f 100644 --- a/infrastructure/auth/.env.template +++ b/infrastructure/auth/.env.template @@ -1,5 +1,5 @@ -AUTH_GRPC_PORT=50052 -AUTH_HTTP_PORT=8081 +AUTH_GRPC_PORT=50051 +AUTH_HTTP_PORT=8080 AUTH_GRPC_ENABLE_REFLECTION=true POSTGRES_HOST=postgres diff --git a/infrastructure/competition/.env.template b/infrastructure/competition/.env.template new file mode 100644 index 0000000..02f122f --- /dev/null +++ b/infrastructure/competition/.env.template @@ -0,0 +1,19 @@ +COMPETITION_GRPC_PORT=50051 +COMPETITION_GRPC_ENABLE_REFLECTION=true +COMPETITION_HTTP_PORT=8080 + +LOG_LEVEL=info + +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_USERNAME=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DATABASE=postgres + +AUTH_SVC_ADDR=auth:50051 + +REDIS_ADDR=redis:6379 +REDIS_PASSWORD= +REDIS_DB=0 + +CACHE_ENABLED=true diff --git a/infrastructure/core/.env.template b/infrastructure/core/.env.template deleted file mode 100644 index e759de3..0000000 --- a/infrastructure/core/.env.template +++ /dev/null @@ -1,11 +0,0 @@ -GRPC_ENABLE_REFLECTION=true -HTTP_HANDLER_ENABLE=true - -POSTGRES_HOST=postgres -POSTGRES_PORT=5432 -POSTGRES_USERNAME=postgres -POSTGRES_PASSWORD=postgres -POSTGRES_DATABASE=postgres - -REDIS_URI=redis://redis:6379 -AUTH_GRPC_ADDR=auth:50052 \ No newline at end of file diff --git a/infrastructure/gw/.env.template b/infrastructure/gw/.env.template new file mode 100644 index 0000000..3e59c69 --- /dev/null +++ b/infrastructure/gw/.env.template @@ -0,0 +1,19 @@ +SERVER_PORT=8080 +SERVER_HOST=0.0.0.0 + +AUTH_SERVICE_ADDR=auth:50051 +USER_SERVICE_ADDR=user:50051 +COMPETITION_SERVICE_ADDR=competition:50051 +TASK_SERVICE_ADDR=task:50051 +SUBMISSION_SERVICE_ADDR=submission:50051 +RESULTS_SERVICE_ADDR=results:50051 +REVIEW_SERVICE_ADDR=review:50051 +ACHIEVEMENTS_SERVICE_ADDR=achievements:50051 + +AWS_ACCESS_KEY_ID=admin +AWS_SECRET_ACCESS_KEY=password +AWS_REGION= +S3_BUCKET=datarush +S3_ENDPOINT=http://localhost:9000 + +JWT_SECRET=your_jwt_secret_here diff --git a/infrastructure/migrate/.env.template b/infrastructure/migrate/.env.template new file mode 100644 index 0000000..cf0da61 --- /dev/null +++ b/infrastructure/migrate/.env.template @@ -0,0 +1,5 @@ +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USERNAME=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DATABASE=postgres diff --git a/infrastructure/minio/.env.template b/infrastructure/minio/.env.template new file mode 100644 index 0000000..9d55c3a --- /dev/null +++ b/infrastructure/minio/.env.template @@ -0,0 +1,3 @@ +MINIO_ROOT_USER=admin +MINIO_ROOT_PASSWORD=password +MINIO_VOLUMES=/data diff --git a/infrastructure/nginx/nginx.conf b/infrastructure/nginx/nginx.conf new file mode 100644 index 0000000..e00cef4 --- /dev/null +++ b/infrastructure/nginx/nginx.conf @@ -0,0 +1,156 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 4096; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ssl_session_cache shared:SSL:50m; + ssl_session_timeout 1d; + ssl_session_tickets off; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256; + ssl_ecdh_curve X25519:secp521r1:secp384r1; + ssl_prefer_server_ciphers on; + ssl_stapling on; + ssl_stapling_verify on; + resolver 127.0.0.11 valid=30s; + resolver_timeout 5s; + server_names_hash_bucket_size 128; + + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "0"; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; img-src 'self' data: https:; style-src 'self' 'unsafe-inline' https:; font-src 'self' https:; frame-ancestors 'none'; form-action 'self'; object-src 'none'; base-uri 'self';" always; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 75; + client_body_timeout 15; + client_header_timeout 15; + reset_timedout_connection on; + send_timeout 15; + + client_body_buffer_size 128k; + client_header_buffer_size 4k; + client_max_body_size 100M; + large_client_header_buffers 4 16k; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_min_length 1024; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/x-javascript + application/xml + application/xml+rss + font/woff + font/woff2 + image/svg+xml; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + server_tokens off; + + upstream gw { + zone gw 64k; + server gw:8080 resolve; + } + + upstream minio { + zone minio 64k; + server minio:9000 resolve; + } + + upstream minio-ui { + zone minio-ui 64k; + server minio:9001 resolve; + } + + server { + listen 80; + server_name _; + + location / { + proxy_pass http://gw; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } + + server { + listen 80; + listen [::]:80; + server_name minio; + + http2 on; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + + ignore_invalid_headers off; + client_max_body_size 0; + proxy_buffering off; + proxy_request_buffering off; + + location / { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 300; + proxy_http_version 1.1; + proxy_set_header Connection ""; + chunked_transfer_encoding off; + + proxy_pass http://minio; + } + + location /minio/ui/ { + rewrite ^/minio/ui/(.*) /$1 break; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-NginX-Proxy true; + + real_ip_header X-Real-IP; + + proxy_connect_timeout 300; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + chunked_transfer_encoding off; + + proxy_pass http://minio-ui; + } + } +} diff --git a/internal/auth/config/config.go b/internal/auth/config/config.go index c29d095..4e8891d 100644 --- a/internal/auth/config/config.go +++ b/internal/auth/config/config.go @@ -27,7 +27,7 @@ func Load() (*Config, error) { _ = godotenv.Load() return &Config{ - GRPCPort: mustGetInt("AUTH_GRPC_PORT", 50052), + GRPCPort: mustGetInt("AUTH_GRPC_PORT", 50051), GRPCEnableReflection: mustGetBool("AUTH_GRPC_ENABLE_REFLECTION", false), HTTPPort: mustGetInt("AUTH_HTTP_PORT", 8081), LogLevel: getEnv("LOG_LEVEL", "info"), diff --git a/internal/auth/handler/grpc/auth.go b/internal/auth/handler/grpc/auth.go index f8d1907..4c593d2 100644 --- a/internal/auth/handler/grpc/auth.go +++ b/internal/auth/handler/grpc/auth.go @@ -53,7 +53,10 @@ func (h *AuthHandler) SignIn(ctx context.Context, req *pb.SignInRequest) (*pb.Si return &pb.SignInResponse{Token: token}, nil } -func (h *AuthHandler) ValidateToken(ctx context.Context, req *pb.ValidateTokenRequest) (*pb.ValidateTokenResponse, error) { +func (h *AuthHandler) ValidateToken( + ctx context.Context, + req *pb.ValidateTokenRequest, +) (*pb.ValidateTokenResponse, error) { token := req.Token if token == "" { diff --git a/internal/auth/handler/http/auth.go b/internal/auth/handler/http/auth.go index 682c4b5..67e046d 100644 --- a/internal/auth/handler/http/auth.go +++ b/internal/auth/handler/http/auth.go @@ -12,13 +12,13 @@ import ( ) type SignUpRequest struct { - Email string `json:"email" binding:"required,email"` + Email string `json:"email" binding:"required,email"` Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } type SignInRequest struct { - Email string `json:"email" binding:"required,email"` + Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` } diff --git a/internal/auth/server/server.go b/internal/auth/server/server.go index 7e1d537..debf441 100644 --- a/internal/auth/server/server.go +++ b/internal/auth/server/server.go @@ -1,7 +1,6 @@ package server import ( - "context" "fmt" "log" "net" @@ -10,7 +9,6 @@ import ( "datarush/internal/auth/config" grpcHandlers "datarush/internal/auth/handler/grpc" - httpHandlers "datarush/internal/auth/handler/http" authPostgresRepo "datarush/internal/auth/repository/postgres" "datarush/internal/auth/service" @@ -64,10 +62,6 @@ func (s *Server) Start() error { } }() - if err := s.startHTTPServer(); err != nil { - return fmt.Errorf("failed to start HTTP server: %w", err) - } - return nil } @@ -88,60 +82,9 @@ func (s *Server) registerGRPCServices() error { return nil } -func (s *Server) startHTTPServer() error { - userRepo := authPostgresRepo.NewUserRepository(s.db) - - authService := service.NewAuthService(userRepo, s.config.JWTSecret) - - authHandler := httpHandlers.NewAuthHandler(authService) - - mux := http.NewServeMux() - - mux.HandleFunc("/api/v1/sign-up", func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - authHandler.SignUp(w, r) - }) - - mux.HandleFunc("/api/v1/sign-in", func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - authHandler.SignIn(w, r) - }) - - s.httpServer = &http.Server{ - Addr: fmt.Sprintf(":%d", s.config.HTTPPort), - Handler: mux, - ReadTimeout: httpReadTimeout, - WriteTimeout: httpWriteTimeout, - IdleTimeout: httpIdleTimeout, - } - - go func() { - log.Printf("starting HTTP server on port %d", s.config.HTTPPort) - if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("failed to start HTTP server: %v", err) - } - }() - - return nil -} - func (s *Server) Stop() { log.Println("shutting down auth server...") - if s.httpServer != nil { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := s.httpServer.Shutdown(ctx); err != nil { - log.Printf("failed to shutdown HTTP server: %v", err) - } - } - if s.grpcServer != nil { s.grpcServer.GracefulStop() } diff --git a/internal/competition/config/config.go b/internal/competition/config/config.go new file mode 100644 index 0000000..b1f97af --- /dev/null +++ b/internal/competition/config/config.go @@ -0,0 +1,84 @@ +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 + AuthSvcAddr string + RedisAddr string + RedisPassword string + RedisDB int + CacheEnabled bool +} + +func Load() (*Config, error) { + _ = godotenv.Load() + + return &Config{ + GRPCPort: mustGetInt("COMPETITION_GRPC_PORT", 50051), + GRPCEnableReflection: mustGetBool("COMPETITION_GRPC_ENABLE_REFLECTION", false), + HTTPPort: mustGetInt("COMPETITION_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"), + AuthSvcAddr: getEnv("AUTH_SVC_ADDR", "localhost:50051"), + RedisAddr: getEnv("REDIS_ADDR", "localhost:6379"), + RedisPassword: getEnv("REDIS_PASSWORD", ""), + RedisDB: mustGetInt("REDIS_DB", 0), + CacheEnabled: mustGetBool("CACHE_ENABLED", true), + }, 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) +} diff --git a/internal/competition/handler/grpc/handler.go b/internal/competition/handler/grpc/handler.go new file mode 100644 index 0000000..71a8ad9 --- /dev/null +++ b/internal/competition/handler/grpc/handler.go @@ -0,0 +1,63 @@ +package grpc + +import ( + "context" + + "google.golang.org/protobuf/types/known/emptypb" + + pb "datarush/pkg/api/competition" +) + +type CompetitionService interface { + CreateCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) + GetCompetition(ctx context.Context, req *pb.GetCompetitionRequest) (*pb.Competition, error) + EditCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) + DeleteCompetition(ctx context.Context, req *pb.DeleteCompetitionRequest) (*emptypb.Empty, error) + ListCompetitions(ctx context.Context, req *pb.ListCompetitionsRequest) (*pb.ListCompetitionsResponse, error) + ChangeCompetitionState(ctx context.Context, req *pb.ChangeCompetitionStateRequest) (*pb.Competition, error) +} + +type CompetitionHandler struct { + pb.UnimplementedCompetitionServiceServer + service CompetitionService +} + +func NewCompetitionHandler(service CompetitionService) *CompetitionHandler { + return &CompetitionHandler{service: service} +} + +func (h *CompetitionHandler) CreateCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) { + return h.service.CreateCompetition(ctx, req) +} + +func (h *CompetitionHandler) GetCompetition( + ctx context.Context, + req *pb.GetCompetitionRequest, +) (*pb.Competition, error) { + return h.service.GetCompetition(ctx, req) +} + +func (h *CompetitionHandler) EditCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) { + return h.service.EditCompetition(ctx, req) +} + +func (h *CompetitionHandler) DeleteCompetition( + ctx context.Context, + req *pb.DeleteCompetitionRequest, +) (*emptypb.Empty, error) { + return h.service.DeleteCompetition(ctx, req) +} + +func (h *CompetitionHandler) ListCompetitions( + ctx context.Context, + req *pb.ListCompetitionsRequest, +) (*pb.ListCompetitionsResponse, error) { + return h.service.ListCompetitions(ctx, req) +} + +func (h *CompetitionHandler) ChangeCompetitionState( + ctx context.Context, + req *pb.ChangeCompetitionStateRequest, +) (*pb.Competition, error) { + return h.service.ChangeCompetitionState(ctx, req) +} diff --git a/internal/competition/repository/competition.go b/internal/competition/repository/competition.go new file mode 100644 index 0000000..c82717a --- /dev/null +++ b/internal/competition/repository/competition.go @@ -0,0 +1,26 @@ +package repository + +import ( + "context" + + "github.com/google/uuid" + + pb "datarush/pkg/api/competition" +) + +type ListCompetitionsOptions struct { + Page int + PageSize int + State *pb.CompetitionState + IsParticipating *bool + SearchQuery *string +} + +type CompetitionRepository interface { + Create(ctx context.Context, competition *pb.Competition) (*pb.Competition, error) + Get(ctx context.Context, id uuid.UUID) (*pb.Competition, error) + Update(ctx context.Context, competition *pb.Competition) (*pb.Competition, error) + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, opts ListCompetitionsOptions) ([]*pb.Competition, int, error) + ChangeState(ctx context.Context, id uuid.UUID, state pb.CompetitionState) (*pb.Competition, error) +} diff --git a/internal/competition/repository/postgres/competition.go b/internal/competition/repository/postgres/competition.go new file mode 100644 index 0000000..476d929 --- /dev/null +++ b/internal/competition/repository/postgres/competition.go @@ -0,0 +1,230 @@ +package postgres + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "datarush/internal/competition/repository" + pb "datarush/pkg/api/competition" + + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/redis/go-redis/v9" +) + +const ( + competitionCachePrefix = "competition:" +) + +type CompetitionRepository struct { + db *sqlx.DB + redisClient *redis.Client + cacheEnabled bool +} + +func NewCompetitionRepository( + db *sqlx.DB, + redisClient *redis.Client, + cacheEnabled bool, +) repository.CompetitionRepository { + return &CompetitionRepository{ + db: db, + redisClient: redisClient, + cacheEnabled: cacheEnabled, + } +} + +func (r *CompetitionRepository) cacheKey(id string) string { + return competitionCachePrefix + id +} + +func (r *CompetitionRepository) Create(ctx context.Context, c *pb.Competition) (*pb.Competition, error) { + query := `INSERT INTO competitions (state, title, description, image_url, start_time, end_time, type, participation_type) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at, updated_at` + + var createdCompetition pb.Competition + err := r.db.QueryRowxContext(ctx, query, + c.State, + c.Title, + c.Description, + c.ImageUrl, + c.StartTime.AsTime(), + c.EndTime.AsTime(), + c.Type, + c.ParticipationType, + ).StructScan(&createdCompetition) + if err != nil { + return nil, err + } + c.Id = createdCompetition.Id + c.CreatedAt = createdCompetition.CreatedAt + c.UpdatedAt = createdCompetition.UpdatedAt + + if r.cacheEnabled { + data, err := json.Marshal(c) + if err == nil { + r.redisClient.Set(ctx, r.cacheKey(c.Id), data, 10*time.Minute).Err() + } + } + + return c, nil +} + +func (r *CompetitionRepository) Get(ctx context.Context, id uuid.UUID) (*pb.Competition, error) { + if r.cacheEnabled { + val, err := r.redisClient.Get(ctx, r.cacheKey(id.String())).Result() + if err == nil { + var competition pb.Competition + if json.Unmarshal([]byte(val), &competition) == nil { + return &competition, nil + } + } + } + + query := `SELECT id, state, title, description, image_url, start_time, end_time, type, participation_type, created_at, updated_at FROM competitions WHERE id = $1` + var competition pb.Competition + err := r.db.GetContext(ctx, &competition, query, id) + if err != nil { + return nil, err + } + + if r.cacheEnabled { + data, err := json.Marshal(&competition) + if err == nil { + r.redisClient.Set(ctx, r.cacheKey(id.String()), data, 10*time.Minute).Err() + } + } + + return &competition, nil +} + +func (r *CompetitionRepository) Update(ctx context.Context, c *pb.Competition) (*pb.Competition, error) { + query := `UPDATE competitions SET + state = $2, title = $3, description = $4, image_url = $5, start_time = $6, end_time = $7, type = $8, participation_type = $9, updated_at = now() + WHERE id = $1 RETURNING updated_at` + + var updatedCompetition pb.Competition + err := r.db.QueryRowxContext(ctx, query, + c.Id, + c.State, + c.Title, + c.Description, + c.ImageUrl, + c.StartTime.AsTime(), + c.EndTime.AsTime(), + c.Type, + c.ParticipationType, + ).StructScan(&updatedCompetition) + if err != nil { + return nil, err + } + c.UpdatedAt = updatedCompetition.UpdatedAt + + if r.cacheEnabled { + r.redisClient.Del(ctx, r.cacheKey(c.Id)).Err() + } + + return c, nil +} + +func (r *CompetitionRepository) Delete(ctx context.Context, id uuid.UUID) error { + query := `DELETE FROM competitions WHERE id = $1` + _, err := r.db.ExecContext(ctx, query, id) + if err != nil { + return err + } + + if r.cacheEnabled { + r.redisClient.Del(ctx, r.cacheKey(id.String())).Err() + } + + return nil +} + +func (r *CompetitionRepository) List( + ctx context.Context, + opts repository.ListCompetitionsOptions, +) ([]*pb.Competition, int, error) { + var args []interface{} + var whereClauses []string + argId := 1 + + if opts.State != nil { + whereClauses = append(whereClauses, fmt.Sprintf("state = $%d", argId)) + args = append(args, *opts.State) + argId++ + } + if opts.SearchQuery != nil { + whereClauses = append(whereClauses, fmt.Sprintf("title ILIKE $%d", argId)) + args = append(args, "%"+*opts.SearchQuery+"%") + argId++ + } + + if opts.IsParticipating != nil { + userID, ok := ctx.Value("user_id").(string) + if !ok { + return nil, 0, fmt.Errorf("user not authenticated or user_id not in context") + } + + if *opts.IsParticipating { + whereClauses = append( + whereClauses, + fmt.Sprintf("id IN (SELECT competition_id FROM user_competitions WHERE user_id = $%d)", argId), + ) + } else { + whereClauses = append(whereClauses, fmt.Sprintf("id NOT IN (SELECT competition_id FROM user_competitions WHERE user_id = $%d)", argId)) + } + args = append(args, userID) + argId++ + } + + where := "" + if len(whereClauses) > 0 { + where = "WHERE " + strings.Join(whereClauses, " AND ") + } + + countQuery := "SELECT COUNT(*) FROM competitions " + where + var total int + if err := r.db.GetContext(ctx, &total, countQuery, args...); err != nil { + return nil, 0, err + } + + query := fmt.Sprintf( + `SELECT id, state, title, description, image_url, start_time, end_time, type, participation_type, created_at, updated_at FROM competitions %s ORDER BY created_at DESC LIMIT $%d OFFSET $%d`, + where, + argId, + argId+1, + ) + args = append(args, opts.PageSize, (opts.Page-1)*opts.PageSize) + + var competitions []*pb.Competition + err := r.db.SelectContext(ctx, &competitions, query, args...) + if err != nil { + return nil, 0, err + } + + return competitions, total, nil +} + +func (r *CompetitionRepository) ChangeState( + ctx context.Context, + id uuid.UUID, + state pb.CompetitionState, +) (*pb.Competition, error) { + query := `UPDATE competitions SET state = $1, updated_at = $2 WHERE id = $3 RETURNING updated_at` + now := time.Now() + var competition pb.Competition + err := r.db.QueryRowxContext(ctx, query, state, now, id).StructScan(&competition) + if err != nil { + return nil, err + } + + if r.cacheEnabled { + r.redisClient.Del(ctx, r.cacheKey(id.String())).Err() + } + + return r.Get(ctx, id) +} diff --git a/internal/competition/server/server.go b/internal/competition/server/server.go new file mode 100644 index 0000000..a0d821a --- /dev/null +++ b/internal/competition/server/server.go @@ -0,0 +1,130 @@ +package server + +import ( + "fmt" + "log" + "net" + "time" + + "datarush/internal/competition/config" + grpcHandlers "datarush/internal/competition/handler/grpc" + "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" + "github.com/redis/go-redis/v9" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "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 + redisClient *redis.Client + authConn *grpc.ClientConn +} + +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 + + s.redisClient = redis.NewClient(&redis.Options{ + Addr: s.config.RedisAddr, + Password: s.config.RedisPassword, + DB: s.config.RedisDB, + }) + + authConn, err := grpc.Dial(s.config.AuthSvcAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return fmt.Errorf("failed to connect to auth service: %w", err) + } + s.authConn = authConn + + 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 { + authClient := authpb.NewAuthServiceClient(s.authConn) + authInterceptor := interceptor.NewAuthInterceptor(authClient) + + s.grpcServer = grpc.NewServer( + grpc.UnaryInterceptor(authInterceptor.Unary()), + ) + + compRepo := postgres.NewCompetitionRepository(s.db, s.redisClient, s.config.CacheEnabled) + compService := service.NewCompetitionService(compRepo) + compHandler := grpcHandlers.NewCompetitionHandler(compService) + + pb.RegisterCompetitionServiceServer(s.grpcServer, compHandler) + + if s.config.GRPCEnableReflection { + reflection.Register(s.grpcServer) + } + + return nil +} + +func (s *Server) Stop() { + log.Println("shutting down competition server...") + + if s.grpcServer != nil { + s.grpcServer.GracefulStop() + } + + if s.authConn != nil { + if err := s.authConn.Close(); err != nil { + log.Printf("failed to close auth service connection: %v", err) + } + } + + if s.db != nil { + if err := s.db.Close(); err != nil { + log.Printf("failed to close database: %v", err) + } + } + + if s.redisClient != nil { + if err := s.redisClient.Close(); err != nil { + log.Printf("failed to close redis client: %v", err) + } + } + + log.Println("competition server stopped") +} diff --git a/internal/competition/service/service.go b/internal/competition/service/service.go new file mode 100644 index 0000000..7007b9a --- /dev/null +++ b/internal/competition/service/service.go @@ -0,0 +1,90 @@ +package service + +import ( + "context" + + "datarush/internal/competition/repository" + pb "datarush/pkg/api/competition" + + "github.com/google/uuid" + "google.golang.org/protobuf/types/known/emptypb" +) + +type CompetitionService struct { + repo repository.CompetitionRepository +} + +func NewCompetitionService(repo repository.CompetitionRepository) *CompetitionService { + return &CompetitionService{repo: repo} +} + +func (s *CompetitionService) CreateCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) { + return s.repo.Create(ctx, req) +} + +func (s *CompetitionService) GetCompetition( + ctx context.Context, + req *pb.GetCompetitionRequest, +) (*pb.Competition, error) { + id, err := uuid.Parse(req.CompetitionId) + if err != nil { + return nil, err + } + return s.repo.Get(ctx, id) +} + +func (s *CompetitionService) EditCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) { + return s.repo.Update(ctx, req) +} + +func (s *CompetitionService) DeleteCompetition( + ctx context.Context, + req *pb.DeleteCompetitionRequest, +) (*emptypb.Empty, error) { + id, err := uuid.Parse(req.CompetitionId) + if err != nil { + return nil, err + } + err = s.repo.Delete(ctx, id) + return &emptypb.Empty{}, err +} + +func (s *CompetitionService) ListCompetitions( + ctx context.Context, + req *pb.ListCompetitionsRequest, +) (*pb.ListCompetitionsResponse, error) { + opts := repository.ListCompetitionsOptions{ + Page: int(req.PageToken), + PageSize: int(req.PageSize), + State: req.State, + IsParticipating: req.IsParticipating, + SearchQuery: req.SearchQuery, + } + + competitions, total, err := s.repo.List(ctx, opts) + if err != nil { + return nil, err + } + + var nextPageToken int32 + if (opts.Page+1)*opts.PageSize < total { + nextPageToken = int32(opts.Page + 1) + } + + return &pb.ListCompetitionsResponse{ + Competitions: competitions, + TotalCount: int32(total), + NextPageToken: nextPageToken, + }, nil +} + +func (s *CompetitionService) ChangeCompetitionState( + ctx context.Context, + req *pb.ChangeCompetitionStateRequest, +) (*pb.Competition, error) { + id, err := uuid.Parse(req.CompetitionId) + if err != nil { + return nil, err + } + return s.repo.ChangeState(ctx, id, req.State) +} diff --git a/internal/gw/config/config.go b/internal/gw/config/config.go new file mode 100644 index 0000000..80f0efd --- /dev/null +++ b/internal/gw/config/config.go @@ -0,0 +1,101 @@ +package config + +import ( + "fmt" + "os" + "strconv" +) + +type Config struct { + Server ServerConfig + GRPC GRPCConfig + S3 S3Config + Auth AuthConfig +} + +type ServerConfig struct { + Port string + Host string +} + +type GRPCConfig struct { + AuthServiceAddr string + UserServiceAddr string + CompetitionServiceAddr string + TaskServiceAddr string + SubmissionServiceAddr string + ResultsServiceAddr string + ReviewServiceAddr string + AchievementsServiceAddr string +} + +type S3Config struct { + AccessKeyID string + SecretAccessKey string + Region string + Bucket string + Endpoint string +} + +type AuthConfig struct { + JWTSecret string +} + +func Load() (*Config, error) { + cfg := &Config{ + Server: ServerConfig{ + Port: getEnv("SERVER_PORT", "8080"), + Host: getEnv("SERVER_HOST", "0.0.0.0"), + }, + GRPC: GRPCConfig{ + AuthServiceAddr: getEnvRequired("AUTH_SERVICE_ADDR"), + UserServiceAddr: getEnvRequired("USER_SERVICE_ADDR"), + CompetitionServiceAddr: getEnvRequired("COMPETITION_SERVICE_ADDR"), + TaskServiceAddr: getEnvRequired("TASK_SERVICE_ADDR"), + SubmissionServiceAddr: getEnvRequired("SUBMISSION_SERVICE_ADDR"), + ResultsServiceAddr: getEnvRequired("RESULTS_SERVICE_ADDR"), + ReviewServiceAddr: getEnvRequired("REVIEW_SERVICE_ADDR"), + AchievementsServiceAddr: getEnvRequired("ACHIEVEMENTS_SERVICE_ADDR"), + }, + S3: S3Config{ + AccessKeyID: getEnvRequired("AWS_ACCESS_KEY_ID"), + SecretAccessKey: getEnvRequired("AWS_SECRET_ACCESS_KEY"), + Region: getEnv("AWS_REGION", ""), + Bucket: getEnvRequired("S3_BUCKET"), + Endpoint: getEnv("S3_ENDPOINT", ""), + }, + Auth: AuthConfig{ + JWTSecret: getEnv("JWT_SECRET", ""), + }, + } + + return cfg, nil +} + +func getEnv(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} + +func getEnvRequired(key string) string { + value := os.Getenv(key) + if value == "" { + panic(fmt.Sprintf("required environment variable %s is not set", key)) + } + return value +} + +func getEnvAsInt(key string, defaultValue int) int { + valueStr := os.Getenv(key) + if valueStr == "" { + return defaultValue + } + value, err := strconv.Atoi(valueStr) + if err != nil { + return defaultValue + } + return value +} diff --git a/internal/gw/domain/errors.go b/internal/gw/domain/errors.go new file mode 100644 index 0000000..39016ed --- /dev/null +++ b/internal/gw/domain/errors.go @@ -0,0 +1,137 @@ +package domain + +import ( + "errors" + "fmt" + "net/http" +) + +var ( + ErrUnauthorized = errors.New("unauthorized") + ErrForbidden = errors.New("forbidden") + ErrNotFound = errors.New("not found") + ErrBadRequest = errors.New("bad request") + ErrInternalServer = errors.New("internal server error") + ErrConflict = errors.New("conflict") + ErrInvalidToken = errors.New("invalid token") + ErrMissingAuthHeader = errors.New("missing authorization header") + ErrInvalidFile = errors.New("invalid file") + ErrFileTooLarge = errors.New("file too large") +) + +type AppError struct { + Err error + Message string + StatusCode int +} + +func (e *AppError) Error() string { + if e.Message != "" { + return e.Message + } + if e.Err != nil { + return e.Err.Error() + } + return "unknown error" +} + +func NewAppError(err error, message string, statusCode int) *AppError { + return &AppError{ + Err: err, + Message: message, + StatusCode: statusCode, + } +} + +func NewBadRequestError(message string) *AppError { + return &AppError{ + Err: ErrBadRequest, + Message: message, + StatusCode: http.StatusBadRequest, + } +} + +func NewUnauthorizedError(message string) *AppError { + return &AppError{ + Err: ErrUnauthorized, + Message: message, + StatusCode: http.StatusUnauthorized, + } +} + +func NewForbiddenError(message string) *AppError { + return &AppError{ + Err: ErrForbidden, + Message: message, + StatusCode: http.StatusForbidden, + } +} + +func NewNotFoundError(message string) *AppError { + return &AppError{ + Err: ErrNotFound, + Message: message, + StatusCode: http.StatusNotFound, + } +} + +func NewConflictError(message string) *AppError { + return &AppError{ + Err: ErrConflict, + Message: message, + StatusCode: http.StatusConflict, + } +} + +func NewInternalServerError(message string) *AppError { + return &AppError{ + Err: ErrInternalServer, + Message: message, + StatusCode: http.StatusInternalServerError, + } +} + +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message,omitempty"` + Details string `json:"details,omitempty"` +} + +func NewErrorResponse(err error, message string) *ErrorResponse { + errMsg := "internal server error" + if err != nil { + errMsg = err.Error() + } + + return &ErrorResponse{ + Error: errMsg, + Message: message, + } +} + +func GRPCErrorToHTTPStatus(err error) int { + if err == nil { + return http.StatusOK + } + + errMsg := err.Error() + + switch { + case contains(errMsg, "not found"): + return http.StatusNotFound + case contains(errMsg, "already exists"), contains(errMsg, "conflict"): + return http.StatusConflict + case contains(errMsg, "invalid"), contains(errMsg, "bad request"): + return http.StatusBadRequest + case contains(errMsg, "unauthorized"), contains(errMsg, "unauthenticated"): + return http.StatusUnauthorized + case contains(errMsg, "forbidden"), contains(errMsg, "permission denied"): + return http.StatusForbidden + default: + return http.StatusInternalServerError + } +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || fmt.Sprintf("%s", s) != s) +} diff --git a/internal/gw/domain/models.go b/internal/gw/domain/models.go new file mode 100644 index 0000000..406572a --- /dev/null +++ b/internal/gw/domain/models.go @@ -0,0 +1,207 @@ +package domain + +import "time" + +type SignUpRequest struct { + Email string `json:"email" validate:"required,email"` + Username string `json:"username" validate:"required,min=3,max=50"` + Password string `json:"password" validate:"required,min=6"` +} + +type SignInRequest struct { + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required"` +} + +type TokenResponse struct { + Token string `json:"token"` +} + +type UserResponse struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + FullName *string `json:"full_name,omitempty"` + AvatarURL *string `json:"avatar_url,omitempty"` +} + +type CompetitionRequest struct { + ID string `json:"id,omitempty"` + Title string `json:"title" validate:"required,max=200"` + Description string `json:"description" validate:"required"` + ImageURL *string `json:"image_url,omitempty"` + StartTime time.Time `json:"start_time" validate:"required"` + EndTime time.Time `json:"end_time" validate:"required"` + Type string `json:"type" validate:"required,oneof=educative competitive"` + ParticipationType string `json:"participation_type" validate:"required,oneof=individual team"` +} + +type CompetitionResponse struct { + ID string `json:"id"` + State string `json:"state"` + Title string `json:"title"` + Description string `json:"description"` + ImageURL *string `json:"image_url,omitempty"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + Type string `json:"type"` + ParticipationType string `json:"participation_type"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListCompetitionsResponse struct { + TotalCount int32 `json:"total_count"` + NextPageToken int32 `json:"next_page_token"` + Competitions []CompetitionResponse `json:"competitions"` +} + +type ChangeCompetitionStateRequest struct { + State string `json:"state" validate:"required,oneof=draft not_started started finished archived"` +} + +type TaskRequest struct { + ID string `json:"id,omitempty"` + CompetitionID string `json:"competition_id,omitempty"` + Title string `json:"title" validate:"required,max=50"` + Description string `json:"description" validate:"required"` + InCompetitionPosition int32 `json:"in_competition_position" validate:"required"` + MaxPoints int32 `json:"max_points,omitempty"` + MaxAttempts int32 `json:"max_attempts,omitempty"` + Type string `json:"type" validate:"required,oneof=input checker review"` +} + +type TaskResponse struct { + ID string `json:"id"` + CompetitionID string `json:"competition_id"` + Title string `json:"title"` + Description string `json:"description"` + InCompetitionPosition int32 `json:"in_competition_position"` + MaxPoints int32 `json:"max_points,omitempty"` + MaxAttempts int32 `json:"max_attempts,omitempty"` + Type string `json:"type"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListTasksResponse struct { + Tasks []TaskResponse `json:"tasks"` +} + +type TaskAttachmentResponse struct { + ID string `json:"id"` + FileURL string `json:"file_url"` + IsPublic bool `json:"is_public"` +} + +type SubmissionResponse struct { + ID string `json:"id"` + UserID string `json:"user_id"` + CompetitionID string `json:"competition_id"` + TaskID string `json:"task_id"` + Status string `json:"status"` + EarnedPoints int32 `json:"earned_points"` + SubmittedAt time.Time `json:"submitted_at"` + CheckedAt time.Time `json:"checked_at,omitempty"` + FileURL string `json:"file_url"` +} + +type SubmitTaskResponse struct { + SubmissionID string `json:"submission_id"` +} + +type SubmissionHistoryResponse struct { + Submissions []SubmissionResponse `json:"submissions"` +} + +type TaskStatusResponse struct { + TaskID string `json:"task_id"` + TaskTitle string `json:"task_title"` + EarnedPoints int32 `json:"earned_points"` + MaxPoints int32 `json:"max_points"` + Position *int32 `json:"position,omitempty"` +} + +type UserResultResponse struct { + UserID string `json:"user_id"` + Username string `json:"username"` + TotalScore int32 `json:"total_score"` + OverallPosition int32 `json:"overall_position"` + TaskStatuses []TaskStatusResponse `json:"task_statuses"` +} + +type CompetitionResultsResponse struct { + Results []UserResultResponse `json:"results"` + TotalCount int32 `json:"total_count"` + NextPageToken int32 `json:"next_page_token"` +} + +type CriteriaMarkRequest struct { + Slug string `json:"slug" validate:"required"` + Mark float64 `json:"mark" validate:"required"` +} + +type EvaluateSubmissionRequest struct { + EarnedPoints int32 `json:"earned_points" validate:"required"` + ReviewerComment string `json:"reviewer_comment"` + Marks []CriteriaMarkRequest `json:"marks"` +} + +type SubmissionSummaryResponse struct { + ID string `json:"id"` + CompetitionID string `json:"competition_id"` + TaskID string `json:"task_id"` + CompetitionTitle string `json:"competition_title"` + TaskTitle string `json:"task_title"` + SubmittedAt time.Time `json:"submitted_at"` + ReviewStatus string `json:"review_status"` +} + +type SubmissionForReviewResponse struct { + ID string `json:"id"` + CompetitionID string `json:"competition_id"` + TaskID string `json:"task_id"` + Content string `json:"content"` + Description string `json:"description"` + ReviewStatus string `json:"review_status"` + SubmittedAt time.Time `json:"submitted_at"` + CheckedAt *time.Time `json:"checked_at,omitempty"` +} + +type ListSubmissionsForReviewResponse struct { + TotalCount int32 `json:"total_count"` + NextPageToken int32 `json:"next_page_token"` + Submissions []SubmissionSummaryResponse `json:"submissions"` +} + +type EvaluateSubmissionResponse struct { + SubmissionID string `json:"submission_id"` + FinalScore int32 `json:"final_score"` + NewStatus string `json:"new_status"` +} + +type AchievementResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + IconURL string `json:"icon_url"` +} + +type ListAchievementsResponse struct { + Achievements []AchievementResponse `json:"achievements"` +} + +type UserAchievementResponse struct { + AchievementID string `json:"achievement_id"` + UserID string `json:"user_id"` + EarnedAt time.Time `json:"earned_at"` +} + +type ListUserAchievementsResponse struct { + Achievements []UserAchievementResponse `json:"achievements"` +} + +type PingResponse struct { + Message string `json:"message"` + Status string `json:"status"` +} diff --git a/internal/gw/grpc_client/achievements_client.go b/internal/gw/grpc_client/achievements_client.go new file mode 100644 index 0000000..4a4148d --- /dev/null +++ b/internal/gw/grpc_client/achievements_client.go @@ -0,0 +1,49 @@ +package grpc_client + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + + pb "datarush/pkg/api/achievements" +) + +type AchievementsClient struct { + client pb.AchievementsServiceClient + conn *grpc.ClientConn +} + +func NewAchievementsClient(ctx context.Context, address string, factory *ClientFactory) (*AchievementsClient, error) { + conn, err := factory.GetConnection(ctx, address) + if err != nil { + return nil, fmt.Errorf("failed to create achievements client: %w", err) + } + return &AchievementsClient{client: pb.NewAchievementsServiceClient(conn), conn: conn}, nil +} + +func (c *AchievementsClient) GetAchievement(ctx context.Context, achievementID string) (*pb.Achievement, error) { + req := &pb.GetAchievementRequest{Id: achievementID} + resp, err := c.client.GetAchievement(ctx, req) + if err != nil { + return nil, fmt.Errorf("get achievement failed: %w", err) + } + return resp, nil +} + +func (c *AchievementsClient) ListAchievements(ctx context.Context) ([]*pb.Achievement, error) { + resp, err := c.client.ListAchievements(ctx, nil) + if err != nil { + return nil, fmt.Errorf("list achievements failed: %w", err) + } + return resp.Achievements, nil +} + +func (c *AchievementsClient) GetUserAchievements(ctx context.Context, userID string) ([]*pb.AchievementUser, error) { + req := &pb.GetUserAchievementsRequest{UserId: userID} + resp, err := c.client.GetUserAchievements(ctx, req) + if err != nil { + return nil, fmt.Errorf("get user achievements failed: %w", err) + } + return resp.UserAchievements, nil +} diff --git a/internal/gw/grpc_client/auth_client.go b/internal/gw/grpc_client/auth_client.go new file mode 100644 index 0000000..63383e9 --- /dev/null +++ b/internal/gw/grpc_client/auth_client.go @@ -0,0 +1,69 @@ +package grpc_client + +import ( + "context" + "fmt" + + pb "datarush/pkg/api/auth" + + "google.golang.org/grpc" +) + +type AuthClient struct { + client pb.AuthServiceClient + conn *grpc.ClientConn +} + +func NewAuthClient(ctx context.Context, address string, factory *ClientFactory) (*AuthClient, error) { + conn, err := factory.GetConnection(ctx, address) + if err != nil { + return nil, fmt.Errorf("failed to create auth client: %w", err) + } + + return &AuthClient{ + client: pb.NewAuthServiceClient(conn), + conn: conn, + }, nil +} + +func (c *AuthClient) SignUp(ctx context.Context, email, username, password string) (string, error) { + req := &pb.SignUpRequest{ + Email: email, + Username: username, + Password: password, + } + + resp, err := c.client.SignUp(ctx, req) + if err != nil { + return "", fmt.Errorf("sign up failed: %w", err) + } + + return resp.Token, nil +} + +func (c *AuthClient) SignIn(ctx context.Context, email, password string) (string, error) { + req := &pb.SignInRequest{ + Email: email, + Password: password, + } + + resp, err := c.client.SignIn(ctx, req) + if err != nil { + return "", fmt.Errorf("sign in failed: %w", err) + } + + return resp.Token, nil +} + +func (c *AuthClient) ValidateToken(ctx context.Context, token string) (string, error) { + req := &pb.ValidateTokenRequest{ + Token: token, + } + + resp, err := c.client.ValidateToken(ctx, req) + if err != nil { + return "", fmt.Errorf("token validation failed: %w", err) + } + + return resp.UserId, nil +} diff --git a/internal/gw/grpc_client/client.go b/internal/gw/grpc_client/client.go new file mode 100644 index 0000000..195d4d6 --- /dev/null +++ b/internal/gw/grpc_client/client.go @@ -0,0 +1,49 @@ +package grpc_client + +import ( + "context" + "fmt" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type ClientFactory struct { + connections map[string]*grpc.ClientConn +} + +func NewClientFactory() *ClientFactory { + return &ClientFactory{ + connections: make(map[string]*grpc.ClientConn), + } +} + +func (f *ClientFactory) GetConnection(ctx context.Context, address string) (*grpc.ClientConn, error) { + if conn, ok := f.connections[address]; ok { + return conn, nil + } + + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + conn, err := grpc.DialContext(ctx, address, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithBlock(), + ) + if err != nil { + return nil, fmt.Errorf("failed to connect to %s: %w", address, err) + } + + f.connections[address] = conn + return conn, nil +} + +func (f *ClientFactory) Close() error { + for addr, conn := range f.connections { + if err := conn.Close(); err != nil { + return fmt.Errorf("failed to close connection to %s: %w", addr, err) + } + } + return nil +} diff --git a/internal/gw/grpc_client/competition_client.go b/internal/gw/grpc_client/competition_client.go new file mode 100644 index 0000000..4476597 --- /dev/null +++ b/internal/gw/grpc_client/competition_client.go @@ -0,0 +1,98 @@ +package grpc_client + +import ( + "context" + "fmt" + + pb "datarush/pkg/api/competition" + + "google.golang.org/grpc" +) + +type CompetitionClient struct { + client pb.CompetitionServiceClient + conn *grpc.ClientConn +} + +func NewCompetitionClient(ctx context.Context, address string, factory *ClientFactory) (*CompetitionClient, error) { + conn, err := factory.GetConnection(ctx, address) + if err != nil { + return nil, fmt.Errorf("failed to create competition client: %w", err) + } + + return &CompetitionClient{ + client: pb.NewCompetitionServiceClient(conn), + conn: conn, + }, nil +} + +func (c *CompetitionClient) CreateCompetition( + ctx context.Context, + competition *pb.Competition, +) (*pb.Competition, error) { + resp, err := c.client.CreateCompetition(ctx, competition) + if err != nil { + return nil, fmt.Errorf("create competition failed: %w", err) + } + return resp, nil +} + +func (c *CompetitionClient) GetCompetition(ctx context.Context, competitionID string) (*pb.Competition, error) { + req := &pb.GetCompetitionRequest{ + CompetitionId: competitionID, + } + + resp, err := c.client.GetCompetition(ctx, req) + if err != nil { + return nil, fmt.Errorf("get competition failed: %w", err) + } + return resp, nil +} + +func (c *CompetitionClient) EditCompetition(ctx context.Context, competition *pb.Competition) (*pb.Competition, error) { + resp, err := c.client.EditCompetition(ctx, competition) + if err != nil { + return nil, fmt.Errorf("edit competition failed: %w", err) + } + return resp, nil +} + +func (c *CompetitionClient) DeleteCompetition(ctx context.Context, competitionID string) error { + req := &pb.DeleteCompetitionRequest{ + CompetitionId: competitionID, + } + + _, err := c.client.DeleteCompetition(ctx, req) + if err != nil { + return fmt.Errorf("delete competition failed: %w", err) + } + return nil +} + +func (c *CompetitionClient) ListCompetitions( + ctx context.Context, + req *pb.ListCompetitionsRequest, +) (*pb.ListCompetitionsResponse, error) { + resp, err := c.client.ListCompetitions(ctx, req) + if err != nil { + return nil, fmt.Errorf("list competitions failed: %w", err) + } + return resp, nil +} + +func (c *CompetitionClient) ChangeCompetitionState( + ctx context.Context, + competitionID string, + state pb.CompetitionState, +) (*pb.Competition, error) { + req := &pb.ChangeCompetitionStateRequest{ + CompetitionId: competitionID, + State: state, + } + + resp, err := c.client.ChangeCompetitionState(ctx, req) + if err != nil { + return nil, fmt.Errorf("change competition state failed: %w", err) + } + return resp, nil +} diff --git a/internal/gw/grpc_client/results_client.go b/internal/gw/grpc_client/results_client.go new file mode 100644 index 0000000..1d5494e --- /dev/null +++ b/internal/gw/grpc_client/results_client.go @@ -0,0 +1,58 @@ +package grpc_client + +import ( + "context" + "fmt" + + pb "datarush/pkg/api/results" + + "google.golang.org/grpc" +) + +type ResultsClient struct { + client pb.ResultsServiceClient + conn *grpc.ClientConn +} + +func NewResultsClient(ctx context.Context, address string, factory *ClientFactory) (*ResultsClient, error) { + conn, err := factory.GetConnection(ctx, address) + if err != nil { + return nil, fmt.Errorf("failed to create results client: %w", err) + } + return &ResultsClient{client: pb.NewResultsServiceClient(conn), conn: conn}, nil +} + +func (c *ResultsClient) GetCompetitionResults( + ctx context.Context, + req *pb.GetCompetitionResultsRequest, +) (*pb.GetCompetitionResultsResponse, error) { + resp, err := c.client.GetCompetitionResults(ctx, req) + if err != nil { + return nil, fmt.Errorf("get competition results failed: %w", err) + } + return resp, nil +} + +func (c *ResultsClient) GetUserCompetitionResults( + ctx context.Context, + competitionID, userID string, +) (*pb.UserResult, error) { + req := &pb.GetUserCompetitionResultsRequest{ + CompetitionId: competitionID, + UserId: userID, + } + resp, err := c.client.GetUserCompetitionResults(ctx, req) + if err != nil { + return nil, fmt.Errorf("get user competition results failed: %w", err) + } + return resp.Result, nil +} + +func (c *ResultsClient) RecalculateResults(ctx context.Context, competitionID string) error { + req := &pb.RecalculateResultsRequest{CompetitionId: competitionID} + _, err := c.client.RecalculateResults(ctx, req) + if err != nil { + return fmt.Errorf("recalculate results failed: %w", err) + } + return nil +} diff --git a/internal/gw/grpc_client/review_client.go b/internal/gw/grpc_client/review_client.go new file mode 100644 index 0000000..4d0fb09 --- /dev/null +++ b/internal/gw/grpc_client/review_client.go @@ -0,0 +1,81 @@ +package grpc_client + +import ( + "context" + "fmt" + + pb "datarush/pkg/api/review" + + "google.golang.org/grpc" +) + +type ReviewClient struct { + client pb.ReviewServiceClient + conn *grpc.ClientConn +} + +func NewReviewClient(ctx context.Context, address string, factory *ClientFactory) (*ReviewClient, error) { + conn, err := factory.GetConnection(ctx, address) + if err != nil { + return nil, fmt.Errorf("failed to create review client: %w", err) + } + return &ReviewClient{client: pb.NewReviewServiceClient(conn), conn: conn}, nil +} + +func (c *ReviewClient) ValidateReviewToken(ctx context.Context, token string) (*pb.ValidateReviewTokenResponse, error) { + req := &pb.ValidateReviewTokenRequest{Token: token} + resp, err := c.client.ValidateReviewToken(ctx, req) + if err != nil { + return nil, fmt.Errorf("validate review token failed: %w", err) + } + return resp, nil +} + +func (c *ReviewClient) ListSubmissionsForReview( + ctx context.Context, + req *pb.ListSubmissionsForReviewRequest, +) (*pb.ListSubmissionsForReviewResponse, error) { + resp, err := c.client.ListSubmissionsForReview(ctx, req) + if err != nil { + return nil, fmt.Errorf("list submissions for review failed: %w", err) + } + return resp, nil +} + +func (c *ReviewClient) GetSubmissionForReview( + ctx context.Context, + token, submissionID string, +) (*pb.SubmissionForReview, error) { + req := &pb.GetSubmissionForReviewRequest{ + Token: token, + SubmissionId: submissionID, + } + resp, err := c.client.GetSubmissionForReview(ctx, req) + if err != nil { + return nil, fmt.Errorf("get submission for review failed: %w", err) + } + return resp, nil +} + +func (c *ReviewClient) EvaluateSubmission( + ctx context.Context, + req *pb.EvaluateSubmissionRequest, +) (*pb.EvaluateSubmissionResponse, error) { + resp, err := c.client.EvaluateSubmission(ctx, req) + if err != nil { + return nil, fmt.Errorf("evaluate submission failed: %w", err) + } + return resp, nil +} + +func (c *ReviewClient) ReleaseSubmission(ctx context.Context, token, submissionID string) error { + req := &pb.ReleaseSubmissionRequest{ + Token: token, + SubmissionId: submissionID, + } + _, err := c.client.ReleaseSubmission(ctx, req) + if err != nil { + return fmt.Errorf("release submission failed: %w", err) + } + return nil +} diff --git a/internal/gw/grpc_client/submission_client.go b/internal/gw/grpc_client/submission_client.go new file mode 100644 index 0000000..fec6d10 --- /dev/null +++ b/internal/gw/grpc_client/submission_client.go @@ -0,0 +1,85 @@ +package grpc_client + +import ( + "context" + "fmt" + + pb "datarush/pkg/api/submission" + + "google.golang.org/grpc" +) + +type SubmissionClient struct { + client pb.SubmissionServiceClient + conn *grpc.ClientConn +} + +func NewSubmissionClient(ctx context.Context, address string, factory *ClientFactory) (*SubmissionClient, error) { + conn, err := factory.GetConnection(ctx, address) + if err != nil { + return nil, fmt.Errorf("failed to create submission client: %w", err) + } + + return &SubmissionClient{ + client: pb.NewSubmissionServiceClient(conn), + conn: conn, + }, nil +} + +func (c *SubmissionClient) SubmitTask( + ctx context.Context, + userID, competitionID, taskID, fileURL string, +) (*pb.Submission, error) { + req := &pb.SubmitTaskRequest{ + UserId: userID, + CompetitionId: competitionID, + TaskId: taskID, + FileUrl: fileURL, + } + + resp, err := c.client.SubmitTask(ctx, req) + if err != nil { + return nil, fmt.Errorf("submit task failed: %w", err) + } + return resp, nil +} + +func (c *SubmissionClient) GetSubmissionsHistory( + ctx context.Context, + userID, competitionID, taskID string, +) ([]*pb.Submission, error) { + req := &pb.GetSubmissionsHistoryRequest{ + UserId: userID, + CompetitionId: competitionID, + TaskId: taskID, + } + + resp, err := c.client.GetSubmissionsHistory(ctx, req) + if err != nil { + return nil, fmt.Errorf("get submissions history failed: %w", err) + } + return resp.Submissions, nil +} + +func (c *SubmissionClient) GetSubmission(ctx context.Context, submissionID string) (*pb.Submission, error) { + req := &pb.GetSubmissionRequest{ + SubmissionId: submissionID, + } + + resp, err := c.client.GetSubmission(ctx, req) + if err != nil { + return nil, fmt.Errorf("get submission failed: %w", err) + } + return resp, nil +} + +func (c *SubmissionClient) ListSubmissions( + ctx context.Context, + req *pb.ListSubmissionsRequest, +) (*pb.ListSubmissionsResponse, error) { + resp, err := c.client.ListSubmissions(ctx, req) + if err != nil { + return nil, fmt.Errorf("list submissions failed: %w", err) + } + return resp, nil +} diff --git a/internal/gw/grpc_client/task_client.go b/internal/gw/grpc_client/task_client.go new file mode 100644 index 0000000..6ccfb66 --- /dev/null +++ b/internal/gw/grpc_client/task_client.go @@ -0,0 +1,82 @@ +package grpc_client + +import ( + "context" + "fmt" + + pb "datarush/pkg/api/task" + + "google.golang.org/grpc" +) + +type TaskClient struct { + client pb.TaskServiceClient + conn *grpc.ClientConn +} + +func NewTaskClient(ctx context.Context, address string, factory *ClientFactory) (*TaskClient, error) { + conn, err := factory.GetConnection(ctx, address) + if err != nil { + return nil, fmt.Errorf("failed to create task client: %w", err) + } + return &TaskClient{client: pb.NewTaskServiceClient(conn), conn: conn}, nil +} + +func (c *TaskClient) CreateTask(ctx context.Context, task *pb.Task) (*pb.Task, error) { + resp, err := c.client.CreateTask(ctx, task) + if err != nil { + return nil, fmt.Errorf("create task failed: %w", err) + } + return resp, nil +} + +func (c *TaskClient) GetTask(ctx context.Context, taskID string) (*pb.Task, error) { + req := &pb.GetTaskRequest{TaskId: taskID} + resp, err := c.client.GetTask(ctx, req) + if err != nil { + return nil, fmt.Errorf("get task failed: %w", err) + } + return resp, nil +} + +func (c *TaskClient) EditTask(ctx context.Context, task *pb.Task) (*pb.Task, error) { + resp, err := c.client.EditTask(ctx, task) + if err != nil { + return nil, fmt.Errorf("edit task failed: %w", err) + } + return resp, nil +} + +func (c *TaskClient) DeleteTask(ctx context.Context, taskID string) error { + req := &pb.DeleteTaskRequest{TaskId: taskID} + _, err := c.client.DeleteTask(ctx, req) + if err != nil { + return fmt.Errorf("delete task failed: %w", err) + } + return nil +} + +func (c *TaskClient) ListCompetitionTasks(ctx context.Context, competitionID string) ([]*pb.Task, error) { + req := &pb.ListCompetitionTasksRequest{CompetitionId: competitionID} + resp, err := c.client.ListCompetitionTasks(ctx, req) + if err != nil { + return nil, fmt.Errorf("list tasks failed: %w", err) + } + return resp.Tasks, nil +} + +func (c *TaskClient) GetTaskAttachments( + ctx context.Context, + taskID string, + showPrivate bool, +) ([]*pb.TaskAttachment, error) { + req := &pb.GetTaskAttachmentsRequest{ + TaskId: taskID, + ShowPrivate: &showPrivate, + } + resp, err := c.client.GetTaskAttachments(ctx, req) + if err != nil { + return nil, fmt.Errorf("get task attachments failed: %w", err) + } + return resp.Attachments, nil +} diff --git a/internal/gw/grpc_client/user_client.go b/internal/gw/grpc_client/user_client.go new file mode 100644 index 0000000..e21eb0c --- /dev/null +++ b/internal/gw/grpc_client/user_client.go @@ -0,0 +1,79 @@ +package grpc_client + +import ( + "context" + "fmt" + + pb "datarush/pkg/api/user" + + "google.golang.org/grpc" +) + +type UserClient struct { + client pb.UserServiceClient + conn *grpc.ClientConn +} + +func NewUserClient(ctx context.Context, address string, factory *ClientFactory) (*UserClient, error) { + conn, err := factory.GetConnection(ctx, address) + if err != nil { + return nil, fmt.Errorf("failed to create user client: %w", err) + } + + return &UserClient{ + client: pb.NewUserServiceClient(conn), + conn: conn, + }, nil +} + +func (c *UserClient) GetProfile(ctx context.Context, userID string) (*pb.User, error) { + req := &pb.GetProfileRequest{ + UserId: userID, + } + + resp, err := c.client.GetProfile(ctx, req) + if err != nil { + return nil, fmt.Errorf("get profile failed: %w", err) + } + + return resp, nil +} + +func (c *UserClient) RegisterForCompetition(ctx context.Context, competitionID string) error { + req := &pb.RegisterForCompetitionRequest{ + CompetitionId: competitionID, + } + + _, err := c.client.RegisterForCompetition(ctx, req) + if err != nil { + return fmt.Errorf("register for competition failed: %w", err) + } + + return nil +} + +func (c *UserClient) UnregisterFromCompetition(ctx context.Context, competitionID string) error { + req := &pb.UnregisterFromCompetitionRequest{ + CompetitionId: competitionID, + } + + _, err := c.client.UnregisterFromCompetition(ctx, req) + if err != nil { + return fmt.Errorf("unregister from competition failed: %w", err) + } + + return nil +} + +func (c *UserClient) ListUserCompetitions(ctx context.Context, userID string) ([]string, error) { + req := &pb.ListUserCompetitionsRequest{ + UserId: userID, + } + + resp, err := c.client.ListUserCompetitions(ctx, req) + if err != nil { + return nil, fmt.Errorf("list user competitions failed: %w", err) + } + + return resp.CompetitionIds, nil +} diff --git a/internal/gw/handler/achievements_handler.go b/internal/gw/handler/achievements_handler.go new file mode 100644 index 0000000..1a083f1 --- /dev/null +++ b/internal/gw/handler/achievements_handler.go @@ -0,0 +1,69 @@ +package handler + +import ( + "net/http" + + "datarush/internal/gw/domain" + "datarush/internal/gw/grpc_client" + "datarush/internal/gw/utils" +) + +type AchievementsHandler struct { + achievementsClient *grpc_client.AchievementsClient +} + +func NewAchievementsHandler(achievementsClient *grpc_client.AchievementsClient) *AchievementsHandler { + return &AchievementsHandler{achievementsClient: achievementsClient} +} + +func (h *AchievementsHandler) ListAchievements(w http.ResponseWriter, r *http.Request) { + achievements, err := h.achievementsClient.ListAchievements(r.Context()) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to list achievements")) + return + } + + response := make([]domain.AchievementResponse, len(achievements)) + for i, ach := range achievements { + response[i] = *utils.AchievementProtoToHTTP(ach) + } + + utils.RespondJSON(w, http.StatusOK, &domain.ListAchievementsResponse{ + Achievements: response, + }) +} + +func (h *AchievementsHandler) GetAchievement(w http.ResponseWriter, r *http.Request) { + achievementID := getPathParam(r, "achievement_id") + + achievement, err := h.achievementsClient.GetAchievement(r.Context(), achievementID) + if err != nil { + utils.RespondError(w, domain.NewNotFoundError("achievement not found")) + return + } + + utils.RespondJSON(w, http.StatusOK, utils.AchievementProtoToHTTP(achievement)) +} + +func (h *AchievementsHandler) GetUserAchievements(w http.ResponseWriter, r *http.Request) { + userID := getPathParam(r, "user_id") + + achievements, err := h.achievementsClient.GetUserAchievements(r.Context(), userID) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to get user achievements")) + return + } + + response := make([]domain.UserAchievementResponse, len(achievements)) + for i, ach := range achievements { + response[i] = domain.UserAchievementResponse{ + AchievementID: ach.Achievement.Id, + UserID: userID, + EarnedAt: ach.ReceivedAt.AsTime(), + } + } + + utils.RespondJSON(w, http.StatusOK, &domain.ListUserAchievementsResponse{ + Achievements: response, + }) +} diff --git a/internal/gw/handler/auth_handler.go b/internal/gw/handler/auth_handler.go new file mode 100644 index 0000000..4a02bd2 --- /dev/null +++ b/internal/gw/handler/auth_handler.go @@ -0,0 +1,69 @@ +package handler + +import ( + "net/http" + + "datarush/internal/gw/domain" + "datarush/internal/gw/grpc_client" + "datarush/internal/gw/utils" +) + +type AuthHandler struct { + authClient *grpc_client.AuthClient + userClient *grpc_client.UserClient +} + +func NewAuthHandler(authClient *grpc_client.AuthClient, userClient *grpc_client.UserClient) *AuthHandler { + return &AuthHandler{ + authClient: authClient, + userClient: userClient, + } +} + +func (h *AuthHandler) SignUp(w http.ResponseWriter, r *http.Request) { + var req domain.SignUpRequest + if err := utils.DecodeJSON(r, &req); err != nil { + utils.RespondError(w, err) + return + } + + token, err := h.authClient.SignUp(r.Context(), req.Email, req.Username, req.Password) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("sign up failed")) + return + } + + utils.RespondJSON(w, http.StatusCreated, &domain.TokenResponse{Token: token}) +} + +func (h *AuthHandler) SignIn(w http.ResponseWriter, r *http.Request) { + var req domain.SignInRequest + if err := utils.DecodeJSON(r, &req); err != nil { + utils.RespondError(w, err) + return + } + + token, err := h.authClient.SignIn(r.Context(), req.Email, req.Password) + if err != nil { + utils.RespondError(w, domain.NewUnauthorizedError("invalid credentials")) + return + } + + utils.RespondJSON(w, http.StatusOK, &domain.TokenResponse{Token: token}) +} + +func (h *AuthHandler) GetMe(w http.ResponseWriter, r *http.Request) { + userID, err := getUserIDFromContext(r.Context()) + if err != nil { + utils.RespondError(w, domain.NewUnauthorizedError("unauthorized")) + return + } + + user, err := h.userClient.GetProfile(r.Context(), userID) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to get user profile")) + return + } + + utils.RespondJSON(w, http.StatusOK, utils.UserProtoToHTTP(user)) +} diff --git a/internal/gw/handler/common.go b/internal/gw/handler/common.go new file mode 100644 index 0000000..0d664f3 --- /dev/null +++ b/internal/gw/handler/common.go @@ -0,0 +1,75 @@ +package handler + +import ( + "context" + "encoding/json" + "net/http" + "strconv" + + "datarush/internal/gw/domain" + "datarush/internal/gw/middleware" + + "github.com/gorilla/mux" +) + +func getUserIDFromContext(ctx context.Context) (string, error) { + return middleware.GetUserIDFromContext(ctx) +} + +func getPathParam(r *http.Request, key string) string { + vars := mux.Vars(r) + return vars[key] +} + +func getQueryParam(r *http.Request, key string) string { + return r.URL.Query().Get(key) +} + +func getQueryParamInt(r *http.Request, key string, defaultValue int) int { + val := r.URL.Query().Get(key) + if val == "" { + return defaultValue + } + + intVal, err := strconv.Atoi(val) + if err != nil { + return defaultValue + } + + return intVal +} + +func getQueryParamInt32(r *http.Request, key string, defaultValue int32) int32 { + return int32(getQueryParamInt(r, key, int(defaultValue))) +} + +func getQueryParamBool(r *http.Request, key string) *bool { + val := r.URL.Query().Get(key) + if val == "" { + return nil + } + + boolVal, err := strconv.ParseBool(val) + if err != nil { + return nil + } + + return &boolVal +} + +type PingHandler struct{} + +func NewPingHandler() *PingHandler { + return &PingHandler{} +} + +func (h *PingHandler) Ping(w http.ResponseWriter, r *http.Request) { + response := &domain.PingResponse{ + Message: "pong", + Status: "ok", + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} diff --git a/internal/gw/handler/competition_handler.go b/internal/gw/handler/competition_handler.go new file mode 100644 index 0000000..0ce6aff --- /dev/null +++ b/internal/gw/handler/competition_handler.go @@ -0,0 +1,158 @@ +package handler + +import ( + "net/http" + + "datarush/internal/gw/domain" + "datarush/internal/gw/grpc_client" + "datarush/internal/gw/utils" + + comppb "datarush/pkg/api/competition" +) + +type CompetitionHandler struct { + competitionClient *grpc_client.CompetitionClient + userClient *grpc_client.UserClient +} + +func NewCompetitionHandler( + competitionClient *grpc_client.CompetitionClient, + userClient *grpc_client.UserClient, +) *CompetitionHandler { + return &CompetitionHandler{ + competitionClient: competitionClient, + userClient: userClient, + } +} + +func (h *CompetitionHandler) CreateCompetition(w http.ResponseWriter, r *http.Request) { + var req domain.CompetitionRequest + if err := utils.DecodeJSON(r, &req); err != nil { + utils.RespondError(w, err) + return + } + + competition := utils.CompetitionHTTPToProto(&req) + resp, err := h.competitionClient.CreateCompetition(r.Context(), competition) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to create competition")) + return + } + + utils.RespondJSON(w, http.StatusCreated, utils.CompetitionProtoToHTTP(resp)) +} + +func (h *CompetitionHandler) GetCompetition(w http.ResponseWriter, r *http.Request) { + competitionID := getPathParam(r, "competition_id") + + competition, err := h.competitionClient.GetCompetition(r.Context(), competitionID) + if err != nil { + utils.RespondError(w, domain.NewNotFoundError("competition not found")) + return + } + + utils.RespondJSON(w, http.StatusOK, utils.CompetitionProtoToHTTP(competition)) +} + +func (h *CompetitionHandler) UpdateCompetition(w http.ResponseWriter, r *http.Request) { + competitionID := getPathParam(r, "competition_id") + + var req domain.CompetitionRequest + if err := utils.DecodeJSON(r, &req); err != nil { + utils.RespondError(w, err) + return + } + + req.ID = competitionID + competition := utils.CompetitionHTTPToProto(&req) + resp, err := h.competitionClient.EditCompetition(r.Context(), competition) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to update competition")) + return + } + + utils.RespondJSON(w, http.StatusOK, utils.CompetitionProtoToHTTP(resp)) +} + +func (h *CompetitionHandler) DeleteCompetition(w http.ResponseWriter, r *http.Request) { + competitionID := getPathParam(r, "competition_id") + + if err := h.competitionClient.DeleteCompetition(r.Context(), competitionID); err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to delete competition")) + return + } + + w.WriteHeader(http.StatusNoContent) +} +func (h *CompetitionHandler) ListCompetitions(w http.ResponseWriter, r *http.Request) { + pageSize := getQueryParamInt32(r, "page_size", 20) + pageToken := getQueryParamInt32(r, "page_token", 0) + state := getQueryParam(r, "state") + searchQuery := getQueryParam(r, "search_query") + isParticipating := getQueryParamBool(r, "is_participating") + + req := &comppb.ListCompetitionsRequest{ + PageSize: pageSize, + PageToken: pageToken, + } + + if state != "" { + s := utils.StringToCompetitionState(state) + req.State = &s + } + + if searchQuery != "" { + req.SearchQuery = &searchQuery + } + + if isParticipating != nil { + req.IsParticipating = isParticipating + } + + resp, err := h.competitionClient.ListCompetitions(r.Context(), req) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to list competitions")) + return + } + + competitions := make([]domain.CompetitionResponse, len(resp.Competitions)) + for i, comp := range resp.Competitions { + competitions[i] = *utils.CompetitionProtoToHTTP(comp) + } + + utils.RespondJSON(w, http.StatusOK, &domain.ListCompetitionsResponse{ + TotalCount: resp.TotalCount, + NextPageToken: resp.NextPageToken, + Competitions: competitions, + }) +} + +func (h *CompetitionHandler) ChangeCompetitionState(w http.ResponseWriter, r *http.Request) { + competitionID := getPathParam(r, "competition_id") + + var req domain.ChangeCompetitionStateRequest + if err := utils.DecodeJSON(r, &req); err != nil { + utils.RespondError(w, err) + return + } + + state := utils.StringToCompetitionState(req.State) + competition, err := h.competitionClient.ChangeCompetitionState(r.Context(), competitionID, state) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to change competition state")) + return + } + + utils.RespondJSON(w, http.StatusOK, utils.CompetitionProtoToHTTP(competition)) +} + +func (h *CompetitionHandler) JoinCompetition(w http.ResponseWriter, r *http.Request) { + competitionID := getPathParam(r, "competition_id") + + if err := h.userClient.RegisterForCompetition(r.Context(), competitionID); err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to join competition")) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/gw/handler/results_handler.go b/internal/gw/handler/results_handler.go new file mode 100644 index 0000000..6226ef4 --- /dev/null +++ b/internal/gw/handler/results_handler.go @@ -0,0 +1,77 @@ +package handler + +import ( + "net/http" + + "datarush/internal/gw/domain" + "datarush/internal/gw/grpc_client" + "datarush/internal/gw/utils" + + resultspb "datarush/pkg/api/results" +) + +type ResultsHandler struct { + resultsClient *grpc_client.ResultsClient +} + +func NewResultsHandler(resultsClient *grpc_client.ResultsClient) *ResultsHandler { + return &ResultsHandler{resultsClient: resultsClient} +} + +func (h *ResultsHandler) GetCompetitionResults(w http.ResponseWriter, r *http.Request) { + competitionID := getPathParam(r, "competition_id") + pageSize := getQueryParamInt32(r, "page_size", 20) + pageToken := getQueryParamInt32(r, "page_token", 0) + + req := &resultspb.GetCompetitionResultsRequest{ + CompetitionId: competitionID, + PageSize: pageSize, + PageToken: pageToken, + } + + resp, err := h.resultsClient.GetCompetitionResults(r.Context(), req) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to get competition results")) + return + } + + results := make([]domain.UserResultResponse, len(resp.Results)) + for i, result := range resp.Results { + results[i] = *utils.UserResultProtoToHTTP(result) + } + + utils.RespondJSON(w, http.StatusOK, &domain.CompetitionResultsResponse{ + Results: results, + TotalCount: resp.TotalCount, + NextPageToken: resp.NextPageToken, + }) +} + +func (h *ResultsHandler) GetMyResults(w http.ResponseWriter, r *http.Request) { + userID, err := getUserIDFromContext(r.Context()) + if err != nil { + utils.RespondError(w, domain.NewUnauthorizedError("unauthorized")) + return + } + + competitionID := getPathParam(r, "competition_id") + + result, err := h.resultsClient.GetUserCompetitionResults(r.Context(), competitionID, userID) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to get user results")) + return + } + + utils.RespondJSON(w, http.StatusOK, utils.UserResultProtoToHTTP(result)) +} + +func (h *ResultsHandler) RecalculateResults(w http.ResponseWriter, r *http.Request) { + competitionID := getPathParam(r, "competition_id") + + if err := h.resultsClient.RecalculateResults(r.Context(), competitionID); err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to recalculate results")) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/gw/handler/review_handler.go b/internal/gw/handler/review_handler.go new file mode 100644 index 0000000..f7eff59 --- /dev/null +++ b/internal/gw/handler/review_handler.go @@ -0,0 +1,133 @@ +package handler + +import ( + "net/http" + + "datarush/internal/gw/domain" + "datarush/internal/gw/grpc_client" + "datarush/internal/gw/utils" + + reviewpb "datarush/pkg/api/review" +) + +type ReviewHandler struct { + reviewClient *grpc_client.ReviewClient +} + +func NewReviewHandler(reviewClient *grpc_client.ReviewClient) *ReviewHandler { + return &ReviewHandler{reviewClient: reviewClient} +} + +func (h *ReviewHandler) ListSubmissionsForReview(w http.ResponseWriter, r *http.Request) { + token := getPathParam(r, "token") + pageSize := getQueryParamInt32(r, "page_size", 20) + pageToken := getQueryParamInt32(r, "page_token", 0) + statusStr := getQueryParam(r, "status") + + req := &reviewpb.ListSubmissionsForReviewRequest{ + Token: token, + PageSize: pageSize, + PageToken: pageToken, + } + + if statusStr != "" { + status := stringToReviewStatus(statusStr) + req.Status = &status + } + + resp, err := h.reviewClient.ListSubmissionsForReview(r.Context(), req) + if err != nil { + utils.RespondError(w, domain.NewUnauthorizedError("invalid review token")) + return + } + + submissions := make([]domain.SubmissionSummaryResponse, len(resp.Submissions)) + for i, sub := range resp.Submissions { + submissions[i] = *utils.SubmissionSummaryProtoToHTTP(sub) + } + + utils.RespondJSON(w, http.StatusOK, &domain.ListSubmissionsForReviewResponse{ + TotalCount: resp.TotalCount, + NextPageToken: resp.NextPageToken, + Submissions: submissions, + }) +} + +func (h *ReviewHandler) GetSubmissionForReview(w http.ResponseWriter, r *http.Request) { + token := getPathParam(r, "token") + submissionID := getPathParam(r, "submission_id") + + submission, err := h.reviewClient.GetSubmissionForReview(r.Context(), token, submissionID) + if err != nil { + utils.RespondError(w, domain.NewNotFoundError("submission not found")) + return + } + + utils.RespondJSON(w, http.StatusOK, utils.SubmissionForReviewProtoToHTTP(submission)) +} + +func (h *ReviewHandler) EvaluateSubmission(w http.ResponseWriter, r *http.Request) { + token := getPathParam(r, "token") + submissionID := getPathParam(r, "submission_id") + + var reqBody domain.EvaluateSubmissionRequest + if err := utils.DecodeJSON(r, &reqBody); err != nil { + utils.RespondError(w, err) + return + } + + marks := make([]*reviewpb.CriteriaMark, len(reqBody.Marks)) + for i, m := range reqBody.Marks { + marks[i] = &reviewpb.CriteriaMark{ + Slug: m.Slug, + Mark: m.Mark, + } + } + + req := &reviewpb.EvaluateSubmissionRequest{ + Token: token, + SubmissionId: submissionID, + EarnedPoints: reqBody.EarnedPoints, + ReviewerComment: reqBody.ReviewerComment, + Marks: marks, + } + + resp, err := h.reviewClient.EvaluateSubmission(r.Context(), req) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to evaluate submission")) + return + } + + utils.RespondJSON(w, http.StatusOK, &domain.EvaluateSubmissionResponse{ + SubmissionID: resp.SubmissionId, + FinalScore: resp.FinalScore, + NewStatus: utils.ReviewStatusToString(resp.NewStatus), + }) +} + +func (h *ReviewHandler) ReleaseSubmission(w http.ResponseWriter, r *http.Request) { + token := getPathParam(r, "token") + submissionID := getPathParam(r, "submission_id") + + if err := h.reviewClient.ReleaseSubmission(r.Context(), token, submissionID); err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to release submission")) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func stringToReviewStatus(s string) reviewpb.ReviewStatus { + switch s { + case "pending": + return reviewpb.ReviewStatus_REVIEW_STATUS_PENDING + case "in_review": + return reviewpb.ReviewStatus_REVIEW_STATUS_IN_REVIEW + case "completed": + return reviewpb.ReviewStatus_REVIEW_STATUS_COMPLETED + case "rejected": + return reviewpb.ReviewStatus_REVIEW_STATUS_REJECTED + default: + return reviewpb.ReviewStatus_REVIEW_STATUS_UNSPECIFIED + } +} diff --git a/internal/gw/handler/submission_handler.go b/internal/gw/handler/submission_handler.go new file mode 100644 index 0000000..d416cc7 --- /dev/null +++ b/internal/gw/handler/submission_handler.go @@ -0,0 +1,90 @@ +package handler + +import ( + "net/http" + + "datarush/internal/gw/domain" + "datarush/internal/gw/grpc_client" + "datarush/internal/gw/storage" + "datarush/internal/gw/utils" +) + +type SubmissionHandler struct { + submissionClient *grpc_client.SubmissionClient + s3Storage *storage.S3Storage +} + +func NewSubmissionHandler( + submissionClient *grpc_client.SubmissionClient, + s3Storage *storage.S3Storage, +) *SubmissionHandler { + return &SubmissionHandler{ + submissionClient: submissionClient, + s3Storage: s3Storage, + } +} + +func (h *SubmissionHandler) SubmitTask(w http.ResponseWriter, r *http.Request) { + userID, err := getUserIDFromContext(r.Context()) + if err != nil { + utils.RespondError(w, domain.NewUnauthorizedError("unauthorized")) + return + } + + competitionID := getPathParam(r, "competition_id") + taskID := getPathParam(r, "task_id") + + if err := r.ParseMultipartForm(32 << 20); err != nil { // 32 MB max + utils.RespondError(w, domain.NewBadRequestError("failed to parse form")) + return + } + + file, header, err := r.FormFile("content") + if err != nil { + utils.RespondError(w, domain.NewBadRequestError("missing or invalid file")) + return + } + defer file.Close() + + fileURL, err := h.s3Storage.UploadFile(r.Context(), file, header) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to upload file")) + return + } + + submission, err := h.submissionClient.SubmitTask(r.Context(), userID, competitionID, taskID, fileURL) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to submit task")) + return + } + + utils.RespondJSON(w, http.StatusCreated, &domain.SubmitTaskResponse{ + SubmissionID: submission.Id, + }) +} + +func (h *SubmissionHandler) GetSubmissionHistory(w http.ResponseWriter, r *http.Request) { + userID, err := getUserIDFromContext(r.Context()) + if err != nil { + utils.RespondError(w, domain.NewUnauthorizedError("unauthorized")) + return + } + + competitionID := getPathParam(r, "competition_id") + taskID := getPathParam(r, "task_id") + + submissions, err := h.submissionClient.GetSubmissionsHistory(r.Context(), userID, competitionID, taskID) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to get submission history")) + return + } + + response := make([]domain.SubmissionResponse, len(submissions)) + for i, sub := range submissions { + response[i] = *utils.SubmissionProtoToHTTP(sub) + } + + utils.RespondJSON(w, http.StatusOK, &domain.SubmissionHistoryResponse{ + Submissions: response, + }) +} diff --git a/internal/gw/handler/task_handler.go b/internal/gw/handler/task_handler.go new file mode 100644 index 0000000..c4c39c7 --- /dev/null +++ b/internal/gw/handler/task_handler.go @@ -0,0 +1,99 @@ +package handler + +import ( + "net/http" + + "datarush/internal/gw/domain" + "datarush/internal/gw/grpc_client" + "datarush/internal/gw/utils" +) + +type TaskHandler struct { + taskClient *grpc_client.TaskClient +} + +func NewTaskHandler(taskClient *grpc_client.TaskClient) *TaskHandler { + return &TaskHandler{taskClient: taskClient} +} + +func (h *TaskHandler) CreateTask(w http.ResponseWriter, r *http.Request) { + competitionID := getPathParam(r, "competition_id") + + var req domain.TaskRequest + if err := utils.DecodeJSON(r, &req); err != nil { + utils.RespondError(w, err) + return + } + + req.CompetitionID = competitionID + task := utils.TaskHTTPToProto(&req) + resp, err := h.taskClient.CreateTask(r.Context(), task) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to create task")) + return + } + + utils.RespondJSON(w, http.StatusCreated, utils.TaskProtoToHTTP(resp)) +} + +func (h *TaskHandler) GetTask(w http.ResponseWriter, r *http.Request) { + taskID := getPathParam(r, "task_id") + + task, err := h.taskClient.GetTask(r.Context(), taskID) + if err != nil { + utils.RespondError(w, domain.NewNotFoundError("task not found")) + return + } + + utils.RespondJSON(w, http.StatusOK, utils.TaskProtoToHTTP(task)) +} + +func (h *TaskHandler) UpdateTask(w http.ResponseWriter, r *http.Request) { + competitionID := getPathParam(r, "competition_id") + taskID := getPathParam(r, "task_id") + + var req domain.TaskRequest + if err := utils.DecodeJSON(r, &req); err != nil { + utils.RespondError(w, err) + return + } + + req.ID = taskID + req.CompetitionID = competitionID + task := utils.TaskHTTPToProto(&req) + resp, err := h.taskClient.EditTask(r.Context(), task) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to update task")) + return + } + + utils.RespondJSON(w, http.StatusOK, utils.TaskProtoToHTTP(resp)) +} + +func (h *TaskHandler) DeleteTask(w http.ResponseWriter, r *http.Request) { + taskID := getPathParam(r, "task_id") + + if err := h.taskClient.DeleteTask(r.Context(), taskID); err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to delete task")) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) { + competitionID := getPathParam(r, "competition_id") + + tasks, err := h.taskClient.ListCompetitionTasks(r.Context(), competitionID) + if err != nil { + utils.RespondError(w, domain.NewInternalServerError("failed to list tasks")) + return + } + + response := make([]domain.TaskResponse, len(tasks)) + for i, task := range tasks { + response[i] = *utils.TaskProtoToHTTP(task) + } + + utils.RespondJSON(w, http.StatusOK, &domain.ListTasksResponse{Tasks: response}) +} diff --git a/internal/gw/middleware/auth.go b/internal/gw/middleware/auth.go new file mode 100644 index 0000000..5022d27 --- /dev/null +++ b/internal/gw/middleware/auth.go @@ -0,0 +1,69 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + + "datarush/internal/gw/domain" + "datarush/internal/gw/grpc_client" +) + +type contextKey string + +const ( + UserIDKey contextKey = "user_id" +) + +type AuthMiddleware struct { + authClient *grpc_client.AuthClient +} + +func NewAuthMiddleware(authClient *grpc_client.AuthClient) *AuthMiddleware { + return &AuthMiddleware{ + authClient: authClient, + } +} + +func (m *AuthMiddleware) Authenticate(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + respondWithError(w, domain.NewUnauthorizedError("missing authorization header")) + return + } + + parts := strings.Split(authHeader, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + respondWithError(w, domain.NewUnauthorizedError("invalid authorization header format")) + return + } + + token := parts[1] + + userID, err := m.authClient.ValidateToken(r.Context(), token) + if err != nil { + respondWithError(w, domain.NewUnauthorizedError("invalid token")) + return + } + + ctx := context.WithValue(r.Context(), UserIDKey, userID) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func GetUserIDFromContext(ctx context.Context) (string, error) { + userID, ok := ctx.Value(UserIDKey).(string) + if !ok || userID == "" { + return "", domain.ErrUnauthorized + } + return userID, nil +} + +func respondWithError(w http.ResponseWriter, err *domain.AppError) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(err.StatusCode) + + response := domain.NewErrorResponse(err.Err, err.Message) + w.Write([]byte(`{"error":"` + response.Error + `","message":"` + response.Message + `"}`)) +} diff --git a/internal/gw/middleware/cors.go b/internal/gw/middleware/cors.go new file mode 100644 index 0000000..a16b26f --- /dev/null +++ b/internal/gw/middleware/cors.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "net/http" +) + +func CORSMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Access-Control-Max-Age", "3600") + + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/internal/gw/middleware/logging.go b/internal/gw/middleware/logging.go new file mode 100644 index 0000000..565b1ce --- /dev/null +++ b/internal/gw/middleware/logging.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "log" + "net/http" + "time" +) + +type responseWriter struct { + http.ResponseWriter + statusCode int + written int64 +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + +func (rw *responseWriter) Write(b []byte) (int, error) { + n, err := rw.ResponseWriter.Write(b) + rw.written += int64(n) + return n, err +} + +func LoggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + wrapped := &responseWriter{ + ResponseWriter: w, + statusCode: http.StatusOK, + } + + next.ServeHTTP(wrapped, r) + + duration := time.Since(start) + log.Printf( + "%s %s %d %s %s", + r.Method, + r.RequestURI, + wrapped.statusCode, + duration, + r.RemoteAddr, + ) + }) +} diff --git a/internal/gw/router/router.go b/internal/gw/router/router.go new file mode 100644 index 0000000..73a3b80 --- /dev/null +++ b/internal/gw/router/router.go @@ -0,0 +1,113 @@ +package router + +import ( + "net/http" + + "datarush/internal/gw/handler" + "datarush/internal/gw/middleware" + + "github.com/gorilla/mux" +) + +type Router struct { + authHandler *handler.AuthHandler + competitionHandler *handler.CompetitionHandler + taskHandler *handler.TaskHandler + submissionHandler *handler.SubmissionHandler + resultsHandler *handler.ResultsHandler + reviewHandler *handler.ReviewHandler + achievementsHandler *handler.AchievementsHandler + pingHandler *handler.PingHandler + authMiddleware *middleware.AuthMiddleware +} + +func NewRouter( + authHandler *handler.AuthHandler, + competitionHandler *handler.CompetitionHandler, + taskHandler *handler.TaskHandler, + submissionHandler *handler.SubmissionHandler, + resultsHandler *handler.ResultsHandler, + reviewHandler *handler.ReviewHandler, + achievementsHandler *handler.AchievementsHandler, + pingHandler *handler.PingHandler, + authMiddleware *middleware.AuthMiddleware, +) *Router { + return &Router{ + authHandler: authHandler, + competitionHandler: competitionHandler, + taskHandler: taskHandler, + submissionHandler: submissionHandler, + resultsHandler: resultsHandler, + reviewHandler: reviewHandler, + achievementsHandler: achievementsHandler, + pingHandler: pingHandler, + authMiddleware: authMiddleware, + } +} + +func (rt *Router) Setup() http.Handler { + r := mux.NewRouter() + + r.Use(middleware.LoggingMiddleware) + r.Use(middleware.CORSMiddleware) + + api := r.PathPrefix("/api/v1").Subrouter() + + api.HandleFunc("/ping", rt.pingHandler.Ping).Methods(http.MethodGet) + api.HandleFunc("/sign-up", rt.authHandler.SignUp).Methods(http.MethodPost) + api.HandleFunc("/sign-in", rt.authHandler.SignIn).Methods(http.MethodPost) + + protected := api.PathPrefix("").Subrouter() + protected.Use(rt.authMiddleware.Authenticate) + + protected.HandleFunc("/me", rt.authHandler.GetMe).Methods(http.MethodGet) + + protected.HandleFunc("/competitions", rt.competitionHandler.CreateCompetition).Methods(http.MethodPost) + protected.HandleFunc("/competitions", rt.competitionHandler.ListCompetitions).Methods(http.MethodGet) + protected.HandleFunc("/competitions/{competition_id}", rt.competitionHandler.GetCompetition).Methods(http.MethodGet) + protected.HandleFunc("/competitions/{competition_id}", rt.competitionHandler.UpdateCompetition). + Methods(http.MethodPut) + protected.HandleFunc("/competitions/{competition_id}", rt.competitionHandler.DeleteCompetition). + Methods(http.MethodDelete) + protected.HandleFunc("/competitions/{competition_id}/state", rt.competitionHandler.ChangeCompetitionState). + Methods(http.MethodPatch) + protected.HandleFunc("/competitions/{competition_id}/join", rt.competitionHandler.JoinCompetition). + Methods(http.MethodPost) + + protected.HandleFunc("/competitions/{competition_id}/tasks", rt.taskHandler.CreateTask).Methods(http.MethodPost) + protected.HandleFunc("/competitions/{competition_id}/tasks", rt.taskHandler.ListTasks).Methods(http.MethodGet) + protected.HandleFunc("/competitions/{competition_id}/tasks/{task_id}", rt.taskHandler.GetTask). + Methods(http.MethodGet) + protected.HandleFunc("/competitions/{competition_id}/tasks/{task_id}", rt.taskHandler.UpdateTask). + Methods(http.MethodPut) + protected.HandleFunc("/competitions/{competition_id}/tasks/{task_id}", rt.taskHandler.DeleteTask). + Methods(http.MethodDelete) + + protected.HandleFunc("/competitions/{competition_id}/tasks/{task_id}/submit", rt.submissionHandler.SubmitTask). + Methods(http.MethodPost) + protected.HandleFunc("/competitions/{competition_id}/tasks/{task_id}/history", rt.submissionHandler.GetSubmissionHistory). + Methods(http.MethodGet) + + protected.HandleFunc("/competitions/{competition_id}/results", rt.resultsHandler.GetCompetitionResults). + Methods(http.MethodGet) + protected.HandleFunc("/competitions/{competition_id}/results/me", rt.resultsHandler.GetMyResults). + Methods(http.MethodGet) + protected.HandleFunc("/competitions/{competition_id}/results/recalculate", rt.resultsHandler.RecalculateResults). + Methods(http.MethodPost) + + protected.HandleFunc("/achievements", rt.achievementsHandler.ListAchievements).Methods(http.MethodGet) + protected.HandleFunc("/achievements/{achievement_id}", rt.achievementsHandler.GetAchievement). + Methods(http.MethodGet) + protected.HandleFunc("/users/{user_id}/achievements", rt.achievementsHandler.GetUserAchievements). + Methods(http.MethodGet) + + api.HandleFunc("/review/{token}/submissions", rt.reviewHandler.ListSubmissionsForReview).Methods(http.MethodGet) + api.HandleFunc("/review/{token}/submissions/{submission_id}", rt.reviewHandler.GetSubmissionForReview). + Methods(http.MethodGet) + api.HandleFunc("/review/{token}/submissions/{submission_id}/evaluate", rt.reviewHandler.EvaluateSubmission). + Methods(http.MethodPost) + api.HandleFunc("/review/{token}/submissions/{submission_id}/release", rt.reviewHandler.ReleaseSubmission). + Methods(http.MethodPost) + + return r +} diff --git a/internal/gw/storage/s3.go b/internal/gw/storage/s3.go new file mode 100644 index 0000000..87f8004 --- /dev/null +++ b/internal/gw/storage/s3.go @@ -0,0 +1,187 @@ +package storage + +import ( + "bytes" + "context" + "fmt" + "io" + "mime/multipart" + "net/url" + "path/filepath" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/google/uuid" +) + +type S3Storage struct { + client *s3.Client + bucket string + region string + endpoint string +} + +type S3Config struct { + AccessKeyID string + SecretAccessKey string + Region string + Bucket string + Endpoint string +} + +func NewS3Storage(cfg S3Config) (*S3Storage, error) { + var loadOpts []func(*config.LoadOptions) error + + if cfg.Region != "" { + loadOpts = append(loadOpts, config.WithRegion(cfg.Region)) + } + + if cfg.Endpoint != "" { + customResolver := aws.EndpointResolverWithOptionsFunc( + func(service, region string, options ...interface{}) (aws.Endpoint, error) { + return aws.Endpoint{ + URL: cfg.Endpoint, + SigningRegion: cfg.Region, + HostnameImmutable: true, + }, nil + }, + ) + loadOpts = append(loadOpts, config.WithEndpointResolverWithOptions(customResolver)) + } + + if cfg.AccessKeyID != "" || cfg.SecretAccessKey != "" { + loadOpts = append(loadOpts, config.WithCredentialsProvider( + credentials.NewStaticCredentialsProvider(cfg.AccessKeyID, cfg.SecretAccessKey, ""), + )) + } + + awsCfg, err := config.LoadDefaultConfig(context.TODO(), loadOpts...) + if err != nil { + return nil, fmt.Errorf("failed to load AWS config: %w", err) + } + + var client *s3.Client + if cfg.Endpoint != "" { + client = s3.NewFromConfig(awsCfg, func(o *s3.Options) { + o.UsePathStyle = true + }) + } else { + client = s3.NewFromConfig(awsCfg) + } + + return &S3Storage{ + client: client, + bucket: cfg.Bucket, + region: cfg.Region, + endpoint: cfg.Endpoint, + }, nil +} + +func (s *S3Storage) UploadFile(ctx context.Context, file multipart.File, header *multipart.FileHeader) (string, error) { + fileBytes, err := io.ReadAll(file) + if err != nil { + return "", fmt.Errorf("failed to read file: %w", err) + } + + ext := filepath.Ext(header.Filename) + key := fmt.Sprintf("submissions/%s/%s%s", + time.Now().Format("2006/01/02"), + uuid.New().String(), + ext, + ) + + _, err = s.client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(key), + Body: bytes.NewReader(fileBytes), + ContentType: aws.String(header.Header.Get("Content-Type")), + }) + if err != nil { + return "", fmt.Errorf("failed to upload file to S3: %w", err) + } + + return s.buildObjectURL(key), nil +} + +func (s *S3Storage) UploadFileFromBytes( + ctx context.Context, + content []byte, + filename string, + contentType string, +) (string, error) { + ext := filepath.Ext(filename) + key := fmt.Sprintf("submissions/%s/%s%s", + time.Now().Format("2006/01/02"), + uuid.New().String(), + ext, + ) + + _, err := s.client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(key), + Body: bytes.NewReader(content), + ContentType: aws.String(contentType), + }) + if err != nil { + return "", fmt.Errorf("failed to upload file to S3: %w", err) + } + + return s.buildObjectURL(key), nil +} + +func (s *S3Storage) DeleteFile(ctx context.Context, fileURL string) error { + key := s.extractKeyFromURL(fileURL) + + _, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(key), + }) + if err != nil { + return fmt.Errorf("failed to delete file from S3: %w", err) + } + + return nil +} + +func (s *S3Storage) buildObjectURL(key string) string { + if s.endpoint != "" { + ep := strings.TrimRight(s.endpoint, "/") + return fmt.Sprintf("%s/%s/%s", ep, s.bucket, key) + } + + return fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", s.bucket, s.region, key) +} + +func (s *S3Storage) extractKeyFromURL(urlStr string) string { + u, err := url.Parse(urlStr) + if err != nil { + return urlStr + } + + path := strings.TrimPrefix(u.Path, "/") + + if strings.HasPrefix(u.Host, s.bucket+".") { + return path + } + + if strings.HasPrefix(path, s.bucket+"/") { + return strings.TrimPrefix(path, s.bucket+"/") + } + + if s.endpoint != "" { + ep := strings.TrimPrefix(strings.TrimRight(s.endpoint, "/"), "http://") + ep = strings.TrimPrefix(ep, "https://") + if strings.HasPrefix(u.Host, ep) { + if strings.HasPrefix(path, s.bucket+"/") { + return strings.TrimPrefix(path, s.bucket+"/") + } + return path + } + } + + return path +} diff --git a/internal/gw/utils/converter.go b/internal/gw/utils/converter.go new file mode 100644 index 0000000..697aecd --- /dev/null +++ b/internal/gw/utils/converter.go @@ -0,0 +1,291 @@ +package utils + +import ( + "datarush/internal/gw/domain" + + achievepb "datarush/pkg/api/achievements" + comppb "datarush/pkg/api/competition" + resultspb "datarush/pkg/api/results" + reviewpb "datarush/pkg/api/review" + subpb "datarush/pkg/api/submission" + taskpb "datarush/pkg/api/task" + userpb "datarush/pkg/api/user" + + "google.golang.org/protobuf/types/known/timestamppb" +) + +func UserProtoToHTTP(u *userpb.User) *domain.UserResponse { + return &domain.UserResponse{ + ID: u.Id, + Username: u.Username, + Email: u.Email, + FullName: u.FullName, + AvatarURL: u.AvatarUrl, + } +} + +func CompetitionProtoToHTTP(c *comppb.Competition) *domain.CompetitionResponse { + return &domain.CompetitionResponse{ + ID: c.Id, + State: CompetitionStateToString(c.State), + Title: c.Title, + Description: c.Description, + ImageURL: c.ImageUrl, + StartTime: c.StartTime.AsTime(), + EndTime: c.EndTime.AsTime(), + Type: CompetitionTypeToString(c.Type), + ParticipationType: ParticipationTypeToString(c.ParticipationType), + CreatedAt: c.CreatedAt.AsTime(), + UpdatedAt: c.UpdatedAt.AsTime(), + } +} + +func TaskProtoToHTTP(t *taskpb.Task) *domain.TaskResponse { + return &domain.TaskResponse{ + ID: t.Id, + CompetitionID: t.CompetitionId, + Title: t.Title, + Description: t.Description, + InCompetitionPosition: t.InCompetitionPosition, + MaxPoints: t.MaxPoints, + MaxAttempts: t.MaxAttempts, + Type: TaskTypeToString(t.Type), + CreatedAt: t.CreatedAt.AsTime(), + UpdatedAt: t.UpdatedAt.AsTime(), + } +} + +func SubmissionProtoToHTTP(s *subpb.Submission) *domain.SubmissionResponse { + return &domain.SubmissionResponse{ + ID: s.Id, + UserID: s.UserId, + CompetitionID: s.CompetitionId, + TaskID: s.TaskId, + Status: SubmissionStatusToString(s.Status), + EarnedPoints: s.EarnedPoints, + SubmittedAt: s.SubmittedAt.AsTime(), + CheckedAt: s.CheckedAt.AsTime(), + FileURL: s.FileUrl, + } +} + +func UserResultProtoToHTTP(r *resultspb.UserResult) *domain.UserResultResponse { + taskStatuses := make([]domain.TaskStatusResponse, len(r.TaskStatuses)) + for i, ts := range r.TaskStatuses { + taskStatuses[i] = domain.TaskStatusResponse{ + TaskID: ts.TaskId, + TaskTitle: ts.TaskTitle, + EarnedPoints: ts.EarnedPoints, + MaxPoints: ts.MaxPoints, + Position: ts.Position, + } + } + + return &domain.UserResultResponse{ + UserID: r.UserId, + Username: r.Username, + TotalScore: r.TotalScore, + OverallPosition: r.OverallPosition, + TaskStatuses: taskStatuses, + } +} + +func SubmissionSummaryProtoToHTTP(s *reviewpb.SubmissionSummary) *domain.SubmissionSummaryResponse { + return &domain.SubmissionSummaryResponse{ + ID: s.Id, + CompetitionID: s.CompetitionId, + TaskID: s.TaskId, + CompetitionTitle: s.CompetitionTitle, + TaskTitle: s.TaskTitle, + SubmittedAt: s.SubmittedAt.AsTime(), + ReviewStatus: ReviewStatusToString(s.ReviewStatus), + } +} + +func SubmissionForReviewProtoToHTTP(s *reviewpb.SubmissionForReview) *domain.SubmissionForReviewResponse { + resp := &domain.SubmissionForReviewResponse{ + ID: s.Id, + CompetitionID: s.CompetitionId, + TaskID: s.TaskId, + Content: s.Content, + Description: s.Description, + ReviewStatus: ReviewStatusToString(s.ReviewStatus), + SubmittedAt: s.SubmittedAt.AsTime(), + } + if s.CheckedAt != nil { + t := s.CheckedAt.AsTime() + resp.CheckedAt = &t + } + return resp +} + +func AchievementProtoToHTTP(a *achievepb.Achievement) *domain.AchievementResponse { + return &domain.AchievementResponse{ + ID: a.Id, + Name: a.Name, + Description: a.Description, + IconURL: a.IconUrl, + } +} + +func CompetitionHTTPToProto(req *domain.CompetitionRequest) *comppb.Competition { + return &comppb.Competition{ + Id: req.ID, + Title: req.Title, + Description: req.Description, + ImageUrl: req.ImageURL, + StartTime: timestamppb.New(req.StartTime), + EndTime: timestamppb.New(req.EndTime), + Type: StringToCompetitionType(req.Type), + ParticipationType: StringToParticipationType(req.ParticipationType), + } +} + +func TaskHTTPToProto(req *domain.TaskRequest) *taskpb.Task { + return &taskpb.Task{ + Id: req.ID, + CompetitionId: req.CompetitionID, + Title: req.Title, + Description: req.Description, + InCompetitionPosition: req.InCompetitionPosition, + MaxPoints: req.MaxPoints, + MaxAttempts: req.MaxAttempts, + Type: StringToTaskType(req.Type), + } +} + +func CompetitionStateToString(state comppb.CompetitionState) string { + switch state { + case comppb.CompetitionState_COMPETITION_STATE_DRAFT: + return "draft" + case comppb.CompetitionState_COMPETITION_STATE_NOT_STARTED: + return "not_started" + case comppb.CompetitionState_COMPETITION_STATE_STARTED: + return "started" + case comppb.CompetitionState_COMPETITION_STATE_FINISHED: + return "finished" + case comppb.CompetitionState_COMPETITION_STATE_ARCHIVED: + return "archived" + default: + return "unspecified" + } +} + +func StringToCompetitionState(state string) comppb.CompetitionState { + switch state { + case "draft": + return comppb.CompetitionState_COMPETITION_STATE_DRAFT + case "not_started": + return comppb.CompetitionState_COMPETITION_STATE_NOT_STARTED + case "started": + return comppb.CompetitionState_COMPETITION_STATE_STARTED + case "finished": + return comppb.CompetitionState_COMPETITION_STATE_FINISHED + case "archived": + return comppb.CompetitionState_COMPETITION_STATE_ARCHIVED + default: + return comppb.CompetitionState_COMPETITION_STATE_UNSPECIFIED + } +} + +func CompetitionTypeToString(t comppb.CompetitionType) string { + switch t { + case comppb.CompetitionType_COMPETITION_TYPE_EDUCATIVE: + return "educative" + case comppb.CompetitionType_COMPETITION_TYPE_COMPETETIVE: + return "competitive" + default: + return "unspecified" + } +} + +func StringToCompetitionType(t string) comppb.CompetitionType { + switch t { + case "educative": + return comppb.CompetitionType_COMPETITION_TYPE_EDUCATIVE + case "competitive": + return comppb.CompetitionType_COMPETITION_TYPE_COMPETETIVE + default: + return comppb.CompetitionType_COMPETITION_TYPE_UNSPECIFIED + } +} + +func ParticipationTypeToString(t comppb.ParticipationType) string { + switch t { + case comppb.ParticipationType_PARTICIPATION_TYPE_INDIVIDUAL: + return "individual" + case comppb.ParticipationType_PARTICIPATION_TYPE_TEAM: + return "team" + default: + return "unspecified" + } +} + +func StringToParticipationType(t string) comppb.ParticipationType { + switch t { + case "individual": + return comppb.ParticipationType_PARTICIPATION_TYPE_INDIVIDUAL + case "team": + return comppb.ParticipationType_PARTICIPATION_TYPE_TEAM + default: + return comppb.ParticipationType_PARTICIPATION_TYPE_UNSPECIFIED + } +} + +func TaskTypeToString(t taskpb.TaskType) string { + switch t { + case taskpb.TaskType_TASK_TYPE_INPUT: + return "input" + case taskpb.TaskType_TASK_TYPE_CHECKER: + return "checker" + case taskpb.TaskType_TASK_TYPE_REVIEW: + return "review" + default: + return "unspecified" + } +} + +func StringToTaskType(t string) taskpb.TaskType { + switch t { + case "input": + return taskpb.TaskType_TASK_TYPE_INPUT + case "checker": + return taskpb.TaskType_TASK_TYPE_CHECKER + case "review": + return taskpb.TaskType_TASK_TYPE_REVIEW + default: + return taskpb.TaskType_TASK_TYPE_UNSPECIFIED + } +} + +func SubmissionStatusToString(s subpb.SubmissionStatus) string { + switch s { + case subpb.SubmissionStatus_SUBMISSION_STATUS_PENDING: + return "pending" + case subpb.SubmissionStatus_SUBMISSION_STATUS_SENT_FOR_CHECK: + return "sent" + case subpb.SubmissionStatus_SUBMISSION_STATUS_CHECKING: + return "checking" + case subpb.SubmissionStatus_SUBMISSION_STATUS_CHECKED: + return "checked" + case subpb.SubmissionStatus_SUBMISSION_STATUS_FAILED: + return "failed" + default: + return "unspecified" + } +} + +func ReviewStatusToString(s reviewpb.ReviewStatus) string { + switch s { + case reviewpb.ReviewStatus_REVIEW_STATUS_PENDING: + return "pending" + case reviewpb.ReviewStatus_REVIEW_STATUS_IN_REVIEW: + return "in_review" + case reviewpb.ReviewStatus_REVIEW_STATUS_COMPLETED: + return "completed" + case reviewpb.ReviewStatus_REVIEW_STATUS_REJECTED: + return "rejected" + default: + return "unspecified" + } +} diff --git a/internal/gw/utils/http.go b/internal/gw/utils/http.go new file mode 100644 index 0000000..a2fbee2 --- /dev/null +++ b/internal/gw/utils/http.go @@ -0,0 +1,35 @@ +package utils + +import ( + "encoding/json" + "net/http" + + "datarush/internal/gw/domain" +) + +func RespondJSON(w http.ResponseWriter, statusCode int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + + if data != nil { + if err := json.NewEncoder(w).Encode(data); err != nil { + http.Error(w, "internal server error", http.StatusInternalServerError) + } + } +} + +func RespondError(w http.ResponseWriter, err error) { + if appErr, ok := err.(*domain.AppError); ok { + RespondJSON(w, appErr.StatusCode, domain.NewErrorResponse(appErr.Err, appErr.Message)) + return + } + + RespondJSON(w, http.StatusInternalServerError, domain.NewErrorResponse(err, "internal server error")) +} + +func DecodeJSON(r *http.Request, v interface{}) error { + if err := json.NewDecoder(r.Body).Decode(v); err != nil { + return domain.NewBadRequestError("invalid JSON body") + } + return nil +} diff --git a/internal/lms/domain/id.go b/internal/lms/domain/id.go deleted file mode 100644 index beb671d..0000000 --- a/internal/lms/domain/id.go +++ /dev/null @@ -1,9 +0,0 @@ -package domain - -import ( - "errors" -) - -var ( - ErrInvalidID = errors.New("invalid uuid") -) diff --git a/internal/lms/domain/order.go b/internal/lms/domain/order.go deleted file mode 100644 index a6ce44e..0000000 --- a/internal/lms/domain/order.go +++ /dev/null @@ -1,42 +0,0 @@ -package domain - -import ( - "errors" - "fmt" - - "github.com/go-playground/validator/v10" - "github.com/google/uuid" -) - -var ( - ErrOrderAlreadyExist = errors.New("order already exist") - ErrOrderNotFound = errors.New("order not found") - ErrInvalidOrderData = errors.New("invalid order data") -) - -type Order struct { - ID uuid.UUID `db:"id" json:"id" validate:"required"` - Item string `db:"item" json:"item" validate:"required"` - Quantity int32 `db:"quantity" json:"quantity" validate:"required,gt=0"` -} - -func NewOrder(id uuid.UUID, item string, quantity int32) (*Order, error) { - order := &Order{ - ID: id, - Item: item, - Quantity: quantity, - } - - err := order.Validate() - if err != nil { - return nil, err - } - - return order, nil -} - -func (o *Order) Validate() error { - validate := validator.New() - - return fmt.Errorf("%w: %w", ErrInvalidOrderData, validate.Struct(o)) -} diff --git a/internal/lms/gateway/gateway.go b/internal/lms/gateway/gateway.go deleted file mode 100644 index 16d1fc2..0000000 --- a/internal/lms/gateway/gateway.go +++ /dev/null @@ -1,51 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - "log" - "net" - "net/http" - "time" - - authPb "datarush/pkg/api/auth" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" -) - -const ( - httpReadTimeout = 10 * time.Second - httpWriteTimeout = 10 * time.Second - httpIdleTimeout = 60 * time.Second -) - -func StartGateway(grpcPort, httpPort int, authGrpcAddr string) error { - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - gwmux := runtime.NewServeMux() - opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} - - // Register auth service - if err := authPb.RegisterAuthServiceHandlerFromEndpoint(ctx, gwmux, authGrpcAddr, opts); err != nil { - return fmt.Errorf("failed to register auth service: %w", err) - } - - srv := &http.Server{ - Addr: fmt.Sprintf(":%d", httpPort), - Handler: gwmux, - ReadTimeout: httpReadTimeout, - WriteTimeout: httpWriteTimeout, - IdleTimeout: httpIdleTimeout, - } - - log.Printf("starting gRPC-Gateway on port %d", httpPort) - return srv.ListenAndServe() -} - -func GetGRPCListener(port int) (net.Listener, error) { - return net.Listen("tcp", fmt.Sprintf(":%d", port)) -} diff --git a/internal/lms/handler/grpc/errors.go b/internal/lms/handler/grpc/errors.go deleted file mode 100644 index 08b4bd8..0000000 --- a/internal/lms/handler/grpc/errors.go +++ /dev/null @@ -1,26 +0,0 @@ -package handler - -import ( - "errors" - "log" - - "datarush/internal/lms/domain" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -func mapError(err error) error { - if errors.Is(err, domain.ErrOrderNotFound) { - return status.Error(codes.NotFound, err.Error()) - } - if errors.Is(err, domain.ErrOrderAlreadyExist) { - return status.Error(codes.AlreadyExists, err.Error()) - } - if errors.Is(err, domain.ErrInvalidOrderData) || errors.Is(err, domain.ErrInvalidID) { - return status.Error(codes.InvalidArgument, err.Error()) - } - - log.Printf("internal server error: %v", err) - return status.Error(codes.Internal, "internal server error") -} diff --git a/internal/lms/handler/http/health.go b/internal/lms/handler/http/health.go deleted file mode 100644 index 498c6f2..0000000 --- a/internal/lms/handler/http/health.go +++ /dev/null @@ -1,63 +0,0 @@ -package http - -import ( - "context" - "encoding/json" - "net/http" - "time" - - "github.com/jmoiron/sqlx" - "github.com/redis/go-redis/v9" -) - -type HealthHandler struct { - DB *sqlx.DB - Redis *redis.Client -} - -func NewHealthHandler(db *sqlx.DB, redisDB *redis.Client) *HealthHandler { - return &HealthHandler{ - DB: db, - Redis: redisDB, - } -} - -type HealthResponse struct { - Status string `json:"status"` - Details map[string]string `json:"details"` -} - -func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) - defer cancel() - - details := map[string]string{} - - if err := h.DB.PingContext(ctx); err != nil { - details["postgres"] = "unhealthy: " + err.Error() - } else { - details["postgres"] = "ok" - } - - if err := h.Redis.Ping(ctx).Err(); err != nil { - details["redis"] = "unhealthy: " + err.Error() - } else { - details["redis"] = "ok" - } - - status := "ok" - for _, v := range details { - if v != "ok" { - status = "unhealthy" - break - } - } - - resp := HealthResponse{Status: status, Details: details} - - w.Header().Set("Content-Type", "application/json") - if status != "ok" { - w.WriteHeader(http.StatusServiceUnavailable) - } - _ = json.NewEncoder(w).Encode(resp) -} diff --git a/internal/lms/repository/inmemory/order.go b/internal/lms/repository/inmemory/order.go deleted file mode 100644 index 3294c07..0000000 --- a/internal/lms/repository/inmemory/order.go +++ /dev/null @@ -1,102 +0,0 @@ -package inmemory - -import ( - "context" - "sync" - - "datarush/internal/lms/domain" - - "github.com/google/uuid" -) - -type OrderRepository struct { - mu sync.RWMutex - orders map[string]*domain.Order -} - -func NewOrderRepository() *OrderRepository { - return &OrderRepository{ - orders: make(map[string]*domain.Order), - } -} - -func (r *OrderRepository) Create(ctx context.Context, order *domain.Order) error { - if err := ctx.Err(); err != nil { - return err - } - - r.mu.Lock() - defer r.mu.Unlock() - - if _, ok := r.orders[order.ID.String()]; ok { - return domain.ErrOrderAlreadyExist - } - r.orders[order.ID.String()] = order - - return nil -} - -func (r *OrderRepository) Get(ctx context.Context, id uuid.UUID) (*domain.Order, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - - r.mu.RLock() - defer r.mu.RUnlock() - - order, ok := r.orders[id.String()] - if !ok { - return nil, domain.ErrOrderNotFound - } - - return order, nil -} - -func (r *OrderRepository) Update(ctx context.Context, order *domain.Order) error { - if err := ctx.Err(); err != nil { - return err - } - - r.mu.Lock() - defer r.mu.Unlock() - - if _, ok := r.orders[order.ID.String()]; !ok { - return domain.ErrOrderNotFound - } - r.orders[order.ID.String()] = order - - return nil -} - -func (r *OrderRepository) Delete(ctx context.Context, id uuid.UUID) error { - if err := ctx.Err(); err != nil { - return err - } - - r.mu.Lock() - defer r.mu.Unlock() - - if _, ok := r.orders[id.String()]; !ok { - return domain.ErrOrderNotFound - } - - delete(r.orders, id.String()) - - return nil -} - -func (r *OrderRepository) List(ctx context.Context) ([]*domain.Order, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - - r.mu.RLock() - defer r.mu.RUnlock() - - orders := make([]*domain.Order, 0, len(r.orders)) - for _, order := range r.orders { - orders = append(orders, order) - } - - return orders, nil -} diff --git a/internal/lms/repository/order.go b/internal/lms/repository/order.go deleted file mode 100644 index c122248..0000000 --- a/internal/lms/repository/order.go +++ /dev/null @@ -1,17 +0,0 @@ -package repository - -import ( - "context" - - "datarush/internal/lms/domain" - - "github.com/google/uuid" -) - -type OrderRepository interface { - Create(ctx context.Context, order *domain.Order) error - Get(ctx context.Context, id uuid.UUID) (*domain.Order, error) - Update(ctx context.Context, order *domain.Order) error - Delete(ctx context.Context, id uuid.UUID) error - List(ctx context.Context) ([]*domain.Order, error) -} diff --git a/internal/lms/repository/postgres/order.go b/internal/lms/repository/postgres/order.go deleted file mode 100644 index ba147aa..0000000 --- a/internal/lms/repository/postgres/order.go +++ /dev/null @@ -1,243 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "fmt" - "log" - "time" - - "datarush/internal/lms/domain" - - "github.com/google/uuid" - "github.com/jmoiron/sqlx" - "github.com/redis/go-redis/v9" -) - -const ( - orderCachePrefix = "order:" - cacheTTL = 5 * time.Minute -) - -type OrderRepository struct { - db *sqlx.DB - redisClient *redis.Client - cacheEnable bool -} - -type Config struct { - CacheEnable bool -} - -func NewOrderRepository(db *sqlx.DB, redisClient *redis.Client, config *Config) *OrderRepository { - if config == nil { - config = &Config{ - CacheEnable: true, - } - } - - return &OrderRepository{ - db: db, - redisClient: redisClient, - cacheEnable: config.CacheEnable, - } -} - -func (r *OrderRepository) cacheKey(id string) string { - return orderCachePrefix + id -} - -func (r *OrderRepository) Create(ctx context.Context, order *domain.Order) error { - tx, err := r.db.BeginTxx(ctx, nil) - if err != nil { - return fmt.Errorf("begin transaction: %w", err) - } - defer tx.Rollback() - - query := ` - insert into orders (id, item, quantity) - values (:id, :item, :quantity) - ` - - if _, err := tx.NamedExecContext(ctx, query, order); err != nil { - return fmt.Errorf("create order: %w", err) - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("commit transaction: %w", err) - } - - if r.cacheEnable { - if err := r.setCacheWithRetry(ctx, order); err != nil { - log.Printf("warn: cache set error for order %s: %v", order.ID, err) - } - } - - return nil -} - -func (r *OrderRepository) Get(ctx context.Context, id uuid.UUID) (*domain.Order, error) { - if r.cacheEnable { - if order, err := r.getFromCache(ctx, id.String()); err == nil { - return order, nil - } else if !errors.Is(err, redis.Nil) { - log.Printf("warn: cache get error for order %s: %v", id, err) - } - } - - const query = ` - select id, item, quantity - from orders - where id = $1 - ` - - var order domain.Order - if err := r.db.GetContext(ctx, &order, query, id); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, domain.ErrOrderNotFound - } - return nil, fmt.Errorf("get order by id: %w", err) - } - - if r.cacheEnable { - if err := r.setCacheWithRetry(ctx, &order); err != nil { - log.Printf("warn: cache set error for order %s: %v", id, err) - } - } - - return &order, nil -} - -func (r *OrderRepository) Update(ctx context.Context, order *domain.Order) error { - tx, err := r.db.BeginTxx(ctx, nil) - if err != nil { - return fmt.Errorf("begin transaction: %w", err) - } - defer tx.Rollback() - - query := ` - update orders - set item = :item, quantity = :quantity - where id = :id - ` - - result, err := tx.NamedExecContext(ctx, query, order) - if err != nil { - return fmt.Errorf("update order: %w", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("get rows affected: %w", err) - } - - if rowsAffected == 0 { - return domain.ErrOrderNotFound - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("commit transaction: %w", err) - } - - if r.cacheEnable { - if err := r.setCacheWithRetry(ctx, order); err != nil { - log.Printf("warn: cache set error for order %s: %v", order.ID, err) - } - } - - return nil -} - -func (r *OrderRepository) Delete(ctx context.Context, id uuid.UUID) error { - tx, err := r.db.BeginTxx(ctx, nil) - if err != nil { - return fmt.Errorf("begin transaction: %w", err) - } - defer tx.Rollback() - - const query = ` - delete from orders - where id = $1 - ` - - result, err := tx.ExecContext(ctx, query, id) - if err != nil { - return fmt.Errorf("delete order: %w", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("get rows affected: %w", err) - } - - if rowsAffected == 0 { - return domain.ErrOrderNotFound - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("commit transaction: %w", err) - } - - r.invalidateCache(ctx, id.String()) - return nil -} - -func (r *OrderRepository) List(ctx context.Context) ([]*domain.Order, error) { - const query = ` - select id, item, quantity - from orders - order by id - ` - - var orders []*domain.Order - if err := r.db.SelectContext(ctx, &orders, query); err != nil { - return nil, fmt.Errorf("list orders: %w", err) - } - - return orders, nil -} - -func (r *OrderRepository) getFromCache(ctx context.Context, id string) (*domain.Order, error) { - data, err := r.redisClient.Get(ctx, r.cacheKey(id)).Bytes() - if err != nil { - return nil, err - } - - var order domain.Order - if err := json.Unmarshal(data, &order); err != nil { - r.redisClient.Del(ctx, r.cacheKey(id)) - return nil, err - } - - return &order, nil -} - -func (r *OrderRepository) setCacheWithRetry(ctx context.Context, order *domain.Order) error { - data, err := json.Marshal(order) - if err != nil { - return err - } - - key := r.cacheKey(order.ID.String()) - - err = r.redisClient.Set(ctx, key, data, cacheTTL).Err() - - return err -} - -func (r *OrderRepository) invalidateCache(_ context.Context, id string) { - if !r.cacheEnable { - return - } - - go func() { - ctx, cancel := context.WithTimeout(context.Background(), r.redisClient.Options().ReadTimeout) - defer cancel() - - if err := r.redisClient.Del(ctx, r.cacheKey(id)).Err(); err != nil { - log.Printf("warn: cache invalidation failed for order %s: %v", id, err) - } - }() -} diff --git a/internal/lms/server/server.go b/internal/lms/server/server.go deleted file mode 100644 index 19880bb..0000000 --- a/internal/lms/server/server.go +++ /dev/null @@ -1,182 +0,0 @@ -package server - -import ( - "context" - "fmt" - "log" - "net" - "net/http" - "time" - - "datarush/internal/lms/config" - "datarush/internal/lms/interceptor" - - httpHandlers "datarush/internal/lms/handler/http" - - authPb "datarush/pkg/api/auth" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/jmoiron/sqlx" - _ "github.com/lib/pq" // postgres driver - "github.com/redis/go-redis/v9" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/reflection" -) - -const ( - httpReadTimeout = 10 * time.Second - httpWriteTimeout = 10 * time.Second - httpIdleTimeout = 60 * time.Second - redisRetryCount = 2 - redisMinRetryBackoff = 50 * time.Millisecond - redisMaxRetryBackoff = 200 * time.Millisecond - redisDialTimeout = 1 * time.Second - redisDialerRetries = 3 - redisTimeout = 2 * time.Second -) - -type Server struct { - grpcServer *grpc.Server - config *config.Config - db *sqlx.DB - redisDB *redis.Client -} - -func New(cfg *config.Config) *Server { - loggerInterceptor := interceptor.NewLoggerInterceptor() - - grpcServer := grpc.NewServer( - grpc.UnaryInterceptor(loggerInterceptor.Unary()), - grpc.StreamInterceptor(loggerInterceptor.Stream()), - ) - - return &Server{ - grpcServer: grpcServer, - config: cfg, - } -} - -func runHTTPHandler(s *Server, grpcServerEndpoint *string) error { - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - gwmux := runtime.NewServeMux() - opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} - - // Register auth service - if err := registerAuthService(ctx, gwmux, s.config.AuthGRPCAddr, opts); err != nil { - log.Printf("failed to register auth service: %v", err) - } - - mux := http.NewServeMux() - mux.Handle("/healthz", httpHandlers.NewHealthHandler(s.db, s.redisDB)) - mux.Handle("/", gwmux) - - srv := &http.Server{ - Addr: fmt.Sprintf(":%d", s.config.HTTPPort), - Handler: mux, - ReadTimeout: httpReadTimeout, - WriteTimeout: httpWriteTimeout, - IdleTimeout: httpIdleTimeout, - } - - return srv.ListenAndServe() -} - -func registerAuthService(ctx context.Context, gwmux *runtime.ServeMux, authAddr string, opts []grpc.DialOption) error { - if err := authPb.RegisterAuthServiceHandlerFromEndpoint(ctx, gwmux, authAddr, opts); err != nil { - return fmt.Errorf("register auth service handler: %w", err) - } - log.Printf("registered auth service from %s", authAddr) - return nil -} - -func registerAuthHandlerFromEndpoint(ctx context.Context, gwmux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error { - // We'll import the proto and register it here - // For now, this is a placeholder that will be called from gateway integration - return nil -} -func getDatabase(cfg config.Config) (*sqlx.DB, error) { - db, err := sqlx.Connect("postgres", cfg.BuildPostgresConnStr()) - if err != nil { - return nil, fmt.Errorf("connect to database: %w", err) - } - - return db, nil -} - -func getRedis(cfg config.Config) (*redis.Client, error) { - conn, err := redis.ParseURL(cfg.RedisURI) - client := redis.NewClient(&redis.Options{ - Addr: conn.Addr, - MaxRetries: redisRetryCount, - MinRetryBackoff: redisMinRetryBackoff, - MaxRetryBackoff: redisMaxRetryBackoff, - DialTimeout: redisDialTimeout, - DialerRetries: redisDialerRetries, - DialerRetryTimeout: redisDialTimeout, - ReadTimeout: redisTimeout, - WriteTimeout: redisTimeout, - }) - if err != nil { - return nil, fmt.Errorf("parse Redis URI: %w", err) - } - - _, err = client.Ping(context.Background()).Result() - if err != nil { - return nil, fmt.Errorf("connect to Redis server: %w", err) - } - - return client, nil -} - -func (s *Server) RegisterServices() { - db, err := getDatabase(*s.config) - if err != nil { - log.Print(err) - } - s.db = db - - redisDB, err := getRedis(*s.config) - if err != nil { - log.Print(err) - } - s.redisDB = redisDB - - if s.config.GRPCEnableReflection { - reflection.Register(s.grpcServer) - log.Println("gRPC server will start with reflection") - } -} - -func (s *Server) Start() error { - addr := fmt.Sprintf(":%d", s.config.GRPCPort) - lis, err := net.Listen("tcp", addr) - if err != nil { - return fmt.Errorf("failed to listen: %w", err) - } - - if s.config.EnableHTTPHandler { - go func() { - log.Printf("starting HTTP gateway on port %d", s.config.HTTPPort) - if err := runHTTPHandler(s, &addr); err != nil { - log.Printf("HTTP gateway failed: %v", err) - } - }() - } - - log.Printf("starting gRPC server on port %d", s.config.GRPCPort) - - if err := s.grpcServer.Serve(lis); err != nil { - return fmt.Errorf("failed to serve: %w", err) - } - - return nil -} - -func (s *Server) Stop() { - s.grpcServer.GracefulStop() - log.Println("gRPC server stopped gracefully") -} diff --git a/internal/lms/service/order.go b/internal/lms/service/order.go deleted file mode 100644 index 5cf4a05..0000000 --- a/internal/lms/service/order.go +++ /dev/null @@ -1,56 +0,0 @@ -package service - -import ( - "context" - - "datarush/internal/lms/domain" - "datarush/internal/lms/repository" - - "github.com/google/uuid" -) - -type OrderService struct { - repo repository.OrderRepository -} - -func NewOrderService(repo repository.OrderRepository) *OrderService { - return &OrderService{ - repo: repo, - } -} - -func (s *OrderService) Create(ctx context.Context, item string, quantity int32) (*domain.Order, error) { - order, err := domain.NewOrder(uuid.New(), item, quantity) - if err != nil { - return nil, err - } - - if err := s.repo.Create(ctx, order); err != nil { - return nil, err - } - return order, nil -} - -func (s *OrderService) Get(ctx context.Context, id uuid.UUID) (*domain.Order, error) { - return s.repo.Get(ctx, id) -} - -func (s *OrderService) Update(ctx context.Context, id uuid.UUID, item string, quantity int32) (*domain.Order, error) { - order, err := domain.NewOrder(id, item, quantity) - if err != nil { - return nil, err - } - - if err := s.repo.Update(ctx, order); err != nil { - return nil, err - } - return order, nil -} - -func (s *OrderService) Delete(ctx context.Context, id uuid.UUID) error { - return s.repo.Delete(ctx, id) -} - -func (s *OrderService) List(ctx context.Context) ([]*domain.Order, error) { - return s.repo.List(ctx) -} diff --git a/internal/lms/config/config.go b/internal/migrate/config/config.go similarity index 76% rename from internal/lms/config/config.go rename to internal/migrate/config/config.go index 8a27c43..0d2a479 100644 --- a/internal/lms/config/config.go +++ b/internal/migrate/config/config.go @@ -22,25 +22,17 @@ type Config struct { DBPassword string DBName string RedisURI string - AuthGRPCAddr string } func Load() (*Config, error) { _ = godotenv.Load() return &Config{ - GRPCPort: mustGetInt("GRPC_PORT", 50051), //nolint:mnd // false-positive - GRPCEnableReflection: mustGetBool("GRPC_ENABLE_REFLECTION", false), - EnableHTTPHandler: mustGetBool("HTTP_HANDLER_ENABLE", false), - HTTPPort: mustGetInt("HTTP_PORT", 8080), //nolint:mnd // false-positive - LogLevel: getEnv("LOG_LEVEL", "info"), DBHost: getEnv("POSTGRES_HOST", "localhost"), DBPort: mustGetInt("POSTGRES_PORT", 5432), //nolint:mnd // false-positive DBUser: getEnv("POSTGRES_USERNAME", "postgres"), DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"), DBName: getEnv("POSTGRES_DATABASE", "postgres"), - RedisURI: getEnv("REDIS_URI", "redis://localhost:6379"), - AuthGRPCAddr: getEnv("AUTH_GRPC_ADDR", "localhost:50052"), }, nil } diff --git a/internal/task/config/config.go b/internal/task/config/config.go new file mode 100644 index 0000000..df5206f --- /dev/null +++ b/internal/task/config/config.go @@ -0,0 +1,76 @@ +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 + AuthSvcAddr 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"), + AuthSvcAddr: getEnv("AUTH_SVC_ADDR", "localhost:50051"), + }, 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) +} 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..b2f94bf --- /dev/null +++ b/internal/task/server/server.go @@ -0,0 +1,102 @@ +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" + authpb "datarush/pkg/api/auth" + pb "datarush/pkg/api/task" + "datarush/pkg/interceptor" + + "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 + authConn *grpc.ClientConn +} + +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 { + authClient := authpb.NewAuthServiceClient(s.authConn) + authInterceptor := interceptor.NewAuthInterceptor(authClient) + + 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") +} 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 diff --git a/pkg/api/achievements/achievements.pb.go b/pkg/api/achievements/achievements.pb.go new file mode 100644 index 0000000..9995214 --- /dev/null +++ b/pkg/api/achievements/achievements.pb.go @@ -0,0 +1,468 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.2 +// source: api/proto/achievements.proto + +package achievements + +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) +) + +type Achievement struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + IconUrl string `protobuf:"bytes,4,opt,name=icon_url,json=iconUrl,proto3" json:"icon_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Achievement) Reset() { + *x = Achievement{} + mi := &file_api_proto_achievements_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Achievement) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Achievement) ProtoMessage() {} + +func (x *Achievement) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_achievements_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 Achievement.ProtoReflect.Descriptor instead. +func (*Achievement) Descriptor() ([]byte, []int) { + return file_api_proto_achievements_proto_rawDescGZIP(), []int{0} +} + +func (x *Achievement) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Achievement) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Achievement) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Achievement) GetIconUrl() string { + if x != nil { + return x.IconUrl + } + return "" +} + +type AchievementUser struct { + state protoimpl.MessageState `protogen:"open.v1"` + Achievement *Achievement `protobuf:"bytes,1,opt,name=achievement,proto3" json:"achievement,omitempty"` + ReceivedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=received_at,json=receivedAt,proto3" json:"received_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AchievementUser) Reset() { + *x = AchievementUser{} + mi := &file_api_proto_achievements_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AchievementUser) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AchievementUser) ProtoMessage() {} + +func (x *AchievementUser) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_achievements_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 AchievementUser.ProtoReflect.Descriptor instead. +func (*AchievementUser) Descriptor() ([]byte, []int) { + return file_api_proto_achievements_proto_rawDescGZIP(), []int{1} +} + +func (x *AchievementUser) GetAchievement() *Achievement { + if x != nil { + return x.Achievement + } + return nil +} + +func (x *AchievementUser) GetReceivedAt() *timestamppb.Timestamp { + if x != nil { + return x.ReceivedAt + } + return nil +} + +type GetAchievementRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAchievementRequest) Reset() { + *x = GetAchievementRequest{} + mi := &file_api_proto_achievements_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAchievementRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAchievementRequest) ProtoMessage() {} + +func (x *GetAchievementRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_achievements_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 GetAchievementRequest.ProtoReflect.Descriptor instead. +func (*GetAchievementRequest) Descriptor() ([]byte, []int) { + return file_api_proto_achievements_proto_rawDescGZIP(), []int{2} +} + +func (x *GetAchievementRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type DeleteAchievementRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + AchievementId string `protobuf:"bytes,1,opt,name=achievement_id,json=achievementId,proto3" json:"achievement_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteAchievementRequest) Reset() { + *x = DeleteAchievementRequest{} + mi := &file_api_proto_achievements_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteAchievementRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAchievementRequest) ProtoMessage() {} + +func (x *DeleteAchievementRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_achievements_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 DeleteAchievementRequest.ProtoReflect.Descriptor instead. +func (*DeleteAchievementRequest) Descriptor() ([]byte, []int) { + return file_api_proto_achievements_proto_rawDescGZIP(), []int{3} +} + +func (x *DeleteAchievementRequest) GetAchievementId() string { + if x != nil { + return x.AchievementId + } + return "" +} + +type ListAchievementsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Achievements []*Achievement `protobuf:"bytes,1,rep,name=achievements,proto3" json:"achievements,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAchievementsResponse) Reset() { + *x = ListAchievementsResponse{} + mi := &file_api_proto_achievements_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAchievementsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAchievementsResponse) ProtoMessage() {} + +func (x *ListAchievementsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_achievements_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 ListAchievementsResponse.ProtoReflect.Descriptor instead. +func (*ListAchievementsResponse) Descriptor() ([]byte, []int) { + return file_api_proto_achievements_proto_rawDescGZIP(), []int{4} +} + +func (x *ListAchievementsResponse) GetAchievements() []*Achievement { + if x != nil { + return x.Achievements + } + return nil +} + +type GetUserAchievementsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserAchievementsRequest) Reset() { + *x = GetUserAchievementsRequest{} + mi := &file_api_proto_achievements_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserAchievementsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserAchievementsRequest) ProtoMessage() {} + +func (x *GetUserAchievementsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_achievements_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 GetUserAchievementsRequest.ProtoReflect.Descriptor instead. +func (*GetUserAchievementsRequest) Descriptor() ([]byte, []int) { + return file_api_proto_achievements_proto_rawDescGZIP(), []int{5} +} + +func (x *GetUserAchievementsRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +type GetUserAchievementsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserAchievements []*AchievementUser `protobuf:"bytes,1,rep,name=user_achievements,json=userAchievements,proto3" json:"user_achievements,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserAchievementsResponse) Reset() { + *x = GetUserAchievementsResponse{} + mi := &file_api_proto_achievements_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserAchievementsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserAchievementsResponse) ProtoMessage() {} + +func (x *GetUserAchievementsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_achievements_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 GetUserAchievementsResponse.ProtoReflect.Descriptor instead. +func (*GetUserAchievementsResponse) Descriptor() ([]byte, []int) { + return file_api_proto_achievements_proto_rawDescGZIP(), []int{6} +} + +func (x *GetUserAchievementsResponse) GetUserAchievements() []*AchievementUser { + if x != nil { + return x.UserAchievements + } + return nil +} + +var File_api_proto_achievements_proto protoreflect.FileDescriptor + +const file_api_proto_achievements_proto_rawDesc = "" + + "\n" + + "\x1capi/proto/achievements.proto\x12\fachievements\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1bgoogle/protobuf/empty.proto\"n\n" + + "\vAchievement\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x19\n" + + "\bicon_url\x18\x04 \x01(\tR\aiconUrl\"\x8b\x01\n" + + "\x0fAchievementUser\x12;\n" + + "\vachievement\x18\x01 \x01(\v2\x19.achievements.AchievementR\vachievement\x12;\n" + + "\vreceived_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "receivedAt\"'\n" + + "\x15GetAchievementRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"A\n" + + "\x18DeleteAchievementRequest\x12%\n" + + "\x0eachievement_id\x18\x01 \x01(\tR\rachievementId\"Y\n" + + "\x18ListAchievementsResponse\x12=\n" + + "\fachievements\x18\x01 \x03(\v2\x19.achievements.AchievementR\fachievements\"5\n" + + "\x1aGetUserAchievementsRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\"i\n" + + "\x1bGetUserAchievementsResponse\x12J\n" + + "\x11user_achievements\x18\x01 \x03(\v2\x1d.achievements.AchievementUserR\x10userAchievements2\x90\x04\n" + + "\x13AchievementsService\x12I\n" + + "\x11CreateAchievement\x12\x19.achievements.Achievement\x1a\x19.achievements.Achievement\x12P\n" + + "\x0eGetAchievement\x12#.achievements.GetAchievementRequest\x1a\x19.achievements.Achievement\x12G\n" + + "\x0fEditAchievement\x12\x19.achievements.Achievement\x1a\x19.achievements.Achievement\x12S\n" + + "\x11DeleteAchievement\x12&.achievements.DeleteAchievementRequest\x1a\x16.google.protobuf.Empty\x12R\n" + + "\x10ListAchievements\x12\x16.google.protobuf.Empty\x1a&.achievements.ListAchievementsResponse\x12j\n" + + "\x13GetUserAchievements\x12(.achievements.GetUserAchievementsRequest\x1a).achievements.GetUserAchievementsResponseB\x16Z\x14pkg/api/achievementsb\x06proto3" + +var ( + file_api_proto_achievements_proto_rawDescOnce sync.Once + file_api_proto_achievements_proto_rawDescData []byte +) + +func file_api_proto_achievements_proto_rawDescGZIP() []byte { + file_api_proto_achievements_proto_rawDescOnce.Do(func() { + file_api_proto_achievements_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_proto_achievements_proto_rawDesc), len(file_api_proto_achievements_proto_rawDesc))) + }) + return file_api_proto_achievements_proto_rawDescData +} + +var file_api_proto_achievements_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_api_proto_achievements_proto_goTypes = []any{ + (*Achievement)(nil), // 0: achievements.Achievement + (*AchievementUser)(nil), // 1: achievements.AchievementUser + (*GetAchievementRequest)(nil), // 2: achievements.GetAchievementRequest + (*DeleteAchievementRequest)(nil), // 3: achievements.DeleteAchievementRequest + (*ListAchievementsResponse)(nil), // 4: achievements.ListAchievementsResponse + (*GetUserAchievementsRequest)(nil), // 5: achievements.GetUserAchievementsRequest + (*GetUserAchievementsResponse)(nil), // 6: achievements.GetUserAchievementsResponse + (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 8: google.protobuf.Empty +} +var file_api_proto_achievements_proto_depIdxs = []int32{ + 0, // 0: achievements.AchievementUser.achievement:type_name -> achievements.Achievement + 7, // 1: achievements.AchievementUser.received_at:type_name -> google.protobuf.Timestamp + 0, // 2: achievements.ListAchievementsResponse.achievements:type_name -> achievements.Achievement + 1, // 3: achievements.GetUserAchievementsResponse.user_achievements:type_name -> achievements.AchievementUser + 0, // 4: achievements.AchievementsService.CreateAchievement:input_type -> achievements.Achievement + 2, // 5: achievements.AchievementsService.GetAchievement:input_type -> achievements.GetAchievementRequest + 0, // 6: achievements.AchievementsService.EditAchievement:input_type -> achievements.Achievement + 3, // 7: achievements.AchievementsService.DeleteAchievement:input_type -> achievements.DeleteAchievementRequest + 8, // 8: achievements.AchievementsService.ListAchievements:input_type -> google.protobuf.Empty + 5, // 9: achievements.AchievementsService.GetUserAchievements:input_type -> achievements.GetUserAchievementsRequest + 0, // 10: achievements.AchievementsService.CreateAchievement:output_type -> achievements.Achievement + 0, // 11: achievements.AchievementsService.GetAchievement:output_type -> achievements.Achievement + 0, // 12: achievements.AchievementsService.EditAchievement:output_type -> achievements.Achievement + 8, // 13: achievements.AchievementsService.DeleteAchievement:output_type -> google.protobuf.Empty + 4, // 14: achievements.AchievementsService.ListAchievements:output_type -> achievements.ListAchievementsResponse + 6, // 15: achievements.AchievementsService.GetUserAchievements:output_type -> achievements.GetUserAchievementsResponse + 10, // [10:16] is the sub-list for method output_type + 4, // [4:10] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_api_proto_achievements_proto_init() } +func file_api_proto_achievements_proto_init() { + if File_api_proto_achievements_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_proto_achievements_proto_rawDesc), len(file_api_proto_achievements_proto_rawDesc)), + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_proto_achievements_proto_goTypes, + DependencyIndexes: file_api_proto_achievements_proto_depIdxs, + MessageInfos: file_api_proto_achievements_proto_msgTypes, + }.Build() + File_api_proto_achievements_proto = out.File + file_api_proto_achievements_proto_goTypes = nil + file_api_proto_achievements_proto_depIdxs = nil +} diff --git a/pkg/api/achievements/achievements_grpc.pb.go b/pkg/api/achievements/achievements_grpc.pb.go new file mode 100644 index 0000000..815fd9f --- /dev/null +++ b/pkg/api/achievements/achievements_grpc.pb.go @@ -0,0 +1,312 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc v6.33.2 +// source: api/proto/achievements.proto + +package achievements + +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 ( + AchievementsService_CreateAchievement_FullMethodName = "/achievements.AchievementsService/CreateAchievement" + AchievementsService_GetAchievement_FullMethodName = "/achievements.AchievementsService/GetAchievement" + AchievementsService_EditAchievement_FullMethodName = "/achievements.AchievementsService/EditAchievement" + AchievementsService_DeleteAchievement_FullMethodName = "/achievements.AchievementsService/DeleteAchievement" + AchievementsService_ListAchievements_FullMethodName = "/achievements.AchievementsService/ListAchievements" + AchievementsService_GetUserAchievements_FullMethodName = "/achievements.AchievementsService/GetUserAchievements" +) + +// AchievementsServiceClient is the client API for AchievementsService 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 AchievementsServiceClient interface { + CreateAchievement(ctx context.Context, in *Achievement, opts ...grpc.CallOption) (*Achievement, error) + GetAchievement(ctx context.Context, in *GetAchievementRequest, opts ...grpc.CallOption) (*Achievement, error) + EditAchievement(ctx context.Context, in *Achievement, opts ...grpc.CallOption) (*Achievement, error) + DeleteAchievement(ctx context.Context, in *DeleteAchievementRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + ListAchievements(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListAchievementsResponse, error) + GetUserAchievements(ctx context.Context, in *GetUserAchievementsRequest, opts ...grpc.CallOption) (*GetUserAchievementsResponse, error) +} + +type achievementsServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewAchievementsServiceClient(cc grpc.ClientConnInterface) AchievementsServiceClient { + return &achievementsServiceClient{cc} +} + +func (c *achievementsServiceClient) CreateAchievement(ctx context.Context, in *Achievement, opts ...grpc.CallOption) (*Achievement, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Achievement) + err := c.cc.Invoke(ctx, AchievementsService_CreateAchievement_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *achievementsServiceClient) GetAchievement(ctx context.Context, in *GetAchievementRequest, opts ...grpc.CallOption) (*Achievement, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Achievement) + err := c.cc.Invoke(ctx, AchievementsService_GetAchievement_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *achievementsServiceClient) EditAchievement(ctx context.Context, in *Achievement, opts ...grpc.CallOption) (*Achievement, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Achievement) + err := c.cc.Invoke(ctx, AchievementsService_EditAchievement_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *achievementsServiceClient) DeleteAchievement(ctx context.Context, in *DeleteAchievementRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, AchievementsService_DeleteAchievement_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *achievementsServiceClient) ListAchievements(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListAchievementsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListAchievementsResponse) + err := c.cc.Invoke(ctx, AchievementsService_ListAchievements_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *achievementsServiceClient) GetUserAchievements(ctx context.Context, in *GetUserAchievementsRequest, opts ...grpc.CallOption) (*GetUserAchievementsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetUserAchievementsResponse) + err := c.cc.Invoke(ctx, AchievementsService_GetUserAchievements_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AchievementsServiceServer is the server API for AchievementsService service. +// All implementations must embed UnimplementedAchievementsServiceServer +// for forward compatibility. +type AchievementsServiceServer interface { + CreateAchievement(context.Context, *Achievement) (*Achievement, error) + GetAchievement(context.Context, *GetAchievementRequest) (*Achievement, error) + EditAchievement(context.Context, *Achievement) (*Achievement, error) + DeleteAchievement(context.Context, *DeleteAchievementRequest) (*emptypb.Empty, error) + ListAchievements(context.Context, *emptypb.Empty) (*ListAchievementsResponse, error) + GetUserAchievements(context.Context, *GetUserAchievementsRequest) (*GetUserAchievementsResponse, error) + mustEmbedUnimplementedAchievementsServiceServer() +} + +// UnimplementedAchievementsServiceServer must 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 UnimplementedAchievementsServiceServer struct{} + +func (UnimplementedAchievementsServiceServer) CreateAchievement(context.Context, *Achievement) (*Achievement, error) { + return nil, status.Error(codes.Unimplemented, "method CreateAchievement not implemented") +} +func (UnimplementedAchievementsServiceServer) GetAchievement(context.Context, *GetAchievementRequest) (*Achievement, error) { + return nil, status.Error(codes.Unimplemented, "method GetAchievement not implemented") +} +func (UnimplementedAchievementsServiceServer) EditAchievement(context.Context, *Achievement) (*Achievement, error) { + return nil, status.Error(codes.Unimplemented, "method EditAchievement not implemented") +} +func (UnimplementedAchievementsServiceServer) DeleteAchievement(context.Context, *DeleteAchievementRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteAchievement not implemented") +} +func (UnimplementedAchievementsServiceServer) ListAchievements(context.Context, *emptypb.Empty) (*ListAchievementsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListAchievements not implemented") +} +func (UnimplementedAchievementsServiceServer) GetUserAchievements(context.Context, *GetUserAchievementsRequest) (*GetUserAchievementsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetUserAchievements not implemented") +} +func (UnimplementedAchievementsServiceServer) mustEmbedUnimplementedAchievementsServiceServer() {} +func (UnimplementedAchievementsServiceServer) testEmbeddedByValue() {} + +// UnsafeAchievementsServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AchievementsServiceServer will +// result in compilation errors. +type UnsafeAchievementsServiceServer interface { + mustEmbedUnimplementedAchievementsServiceServer() +} + +func RegisterAchievementsServiceServer(s grpc.ServiceRegistrar, srv AchievementsServiceServer) { + // If the following call panics, it indicates UnimplementedAchievementsServiceServer 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(&AchievementsService_ServiceDesc, srv) +} + +func _AchievementsService_CreateAchievement_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Achievement) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AchievementsServiceServer).CreateAchievement(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AchievementsService_CreateAchievement_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AchievementsServiceServer).CreateAchievement(ctx, req.(*Achievement)) + } + return interceptor(ctx, in, info, handler) +} + +func _AchievementsService_GetAchievement_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAchievementRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AchievementsServiceServer).GetAchievement(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AchievementsService_GetAchievement_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AchievementsServiceServer).GetAchievement(ctx, req.(*GetAchievementRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AchievementsService_EditAchievement_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Achievement) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AchievementsServiceServer).EditAchievement(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AchievementsService_EditAchievement_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AchievementsServiceServer).EditAchievement(ctx, req.(*Achievement)) + } + return interceptor(ctx, in, info, handler) +} + +func _AchievementsService_DeleteAchievement_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteAchievementRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AchievementsServiceServer).DeleteAchievement(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AchievementsService_DeleteAchievement_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AchievementsServiceServer).DeleteAchievement(ctx, req.(*DeleteAchievementRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AchievementsService_ListAchievements_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AchievementsServiceServer).ListAchievements(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AchievementsService_ListAchievements_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AchievementsServiceServer).ListAchievements(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _AchievementsService_GetUserAchievements_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetUserAchievementsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AchievementsServiceServer).GetUserAchievements(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AchievementsService_GetUserAchievements_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AchievementsServiceServer).GetUserAchievements(ctx, req.(*GetUserAchievementsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// AchievementsService_ServiceDesc is the grpc.ServiceDesc for AchievementsService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AchievementsService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "achievements.AchievementsService", + HandlerType: (*AchievementsServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateAchievement", + Handler: _AchievementsService_CreateAchievement_Handler, + }, + { + MethodName: "GetAchievement", + Handler: _AchievementsService_GetAchievement_Handler, + }, + { + MethodName: "EditAchievement", + Handler: _AchievementsService_EditAchievement_Handler, + }, + { + MethodName: "DeleteAchievement", + Handler: _AchievementsService_DeleteAchievement_Handler, + }, + { + MethodName: "ListAchievements", + Handler: _AchievementsService_ListAchievements_Handler, + }, + { + MethodName: "GetUserAchievements", + Handler: _AchievementsService_GetUserAchievements_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/proto/achievements.proto", +} diff --git a/pkg/api/results/results.pb.go b/pkg/api/results/results.pb.go index 1be0f70..a427ea7 100644 --- a/pkg/api/results/results.pb.go +++ b/pkg/api/results/results.pb.go @@ -236,9 +236,9 @@ func (x *GetCompetitionResultsRequest) GetCompetitionId() string { type GetCompetitionResultsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Results []*UserResult `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` - TotalCount int32 `protobuf:"varint,2,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` - NextPageToken int32 `protobuf:"varint,3,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + TotalCount int32 `protobuf:"varint,1,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` + NextPageToken int32 `protobuf:"varint,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + Results []*UserResult `protobuf:"bytes,3,rep,name=results,proto3" json:"results,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -273,13 +273,6 @@ func (*GetCompetitionResultsResponse) Descriptor() ([]byte, []int) { return file_api_proto_results_proto_rawDescGZIP(), []int{3} } -func (x *GetCompetitionResultsResponse) GetResults() []*UserResult { - if x != nil { - return x.Results - } - return nil -} - func (x *GetCompetitionResultsResponse) GetTotalCount() int32 { if x != nil { return x.TotalCount @@ -294,6 +287,13 @@ func (x *GetCompetitionResultsResponse) GetNextPageToken() int32 { return 0 } +func (x *GetCompetitionResultsResponse) GetResults() []*UserResult { + if x != nil { + return x.Results + } + return nil +} + type GetUserCompetitionResultsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` CompetitionId string `protobuf:"bytes,1,opt,name=competition_id,json=competitionId,proto3" json:"competition_id,omitempty"` @@ -462,11 +462,11 @@ const file_api_proto_results_proto_rawDesc = "" + "\n" + "page_token\x18\x02 \x01(\x05R\tpageToken\x12%\n" + "\x0ecompetition_id\x18\x03 \x01(\tR\rcompetitionId\"\x97\x01\n" + - "\x1dGetCompetitionResultsResponse\x12-\n" + - "\aresults\x18\x01 \x03(\v2\x13.results.UserResultR\aresults\x12\x1f\n" + - "\vtotal_count\x18\x02 \x01(\x05R\n" + + "\x1dGetCompetitionResultsResponse\x12\x1f\n" + + "\vtotal_count\x18\x01 \x01(\x05R\n" + "totalCount\x12&\n" + - "\x0fnext_page_token\x18\x03 \x01(\x05R\rnextPageToken\"b\n" + + "\x0fnext_page_token\x18\x02 \x01(\x05R\rnextPageToken\x12-\n" + + "\aresults\x18\x03 \x03(\v2\x13.results.UserResultR\aresults\"b\n" + " GetUserCompetitionResultsRequest\x12%\n" + "\x0ecompetition_id\x18\x01 \x01(\tR\rcompetitionId\x12\x17\n" + "\auser_id\x18\x02 \x01(\tR\x06userId\"P\n" + diff --git a/pkg/api/review/review.pb.go b/pkg/api/review/review.pb.go index 5b8557b..14458b0 100644 --- a/pkg/api/review/review.pb.go +++ b/pkg/api/review/review.pb.go @@ -322,18 +322,123 @@ func (x *SubmissionForReview) GetCheckedAt() *timestamppb.Timestamp { return nil } +type ValidateReviewTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateReviewTokenRequest) Reset() { + *x = ValidateReviewTokenRequest{} + mi := &file_api_proto_review_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateReviewTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateReviewTokenRequest) ProtoMessage() {} + +func (x *ValidateReviewTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_review_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 ValidateReviewTokenRequest.ProtoReflect.Descriptor instead. +func (*ValidateReviewTokenRequest) Descriptor() ([]byte, []int) { + return file_api_proto_review_proto_rawDescGZIP(), []int{3} +} + +func (x *ValidateReviewTokenRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +type ValidateReviewTokenResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + IsValid bool `protobuf:"varint,1,opt,name=is_valid,json=isValid,proto3" json:"is_valid,omitempty"` + ReviewerId string `protobuf:"bytes,2,opt,name=reviewer_id,json=reviewerId,proto3" json:"reviewer_id,omitempty"` + CompetitionId string `protobuf:"bytes,3,opt,name=competition_id,json=competitionId,proto3" json:"competition_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateReviewTokenResponse) Reset() { + *x = ValidateReviewTokenResponse{} + mi := &file_api_proto_review_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateReviewTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateReviewTokenResponse) ProtoMessage() {} + +func (x *ValidateReviewTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_review_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 ValidateReviewTokenResponse.ProtoReflect.Descriptor instead. +func (*ValidateReviewTokenResponse) Descriptor() ([]byte, []int) { + return file_api_proto_review_proto_rawDescGZIP(), []int{4} +} + +func (x *ValidateReviewTokenResponse) GetIsValid() bool { + if x != nil { + return x.IsValid + } + return false +} + +func (x *ValidateReviewTokenResponse) GetReviewerId() string { + if x != nil { + return x.ReviewerId + } + return "" +} + +func (x *ValidateReviewTokenResponse) GetCompetitionId() string { + if x != nil { + return x.CompetitionId + } + return "" +} + type ListSubmissionsForReviewRequest struct { state protoimpl.MessageState `protogen:"open.v1"` PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` PageToken int32 `protobuf:"varint,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` - Status *ReviewStatus `protobuf:"varint,3,opt,name=status,proto3,enum=review.ReviewStatus,oneof" json:"status,omitempty"` + Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` + Status *ReviewStatus `protobuf:"varint,4,opt,name=status,proto3,enum=review.ReviewStatus,oneof" json:"status,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListSubmissionsForReviewRequest) Reset() { *x = ListSubmissionsForReviewRequest{} - mi := &file_api_proto_review_proto_msgTypes[3] + mi := &file_api_proto_review_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -345,7 +450,7 @@ func (x *ListSubmissionsForReviewRequest) String() string { func (*ListSubmissionsForReviewRequest) ProtoMessage() {} func (x *ListSubmissionsForReviewRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_review_proto_msgTypes[3] + mi := &file_api_proto_review_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -358,7 +463,7 @@ func (x *ListSubmissionsForReviewRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSubmissionsForReviewRequest.ProtoReflect.Descriptor instead. func (*ListSubmissionsForReviewRequest) Descriptor() ([]byte, []int) { - return file_api_proto_review_proto_rawDescGZIP(), []int{3} + return file_api_proto_review_proto_rawDescGZIP(), []int{5} } func (x *ListSubmissionsForReviewRequest) GetPageSize() int32 { @@ -375,6 +480,13 @@ func (x *ListSubmissionsForReviewRequest) GetPageToken() int32 { return 0 } +func (x *ListSubmissionsForReviewRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + func (x *ListSubmissionsForReviewRequest) GetStatus() ReviewStatus { if x != nil && x.Status != nil { return *x.Status @@ -393,7 +505,7 @@ type ListSubmissionsForReviewResponse struct { func (x *ListSubmissionsForReviewResponse) Reset() { *x = ListSubmissionsForReviewResponse{} - mi := &file_api_proto_review_proto_msgTypes[4] + mi := &file_api_proto_review_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -405,7 +517,7 @@ func (x *ListSubmissionsForReviewResponse) String() string { func (*ListSubmissionsForReviewResponse) ProtoMessage() {} func (x *ListSubmissionsForReviewResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_review_proto_msgTypes[4] + mi := &file_api_proto_review_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -418,7 +530,7 @@ func (x *ListSubmissionsForReviewResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSubmissionsForReviewResponse.ProtoReflect.Descriptor instead. func (*ListSubmissionsForReviewResponse) Descriptor() ([]byte, []int) { - return file_api_proto_review_proto_rawDescGZIP(), []int{4} + return file_api_proto_review_proto_rawDescGZIP(), []int{6} } func (x *ListSubmissionsForReviewResponse) GetTotalCount() int32 { @@ -444,14 +556,15 @@ func (x *ListSubmissionsForReviewResponse) GetSubmissions() []*SubmissionSummary type GetSubmissionForReviewRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - SubmissionId string `protobuf:"bytes,1,opt,name=submission_id,json=submissionId,proto3" json:"submission_id,omitempty"` + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + SubmissionId string `protobuf:"bytes,2,opt,name=submission_id,json=submissionId,proto3" json:"submission_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetSubmissionForReviewRequest) Reset() { *x = GetSubmissionForReviewRequest{} - mi := &file_api_proto_review_proto_msgTypes[5] + mi := &file_api_proto_review_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -463,7 +576,7 @@ func (x *GetSubmissionForReviewRequest) String() string { func (*GetSubmissionForReviewRequest) ProtoMessage() {} func (x *GetSubmissionForReviewRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_review_proto_msgTypes[5] + mi := &file_api_proto_review_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -476,7 +589,14 @@ func (x *GetSubmissionForReviewRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSubmissionForReviewRequest.ProtoReflect.Descriptor instead. func (*GetSubmissionForReviewRequest) Descriptor() ([]byte, []int) { - return file_api_proto_review_proto_rawDescGZIP(), []int{5} + return file_api_proto_review_proto_rawDescGZIP(), []int{7} +} + +func (x *GetSubmissionForReviewRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" } func (x *GetSubmissionForReviewRequest) GetSubmissionId() string { @@ -488,17 +608,18 @@ func (x *GetSubmissionForReviewRequest) GetSubmissionId() string { type EvaluateSubmissionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - SubmissionId string `protobuf:"bytes,1,opt,name=submission_id,json=submissionId,proto3" json:"submission_id,omitempty"` - EarnedPoints int32 `protobuf:"varint,2,opt,name=earned_points,json=earnedPoints,proto3" json:"earned_points,omitempty"` - ReviewerComment string `protobuf:"bytes,3,opt,name=reviewer_comment,json=reviewerComment,proto3" json:"reviewer_comment,omitempty"` - Marks []*CriteriaMark `protobuf:"bytes,4,rep,name=marks,proto3" json:"marks,omitempty"` + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + SubmissionId string `protobuf:"bytes,2,opt,name=submission_id,json=submissionId,proto3" json:"submission_id,omitempty"` + EarnedPoints int32 `protobuf:"varint,3,opt,name=earned_points,json=earnedPoints,proto3" json:"earned_points,omitempty"` + ReviewerComment string `protobuf:"bytes,4,opt,name=reviewer_comment,json=reviewerComment,proto3" json:"reviewer_comment,omitempty"` + Marks []*CriteriaMark `protobuf:"bytes,5,rep,name=marks,proto3" json:"marks,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *EvaluateSubmissionRequest) Reset() { *x = EvaluateSubmissionRequest{} - mi := &file_api_proto_review_proto_msgTypes[6] + mi := &file_api_proto_review_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -510,7 +631,7 @@ func (x *EvaluateSubmissionRequest) String() string { func (*EvaluateSubmissionRequest) ProtoMessage() {} func (x *EvaluateSubmissionRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_review_proto_msgTypes[6] + mi := &file_api_proto_review_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -523,7 +644,14 @@ func (x *EvaluateSubmissionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EvaluateSubmissionRequest.ProtoReflect.Descriptor instead. func (*EvaluateSubmissionRequest) Descriptor() ([]byte, []int) { - return file_api_proto_review_proto_rawDescGZIP(), []int{6} + return file_api_proto_review_proto_rawDescGZIP(), []int{8} +} + +func (x *EvaluateSubmissionRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" } func (x *EvaluateSubmissionRequest) GetSubmissionId() string { @@ -565,7 +693,7 @@ type EvaluateSubmissionResponse struct { func (x *EvaluateSubmissionResponse) Reset() { *x = EvaluateSubmissionResponse{} - mi := &file_api_proto_review_proto_msgTypes[7] + mi := &file_api_proto_review_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -577,7 +705,7 @@ func (x *EvaluateSubmissionResponse) String() string { func (*EvaluateSubmissionResponse) ProtoMessage() {} func (x *EvaluateSubmissionResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_review_proto_msgTypes[7] + mi := &file_api_proto_review_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -590,7 +718,7 @@ func (x *EvaluateSubmissionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EvaluateSubmissionResponse.ProtoReflect.Descriptor instead. func (*EvaluateSubmissionResponse) Descriptor() ([]byte, []int) { - return file_api_proto_review_proto_rawDescGZIP(), []int{7} + return file_api_proto_review_proto_rawDescGZIP(), []int{9} } func (x *EvaluateSubmissionResponse) GetSubmissionId() string { @@ -616,14 +744,15 @@ func (x *EvaluateSubmissionResponse) GetNewStatus() ReviewStatus { type ReleaseSubmissionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - SubmissionId string `protobuf:"bytes,1,opt,name=submission_id,json=submissionId,proto3" json:"submission_id,omitempty"` + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + SubmissionId string `protobuf:"bytes,2,opt,name=submission_id,json=submissionId,proto3" json:"submission_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ReleaseSubmissionRequest) Reset() { *x = ReleaseSubmissionRequest{} - mi := &file_api_proto_review_proto_msgTypes[8] + mi := &file_api_proto_review_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -635,7 +764,7 @@ func (x *ReleaseSubmissionRequest) String() string { func (*ReleaseSubmissionRequest) ProtoMessage() {} func (x *ReleaseSubmissionRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_review_proto_msgTypes[8] + mi := &file_api_proto_review_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -648,7 +777,14 @@ func (x *ReleaseSubmissionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReleaseSubmissionRequest.ProtoReflect.Descriptor instead. func (*ReleaseSubmissionRequest) Descriptor() ([]byte, []int) { - return file_api_proto_review_proto_rawDescGZIP(), []int{8} + return file_api_proto_review_proto_rawDescGZIP(), []int{10} +} + +func (x *ReleaseSubmissionRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" } func (x *ReleaseSubmissionRequest) GetSubmissionId() string { @@ -685,40 +821,52 @@ const file_api_proto_review_proto_rawDesc = "" + "\fsubmitted_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\vsubmittedAt\x12>\n" + "\n" + "checked_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampH\x00R\tcheckedAt\x88\x01\x01B\r\n" + - "\v_checked_at\"\x9b\x01\n" + + "\v_checked_at\"2\n" + + "\x1aValidateReviewTokenRequest\x12\x14\n" + + "\x05token\x18\x01 \x01(\tR\x05token\"\x80\x01\n" + + "\x1bValidateReviewTokenResponse\x12\x19\n" + + "\bis_valid\x18\x01 \x01(\bR\aisValid\x12\x1f\n" + + "\vreviewer_id\x18\x02 \x01(\tR\n" + + "reviewerId\x12%\n" + + "\x0ecompetition_id\x18\x03 \x01(\tR\rcompetitionId\"\xb1\x01\n" + "\x1fListSubmissionsForReviewRequest\x12\x1b\n" + "\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" + "\n" + - "page_token\x18\x02 \x01(\x05R\tpageToken\x121\n" + - "\x06status\x18\x03 \x01(\x0e2\x14.review.ReviewStatusH\x00R\x06status\x88\x01\x01B\t\n" + + "page_token\x18\x02 \x01(\x05R\tpageToken\x12\x14\n" + + "\x05token\x18\x03 \x01(\tR\x05token\x121\n" + + "\x06status\x18\x04 \x01(\x0e2\x14.review.ReviewStatusH\x00R\x06status\x88\x01\x01B\t\n" + "\a_status\"\xa8\x01\n" + " ListSubmissionsForReviewResponse\x12\x1f\n" + "\vtotal_count\x18\x01 \x01(\x05R\n" + "totalCount\x12&\n" + "\x0fnext_page_token\x18\x02 \x01(\x05R\rnextPageToken\x12;\n" + - "\vsubmissions\x18\x03 \x03(\v2\x19.review.SubmissionSummaryR\vsubmissions\"D\n" + - "\x1dGetSubmissionForReviewRequest\x12#\n" + - "\rsubmission_id\x18\x01 \x01(\tR\fsubmissionId\"\xbc\x01\n" + - "\x19EvaluateSubmissionRequest\x12#\n" + - "\rsubmission_id\x18\x01 \x01(\tR\fsubmissionId\x12#\n" + - "\rearned_points\x18\x02 \x01(\x05R\fearnedPoints\x12)\n" + - "\x10reviewer_comment\x18\x03 \x01(\tR\x0freviewerComment\x12*\n" + - "\x05marks\x18\x04 \x03(\v2\x14.review.CriteriaMarkR\x05marks\"\x97\x01\n" + + "\vsubmissions\x18\x03 \x03(\v2\x19.review.SubmissionSummaryR\vsubmissions\"Z\n" + + "\x1dGetSubmissionForReviewRequest\x12\x14\n" + + "\x05token\x18\x01 \x01(\tR\x05token\x12#\n" + + "\rsubmission_id\x18\x02 \x01(\tR\fsubmissionId\"\xd2\x01\n" + + "\x19EvaluateSubmissionRequest\x12\x14\n" + + "\x05token\x18\x01 \x01(\tR\x05token\x12#\n" + + "\rsubmission_id\x18\x02 \x01(\tR\fsubmissionId\x12#\n" + + "\rearned_points\x18\x03 \x01(\x05R\fearnedPoints\x12)\n" + + "\x10reviewer_comment\x18\x04 \x01(\tR\x0freviewerComment\x12*\n" + + "\x05marks\x18\x05 \x03(\v2\x14.review.CriteriaMarkR\x05marks\"\x97\x01\n" + "\x1aEvaluateSubmissionResponse\x12#\n" + "\rsubmission_id\x18\x01 \x01(\tR\fsubmissionId\x12\x1f\n" + "\vfinal_score\x18\x02 \x01(\x05R\n" + "finalScore\x123\n" + "\n" + - "new_status\x18\x03 \x01(\x0e2\x14.review.ReviewStatusR\tnewStatus\"?\n" + - "\x18ReleaseSubmissionRequest\x12#\n" + - "\rsubmission_id\x18\x01 \x01(\tR\fsubmissionId*\x9e\x01\n" + + "new_status\x18\x03 \x01(\x0e2\x14.review.ReviewStatusR\tnewStatus\"U\n" + + "\x18ReleaseSubmissionRequest\x12\x14\n" + + "\x05token\x18\x01 \x01(\tR\x05token\x12#\n" + + "\rsubmission_id\x18\x02 \x01(\tR\fsubmissionId*\x9e\x01\n" + "\fReviewStatus\x12\x1d\n" + "\x19REVIEW_STATUS_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15REVIEW_STATUS_PENDING\x10\x01\x12\x1b\n" + "\x17REVIEW_STATUS_IN_REVIEW\x10\x02\x12\x1b\n" + "\x17REVIEW_STATUS_COMPLETED\x10\x03\x12\x1a\n" + - "\x16REVIEW_STATUS_REJECTED\x10\x042\x88\x03\n" + - "\rReviewService\x12m\n" + + "\x16REVIEW_STATUS_REJECTED\x10\x042\xe8\x03\n" + + "\rReviewService\x12^\n" + + "\x13ValidateReviewToken\x12\".review.ValidateReviewTokenRequest\x1a#.review.ValidateReviewTokenResponse\x12m\n" + "\x18ListSubmissionsForReview\x12'.review.ListSubmissionsForReviewRequest\x1a(.review.ListSubmissionsForReviewResponse\x12\\\n" + "\x16GetSubmissionForReview\x12%.review.GetSubmissionForReviewRequest\x1a\x1b.review.SubmissionForReview\x12[\n" + "\x12EvaluateSubmission\x12!.review.EvaluateSubmissionRequest\x1a\".review.EvaluateSubmissionResponse\x12M\n" + @@ -737,41 +885,45 @@ func file_api_proto_review_proto_rawDescGZIP() []byte { } var file_api_proto_review_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_api_proto_review_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_api_proto_review_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_api_proto_review_proto_goTypes = []any{ (ReviewStatus)(0), // 0: review.ReviewStatus (*CriteriaMark)(nil), // 1: review.CriteriaMark (*SubmissionSummary)(nil), // 2: review.SubmissionSummary (*SubmissionForReview)(nil), // 3: review.SubmissionForReview - (*ListSubmissionsForReviewRequest)(nil), // 4: review.ListSubmissionsForReviewRequest - (*ListSubmissionsForReviewResponse)(nil), // 5: review.ListSubmissionsForReviewResponse - (*GetSubmissionForReviewRequest)(nil), // 6: review.GetSubmissionForReviewRequest - (*EvaluateSubmissionRequest)(nil), // 7: review.EvaluateSubmissionRequest - (*EvaluateSubmissionResponse)(nil), // 8: review.EvaluateSubmissionResponse - (*ReleaseSubmissionRequest)(nil), // 9: review.ReleaseSubmissionRequest - (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 11: google.protobuf.Empty + (*ValidateReviewTokenRequest)(nil), // 4: review.ValidateReviewTokenRequest + (*ValidateReviewTokenResponse)(nil), // 5: review.ValidateReviewTokenResponse + (*ListSubmissionsForReviewRequest)(nil), // 6: review.ListSubmissionsForReviewRequest + (*ListSubmissionsForReviewResponse)(nil), // 7: review.ListSubmissionsForReviewResponse + (*GetSubmissionForReviewRequest)(nil), // 8: review.GetSubmissionForReviewRequest + (*EvaluateSubmissionRequest)(nil), // 9: review.EvaluateSubmissionRequest + (*EvaluateSubmissionResponse)(nil), // 10: review.EvaluateSubmissionResponse + (*ReleaseSubmissionRequest)(nil), // 11: review.ReleaseSubmissionRequest + (*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 13: google.protobuf.Empty } var file_api_proto_review_proto_depIdxs = []int32{ - 10, // 0: review.SubmissionSummary.submitted_at:type_name -> google.protobuf.Timestamp + 12, // 0: review.SubmissionSummary.submitted_at:type_name -> google.protobuf.Timestamp 0, // 1: review.SubmissionSummary.review_status:type_name -> review.ReviewStatus 0, // 2: review.SubmissionForReview.review_status:type_name -> review.ReviewStatus - 10, // 3: review.SubmissionForReview.submitted_at:type_name -> google.protobuf.Timestamp - 10, // 4: review.SubmissionForReview.checked_at:type_name -> google.protobuf.Timestamp + 12, // 3: review.SubmissionForReview.submitted_at:type_name -> google.protobuf.Timestamp + 12, // 4: review.SubmissionForReview.checked_at:type_name -> google.protobuf.Timestamp 0, // 5: review.ListSubmissionsForReviewRequest.status:type_name -> review.ReviewStatus 2, // 6: review.ListSubmissionsForReviewResponse.submissions:type_name -> review.SubmissionSummary 1, // 7: review.EvaluateSubmissionRequest.marks:type_name -> review.CriteriaMark 0, // 8: review.EvaluateSubmissionResponse.new_status:type_name -> review.ReviewStatus - 4, // 9: review.ReviewService.ListSubmissionsForReview:input_type -> review.ListSubmissionsForReviewRequest - 6, // 10: review.ReviewService.GetSubmissionForReview:input_type -> review.GetSubmissionForReviewRequest - 7, // 11: review.ReviewService.EvaluateSubmission:input_type -> review.EvaluateSubmissionRequest - 9, // 12: review.ReviewService.ReleaseSubmission:input_type -> review.ReleaseSubmissionRequest - 5, // 13: review.ReviewService.ListSubmissionsForReview:output_type -> review.ListSubmissionsForReviewResponse - 3, // 14: review.ReviewService.GetSubmissionForReview:output_type -> review.SubmissionForReview - 8, // 15: review.ReviewService.EvaluateSubmission:output_type -> review.EvaluateSubmissionResponse - 11, // 16: review.ReviewService.ReleaseSubmission:output_type -> google.protobuf.Empty - 13, // [13:17] is the sub-list for method output_type - 9, // [9:13] is the sub-list for method input_type + 4, // 9: review.ReviewService.ValidateReviewToken:input_type -> review.ValidateReviewTokenRequest + 6, // 10: review.ReviewService.ListSubmissionsForReview:input_type -> review.ListSubmissionsForReviewRequest + 8, // 11: review.ReviewService.GetSubmissionForReview:input_type -> review.GetSubmissionForReviewRequest + 9, // 12: review.ReviewService.EvaluateSubmission:input_type -> review.EvaluateSubmissionRequest + 11, // 13: review.ReviewService.ReleaseSubmission:input_type -> review.ReleaseSubmissionRequest + 5, // 14: review.ReviewService.ValidateReviewToken:output_type -> review.ValidateReviewTokenResponse + 7, // 15: review.ReviewService.ListSubmissionsForReview:output_type -> review.ListSubmissionsForReviewResponse + 3, // 16: review.ReviewService.GetSubmissionForReview:output_type -> review.SubmissionForReview + 10, // 17: review.ReviewService.EvaluateSubmission:output_type -> review.EvaluateSubmissionResponse + 13, // 18: review.ReviewService.ReleaseSubmission:output_type -> google.protobuf.Empty + 14, // [14:19] is the sub-list for method output_type + 9, // [9:14] is the sub-list for method input_type 9, // [9:9] is the sub-list for extension type_name 9, // [9:9] is the sub-list for extension extendee 0, // [0:9] is the sub-list for field type_name @@ -783,14 +935,14 @@ func file_api_proto_review_proto_init() { return } file_api_proto_review_proto_msgTypes[2].OneofWrappers = []any{} - file_api_proto_review_proto_msgTypes[3].OneofWrappers = []any{} + file_api_proto_review_proto_msgTypes[5].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_proto_review_proto_rawDesc), len(file_api_proto_review_proto_rawDesc)), NumEnums: 1, - NumMessages: 9, + NumMessages: 11, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/api/review/review_grpc.pb.go b/pkg/api/review/review_grpc.pb.go index 49a8bed..9e50e80 100644 --- a/pkg/api/review/review_grpc.pb.go +++ b/pkg/api/review/review_grpc.pb.go @@ -20,6 +20,7 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( + ReviewService_ValidateReviewToken_FullMethodName = "/review.ReviewService/ValidateReviewToken" ReviewService_ListSubmissionsForReview_FullMethodName = "/review.ReviewService/ListSubmissionsForReview" ReviewService_GetSubmissionForReview_FullMethodName = "/review.ReviewService/GetSubmissionForReview" ReviewService_EvaluateSubmission_FullMethodName = "/review.ReviewService/EvaluateSubmission" @@ -30,6 +31,7 @@ const ( // // 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 ReviewServiceClient interface { + ValidateReviewToken(ctx context.Context, in *ValidateReviewTokenRequest, opts ...grpc.CallOption) (*ValidateReviewTokenResponse, error) ListSubmissionsForReview(ctx context.Context, in *ListSubmissionsForReviewRequest, opts ...grpc.CallOption) (*ListSubmissionsForReviewResponse, error) GetSubmissionForReview(ctx context.Context, in *GetSubmissionForReviewRequest, opts ...grpc.CallOption) (*SubmissionForReview, error) EvaluateSubmission(ctx context.Context, in *EvaluateSubmissionRequest, opts ...grpc.CallOption) (*EvaluateSubmissionResponse, error) @@ -44,6 +46,16 @@ func NewReviewServiceClient(cc grpc.ClientConnInterface) ReviewServiceClient { return &reviewServiceClient{cc} } +func (c *reviewServiceClient) ValidateReviewToken(ctx context.Context, in *ValidateReviewTokenRequest, opts ...grpc.CallOption) (*ValidateReviewTokenResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ValidateReviewTokenResponse) + err := c.cc.Invoke(ctx, ReviewService_ValidateReviewToken_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *reviewServiceClient) ListSubmissionsForReview(ctx context.Context, in *ListSubmissionsForReviewRequest, opts ...grpc.CallOption) (*ListSubmissionsForReviewResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListSubmissionsForReviewResponse) @@ -88,6 +100,7 @@ func (c *reviewServiceClient) ReleaseSubmission(ctx context.Context, in *Release // All implementations must embed UnimplementedReviewServiceServer // for forward compatibility. type ReviewServiceServer interface { + ValidateReviewToken(context.Context, *ValidateReviewTokenRequest) (*ValidateReviewTokenResponse, error) ListSubmissionsForReview(context.Context, *ListSubmissionsForReviewRequest) (*ListSubmissionsForReviewResponse, error) GetSubmissionForReview(context.Context, *GetSubmissionForReviewRequest) (*SubmissionForReview, error) EvaluateSubmission(context.Context, *EvaluateSubmissionRequest) (*EvaluateSubmissionResponse, error) @@ -102,6 +115,9 @@ type ReviewServiceServer interface { // pointer dereference when methods are called. type UnimplementedReviewServiceServer struct{} +func (UnimplementedReviewServiceServer) ValidateReviewToken(context.Context, *ValidateReviewTokenRequest) (*ValidateReviewTokenResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ValidateReviewToken not implemented") +} func (UnimplementedReviewServiceServer) ListSubmissionsForReview(context.Context, *ListSubmissionsForReviewRequest) (*ListSubmissionsForReviewResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListSubmissionsForReview not implemented") } @@ -135,6 +151,24 @@ func RegisterReviewServiceServer(s grpc.ServiceRegistrar, srv ReviewServiceServe s.RegisterService(&ReviewService_ServiceDesc, srv) } +func _ReviewService_ValidateReviewToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ValidateReviewTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ReviewServiceServer).ValidateReviewToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ReviewService_ValidateReviewToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReviewServiceServer).ValidateReviewToken(ctx, req.(*ValidateReviewTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ReviewService_ListSubmissionsForReview_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListSubmissionsForReviewRequest) if err := dec(in); err != nil { @@ -214,6 +248,10 @@ var ReviewService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "review.ReviewService", HandlerType: (*ReviewServiceServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "ValidateReviewToken", + Handler: _ReviewService_ValidateReviewToken_Handler, + }, { MethodName: "ListSubmissionsForReview", Handler: _ReviewService_ListSubmissionsForReview_Handler, diff --git a/pkg/api/submission/submission.pb.go b/pkg/api/submission/submission.pb.go index 6c0feaf..ea5d93f 100644 --- a/pkg/api/submission/submission.pb.go +++ b/pkg/api/submission/submission.pb.go @@ -190,9 +190,10 @@ func (x *Submission) GetFileUrl() string { type SubmitTaskRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - CompetitionId string `protobuf:"bytes,1,opt,name=competition_id,json=competitionId,proto3" json:"competition_id,omitempty"` - TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` - FileUrl string `protobuf:"bytes,3,opt,name=file_url,json=fileUrl,proto3" json:"file_url,omitempty"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + CompetitionId string `protobuf:"bytes,2,opt,name=competition_id,json=competitionId,proto3" json:"competition_id,omitempty"` + TaskId string `protobuf:"bytes,3,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + FileUrl string `protobuf:"bytes,4,opt,name=file_url,json=fileUrl,proto3" json:"file_url,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -227,6 +228,13 @@ func (*SubmitTaskRequest) Descriptor() ([]byte, []int) { return file_api_proto_submission_proto_rawDescGZIP(), []int{1} } +func (x *SubmitTaskRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + func (x *SubmitTaskRequest) GetCompetitionId() string { if x != nil { return x.CompetitionId @@ -250,8 +258,9 @@ func (x *SubmitTaskRequest) GetFileUrl() string { type GetSubmissionsHistoryRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - CompetitionId string `protobuf:"bytes,1,opt,name=competition_id,json=competitionId,proto3" json:"competition_id,omitempty"` - TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + CompetitionId string `protobuf:"bytes,2,opt,name=competition_id,json=competitionId,proto3" json:"competition_id,omitempty"` + TaskId string `protobuf:"bytes,3,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -286,6 +295,13 @@ func (*GetSubmissionsHistoryRequest) Descriptor() ([]byte, []int) { return file_api_proto_submission_proto_rawDescGZIP(), []int{2} } +func (x *GetSubmissionsHistoryRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + func (x *GetSubmissionsHistoryRequest) GetCompetitionId() string { if x != nil { return x.CompetitionId @@ -474,9 +490,9 @@ func (x *ListSubmissionsRequest) GetStatus() SubmissionStatus { type ListSubmissionsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - TotalCount int32 `protobuf:"varint,2,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` - NextPageToken int32 `protobuf:"varint,3,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` - Submissions []*Submission `protobuf:"bytes,1,rep,name=submissions,proto3" json:"submissions,omitempty"` + TotalCount int32 `protobuf:"varint,1,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` + NextPageToken int32 `protobuf:"varint,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + Submissions []*Submission `protobuf:"bytes,3,rep,name=submissions,proto3" json:"submissions,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -549,14 +565,16 @@ const file_api_proto_submission_proto_rawDesc = "" + "\fsubmitted_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\vsubmittedAt\x129\n" + "\n" + "checked_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\tcheckedAt\x12\x19\n" + - "\bfile_url\x18\t \x01(\tR\afileUrl\"n\n" + - "\x11SubmitTaskRequest\x12%\n" + - "\x0ecompetition_id\x18\x01 \x01(\tR\rcompetitionId\x12\x17\n" + - "\atask_id\x18\x02 \x01(\tR\x06taskId\x12\x19\n" + - "\bfile_url\x18\x03 \x01(\tR\afileUrl\"^\n" + - "\x1cGetSubmissionsHistoryRequest\x12%\n" + - "\x0ecompetition_id\x18\x01 \x01(\tR\rcompetitionId\x12\x17\n" + - "\atask_id\x18\x02 \x01(\tR\x06taskId\"Y\n" + + "\bfile_url\x18\t \x01(\tR\afileUrl\"\x87\x01\n" + + "\x11SubmitTaskRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\x12%\n" + + "\x0ecompetition_id\x18\x02 \x01(\tR\rcompetitionId\x12\x17\n" + + "\atask_id\x18\x03 \x01(\tR\x06taskId\x12\x19\n" + + "\bfile_url\x18\x04 \x01(\tR\afileUrl\"w\n" + + "\x1cGetSubmissionsHistoryRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\x12%\n" + + "\x0ecompetition_id\x18\x02 \x01(\tR\rcompetitionId\x12\x17\n" + + "\atask_id\x18\x03 \x01(\tR\x06taskId\"Y\n" + "\x1dGetSubmissionsHistoryResponse\x128\n" + "\vsubmissions\x18\x01 \x03(\v2\x16.submission.SubmissionR\vsubmissions\";\n" + "\x14GetSubmissionRequest\x12#\n" + @@ -570,10 +588,10 @@ const file_api_proto_submission_proto_rawDesc = "" + "\auser_id\x18\x05 \x01(\tR\x06userId\x124\n" + "\x06status\x18\x06 \x01(\x0e2\x1c.submission.SubmissionStatusR\x06status\"\x9c\x01\n" + "\x17ListSubmissionsResponse\x12\x1f\n" + - "\vtotal_count\x18\x02 \x01(\x05R\n" + + "\vtotal_count\x18\x01 \x01(\x05R\n" + "totalCount\x12&\n" + - "\x0fnext_page_token\x18\x03 \x01(\x05R\rnextPageToken\x128\n" + - "\vsubmissions\x18\x01 \x03(\v2\x16.submission.SubmissionR\vsubmissions*\xd7\x01\n" + + "\x0fnext_page_token\x18\x02 \x01(\x05R\rnextPageToken\x128\n" + + "\vsubmissions\x18\x03 \x03(\v2\x16.submission.SubmissionR\vsubmissions*\xd7\x01\n" + "\x10SubmissionStatus\x12!\n" + "\x1dSUBMISSION_STATUS_UNSPECIFIED\x10\x00\x12\x1d\n" + "\x19SUBMISSION_STATUS_PENDING\x10\x01\x12$\n" + 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) + } +} diff --git a/internal/lms/interceptor/logger.go b/pkg/interceptor/logger.go similarity index 100% rename from internal/lms/interceptor/logger.go rename to pkg/interceptor/logger.go