From f1743f4ed88476d47ad7ef3aa26230e473295118 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 18 Feb 2026 17:31:30 -0800 Subject: [PATCH 01/32] sign ci --- .drone.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 712cb98..fe51ca6 100644 --- a/.drone.yml +++ b/.drone.yml @@ -61,4 +61,9 @@ steps: when: branch: - master - - staging \ No newline at end of file + - staging +--- +kind: signature +hmac: 7655eb6dead73d2ad977685120cee8562931036bb5d7fa59d30d5917840c4a22 + +... From 339677888296446816a0d666deae35c51b8a24e1 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 23 Feb 2026 08:42:10 -0800 Subject: [PATCH 02/32] fmt --- pkg/api/handlers/times.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index 73e87ab..e41b3f8 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -2,16 +2,17 @@ package handlers import ( "fmt" + "math" + "net/http" + "strconv" + "strings" + "git.itzana.me/strafesnet/go-grpc/times" "git.itzana.me/strafesnet/public-api/pkg/api/dto" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "math" - "net/http" - "strconv" - "strings" ) // TimesHandler handles HTTP requests related to times. From b7c0f8b91704210cda48a176bf29eb6f00803347 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 23 Feb 2026 09:09:32 -0800 Subject: [PATCH 03/32] bot download endpoint --- pkg/api/dto/times.go | 13 ++- pkg/api/handlers/times.go | 163 +++++++++++++++++++++++++++++++++++++- pkg/api/router.go | 14 +++- 3 files changed, 185 insertions(+), 5 deletions(-) diff --git a/pkg/api/dto/times.go b/pkg/api/dto/times.go index 5a78e89..4a6cf1a 100644 --- a/pkg/api/dto/times.go +++ b/pkg/api/dto/times.go @@ -1,9 +1,10 @@ package dto import ( - "git.itzana.me/strafesnet/go-grpc/times" "strconv" "time" + + "git.itzana.me/strafesnet/go-grpc/times" ) type TimePlacement struct { @@ -41,6 +42,16 @@ type TimeData struct { GameID int32 `json:"game_id"` } // @name Time +type BotDownloadUrl struct { + Url string `json:"url"` +} // @name BotDownloadUrl + +type FileInfo struct { + ID string `json:"ID"` + Created int64 `json:"Created"` + Url string `json:"Url"` +} // @name FileInfo + // FromGRPC converts a TimeResponse protobuf message to a TimeData domain object func (t *TimeData) FromGRPC(resp *times.TimeResponse) *TimeData { if resp == nil { diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index e41b3f8..df5b033 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -1,12 +1,14 @@ package handlers import ( + "encoding/json" "fmt" "math" "net/http" "strconv" "strings" + "git.itzana.me/strafesnet/go-grpc/bots" "git.itzana.me/strafesnet/go-grpc/times" "git.itzana.me/strafesnet/public-api/pkg/api/dto" "github.com/gin-gonic/gin" @@ -18,16 +20,20 @@ import ( // TimesHandler handles HTTP requests related to times. type TimesHandler struct { *Handler + client *http.Client + url string } // NewTimesHandler creates a new TimesHandler with the provided options. -func NewTimesHandler(options ...HandlerOption) (*TimesHandler, error) { +func NewTimesHandler(http_client *http.Client, storage_url string, options ...HandlerOption) (*TimesHandler, error) { baseHandler, err := NewHandler(options...) if err != nil { return nil, err } return &TimesHandler{ Handler: baseHandler, + client: http_client, + url: storage_url, }, nil } @@ -318,3 +324,158 @@ func (h *TimesHandler) GetPlacements(ctx *gin.Context) { Data: ranks, }) } + +// @Summary Get bot download url by time ID +// @Description Get a download url for the bot replay of a time by its ID if it exists +// @Tags times +// @Produce json +// @Security ApiKeyAuth +// @Param id path int true "Time ID" +// @Success 200 {object} dto.Response[dto.BotDownloadUrl] +// @Failure 404 {object} dto.Error "Time not found" +// @Failure 404 {object} dto.Error "Time does not have a Bot" +// @Failure 404 {object} dto.Error "Bot not found" +// @Failure default {object} dto.Error "General error response" +// @Router /time/{id} [get] +func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { + // Extract time ID from path parameter + id := ctx.Param("id") + timeID, err := strconv.ParseInt(id, 10, 64) + if err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error{ + Error: "Invalid time ID format", + }) + return + } + + // Call the gRPC service + timeData, err := times.NewTimesServiceClient(h.dataClient).Get(ctx, ×.IdMessage{ + ID: timeID, + }) + if err != nil { + statusCode := http.StatusInternalServerError + errorMessage := "Failed to get time" + + // Check if it's a "not found" error + if status.Code(err) == codes.NotFound { + statusCode = http.StatusNotFound + errorMessage = "Time not found" + } + + ctx.JSON(statusCode, dto.Error{ + Error: errorMessage, + }) + log.WithError(err).Error( + "Failed to get time", + ) + return + } + + // check if bot exists + if timeData.Bot == nil { + statusCode := http.StatusNotFound + errorMessage := "Time does not have a Bot" + + ctx.JSON(statusCode, dto.Error{ + Error: errorMessage, + }) + log.WithError(err).Error( + "Time does not have a Bot", + ) + return + } + + // Call the gRPC service + botData, err := bots.NewBotsServiceClient(h.dataClient).Get(ctx, &bots.IdMessage{ + ID: timeData.Bot.ID, + }) + if err != nil { + statusCode := http.StatusInternalServerError + errorMessage := "Failed to get bot" + + // Check if it's a "not found" error + if status.Code(err) == codes.NotFound { + statusCode = http.StatusNotFound + errorMessage = "Bot not found" + } + + ctx.JSON(statusCode, dto.Error{ + Error: errorMessage, + }) + log.WithError(err).Error( + "Failed to get bot", + ) + return + } + + // fetch download url from storage service + // Build the full URL. + fullURL := h.url + botData.FileID + + // Create the request with the supplied context so callers can cancel it. + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil) + if err != nil { + statusCode := http.StatusInternalServerError + errorMessage := "Error creating http request to storage" + + ctx.JSON(statusCode, dto.Error{ + Error: errorMessage, + }) + log.WithError(err).Error( + "Error creating http request to storage", + ) + return + } + + // Send the request. + resp, err := h.client.Do(req) + if err != nil { + statusCode := http.StatusInternalServerError + errorMessage := "Storage http request failed" + + ctx.JSON(statusCode, dto.Error{ + Error: errorMessage, + }) + log.WithError(err).Error( + "Storage http request failed", + ) + return + } + defer resp.Body.Close() + + // check status + if resp.StatusCode != 200 { + statusCode := http.StatusInternalServerError + errorMessage := "Unexpected status" + + ctx.JSON(statusCode, dto.Error{ + Error: errorMessage, + }) + log.WithError(err).Error( + "Unexpected status", + ) + return + } + + // Decode the JSON body into the FileInfo struct. + var info dto.FileInfo + if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { + statusCode := http.StatusInternalServerError + errorMessage := "Error decoding json" + + ctx.JSON(statusCode, dto.Error{ + Error: errorMessage, + }) + log.WithError(err).Error( + "Error decoding json", + ) + return + } + + // Return the time data + ctx.JSON(http.StatusOK, dto.Response[dto.BotDownloadUrl]{ + Data: dto.BotDownloadUrl{ + Url: info.Url, + }, + }) +} diff --git a/pkg/api/router.go b/pkg/api/router.go index 6d56d64..181cd23 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -4,6 +4,9 @@ import ( "context" "errors" "fmt" + "net/http" + "time" + "git.itzana.me/StrafesNET/dev-service/pkg/api/middleware" "git.itzana.me/strafesnet/public-api/docs" "git.itzana.me/strafesnet/public-api/pkg/api/handlers" @@ -13,8 +16,6 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" "github.com/urfave/cli/v2" "google.golang.org/grpc" - "net/http" - "time" ) // Option defines a function that configures a Router @@ -75,7 +76,7 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) { } // Times handler - timesHandler, err := handlers.NewTimesHandler(handlerOptions...) + timesHandler, err := handlers.NewTimesHandler(HTTP_CLIENT, STORAGE_URL, handlerOptions...) if err != nil { return nil, err } @@ -121,7 +122,14 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) { // Rank v1.GET("/rank", rankHandler.List) + } + v1_bots := r.Group("/api/v1") + { + // Auth middleware + v1_bots.Use(middleware.ValidateRequest("Storage", "Read", cfg.devClient)) + + v1_bots.GET("/time/:id/download-url", timesHandler.GetDownloadUrl) } r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) r.GET("/", func(ctx *gin.Context) { From 33f55524a8e29c194cd54ed35831108019f18bc3 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 23 Feb 2026 09:21:27 -0800 Subject: [PATCH 04/32] initialize properly --- pkg/api/router.go | 18 +++++++++++++++++- pkg/cmds/api.go | 13 +++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pkg/api/router.go b/pkg/api/router.go index 181cd23..c0f4792 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -26,6 +26,8 @@ type RouterConfig struct { port int devClient *grpc.ClientConn dataClient *grpc.ClientConn + httpClient *http.Client + storageUrl string context *cli.Context shutdownTimeout time.Duration } @@ -58,6 +60,20 @@ func WithDataClient(conn *grpc.ClientConn) Option { } } +// WithStorageUrl sets the storage url +func WithStorageUrl(storageUrl string) Option { + return func(cfg *RouterConfig) { + cfg.storageUrl = storageUrl + } +} + +// WithStorageUrl sets the data gRPC client +func WithHttpClient(httpClient *http.Client) Option { + return func(cfg *RouterConfig) { + cfg.httpClient = httpClient + } +} + // WithShutdownTimeout sets the graceful shutdown timeout func WithShutdownTimeout(timeout time.Duration) Option { return func(cfg *RouterConfig) { @@ -76,7 +92,7 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) { } // Times handler - timesHandler, err := handlers.NewTimesHandler(HTTP_CLIENT, STORAGE_URL, handlerOptions...) + timesHandler, err := handlers.NewTimesHandler(cfg.httpClient, cfg.storageUrl, handlerOptions...) if err != nil { return nil, err } diff --git a/pkg/cmds/api.go b/pkg/cmds/api.go index 1594d91..053a157 100644 --- a/pkg/cmds/api.go +++ b/pkg/cmds/api.go @@ -1,6 +1,8 @@ package cmds import ( + "net/http" + "git.itzana.me/strafesnet/public-api/pkg/api" "github.com/urfave/cli/v2" "google.golang.org/grpc" @@ -31,6 +33,12 @@ func NewApiCommand() *cli.Command { EnvVars: []string{"DATA_RPC_HOST"}, Value: "data-service:9000", }, + &cli.StringFlag{ + Name: "storage-host", + Usage: "Host of storage", + EnvVars: []string{"STORAGE_HOST"}, + Value: "storage-service:9000", + }, }, } } @@ -48,10 +56,15 @@ func runAPI(ctx *cli.Context) error { return err } + // Data service client + storage_url := ctx.String("storage-host") + return api.NewRouter( api.WithContext(ctx), api.WithPort(ctx.Int("port")), api.WithDevClient(devConn), api.WithDataClient(dataConn), + api.WithHttpClient(&http.Client{}), + api.WithStorageUrl(storage_url), ) } From 03695e773d0c479937ab37d0a6362ae524be00ef Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 23 Feb 2026 10:24:10 -0800 Subject: [PATCH 05/32] generate --- docs/docs.go | 29 +++++++++++++++++++++++++---- docs/swagger.json | 29 +++++++++++++++++++++++++---- docs/swagger.yaml | 21 +++++++++++++++++---- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index e7aecb5..645829d 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -415,14 +415,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get a specific time by its ID", + "description": "Get a download url for the bot replay of a time by its ID if it exists", "produces": [ "application/json" ], "tags": [ "times" ], - "summary": "Get time by ID", + "summary": "Get bot download url by time ID", "parameters": [ { "type": "integer", @@ -436,11 +436,11 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/Response-Time" + "$ref": "#/definitions/Response-BotDownloadUrl" } }, "404": { - "description": "Time not found", + "description": "Bot not found", "schema": { "$ref": "#/definitions/Error" } @@ -621,6 +621,14 @@ const docTemplate = `{ } }, "definitions": { + "BotDownloadUrl": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + } + }, "Error": { "type": "object", "properties": { @@ -809,6 +817,19 @@ const docTemplate = `{ } } }, + "Response-BotDownloadUrl": { + "type": "object", + "properties": { + "data": { + "description": "Data contains the actual response payload", + "allOf": [ + { + "$ref": "#/definitions/BotDownloadUrl" + } + ] + } + } + }, "Response-Map": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 372f700..926981a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -408,14 +408,14 @@ "ApiKeyAuth": [] } ], - "description": "Get a specific time by its ID", + "description": "Get a download url for the bot replay of a time by its ID if it exists", "produces": [ "application/json" ], "tags": [ "times" ], - "summary": "Get time by ID", + "summary": "Get bot download url by time ID", "parameters": [ { "type": "integer", @@ -429,11 +429,11 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/Response-Time" + "$ref": "#/definitions/Response-BotDownloadUrl" } }, "404": { - "description": "Time not found", + "description": "Bot not found", "schema": { "$ref": "#/definitions/Error" } @@ -614,6 +614,14 @@ } }, "definitions": { + "BotDownloadUrl": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + } + }, "Error": { "type": "object", "properties": { @@ -802,6 +810,19 @@ } } }, + "Response-BotDownloadUrl": { + "type": "object", + "properties": { + "data": { + "description": "Data contains the actual response payload", + "allOf": [ + { + "$ref": "#/definitions/BotDownloadUrl" + } + ] + } + } + }, "Response-Map": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6a8bb2b..335f4cb 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,5 +1,10 @@ basePath: /api/v1 definitions: + BotDownloadUrl: + properties: + url: + type: string + type: object Error: properties: error: @@ -119,6 +124,13 @@ definitions: user: $ref: '#/definitions/User' type: object + Response-BotDownloadUrl: + properties: + data: + allOf: + - $ref: '#/definitions/BotDownloadUrl' + description: Data contains the actual response payload + type: object Response-Map: properties: data: @@ -379,7 +391,8 @@ paths: - times /time/{id}: get: - description: Get a specific time by its ID + description: Get a download url for the bot replay of a time by its ID if it + exists parameters: - description: Time ID in: path @@ -392,9 +405,9 @@ paths: "200": description: OK schema: - $ref: '#/definitions/Response-Time' + $ref: '#/definitions/Response-BotDownloadUrl' "404": - description: Time not found + description: Bot not found schema: $ref: '#/definitions/Error' default: @@ -403,7 +416,7 @@ paths: $ref: '#/definitions/Error' security: - ApiKeyAuth: [] - summary: Get time by ID + summary: Get bot download url by time ID tags: - times /time/placement: From 010494ed0e35f54be5b1ba430fede7bd2e1ec686 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 23 Feb 2026 10:25:30 -0800 Subject: [PATCH 06/32] fix api --- docs/docs.go | 46 +++++++++++++++++++++++++++++++++++++++ docs/swagger.json | 46 +++++++++++++++++++++++++++++++++++++++ docs/swagger.yaml | 29 ++++++++++++++++++++++++ pkg/api/handlers/times.go | 2 +- 4 files changed, 122 insertions(+), 1 deletion(-) diff --git a/docs/docs.go b/docs/docs.go index 645829d..56ea81a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -409,6 +409,52 @@ const docTemplate = `{ } }, "/time/{id}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get a specific time by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "times" + ], + "summary": "Get time by ID", + "parameters": [ + { + "type": "integer", + "description": "Time ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Response-Time" + } + }, + "404": { + "description": "Time not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "General error response", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/time/{id}/download-url": { "get": { "security": [ { diff --git a/docs/swagger.json b/docs/swagger.json index 926981a..36dca45 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -402,6 +402,52 @@ } }, "/time/{id}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get a specific time by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "times" + ], + "summary": "Get time by ID", + "parameters": [ + { + "type": "integer", + "description": "Time ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Response-Time" + } + }, + "404": { + "description": "Time not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "General error response", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/time/{id}/download-url": { "get": { "security": [ { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 335f4cb..5d93966 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -390,6 +390,35 @@ paths: tags: - times /time/{id}: + get: + description: Get a specific time by its ID + parameters: + - description: Time ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/Response-Time' + "404": + description: Time not found + schema: + $ref: '#/definitions/Error' + default: + description: General error response + schema: + $ref: '#/definitions/Error' + security: + - ApiKeyAuth: [] + summary: Get time by ID + tags: + - times + /time/{id}/download-url: get: description: Get a download url for the bot replay of a time by its ID if it exists diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index df5b033..e25102d 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -336,7 +336,7 @@ func (h *TimesHandler) GetPlacements(ctx *gin.Context) { // @Failure 404 {object} dto.Error "Time does not have a Bot" // @Failure 404 {object} dto.Error "Bot not found" // @Failure default {object} dto.Error "General error response" -// @Router /time/{id} [get] +// @Router /time/{id}/download-url [get] func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { // Extract time ID from path parameter id := ctx.Param("id") From 79016134b62ab1a0dd2a221f8c342637f792c53e Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 23 Feb 2026 16:50:14 -0800 Subject: [PATCH 07/32] fix comment --- pkg/api/handlers/times.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index e25102d..5e428d8 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -472,7 +472,7 @@ func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { return } - // Return the time data + // Return the download url ctx.JSON(http.StatusOK, dto.Response[dto.BotDownloadUrl]{ Data: dto.BotDownloadUrl{ Url: info.Url, From 31802ac9fc30aaff734913563c6cf4ba5a9251ad Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 24 Feb 2026 07:51:53 -0800 Subject: [PATCH 08/32] use cases consistently --- pkg/api/handlers/times.go | 6 +++--- pkg/cmds/api.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index 5e428d8..fc1b495 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -25,15 +25,15 @@ type TimesHandler struct { } // NewTimesHandler creates a new TimesHandler with the provided options. -func NewTimesHandler(http_client *http.Client, storage_url string, options ...HandlerOption) (*TimesHandler, error) { +func NewTimesHandler(httpClient *http.Client, storageUrl string, options ...HandlerOption) (*TimesHandler, error) { baseHandler, err := NewHandler(options...) if err != nil { return nil, err } return &TimesHandler{ Handler: baseHandler, - client: http_client, - url: storage_url, + client: httpClient, + url: storageUrl, }, nil } diff --git a/pkg/cmds/api.go b/pkg/cmds/api.go index 053a157..c1eb691 100644 --- a/pkg/cmds/api.go +++ b/pkg/cmds/api.go @@ -57,7 +57,7 @@ func runAPI(ctx *cli.Context) error { } // Data service client - storage_url := ctx.String("storage-host") + storageUrl := ctx.String("storage-host") return api.NewRouter( api.WithContext(ctx), @@ -65,6 +65,6 @@ func runAPI(ctx *cli.Context) error { api.WithDevClient(devConn), api.WithDataClient(dataConn), api.WithHttpClient(&http.Client{}), - api.WithStorageUrl(storage_url), + api.WithStorageUrl(storageUrl), ) } From c7c64cd8f7201933078dd942f996ce528a841a65 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 24 Feb 2026 07:53:29 -0800 Subject: [PATCH 09/32] inline internal storage struct --- pkg/api/dto/times.go | 6 ------ pkg/api/handlers/times.go | 9 +++++++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pkg/api/dto/times.go b/pkg/api/dto/times.go index 4a6cf1a..f907217 100644 --- a/pkg/api/dto/times.go +++ b/pkg/api/dto/times.go @@ -46,12 +46,6 @@ type BotDownloadUrl struct { Url string `json:"url"` } // @name BotDownloadUrl -type FileInfo struct { - ID string `json:"ID"` - Created int64 `json:"Created"` - Url string `json:"Url"` -} // @name FileInfo - // FromGRPC converts a TimeResponse protobuf message to a TimeData domain object func (t *TimeData) FromGRPC(resp *times.TimeResponse) *TimeData { if resp == nil { diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index fc1b495..41f3ea6 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -457,8 +457,13 @@ func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { return } - // Decode the JSON body into the FileInfo struct. - var info dto.FileInfo + type storageResp struct { + ID string `json:"ID"` + Created int64 `json:"Created"` + Url string `json:"Url"` + } + // Decode the JSON body into the storageResp struct. + var info storageResp if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { statusCode := http.StatusInternalServerError errorMessage := "Error decoding json" From 1ab40fdc7899cf5e64f249b948bc4b0f0a2c67bd Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 24 Feb 2026 07:54:56 -0800 Subject: [PATCH 10/32] use most likely error msg in doc --- pkg/api/handlers/times.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index 41f3ea6..3a13b7a 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -332,9 +332,7 @@ func (h *TimesHandler) GetPlacements(ctx *gin.Context) { // @Security ApiKeyAuth // @Param id path int true "Time ID" // @Success 200 {object} dto.Response[dto.BotDownloadUrl] -// @Failure 404 {object} dto.Error "Time not found" // @Failure 404 {object} dto.Error "Time does not have a Bot" -// @Failure 404 {object} dto.Error "Bot not found" // @Failure default {object} dto.Error "General error response" // @Router /time/{id}/download-url [get] func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { From 69344551b91370d5e240c6c356823fb126786300 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 24 Feb 2026 07:58:30 -0800 Subject: [PATCH 11/32] there is no err --- pkg/api/handlers/times.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index 3a13b7a..fa8ea61 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -377,9 +377,7 @@ func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { ctx.JSON(statusCode, dto.Error{ Error: errorMessage, }) - log.WithError(err).Error( - "Time does not have a Bot", - ) + log.Error("Time does not have a Bot") return } From 5a9bc0ea6c0d611ca15a6042c2d37c643dda0a60 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 24 Feb 2026 08:00:01 -0800 Subject: [PATCH 12/32] use url.JoinPath --- pkg/api/handlers/times.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index fa8ea61..e9e8c5b 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "net/http" + "net/url" "strconv" "strings" @@ -406,7 +407,19 @@ func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { // fetch download url from storage service // Build the full URL. - fullURL := h.url + botData.FileID + fullURL, err := url.JoinPath(h.url, botData.FileID) + if err != nil { + statusCode := http.StatusInternalServerError + errorMessage := "Error joining Url" + + ctx.JSON(statusCode, dto.Error{ + Error: errorMessage, + }) + log.WithError(err).Error( + "Error joining Url", + ) + return + } // Create the request with the supplied context so callers can cancel it. req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil) From af1c35b6187dfaa6c2787e67253a840755cec83c Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 24 Feb 2026 08:00:29 -0800 Subject: [PATCH 13/32] there is no err --- pkg/api/handlers/times.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index e9e8c5b..e20fe3b 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -460,9 +460,7 @@ func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { ctx.JSON(statusCode, dto.Error{ Error: errorMessage, }) - log.WithError(err).Error( - "Unexpected status", - ) + log.Error("Unexpected status") return } From e9c999c7b50c9196bbf393b488eadb130310278e Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 24 Feb 2026 08:02:00 -0800 Subject: [PATCH 14/32] fix comment --- pkg/api/router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/router.go b/pkg/api/router.go index c0f4792..718fe67 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -67,7 +67,7 @@ func WithStorageUrl(storageUrl string) Option { } } -// WithStorageUrl sets the data gRPC client +// WithHttpClient sets the data http client func WithHttpClient(httpClient *http.Client) Option { return func(cfg *RouterConfig) { cfg.httpClient = httpClient From fee1b968c598d8034201fcdb2b1f5310d8b02a64 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 24 Feb 2026 08:07:10 -0800 Subject: [PATCH 15/32] respond with http 302 --- pkg/api/dto/times.go | 4 ---- pkg/api/handlers/times.go | 6 +----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/pkg/api/dto/times.go b/pkg/api/dto/times.go index f907217..6e46fc5 100644 --- a/pkg/api/dto/times.go +++ b/pkg/api/dto/times.go @@ -42,10 +42,6 @@ type TimeData struct { GameID int32 `json:"game_id"` } // @name Time -type BotDownloadUrl struct { - Url string `json:"url"` -} // @name BotDownloadUrl - // FromGRPC converts a TimeResponse protobuf message to a TimeData domain object func (t *TimeData) FromGRPC(resp *times.TimeResponse) *TimeData { if resp == nil { diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index e20fe3b..e8322ce 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -485,9 +485,5 @@ func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { } // Return the download url - ctx.JSON(http.StatusOK, dto.Response[dto.BotDownloadUrl]{ - Data: dto.BotDownloadUrl{ - Url: info.Url, - }, - }) + ctx.Redirect(http.StatusFound, info.Url) } From 30dee1ec2c4de83c6fdde98f7d50b956f1531002 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 24 Feb 2026 08:09:26 -0800 Subject: [PATCH 16/32] change endpoint --- pkg/api/handlers/times.go | 2 +- pkg/api/router.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index e8322ce..0af3b64 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -335,7 +335,7 @@ func (h *TimesHandler) GetPlacements(ctx *gin.Context) { // @Success 200 {object} dto.Response[dto.BotDownloadUrl] // @Failure 404 {object} dto.Error "Time does not have a Bot" // @Failure default {object} dto.Error "General error response" -// @Router /time/{id}/download-url [get] +// @Router /time/{id}/bot [get] func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { // Extract time ID from path parameter id := ctx.Param("id") diff --git a/pkg/api/router.go b/pkg/api/router.go index 718fe67..13936ce 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -145,7 +145,7 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) { // Auth middleware v1_bots.Use(middleware.ValidateRequest("Storage", "Read", cfg.devClient)) - v1_bots.GET("/time/:id/download-url", timesHandler.GetDownloadUrl) + v1_bots.GET("/time/:id/bot", timesHandler.GetDownloadUrl) } r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) r.GET("/", func(ctx *gin.Context) { From 322951d28b07af0ecf055035b005651048f78521 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 24 Feb 2026 08:19:33 -0800 Subject: [PATCH 17/32] fix doc --- pkg/api/handlers/times.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index 0af3b64..790d39d 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -329,10 +329,10 @@ func (h *TimesHandler) GetPlacements(ctx *gin.Context) { // @Summary Get bot download url by time ID // @Description Get a download url for the bot replay of a time by its ID if it exists // @Tags times -// @Produce json // @Security ApiKeyAuth // @Param id path int true "Time ID" -// @Success 200 {object} dto.Response[dto.BotDownloadUrl] +// @Success 302 +// @Header 302 {string} Location "Redirect URL" // @Failure 404 {object} dto.Error "Time does not have a Bot" // @Failure default {object} dto.Error "General error response" // @Router /time/{id}/bot [get] From a10a18d0a928aba93cafce58e867f543759aebc1 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 24 Feb 2026 08:19:37 -0800 Subject: [PATCH 18/32] generate --- docs/docs.go | 39 +++++++++------------------------------ docs/swagger.json | 39 +++++++++------------------------------ docs/swagger.yaml | 28 ++++++++-------------------- 3 files changed, 26 insertions(+), 80 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 56ea81a..4ae2214 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -454,7 +454,7 @@ const docTemplate = `{ } } }, - "/time/{id}/download-url": { + "/time/{id}/bot": { "get": { "security": [ { @@ -462,9 +462,6 @@ const docTemplate = `{ } ], "description": "Get a download url for the bot replay of a time by its ID if it exists", - "produces": [ - "application/json" - ], "tags": [ "times" ], @@ -479,14 +476,17 @@ const docTemplate = `{ } ], "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/Response-BotDownloadUrl" + "302": { + "description": "Found", + "headers": { + "Location": { + "type": "string", + "description": "Redirect URL" + } } }, "404": { - "description": "Bot not found", + "description": "Time does not have a Bot", "schema": { "$ref": "#/definitions/Error" } @@ -667,14 +667,6 @@ const docTemplate = `{ } }, "definitions": { - "BotDownloadUrl": { - "type": "object", - "properties": { - "url": { - "type": "string" - } - } - }, "Error": { "type": "object", "properties": { @@ -863,19 +855,6 @@ const docTemplate = `{ } } }, - "Response-BotDownloadUrl": { - "type": "object", - "properties": { - "data": { - "description": "Data contains the actual response payload", - "allOf": [ - { - "$ref": "#/definitions/BotDownloadUrl" - } - ] - } - } - }, "Response-Map": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 36dca45..710cf06 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -447,7 +447,7 @@ } } }, - "/time/{id}/download-url": { + "/time/{id}/bot": { "get": { "security": [ { @@ -455,9 +455,6 @@ } ], "description": "Get a download url for the bot replay of a time by its ID if it exists", - "produces": [ - "application/json" - ], "tags": [ "times" ], @@ -472,14 +469,17 @@ } ], "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/Response-BotDownloadUrl" + "302": { + "description": "Found", + "headers": { + "Location": { + "type": "string", + "description": "Redirect URL" + } } }, "404": { - "description": "Bot not found", + "description": "Time does not have a Bot", "schema": { "$ref": "#/definitions/Error" } @@ -660,14 +660,6 @@ } }, "definitions": { - "BotDownloadUrl": { - "type": "object", - "properties": { - "url": { - "type": "string" - } - } - }, "Error": { "type": "object", "properties": { @@ -856,19 +848,6 @@ } } }, - "Response-BotDownloadUrl": { - "type": "object", - "properties": { - "data": { - "description": "Data contains the actual response payload", - "allOf": [ - { - "$ref": "#/definitions/BotDownloadUrl" - } - ] - } - } - }, "Response-Map": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5d93966..bdf515d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,10 +1,5 @@ basePath: /api/v1 definitions: - BotDownloadUrl: - properties: - url: - type: string - type: object Error: properties: error: @@ -124,13 +119,6 @@ definitions: user: $ref: '#/definitions/User' type: object - Response-BotDownloadUrl: - properties: - data: - allOf: - - $ref: '#/definitions/BotDownloadUrl' - description: Data contains the actual response payload - type: object Response-Map: properties: data: @@ -418,7 +406,7 @@ paths: summary: Get time by ID tags: - times - /time/{id}/download-url: + /time/{id}/bot: get: description: Get a download url for the bot replay of a time by its ID if it exists @@ -428,15 +416,15 @@ paths: name: id required: true type: integer - produces: - - application/json responses: - "200": - description: OK - schema: - $ref: '#/definitions/Response-BotDownloadUrl' + "302": + description: Found + headers: + Location: + description: Redirect URL + type: string "404": - description: Bot not found + description: Time does not have a Bot schema: $ref: '#/definitions/Error' default: From c341c881e383f6e9d8b8051221e2fbb0d574f0d4 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 24 Feb 2026 08:21:26 -0800 Subject: [PATCH 19/32] set an http timeout --- pkg/cmds/api.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/cmds/api.go b/pkg/cmds/api.go index c1eb691..a0bdda5 100644 --- a/pkg/cmds/api.go +++ b/pkg/cmds/api.go @@ -58,13 +58,16 @@ func runAPI(ctx *cli.Context) error { // Data service client storageUrl := ctx.String("storage-host") + httpClient := http.Client{ + Timeout: 10, + } return api.NewRouter( api.WithContext(ctx), api.WithPort(ctx.Int("port")), api.WithDevClient(devConn), api.WithDataClient(dataConn), - api.WithHttpClient(&http.Client{}), + api.WithHttpClient(&httpClient), api.WithStorageUrl(storageUrl), ) } From 21a764f2988775629dc43f1abf4daac914c46af3 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 24 Feb 2026 14:26:25 -0800 Subject: [PATCH 20/32] fix comment --- pkg/cmds/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmds/api.go b/pkg/cmds/api.go index a0bdda5..817c99d 100644 --- a/pkg/cmds/api.go +++ b/pkg/cmds/api.go @@ -56,7 +56,7 @@ func runAPI(ctx *cli.Context) error { return err } - // Data service client + // Storage service http client storageUrl := ctx.String("storage-host") httpClient := http.Client{ Timeout: 10, From 069b1fd711967eb099697c6614fd8280fe24e4fa Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 25 Feb 2026 09:10:29 -0800 Subject: [PATCH 21/32] transient http client --- pkg/api/handlers/times.go | 11 ++++++----- pkg/api/router.go | 9 +-------- pkg/cmds/api.go | 6 ------ 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index 790d39d..2e2e7d1 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -8,6 +8,7 @@ import ( "net/url" "strconv" "strings" + "time" "git.itzana.me/strafesnet/go-grpc/bots" "git.itzana.me/strafesnet/go-grpc/times" @@ -21,19 +22,17 @@ import ( // TimesHandler handles HTTP requests related to times. type TimesHandler struct { *Handler - client *http.Client - url string + url string } // NewTimesHandler creates a new TimesHandler with the provided options. -func NewTimesHandler(httpClient *http.Client, storageUrl string, options ...HandlerOption) (*TimesHandler, error) { +func NewTimesHandler(storageUrl string, options ...HandlerOption) (*TimesHandler, error) { baseHandler, err := NewHandler(options...) if err != nil { return nil, err } return &TimesHandler{ Handler: baseHandler, - client: httpClient, url: storageUrl, }, nil } @@ -437,7 +436,9 @@ func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { } // Send the request. - resp, err := h.client.Do(req) + resp, err := (&http.Client{ + Timeout: 10 * time.Second, + }).Do(req) if err != nil { statusCode := http.StatusInternalServerError errorMessage := "Storage http request failed" diff --git a/pkg/api/router.go b/pkg/api/router.go index 13936ce..f5c3a14 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -67,13 +67,6 @@ func WithStorageUrl(storageUrl string) Option { } } -// WithHttpClient sets the data http client -func WithHttpClient(httpClient *http.Client) Option { - return func(cfg *RouterConfig) { - cfg.httpClient = httpClient - } -} - // WithShutdownTimeout sets the graceful shutdown timeout func WithShutdownTimeout(timeout time.Duration) Option { return func(cfg *RouterConfig) { @@ -92,7 +85,7 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) { } // Times handler - timesHandler, err := handlers.NewTimesHandler(cfg.httpClient, cfg.storageUrl, handlerOptions...) + timesHandler, err := handlers.NewTimesHandler(cfg.storageUrl, handlerOptions...) if err != nil { return nil, err } diff --git a/pkg/cmds/api.go b/pkg/cmds/api.go index 817c99d..8a7dbd9 100644 --- a/pkg/cmds/api.go +++ b/pkg/cmds/api.go @@ -1,8 +1,6 @@ package cmds import ( - "net/http" - "git.itzana.me/strafesnet/public-api/pkg/api" "github.com/urfave/cli/v2" "google.golang.org/grpc" @@ -58,16 +56,12 @@ func runAPI(ctx *cli.Context) error { // Storage service http client storageUrl := ctx.String("storage-host") - httpClient := http.Client{ - Timeout: 10, - } return api.NewRouter( api.WithContext(ctx), api.WithPort(ctx.Int("port")), api.WithDevClient(devConn), api.WithDataClient(dataConn), - api.WithHttpClient(&httpClient), api.WithStorageUrl(storageUrl), ) } From c8d05026163a37cc07346e37bf0ddb788f95b710 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 25 Feb 2026 09:16:57 -0800 Subject: [PATCH 22/32] use handler pattern for storage url --- pkg/api/handlers/handler.go | 11 ++++++++++- pkg/api/handlers/times.go | 6 ++---- pkg/api/router.go | 5 ++++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/pkg/api/handlers/handler.go b/pkg/api/handlers/handler.go index 71e6ffa..824a2df 100644 --- a/pkg/api/handlers/handler.go +++ b/pkg/api/handlers/handler.go @@ -2,9 +2,10 @@ package handlers import ( "fmt" + "strconv" + "github.com/gin-gonic/gin" "google.golang.org/grpc" - "strconv" ) const ( @@ -14,6 +15,7 @@ const ( // Handler is a base handler that provides common functionality for all HTTP handlers. type Handler struct { dataClient *grpc.ClientConn + storageUrl string } // HandlerOption defines a functional option for configuring a Handler @@ -26,6 +28,13 @@ func WithDataClient(dataClient *grpc.ClientConn) HandlerOption { } } +// WithStorageUrl sets the storage url +func WithStorageUrl(storageUrl string) HandlerOption { + return func(cfg *Handler) { + cfg.storageUrl = storageUrl + } +} + // NewHandler creates a new Handler with the provided options. // It requires both a datastore and an authentication service to function properly. func NewHandler(options ...HandlerOption) (*Handler, error) { diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index 2e2e7d1..40cafa3 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -22,18 +22,16 @@ import ( // TimesHandler handles HTTP requests related to times. type TimesHandler struct { *Handler - url string } // NewTimesHandler creates a new TimesHandler with the provided options. -func NewTimesHandler(storageUrl string, options ...HandlerOption) (*TimesHandler, error) { +func NewTimesHandler(options ...HandlerOption) (*TimesHandler, error) { baseHandler, err := NewHandler(options...) if err != nil { return nil, err } return &TimesHandler{ Handler: baseHandler, - url: storageUrl, }, nil } @@ -406,7 +404,7 @@ func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { // fetch download url from storage service // Build the full URL. - fullURL, err := url.JoinPath(h.url, botData.FileID) + fullURL, err := url.JoinPath(h.storageUrl, botData.FileID) if err != nil { statusCode := http.StatusInternalServerError errorMessage := "Error joining Url" diff --git a/pkg/api/router.go b/pkg/api/router.go index f5c3a14..5ba0b43 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -85,7 +85,10 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) { } // Times handler - timesHandler, err := handlers.NewTimesHandler(cfg.storageUrl, handlerOptions...) + timesHandler, err := handlers.NewTimesHandler([]handlers.HandlerOption{ + handlers.WithDataClient(cfg.dataClient), + handlers.WithStorageUrl(cfg.storageUrl), + }...) if err != nil { return nil, err } From b38ddd2faed711426f00d3fdcac827274c46bcb1 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 25 Feb 2026 18:13:50 -0800 Subject: [PATCH 23/32] omit HandlerOption slice --- pkg/api/router.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/router.go b/pkg/api/router.go index 5ba0b43..5037170 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -85,10 +85,10 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) { } // Times handler - timesHandler, err := handlers.NewTimesHandler([]handlers.HandlerOption{ + timesHandler, err := handlers.NewTimesHandler( handlers.WithDataClient(cfg.dataClient), handlers.WithStorageUrl(cfg.storageUrl), - }...) + ) if err != nil { return nil, err } From cb86bafa7ca70ef30b8ba117de8f98eb01fdfdd7 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 25 Feb 2026 18:29:22 -0800 Subject: [PATCH 24/32] update summary --- pkg/api/handlers/times.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index 40cafa3..6e016d3 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -323,7 +323,7 @@ func (h *TimesHandler) GetPlacements(ctx *gin.Context) { }) } -// @Summary Get bot download url by time ID +// @Summary Redirect to bot download url by time ID // @Description Get a download url for the bot replay of a time by its ID if it exists // @Tags times // @Security ApiKeyAuth From d3771a874a22c9802610214b110ece4a8ca88310 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 25 Feb 2026 18:31:20 -0800 Subject: [PATCH 25/32] update description --- pkg/api/handlers/times.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index 6e016d3..fc74361 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -323,8 +323,8 @@ func (h *TimesHandler) GetPlacements(ctx *gin.Context) { }) } -// @Summary Redirect to bot download url by time ID -// @Description Get a download url for the bot replay of a time by its ID if it exists +// @Summary Get redirect to bot download url by time ID +// @Description Get a HTTP 302 Redirect to the download url for the bot replay of a time by its ID if it exists // @Tags times // @Security ApiKeyAuth // @Param id path int true "Time ID" From 2ea3808f1264d36bb9cb39a3a85856d3931be5e3 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 25 Feb 2026 18:36:15 -0800 Subject: [PATCH 26/32] generate --- docs/docs.go | 4 ++-- docs/swagger.json | 4 ++-- docs/swagger.yaml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 4ae2214..7840bd3 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -461,11 +461,11 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get a download url for the bot replay of a time by its ID if it exists", + "description": "Get a HTTP 302 Redirect to the download url for the bot replay of a time by its ID if it exists", "tags": [ "times" ], - "summary": "Get bot download url by time ID", + "summary": "Get redirect to bot download url by time ID", "parameters": [ { "type": "integer", diff --git a/docs/swagger.json b/docs/swagger.json index 710cf06..1c1c7a2 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -454,11 +454,11 @@ "ApiKeyAuth": [] } ], - "description": "Get a download url for the bot replay of a time by its ID if it exists", + "description": "Get a HTTP 302 Redirect to the download url for the bot replay of a time by its ID if it exists", "tags": [ "times" ], - "summary": "Get bot download url by time ID", + "summary": "Get redirect to bot download url by time ID", "parameters": [ { "type": "integer", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index bdf515d..78d10af 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -408,8 +408,8 @@ paths: - times /time/{id}/bot: get: - description: Get a download url for the bot replay of a time by its ID if it - exists + description: Get a HTTP 302 Redirect to the download url for the bot replay + of a time by its ID if it exists parameters: - description: Time ID in: path @@ -433,7 +433,7 @@ paths: $ref: '#/definitions/Error' security: - ApiKeyAuth: [] - summary: Get bot download url by time ID + summary: Get redirect to bot download url by time ID tags: - times /time/placement: From 46ebb5574e59c096cf062255662d98ea04117a35 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 25 Feb 2026 18:41:11 -0800 Subject: [PATCH 27/32] change permission name to "Bots" --- pkg/api/router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/router.go b/pkg/api/router.go index 5037170..f3198c8 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -139,7 +139,7 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) { v1_bots := r.Group("/api/v1") { // Auth middleware - v1_bots.Use(middleware.ValidateRequest("Storage", "Read", cfg.devClient)) + v1_bots.Use(middleware.ValidateRequest("Bots", "Read", cfg.devClient)) v1_bots.GET("/time/:id/bot", timesHandler.GetDownloadUrl) } From 9df5e4a8dddef6430aad3ce7568e7520ca77a8a4 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 25 Feb 2026 18:47:34 -0800 Subject: [PATCH 28/32] rename variables --- pkg/api/router.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/api/router.go b/pkg/api/router.go index f3198c8..4878fb8 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -112,36 +112,36 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) { } docs.SwaggerInfo.BasePath = "/api/v1" - v1 := r.Group("/api/v1") + dataGroup := r.Group("/api/v1") { // Auth middleware - v1.Use(middleware.ValidateRequest("Data", "Read", cfg.devClient)) + dataGroup.Use(middleware.ValidateRequest("Data", "Read", cfg.devClient)) // Times - v1.GET("/time", timesHandler.List) - v1.GET("/time/worldrecord", timesHandler.WrList) - v1.GET("/time/placement", timesHandler.GetPlacements) - v1.GET("/time/:id", timesHandler.Get) + dataGroup.GET("/time", timesHandler.List) + dataGroup.GET("/time/worldrecord", timesHandler.WrList) + dataGroup.GET("/time/placement", timesHandler.GetPlacements) + dataGroup.GET("/time/:id", timesHandler.Get) // Users - v1.GET("/user", usersHandler.List) - v1.GET("/user/:id", usersHandler.Get) - v1.GET("/user/:id/rank", usersHandler.GetRank) + dataGroup.GET("/user", usersHandler.List) + dataGroup.GET("/user/:id", usersHandler.Get) + dataGroup.GET("/user/:id/rank", usersHandler.GetRank) // Maps - v1.GET("/map", mapsHandler.List) - v1.GET("/map/:id", mapsHandler.Get) + dataGroup.GET("/map", mapsHandler.List) + dataGroup.GET("/map/:id", mapsHandler.Get) // Rank - v1.GET("/rank", rankHandler.List) + dataGroup.GET("/rank", rankHandler.List) } - v1_bots := r.Group("/api/v1") + botsGroup := r.Group("/api/v1") { // Auth middleware - v1_bots.Use(middleware.ValidateRequest("Bots", "Read", cfg.devClient)) + botsGroup.Use(middleware.ValidateRequest("Bots", "Read", cfg.devClient)) - v1_bots.GET("/time/:id/bot", timesHandler.GetDownloadUrl) + botsGroup.GET("/time/:id/bot", timesHandler.GetDownloadUrl) } r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) r.GET("/", func(ctx *gin.Context) { From 04cfcc9ddc49424dce0b898c14fc36ecd5c068d8 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 25 Feb 2026 20:08:36 -0800 Subject: [PATCH 29/32] Fix file path for bot download --- pkg/api/handlers/times.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index fc74361..829fd6c 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -404,7 +404,7 @@ func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { // fetch download url from storage service // Build the full URL. - fullURL, err := url.JoinPath(h.storageUrl, botData.FileID) + fullURL, err := url.JoinPath(h.storageUrl, "/v1/file/", botData.FileID) if err != nil { statusCode := http.StatusInternalServerError errorMessage := "Error joining Url" From fde19a2378e9c55427624eb3a92a125ffc3cec3d Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 25 Feb 2026 20:19:17 -0800 Subject: [PATCH 30/32] Add scheme prefix to default storage host --- pkg/cmds/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmds/api.go b/pkg/cmds/api.go index 8a7dbd9..45bb48d 100644 --- a/pkg/cmds/api.go +++ b/pkg/cmds/api.go @@ -35,7 +35,7 @@ func NewApiCommand() *cli.Command { Name: "storage-host", Usage: "Host of storage", EnvVars: []string{"STORAGE_HOST"}, - Value: "storage-service:9000", + Value: "http://storage-service:9000", }, }, } From 485860ef523182ccc2e6e26a3e33a23373e21e9e Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 25 Feb 2026 20:24:42 -0800 Subject: [PATCH 31/32] Surface hidden infix --- pkg/api/handlers/times.go | 2 +- pkg/cmds/api.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index 829fd6c..fc74361 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -404,7 +404,7 @@ func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) { // fetch download url from storage service // Build the full URL. - fullURL, err := url.JoinPath(h.storageUrl, "/v1/file/", botData.FileID) + fullURL, err := url.JoinPath(h.storageUrl, botData.FileID) if err != nil { statusCode := http.StatusInternalServerError errorMessage := "Error joining Url" diff --git a/pkg/cmds/api.go b/pkg/cmds/api.go index 45bb48d..873fbc9 100644 --- a/pkg/cmds/api.go +++ b/pkg/cmds/api.go @@ -35,7 +35,7 @@ func NewApiCommand() *cli.Command { Name: "storage-host", Usage: "Host of storage", EnvVars: []string{"STORAGE_HOST"}, - Value: "http://storage-service:9000", + Value: "http://storage-service:9000/v1/file/", }, }, } From 9ca285bb86f7ba1c43d126cfa9c85b2d32b5ad1a Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Thu, 26 Feb 2026 09:43:12 -0800 Subject: [PATCH 32/32] Categorize bots requests as Data.Bots --- pkg/api/router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/router.go b/pkg/api/router.go index 4878fb8..fb09cc0 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -139,7 +139,7 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) { botsGroup := r.Group("/api/v1") { // Auth middleware - botsGroup.Use(middleware.ValidateRequest("Bots", "Read", cfg.devClient)) + botsGroup.Use(middleware.ValidateRequest("Data", "Bots", cfg.devClient)) botsGroup.GET("/time/:id/bot", timesHandler.GetDownloadUrl) }