feat: implement History API service with Swagger documentation and routing
All checks were successful
Build and Release / release (push) Successful in 1m36s
All checks were successful
Build and Release / release (push) Successful in 1m36s
This commit is contained in:
17909
FinalProject.drawio
17909
FinalProject.drawio
File diff suppressed because it is too large
Load Diff
@@ -121,6 +121,7 @@ func (s *FiberServer) SetupServer(
|
|||||||
chatbotService := services.NewChatbotService(raguRepo, usageRepo, chatRepo, raguUtils)
|
chatbotService := services.NewChatbotService(raguRepo, usageRepo, chatRepo, raguUtils)
|
||||||
statisticService := services.NewStatisticService(statisticRepo)
|
statisticService := services.NewStatisticService(statisticRepo)
|
||||||
battleReplayService := services.NewBattleReplayService(battleReplayRepo)
|
battleReplayService := services.NewBattleReplayService(battleReplayRepo)
|
||||||
|
goongService := services.NewGoongService(redis)
|
||||||
|
|
||||||
// controller setup
|
// controller setup
|
||||||
authController := controllers.NewAuthController(authService, oauth)
|
authController := controllers.NewAuthController(authService, oauth)
|
||||||
@@ -139,6 +140,7 @@ func (s *FiberServer) SetupServer(
|
|||||||
chatbotController := controllers.NewChatbotController(chatbotService)
|
chatbotController := controllers.NewChatbotController(chatbotService)
|
||||||
statisticController := controllers.NewStatisticController(statisticService)
|
statisticController := controllers.NewStatisticController(statisticService)
|
||||||
battleReplayController := controllers.NewBattleReplayController(battleReplayService)
|
battleReplayController := controllers.NewBattleReplayController(battleReplayService)
|
||||||
|
goongController := controllers.NewGoongController(goongService)
|
||||||
|
|
||||||
// route setup
|
// route setup
|
||||||
routes.AuthRoutes(s.App, authController, userRepo)
|
routes.AuthRoutes(s.App, authController, userRepo)
|
||||||
@@ -156,5 +158,6 @@ func (s *FiberServer) SetupServer(
|
|||||||
routes.ChatbotRoutes(s.App, chatbotController, userRepo)
|
routes.ChatbotRoutes(s.App, chatbotController, userRepo)
|
||||||
routes.StatisticRoutes(s.App, statisticController, userRepo)
|
routes.StatisticRoutes(s.App, statisticController, userRepo)
|
||||||
routes.BattleReplayRoutes(s.App, battleReplayController)
|
routes.BattleReplayRoutes(s.App, battleReplayController)
|
||||||
|
routes.GoongRoutes(s.App, goongController)
|
||||||
routes.NotFoundRoute(s.App)
|
routes.NotFoundRoute(s.App)
|
||||||
}
|
}
|
||||||
|
|||||||
51
docs/docs.go
51
docs/docs.go
@@ -2491,6 +2491,57 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/proxy/{path}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Transparent proxy for Goong APIs to forward body, params, headers and inject API key automatically.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json",
|
||||||
|
"application/x-protobuf"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Goong"
|
||||||
|
],
|
||||||
|
"summary": "Proxy Goong APIs",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Target URL to proxy, e.g., 'tiles.goong.io/assets/goong_map_web.json'",
|
||||||
|
"name": "path",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Resource content",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/raster-tiles/metadata": {
|
"/raster-tiles/metadata": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Retrieve map metadata",
|
"description": "Retrieve map metadata",
|
||||||
|
|||||||
@@ -2484,6 +2484,57 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/proxy/{path}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Transparent proxy for Goong APIs to forward body, params, headers and inject API key automatically.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json",
|
||||||
|
"application/x-protobuf"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Goong"
|
||||||
|
],
|
||||||
|
"summary": "Proxy Goong APIs",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Target URL to proxy, e.g., 'tiles.goong.io/assets/goong_map_web.json'",
|
||||||
|
"name": "path",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Resource content",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/raster-tiles/metadata": {
|
"/raster-tiles/metadata": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Retrieve map metadata",
|
"description": "Retrieve map metadata",
|
||||||
|
|||||||
@@ -2321,6 +2321,41 @@ paths:
|
|||||||
summary: Get commit by ID
|
summary: Get commit by ID
|
||||||
tags:
|
tags:
|
||||||
- Commits
|
- Commits
|
||||||
|
/proxy/{path}:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Transparent proxy for Goong APIs to forward body, params, headers
|
||||||
|
and inject API key automatically.
|
||||||
|
parameters:
|
||||||
|
- description: Target URL to proxy, e.g., 'tiles.goong.io/assets/goong_map_web.json'
|
||||||
|
in: path
|
||||||
|
name: path
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
- application/x-protobuf
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Resource content
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: Proxy Goong APIs
|
||||||
|
tags:
|
||||||
|
- Goong
|
||||||
/raster-tiles/{z}/{x}/{y}:
|
/raster-tiles/{z}/{x}/{y}:
|
||||||
get:
|
get:
|
||||||
description: Fetch vector or raster map tile data by Z, X, Y coordinates
|
description: Fetch vector or raster map tile data by Z, X, Y coordinates
|
||||||
|
|||||||
83
internal/controllers/goongController.go
Normal file
83
internal/controllers/goongController.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"history-api/internal/services"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GoongController interface {
|
||||||
|
Proxy(c fiber.Ctx) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type goongController struct {
|
||||||
|
goongService services.GoongService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGoongController(goongService services.GoongService) GoongController {
|
||||||
|
return &goongController{
|
||||||
|
goongService: goongService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy godoc
|
||||||
|
// @Summary Proxy Goong APIs
|
||||||
|
// @Description Transparent proxy for Goong APIs to forward body, params, headers and inject API key automatically.
|
||||||
|
// @Tags Goong
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Produce application/x-protobuf
|
||||||
|
// @Param path path string true "Target URL to proxy, e.g., 'tiles.goong.io/assets/goong_map_web.json'"
|
||||||
|
// @Success 200 {string} string "Resource content"
|
||||||
|
// @Failure 400 {string} string "Bad Request"
|
||||||
|
// @Failure 403 {string} string "Forbidden"
|
||||||
|
// @Failure 500 {string} string "Internal Server Error"
|
||||||
|
// @Router /proxy/{path} [get]
|
||||||
|
func (ctrl *goongController) Proxy(c fiber.Ctx) error {
|
||||||
|
path := c.Params("*")
|
||||||
|
if path == "" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).SendString("Invalid proxy URL format")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetURL := path
|
||||||
|
|
||||||
|
if strings.HasPrefix(targetURL, "https:/") && !strings.HasPrefix(targetURL, "https://") {
|
||||||
|
targetURL = strings.Replace(targetURL, "https:/", "https://", 1)
|
||||||
|
} else if strings.HasPrefix(targetURL, "http:/") && !strings.HasPrefix(targetURL, "http://") {
|
||||||
|
targetURL = strings.Replace(targetURL, "http:/", "http://", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Request().URI().QueryString()) > 0 {
|
||||||
|
targetURL += "?" + string(c.Request().URI().QueryString())
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := make(map[string]string)
|
||||||
|
for k, v := range c.GetReqHeaders() {
|
||||||
|
if len(v) > 0 {
|
||||||
|
headers[k] = v[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCode, respHeaders, respBody, err := ctrl.goongService.ProxyRequest(
|
||||||
|
c.Context(),
|
||||||
|
c.Method(),
|
||||||
|
targetURL,
|
||||||
|
headers,
|
||||||
|
c.Body(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(statusCode).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range respHeaders {
|
||||||
|
c.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Method() == "GET" {
|
||||||
|
c.Set("Cache-Control", "public, max-age=86400")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(statusCode).Send(respBody)
|
||||||
|
}
|
||||||
13
internal/routes/goongRoute.go
Normal file
13
internal/routes/goongRoute.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"history-api/internal/controllers"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GoongRoutes(app *fiber.App, goongController controllers.GoongController) {
|
||||||
|
api := app.Group("/proxy")
|
||||||
|
|
||||||
|
api.Get("/*", goongController.Proxy)
|
||||||
|
}
|
||||||
114
internal/services/goongService.go
Normal file
114
internal/services/goongService.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"history-api/pkg/cache"
|
||||||
|
"history-api/pkg/config"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GoongService interface {
|
||||||
|
ProxyRequest(ctx context.Context, method string, targetURL string, headers map[string]string, reqBody []byte) (int, map[string]string, []byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CacheEntry struct {
|
||||||
|
StatusCode int `json:"status_code"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
Body []byte `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type goongService struct {
|
||||||
|
redis cache.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGoongService(redis cache.Cache) GoongService {
|
||||||
|
return &goongService{
|
||||||
|
redis: redis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *goongService) ProxyRequest(ctx context.Context, method string, targetURL string, headers map[string]string, reqBody []byte) (int, map[string]string, []byte, error) {
|
||||||
|
apiKey, err := config.GetConfig("GOONG_API_KEY")
|
||||||
|
if err != nil {
|
||||||
|
return 500, nil, nil, fmt.Errorf("GOONG_API_KEY is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
fullTargetURL := targetURL
|
||||||
|
if !strings.HasPrefix(fullTargetURL, "http://") && !strings.HasPrefix(fullTargetURL, "https://") {
|
||||||
|
fullTargetURL = "https://" + fullTargetURL
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedUrl, err := url.Parse(fullTargetURL)
|
||||||
|
if err != nil || parsedUrl.Host == "" {
|
||||||
|
return 400, nil, nil, fmt.Errorf("invalid target URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(parsedUrl.Host, "goong.io") {
|
||||||
|
return 403, nil, nil, fmt.Errorf("only goong.io domains are allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
q := parsedUrl.Query()
|
||||||
|
q.Set("api_key", apiKey)
|
||||||
|
parsedUrl.RawQuery = q.Encode()
|
||||||
|
finalURL := parsedUrl.String()
|
||||||
|
|
||||||
|
cacheKey := fmt.Sprintf("goong_proxy:%s", finalURL)
|
||||||
|
|
||||||
|
if method == http.MethodGet {
|
||||||
|
var entry CacheEntry
|
||||||
|
if err := s.redis.Get(ctx, cacheKey, &entry); err == nil && len(entry.Body) > 0 {
|
||||||
|
return entry.StatusCode, entry.Headers, entry.Body, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, finalURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 500, nil, nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(reqBody) > 0 {
|
||||||
|
req.Body = io.NopCloser(bytes.NewReader(reqBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range headers {
|
||||||
|
lowerK := strings.ToLower(k)
|
||||||
|
if lowerK == "host" || lowerK == "connection" || lowerK == "accept-encoding" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 15 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 500, nil, nil, fmt.Errorf("failed to fetch from target: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return 500, nil, nil, fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
respHeaders := make(map[string]string)
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
respHeaders[k] = v[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if method == http.MethodGet && resp.StatusCode == http.StatusOK {
|
||||||
|
entry := CacheEntry{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Headers: respHeaders,
|
||||||
|
Body: respBody,
|
||||||
|
}
|
||||||
|
_ = s.redis.Set(ctx, cacheKey, entry, 24*time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.StatusCode, respHeaders, respBody, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user