diff --git a/api/.env b/api/.env
index d5e69e1..d268296 100644
--- a/api/.env
+++ b/api/.env
@@ -7,4 +7,4 @@ MYSQL_ROOT_PASSWORD="Root#123"
MYSQL_HOST="localhost"
MYSQL_PORT=3306
-
+OAUTH_CLIENT=''
\ No newline at end of file
diff --git a/api/.env.deployment b/api/.env.deployment
index 614a6a8..e0b5bfa 100644
--- a/api/.env.deployment
+++ b/api/.env.deployment
@@ -7,3 +7,5 @@ MYSQL_ROOT_PASSWORD="changeme" #TODO change me.
MYSQL_HOST="mysql"
MYSQL_PORT="3306"
+OAUTH_CLIENT='' #TODO set me
+
diff --git a/api/README.md b/api/README.md
index fc048cc..b63d612 100644
--- a/api/README.md
+++ b/api/README.md
@@ -7,14 +7,14 @@ The api will be exposed to port 8080, access it with `localhost:8080`.
## Environment variables
The docker image will use the following environment variables:
-| Key | Default Value | Options |
-|----------------------------------------------------|---------------|--------------------|
-| PORT | "8080" | |
-| GIN_MODE | "release" | "release", "debug" |
-| MYSQL_HOST | "mysql" | |
-| MYSQL_PORT | "3306" | |
-| MYSQL_DATABASE | "api" | |
-| MYSQL_ROOT_USER | "root" | |
-| MYSQL_ROOT_PASSWORD | "changeme" | |
-
+| Key | Default Value | Options |
+|----------------------------------------------------|---------------|------------------------|
+| PORT | "8080" | |
+| GIN_MODE | "release" | "release", "debug" |
+| MYSQL_HOST | "mysql" | |
+| MYSQL_PORT | "3306" | |
+| MYSQL_DATABASE | "api" | |
+| MYSQL_ROOT_USER | "root" | |
+| MYSQL_ROOT_PASSWORD | "changeme" | |
+| OAUTH_CLIENT | | Google OAuth Client ID |
If you use the docker image directly (without our provided docker-compose), you must specify them.
\ No newline at end of file
diff --git a/api/cmd/main.go b/api/cmd/main.go
index cacf756..9df33af 100644
--- a/api/cmd/main.go
+++ b/api/cmd/main.go
@@ -24,19 +24,21 @@ func setupRouter(db *sql.DB) *gin.Engine {
gamesService := services.GameService(gamesRepository)
gamesController := controllers.GameController(gamesService)
+ authService := services.AuthService()
+
// Ping test
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
//Upload a game
- r.POST("/games/", CorsHeader, gamesController.UploadGame)
+ r.POST("/games/", CorsHeader, authService.Authorize, gamesController.UploadGame)
//Get all uploaded games
- r.GET("/games/", CorsHeader, gamesController.GetAllGames)
+ r.GET("/games", CorsHeader, authService.Authorize, gamesController.GetAllGames)
//Get a specific game by its id
- r.GET("/games/:id", CorsHeader, gamesController.GetGameById)
+ r.GET("/games/:id", CorsHeader, authService.Authorize, gamesController.GetGameById)
//Delete a specific game, identified by its id
- r.DELETE("/games/:id", CorsHeader, gamesController.DeleteGameById)
+ r.DELETE("/games/:id", CorsHeader, authService.Authorize, gamesController.DeleteGameById)
return r
}
diff --git a/api/controllers/gameController.go b/api/controllers/gameController.go
index 5f50db5..111a049 100644
--- a/api/controllers/gameController.go
+++ b/api/controllers/gameController.go
@@ -8,6 +8,7 @@ import (
"github.com/dranikpg/dto-mapper"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
+ "log"
"net/http"
)
@@ -24,9 +25,9 @@ type gameController struct {
func (g gameController) GetAllGames(c *gin.Context) {
//Get Games
- games, err := g.service.FindAll()
+ games, err := g.service.FindAllByOwner(c.GetString("subject"))
if err != nil { //TODO handle different errors
- c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
}
@@ -34,11 +35,12 @@ func (g gameController) GetAllGames(c *gin.Context) {
resultDto := []dtos.GetGameByIdResponseBody{}
err = dto.Map(&resultDto, games)
if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
}
c.IndentedJSON(http.StatusOK, resultDto)
+ return
}
func (g gameController) GetGameById(c *gin.Context) {
@@ -47,11 +49,18 @@ func (g gameController) GetGameById(c *gin.Context) {
//Get game by uuid
game, err := g.service.FindByID(_uuid)
if err != nil { //TODO handle different errors
- c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
+ return
+ }
+ if game.Owner != c.GetString("subject") {
+ log.Print(fmt.Printf("%s tried to access an resource of %s", c.GetString("subject"), game.Owner))
+ c.AbortWithStatusJSON(
+ http.StatusForbidden,
+ gin.H{"message": "You don't have permission to access this resource"})
return
}
if game == nil {
- c.JSON(http.StatusNotFound, gin.H{"message": "Game not found"})
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"message": "Game not found"})
return
}
@@ -59,11 +68,12 @@ func (g gameController) GetGameById(c *gin.Context) {
resultDto := dtos.GetGameByIdResponseBody{}
err = dto.Map(&resultDto, game)
if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
}
c.IndentedJSON(http.StatusOK, resultDto)
+ return
}
}
@@ -71,40 +81,64 @@ func (g gameController) UploadGame(c *gin.Context) {
title := c.Query("title")
if len(title) == 0 {
- c.JSON(http.StatusBadRequest, gin.H{"message": "Title is required"})
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "Title is required"})
+ return
}
file, err := c.FormFile("file")
if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": err.Error()})
+ return
+ }
+
+ sub := c.GetString("subject")
+ if len(sub) == 0 {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "IdToken is invalid, sub is missing"})
+ return
}
- _, err = g.service.Save(file, title)
+ _, err = g.service.Save(file, title, sub)
if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
+ return
}
c.Header("content-location", fmt.Sprintf("%s/games/%s", c.Request.Host, file.Filename))
- c.String(http.StatusCreated, "")
+ c.AbortWithStatus(http.StatusCreated)
+ return
}
func (g gameController) DeleteGameById(c *gin.Context) {
_uuid := getUUIDFromRequest(c)
if _uuid != uuid.Nil {
- err := g.service.Delete(_uuid)
- if err != nil { //TODO handle different errors
+
+ //Check if the user has access to the game
+ authorized, err := g.hasAccessToGame(c)
+
+ if err != nil {
if err == sql.ErrNoRows {
- c.JSON(http.StatusNotFound, gin.H{"message": "Game not found"})
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"message": "Game not found"})
+ return
}
- c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
+ return
+ }
+
+ if !authorized {
+ c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"message": "You don't have permission to access this resource"})
+ return
+ }
+
+ err = g.service.Delete(_uuid)
+ if err != nil { //TODO handle different errors
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
+ return
} else {
- c.Status(http.StatusNoContent)
+ c.AbortWithStatus(http.StatusNoContent)
+ return
}
}
-
- //TODO Implement delete game
- c.String(http.StatusNoContent, "")
}
func GameController(service services.IGameService) IGameController {
@@ -118,11 +152,22 @@ func GameController(service services.IGameService) IGameController {
func getUUIDFromRequest(c *gin.Context) uuid.UUID {
_uuid, err := uuid.Parse(c.Param("id"))
if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid game ID"})
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "Invalid game ID"})
return uuid.Nil
} else if _uuid == uuid.Nil {
- c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid game ID"})
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "Invalid game ID"})
return uuid.Nil
}
return _uuid
}
+
+// Returns true if the owner the user who is logged-in has the same subject-id as the game owner.
+// Returns false and error if any other error occurred.
+func (g gameController) hasAccessToGame(c *gin.Context) (bool, error) {
+ owner, err := g.service.ReadOwner(getUUIDFromRequest(c))
+ if err != nil {
+ return false, err
+ }
+
+ return owner == c.GetString("subject"), nil
+}
diff --git a/api/go.mod b/api/go.mod
index ff57214..eeaa89d 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -7,21 +7,32 @@ require (
github.com/dranikpg/dto-mapper v0.2.1
github.com/gin-gonic/gin v1.9.1
github.com/go-sql-driver/mysql v1.8.1
- github.com/google/uuid v1.4.0
+ github.com/google/uuid v1.6.0
+ google.golang.org/api v0.182.0
)
require (
+ cloud.google.com/go/auth v0.4.2 // indirect
+ cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
+ cloud.google.com/go/compute/metadata v0.3.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/sonic v1.11.4 // indirect
github.com/cloudwego/base64x v0.1.1 // indirect
github.com/cloudwego/iasm v0.1.1 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/go-logr/logr v1.4.1 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/s2a-go v0.1.7 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@@ -42,15 +53,23 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
+ go.opencensus.io v0.24.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
+ go.opentelemetry.io/otel v1.24.0 // indirect
+ go.opentelemetry.io/otel/metric v1.24.0 // indirect
+ go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.7.0 // indirect
- golang.org/x/crypto v0.22.0 // indirect
+ golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
- golang.org/x/net v0.24.0 // indirect
- golang.org/x/sys v0.19.0 // indirect
- golang.org/x/text v0.14.0 // indirect
- google.golang.org/protobuf v1.33.0 // indirect
+ golang.org/x/net v0.25.0 // indirect
+ golang.org/x/oauth2 v0.20.0 // indirect
+ golang.org/x/sys v0.20.0 // indirect
+ golang.org/x/text v0.15.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect
+ google.golang.org/grpc v1.64.0 // indirect
+ google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/api/go.sum b/api/go.sum
index 7762e57..1dbd9f8 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -1,25 +1,43 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.114.0 h1:OIPFAdfrFDFO2ve2U7r/H5SwSbBzEdrBdE7xkgwc+kY=
+cloud.google.com/go/auth v0.4.2 h1:sb0eyLkhRtpq5jA+a8KWw0W70YcdVca7KJ8TM0AFYDg=
+cloud.google.com/go/auth v0.4.2/go.mod h1:Kqvlz1cf1sNA0D+sYJnkPQOP+JMHkuHeIgVmCRtZOLc=
+cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
+cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
+cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.4 h1:8+OMLSSDDm2/qJc6ld5K5Sm62NK9VHcUKk0NzBoMAM4=
github.com/bytedance/sonic v1.11.4/go.mod h1:YrWEqYtlBPS6LUA0vpuG79a1trsh4Ae41uWUWUreHhE=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.0/go.mod h1:lM8nFiNbg74QgesNo6EAtv8N9tlRjBWExmHoNDa3PkU=
github.com/cloudwego/base64x v0.1.1 h1:ClI/Dhf8lEZ753TZ6N2CEsTvBgj48LUXqT1rSsKJrf8=
github.com/cloudwego/base64x v0.1.1/go.mod h1:8DeCguxATjLTYFYhoS/CLQuTjr4/ofRb1vOVJLalPPM=
github.com/cloudwego/iasm v0.0.9/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cloudwego/iasm v0.1.1 h1:Py/XoYVR3xFd2pXmvmOnoS5vHTlYT9SnGK28ES8JOIk=
github.com/cloudwego/iasm v0.1.1/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/dranikpg/dto-mapper v0.2.1 h1:1DaphrSfBXZVlVolCP+XspMzBAFYGne91+SK594xyTg=
github.com/dranikpg/dto-mapper v0.2.1/go.mod h1:Hkidt8Lkurm7pLPYOiq3I/LlIBmDdB4J4c/VMqFXHfg=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
@@ -29,6 +47,11 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
@@ -40,10 +63,40 @@ 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/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+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.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@@ -77,6 +130,7 @@ github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtos
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
@@ -110,6 +164,16 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
@@ -117,20 +181,86 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
+golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.182.0 h1:if5fPvudRQ78GeRx3RayIoiuV7modtErPIZC/T2bIvE=
+google.golang.org/api v0.182.0/go.mod h1:cGhjy4caqA5yXRzEhkHI8Y9mfyC2VLTlER2l08xaqtM=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
+google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -139,5 +269,7 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/api/migrations/1_game_ownership.sql b/api/migrations/1_game_ownership.sql
new file mode 100644
index 0000000..40b1110
--- /dev/null
+++ b/api/migrations/1_game_ownership.sql
@@ -0,0 +1,2 @@
+ALTER TABLE games ADD Owner varchar(255);
+INSERT INTO db_state VALUES (1);
\ No newline at end of file
diff --git a/api/models/game.go b/api/models/game.go
index 1462db1..5ea2fc1 100644
--- a/api/models/game.go
+++ b/api/models/game.go
@@ -11,4 +11,5 @@ type Game struct {
StorageLocation string `json:"storageLocation"`
Status shared.GameStatus `json:"status"`
Url string `json:"url"`
-}
+ Owner string `json:"owner"`
+}
diff --git a/api/repositories/gameRepository.go b/api/repositories/gameRepository.go
index 25ef3e4..a2b6b24 100644
--- a/api/repositories/gameRepository.go
+++ b/api/repositories/gameRepository.go
@@ -7,10 +7,11 @@ import (
)
type IGameRepository interface {
- FindAll() ([]models.Game, error)
FindByID(id uuid.UUID) (*models.Game, error)
Save(game *models.Game) error
Delete(id uuid.UUID) error
+ FindAllByOwner(owner string) ([]models.Game, error)
+ ReadOwner(id uuid.UUID) (string, error)
}
type gameRepository struct {
@@ -23,36 +24,35 @@ func GameRepository(db *sql.DB) IGameRepository {
}
}
-// FindAll returns all games from the database or (nil, err) if an error occurred.
-func (g gameRepository) FindAll() ([]models.Game, error) {
- query, err := g.db.Query("SELECT * FROM games")
+// Read the owner of a specific game or empty if the game has not been found
+func (g gameRepository) ReadOwner(id uuid.UUID) (string, error) {
+ var owner string
+ err := g.db.QueryRow("SELECT Owner FROM games WHERE ID = ?", id).Scan(&owner)
if err != nil {
- return nil, err
+ return "", err
}
- defer query.Close()
+ return owner, nil
+}
- var games = []models.Game{}
- for query.Next() {
- var game models.Game
- err = query.Scan(&game.ID, &game.Title, &game.StorageLocation, &game.Status, &game.Url)
- if err != nil {
- return nil, err
- }
- games = append(games, game)
+// FindAll returns all games of a specific owner from the database or (nil, err) if an error occurred.
+func (g gameRepository) FindAllByOwner(owner string) ([]models.Game, error) {
+ stmt, err := g.db.Prepare("SELECT * FROM games WHERE owner = ?")
+ if err != nil {
+ return nil, err
}
-
- err = query.Err()
+ query, err := stmt.Query(owner)
if err != nil {
return nil, err
}
-
- return games, nil
+ defer query.Close()
+ return readGamesFromRows(query)
}
// FindByID finds a game with a specific id or nil if the game has not been found.
func (g gameRepository) FindByID(id uuid.UUID) (*models.Game, error) {
var game models.Game
- err := g.db.QueryRow("SELECT * FROM games WHERE ID = ?", id).Scan(&game.ID, &game.Title, &game.StorageLocation, &game.Status, &game.Url)
+ err := g.db.QueryRow("SELECT * FROM games WHERE ID = ?", id).
+ Scan(&game.ID, &game.Title, &game.StorageLocation, &game.Status, &game.Url, &game.Owner)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
@@ -86,12 +86,12 @@ func (g gameRepository) Save(game *models.Game) error {
}
//If not create a new one
- stmt, err := g.db.Prepare("INSERT INTO games (ID, Title, StorageLocation, Status, Url) VALUES (?,?,?,?,?)")
+ stmt, err := g.db.Prepare("INSERT INTO games (ID, Title, StorageLocation, Status, Url, Owner) VALUES (?,?,?,?,?,?)")
if err != nil {
return err
}
- return checkResult(stmt.Exec(game.ID, game.Title, game.StorageLocation, game.Status, game.Url))
+ return checkResult(stmt.Exec(game.ID, game.Title, game.StorageLocation, game.Status, game.Url, game.Owner))
}
// Delete removes the entry with a specific id from the games database.
@@ -123,3 +123,22 @@ func checkAffectedRows(res sql.Result) error {
}
return nil
}
+
+func readGamesFromRows(query *sql.Rows) ([]models.Game, error) {
+ var games = []models.Game{}
+ for query.Next() {
+ var game models.Game
+ err := query.Scan(&game.ID, &game.Title, &game.StorageLocation, &game.Status, &game.Url, &game.Owner)
+ if err != nil {
+ return nil, err
+ }
+ games = append(games, game)
+ }
+
+ err := query.Err()
+ if err != nil {
+ return nil, err
+ }
+
+ return games, nil
+}
diff --git a/api/services/authService.go b/api/services/authService.go
new file mode 100644
index 0000000..02cb5d7
--- /dev/null
+++ b/api/services/authService.go
@@ -0,0 +1,51 @@
+package services
+
+import (
+ "context"
+ "github.com/gin-gonic/gin"
+ "google.golang.org/api/idtoken"
+ "log"
+ "net/http"
+ "os"
+)
+
+type IAuthService interface {
+ Authorize(_ *gin.Context)
+}
+
+type authService struct {
+}
+
+func (_ authService) Authorize(c *gin.Context) {
+ //Check if OAUTH_CLIENT has been set
+ if len(os.Getenv("OAUTH_CLIENT")) == 0 {
+ //if not set & we are in production mode, abort
+ if os.Getenv("GIN_MODE") == "release" {
+ log.Fatalf("OAUTH_CLIENT is not set, cannot authorize requests")
+ }
+ //otherwise we are in debug mode and we can accept it
+ log.Println("OAUTH_CLIENT is not set, cannot authorize requests")
+ c.Set("subject", "")
+ return
+ }
+
+ tokenString := c.GetHeader("Authorization")
+ payload, err := idtoken.Validate(context.Background(), tokenString, os.Getenv("OAUTH_CLIENT"))
+
+ if err != nil {
+ log.Println(err.Error())
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Invalid token"})
+ return
+ }
+ if payload == nil {
+ c.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+
+ c.Set("subject", payload.Subject)
+
+}
+
+func AuthService() IAuthService {
+ return &authService{}
+}
diff --git a/api/services/gameService.go b/api/services/gameService.go
index deaad06..aedcca7 100644
--- a/api/services/gameService.go
+++ b/api/services/gameService.go
@@ -9,23 +9,29 @@ import (
)
type IGameService interface {
- FindAll() ([]models.Game, error)
FindByID(id uuid.UUID) (*models.Game, error)
- Save(file *multipart.FileHeader, title string) (*models.Game, error)
+ Save(file *multipart.FileHeader, title string, owner string) (*models.Game, error)
Delete(id uuid.UUID) error
+ FindAllByOwner(owner string) ([]models.Game, error)
+ ReadOwner(id uuid.UUID) (string, error)
}
type gameService struct {
repository repositories.IGameRepository
}
-func (g gameService) FindAll() ([]models.Game, error) {
- return g.repository.FindAll()
+func (g gameService) ReadOwner(id uuid.UUID) (string, error) {
+ return g.repository.ReadOwner(id)
+}
+
+func (g gameService) FindAllByOwner(owner string) ([]models.Game, error) {
+ return g.repository.FindAllByOwner(owner)
}
func (g gameService) FindByID(id uuid.UUID) (*models.Game, error) { return g.repository.FindByID(id) }
-func (g gameService) Save(file *multipart.FileHeader, title string) (*models.Game, error) {
+func (g gameService) Save(file *multipart.FileHeader, title string, owner string) (*models.Game, error) {
+
//TODO save file
game := models.Game{
@@ -34,8 +40,8 @@ func (g gameService) Save(file *multipart.FileHeader, title string) (*models.Gam
StorageLocation: "",
Status: shared.Status_New,
Url: "",
+ Owner: owner,
}
-
return &game, g.repository.Save(&game)
}
diff --git a/api/tests/gameRepository_test.go b/api/tests/gameRepository_test.go
index 3532710..42e1d16 100644
--- a/api/tests/gameRepository_test.go
+++ b/api/tests/gameRepository_test.go
@@ -83,14 +83,15 @@ func Test_Create_Game_Without_Id_Should_Succeed_And_SetId(t *testing.T) {
//Define the mock
game := models.Game{
ID: uuid.Nil,
- Title: "",
- StorageLocation: "",
- Status: "",
- Url: "",
+ Title: "MockTitle",
+ StorageLocation: "MockStorageLocation",
+ Status: "MockStatus",
+ Url: "MockUrl",
+ Owner: "MockOwner",
}
mock.ExpectPrepare(regexp.QuoteMeta("INSERT INTO games"))
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO games")).
- WithArgs(sqlmock.AnyArg(), game.Title, game.StorageLocation, game.Status, game.Url).
+ WithArgs(sqlmock.AnyArg(), game.Title, game.StorageLocation, game.Status, game.Url, game.Owner).
WillReturnResult(sqlmock.NewResult(0, 1))
//Run the test
@@ -122,10 +123,11 @@ func Test_Create_Game_With_Id_Should_Succeed(t *testing.T) {
id := uuid.New()
game := models.Game{
ID: id,
- Title: "",
- StorageLocation: "",
- Status: "",
- Url: "",
+ Title: "MockTitle",
+ StorageLocation: "MockStorageLocation",
+ Status: "MockStatus",
+ Url: "MockUrl",
+ Owner: "MockOwner",
}
mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE ID = ?")).
@@ -134,7 +136,7 @@ func Test_Create_Game_With_Id_Should_Succeed(t *testing.T) {
mock.ExpectPrepare("INSERT INTO games")
mock.ExpectExec("INSERT INTO games").
- WithArgs(game.ID, game.Title, game.StorageLocation, game.Status, game.Url).
+ WithArgs(game.ID, game.Title, game.StorageLocation, game.Status, game.Url, game.Owner).
WillReturnResult(sqlmock.NewResult(0, 1))
//Run the test
@@ -169,21 +171,24 @@ func Test_Save_Existing_Game_Should_Succeed(t *testing.T) {
id := uuid.New()
game := models.Game{
ID: id,
- Title: "Mock",
- StorageLocation: "Mock",
- Status: "Mock",
- Url: "Mock",
+ Title: "MockTitle",
+ StorageLocation: "MockStorageLocation",
+ Status: "MockStatus",
+ Url: "MockUrl",
+ Owner: "MockOwner",
}
mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE ID = ?")).
WithArgs(id).WillReturnRows(
- sqlmock.NewRows([]string{"id", "title", "storage_location", "status", "url"}).
- AddRow(id, "", "", "", ""),
+ sqlmock.NewRows([]string{"Id", "Title", "StorageLocation", "Status", "Url", "Owner"}).
+ AddRow(id, "", "", "", "", ""),
)
- mock.ExpectPrepare(regexp.QuoteMeta("UPDATE games SET Title=?, StorageLocation=?, Status=?, Url=? WHERE ID = ?"))
+ mock.ExpectPrepare(regexp.
+ QuoteMeta("UPDATE games SET Title=?, StorageLocation=?, Status=?, Url=? WHERE ID = ?"))
- mock.ExpectExec(regexp.QuoteMeta("UPDATE games SET Title=?, StorageLocation=?, Status=?, Url=? WHERE ID = ?")).
+ mock.ExpectExec(regexp.
+ QuoteMeta("UPDATE games SET Title=?, StorageLocation=?, Status=?, Url=? WHERE ID = ?")).
WithArgs(game.Title, game.StorageLocation, game.Status, game.Url, game.ID).
WillReturnResult(sqlmock.NewResult(0, 1))
@@ -219,16 +224,17 @@ func Test_Find_Game_By_Id_Should_Succeed(t *testing.T) {
id := uuid.New()
game := models.Game{
ID: id,
- Title: "MockA",
- StorageLocation: "MockB",
- Status: "MockC",
- Url: "MockD",
+ Title: "MockTitle",
+ StorageLocation: "MockStorageLocation",
+ Status: "MockStatus",
+ Url: "MockUrl",
+ Owner: "MockOwner",
}
mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE ID = ?")).
WithArgs(id).WillReturnRows(
- sqlmock.NewRows([]string{"id", "title", "storage_location", "status", "url"}).
- AddRow(id, game.Title, game.StorageLocation, game.Status, game.Url),
+ sqlmock.NewRows([]string{"Id", "Title", "StorageLocation", "Status", "Url", "Owner"}).
+ AddRow(id, game.Title, game.StorageLocation, game.Status, game.Url, game.Owner),
)
//Run the test
@@ -239,29 +245,7 @@ func Test_Find_Game_By_Id_Should_Succeed(t *testing.T) {
t.Errorf(err.Error())
}
- if res == nil {
- t.Errorf("game was not returned")
- }
-
- if game.ID != res.ID {
- t.Errorf("game id has been changed")
- }
-
- if game.Title != res.Title {
- t.Errorf("game title has been changed")
- }
-
- if game.StorageLocation != res.StorageLocation {
- t.Errorf("game storage location has been changed")
- }
-
- if game.Status != res.Status {
- t.Errorf("game status has been changed")
- }
-
- if game.Url != res.Url {
- t.Errorf("game url has been changed")
- }
+ compareGames(t, &game, res)
if err = mock.ExpectationsWereMet(); err != nil {
t.Errorf(err.Error())
@@ -297,7 +281,7 @@ func Test_Find_Game_By_Id_Should_Return_Nil(t *testing.T) {
}
}
-func Test_Find_Two_Games_Should_Succeed(t *testing.T) {
+func Test_Find_Two_Games_Of_Same_Owner_Should_Succeed(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf(err.Error())
@@ -310,6 +294,7 @@ func Test_Find_Two_Games_Should_Succeed(t *testing.T) {
StorageLocation: "AMockB",
Status: "AMockC",
Url: "AMockD",
+ Owner: "MockOwner",
}
gameB := models.Game{
@@ -318,19 +303,22 @@ func Test_Find_Two_Games_Should_Succeed(t *testing.T) {
StorageLocation: "BMockB",
Status: "BMockC",
Url: "BMockD",
+ Owner: "MockOwner",
}
- mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games")).
+ mock.ExpectPrepare(regexp.QuoteMeta("SELECT * FROM games WHERE owner = ?"))
+ mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE owner = ?")).
+ WithArgs("MockOwner").
WillReturnRows(
- sqlmock.NewRows([]string{"id", "title", "storage_location", "status", "url"}).
- AddRow(gameA.ID, gameA.Title, gameA.StorageLocation, gameA.Status, gameA.Url).
- AddRow(gameB.ID, gameB.Title, gameB.StorageLocation, gameB.Status, gameB.Url),
+ sqlmock.NewRows([]string{"ID", "Title", "StorageLocation", "Status", "Url", "Owner"}).
+ AddRow(gameA.ID, gameA.Title, gameA.StorageLocation, gameA.Status, gameA.Url, gameA.Owner).
+ AddRow(gameB.ID, gameB.Title, gameB.StorageLocation, gameB.Status, gameB.Url, gameB.Owner),
)
//Run the test
repository := repositories.GameRepository(db)
- res, err := repository.FindAll()
+ res, err := repository.FindAllByOwner("MockOwner")
if err != nil {
t.Errorf(err.Error())
}
@@ -343,44 +331,80 @@ func Test_Find_Two_Games_Should_Succeed(t *testing.T) {
t.Errorf("FindAll should return two games")
}
- if gameA.ID != res[0].ID {
- t.Errorf("game id has been changed")
+ compareGames(t, &gameA, &res[0])
+ compareGames(t, &gameB, &res[1])
+
+ if err = mock.ExpectationsWereMet(); err != nil {
+ t.Errorf(err.Error())
}
+}
- if gameA.Title != res[0].Title {
- t.Errorf("game title has been changed")
+func Test_Find_Games_When_Database_Is_Empty_Should_Succeed(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf(err.Error())
}
+ defer db.Close()
- if gameA.StorageLocation != res[0].StorageLocation {
- t.Errorf("game storage location has been changed")
+ mock.ExpectPrepare(regexp.QuoteMeta("SELECT * FROM games WHERE owner = ?"))
+ mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE owner = ?")).
+ WithArgs("MockOwner").
+ WillReturnRows(
+ sqlmock.NewRows([]string{"Id", "Title", "StorageLocation", "Status", "Url", "Owner"}),
+ )
+
+ //Run the test
+ repository := repositories.GameRepository(db)
+
+ res, err := repository.FindAllByOwner("MockOwner")
+ if err != nil {
+ t.Errorf(err.Error())
}
- if gameA.Status != res[0].Status {
- t.Errorf("game status has been changed")
+ if res == nil {
+ t.Errorf("nil was returned but empty list was expected")
}
- if gameA.Url != res[0].Url {
- t.Errorf("game url has been changed")
+ if len(res) != 0 {
+ t.Errorf("FindAll should return an empty list, but it was not empty")
}
- if gameB.ID != res[1].ID {
- t.Errorf("game id has been changed")
+ if err = mock.ExpectationsWereMet(); err != nil {
+ t.Errorf(err.Error())
}
+}
- if gameB.Title != res[1].Title {
- t.Errorf("game title has been changed")
+func Test_Read_Owner_Should_Succeed(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf(err.Error())
}
+ defer db.Close()
- if gameB.StorageLocation != res[1].StorageLocation {
- t.Errorf("game storage location has been changed")
+ id := uuid.New()
+ game := models.Game{
+ ID: id,
+ Title: "MockTitle",
+ StorageLocation: "MockStorageLocation",
+ Status: "MockStatus",
+ Url: "MockUrl",
+ Owner: "MockOwner",
}
- if gameB.Status != res[1].Status {
- t.Errorf("game status has been changed")
+ mock.ExpectQuery(regexp.QuoteMeta("SELECT Owner FROM games WHERE ID = ?")).
+ WithArgs(id).
+ WillReturnRows(sqlmock.NewRows([]string{"Id", "Title", "StorageLocation", "Status", "Url", "Owner"}))
+
+ //Run the test
+ repository := repositories.GameRepository(db)
+
+ res, err := repository.ReadOwner(game.ID)
+ if err == nil {
+ t.Errorf("error was not returned")
}
- if gameB.Url != res[1].Url {
- t.Errorf("game url has been changed")
+ if res != "" {
+ t.Errorf("Owner should be empty, but got %s", res)
}
if err = mock.ExpectationsWereMet(); err != nil {
@@ -388,32 +412,39 @@ func Test_Find_Two_Games_Should_Succeed(t *testing.T) {
}
}
-func Test_Find_Games_When_Database_Is_Empty_Should_Succeed(t *testing.T) {
+func Test_Read_Owner_Should_Throw_When_Database_Is_Empty(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf(err.Error())
}
defer db.Close()
- mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games")).
- WillReturnRows(
- sqlmock.NewRows([]string{"id", "title", "storage_location", "status", "url"}),
- )
+ id := uuid.New()
+ game := models.Game{
+ ID: id,
+ Title: "MockTitle",
+ StorageLocation: "MockStorageLocation",
+ Status: "MockStatus",
+ Url: "MockUrl",
+ Owner: "MockOwner",
+ }
+
+ mock.ExpectQuery(regexp.QuoteMeta("SELECT Owner FROM games WHERE ID = ?")).
+ WithArgs(id).WillReturnRows(
+ sqlmock.NewRows([]string{"owner"}).
+ AddRow(game.Owner),
+ )
//Run the test
repository := repositories.GameRepository(db)
- res, err := repository.FindAll()
+ res, err := repository.ReadOwner(game.ID)
if err != nil {
t.Errorf(err.Error())
}
- if res == nil {
- t.Errorf("nil was returned but empty list was expected")
- }
-
- if len(res) != 0 {
- t.Errorf("FindAll should return an empty list, but it was not empty")
+ if res != game.Owner {
+ t.Errorf("ReadOwner should return %v, but got %v", game.Owner, res)
}
if err = mock.ExpectationsWereMet(); err != nil {
@@ -422,3 +453,34 @@ func Test_Find_Games_When_Database_Is_Empty_Should_Succeed(t *testing.T) {
}
//************************************ END READ TESTS ************************************
+
+func compareGames(t *testing.T, game *models.Game, res *models.Game) {
+ if res == nil {
+ t.Errorf("game was not returned")
+ }
+
+ if game.ID != res.ID {
+ t.Errorf("game id has been changed")
+ }
+
+ if game.Title != res.Title {
+ t.Errorf("game title has been changed")
+ }
+
+ if game.StorageLocation != res.StorageLocation {
+ t.Errorf("game storage location has been changed")
+ }
+
+ if game.Status != res.Status {
+ t.Errorf("game status has been changed")
+ }
+
+ if game.Url != res.Url {
+ t.Errorf("game url has been changed")
+ }
+
+ if game.Owner != res.Owner {
+ t.Errorf("game owner has been changed")
+ }
+
+}