feat: added API gateway
This commit is contained in:
+138
@@ -0,0 +1,138 @@
|
||||
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.Fatalf("Failed to create auth client: %v", err)
|
||||
}
|
||||
|
||||
userClient, err := grpc_client.NewUserClient(ctx, cfg.GRPC.UserServiceAddr, clientFactory)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create user client: %v", err)
|
||||
}
|
||||
|
||||
competitionClient, err := grpc_client.NewCompetitionClient(ctx, cfg.GRPC.CompetitionServiceAddr, clientFactory)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create competition client: %v", err)
|
||||
}
|
||||
|
||||
taskClient, err := grpc_client.NewTaskClient(ctx, cfg.GRPC.TaskServiceAddr, clientFactory)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create task client: %v", err)
|
||||
}
|
||||
|
||||
submissionClient, err := grpc_client.NewSubmissionClient(ctx, cfg.GRPC.SubmissionServiceAddr, clientFactory)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create submission client: %v", err)
|
||||
}
|
||||
|
||||
resultsClient, err := grpc_client.NewResultsClient(ctx, cfg.GRPC.ResultsServiceAddr, clientFactory)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create results client: %v", err)
|
||||
}
|
||||
|
||||
reviewClient, err := grpc_client.NewReviewClient(ctx, cfg.GRPC.ReviewServiceAddr, clientFactory)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create review client: %v", err)
|
||||
}
|
||||
|
||||
achievementsClient, err := grpc_client.NewAchievementsClient(ctx, cfg.GRPC.AchievementsServiceAddr, clientFactory)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create achievements client: %v", err)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
authHandler := handler.NewAuthHandler(authClient, userClient)
|
||||
competitionHandler := handler.NewCompetitionHandler(competitionClient, userClient)
|
||||
taskHandler := handler.NewTaskHandler(taskClient)
|
||||
submissionHandler := handler.NewSubmissionHandler(submissionClient, s3Storage)
|
||||
resultsHandler := handler.NewResultsHandler(resultsClient)
|
||||
reviewHandler := handler.NewReviewHandler(reviewClient)
|
||||
achievementsHandler := handler.NewAchievementsHandler(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")
|
||||
}
|
||||
@@ -6,11 +6,15 @@ toolchain go1.24.9
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.4
|
||||
github.com/gin-gonic/gin v1.10.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/go-playground/validator/v10 v10.28.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0
|
||||
github.com/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
|
||||
@@ -22,6 +26,21 @@ require (
|
||||
)
|
||||
|
||||
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
|
||||
@@ -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=
|
||||
@@ -35,7 +80,6 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
@@ -52,17 +96,19 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
|
||||
github.com/golang/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 +116,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=
|
||||
@@ -106,6 +154,7 @@ github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERS
|
||||
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
@@ -148,5 +197,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=
|
||||
|
||||
@@ -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: getEnvRequired("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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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 + `"}`))
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user