Files
public-api/pkg/api/router.go
Rhys Lloyd 9df5e4a8dd
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
rename variables
2026-02-25 18:47:34 -08:00

208 lines
4.8 KiB
Go

package api
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"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
swaggerfiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/urfave/cli/v2"
"google.golang.org/grpc"
)
// Option defines a function that configures a Router
type Option func(*RouterConfig)
// RouterConfig holds all router configuration
type RouterConfig struct {
port int
devClient *grpc.ClientConn
dataClient *grpc.ClientConn
httpClient *http.Client
storageUrl string
context *cli.Context
shutdownTimeout time.Duration
}
// WithPort sets the port for the server£
func WithPort(port int) Option {
return func(cfg *RouterConfig) {
cfg.port = port
}
}
// WithContext sets the context for the server
func WithContext(ctx *cli.Context) Option {
return func(cfg *RouterConfig) {
cfg.context = ctx
}
}
// WithDevClient sets the dev gRPC client
func WithDevClient(conn *grpc.ClientConn) Option {
return func(cfg *RouterConfig) {
cfg.devClient = conn
}
}
// WithDataClient sets the data gRPC client
func WithDataClient(conn *grpc.ClientConn) Option {
return func(cfg *RouterConfig) {
cfg.dataClient = conn
}
}
// WithStorageUrl sets the storage url
func WithStorageUrl(storageUrl string) Option {
return func(cfg *RouterConfig) {
cfg.storageUrl = storageUrl
}
}
// WithShutdownTimeout sets the graceful shutdown timeout
func WithShutdownTimeout(timeout time.Duration) Option {
return func(cfg *RouterConfig) {
cfg.shutdownTimeout = timeout
}
}
func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) {
r := gin.Default()
r.ForwardedByClientIP = true
r.Use(gin.Logger())
r.Use(gin.Recovery())
handlerOptions := []handlers.HandlerOption{
handlers.WithDataClient(cfg.dataClient),
}
// Times handler
timesHandler, err := handlers.NewTimesHandler(
handlers.WithDataClient(cfg.dataClient),
handlers.WithStorageUrl(cfg.storageUrl),
)
if err != nil {
return nil, err
}
// Users handler
usersHandler, err := handlers.NewUserHandler(handlerOptions...)
if err != nil {
return nil, err
}
// Maps handler
mapsHandler, err := handlers.NewMapHandler(handlerOptions...)
if err != nil {
return nil, err
}
// Rank handler
rankHandler, err := handlers.NewRankHandler(handlerOptions...)
if err != nil {
return nil, err
}
docs.SwaggerInfo.BasePath = "/api/v1"
dataGroup := r.Group("/api/v1")
{
// Auth middleware
dataGroup.Use(middleware.ValidateRequest("Data", "Read", cfg.devClient))
// Times
dataGroup.GET("/time", timesHandler.List)
dataGroup.GET("/time/worldrecord", timesHandler.WrList)
dataGroup.GET("/time/placement", timesHandler.GetPlacements)
dataGroup.GET("/time/:id", timesHandler.Get)
// Users
dataGroup.GET("/user", usersHandler.List)
dataGroup.GET("/user/:id", usersHandler.Get)
dataGroup.GET("/user/:id/rank", usersHandler.GetRank)
// Maps
dataGroup.GET("/map", mapsHandler.List)
dataGroup.GET("/map/:id", mapsHandler.Get)
// Rank
dataGroup.GET("/rank", rankHandler.List)
}
botsGroup := r.Group("/api/v1")
{
// Auth middleware
botsGroup.Use(middleware.ValidateRequest("Bots", "Read", cfg.devClient))
botsGroup.GET("/time/:id/bot", timesHandler.GetDownloadUrl)
}
r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
r.GET("/", func(ctx *gin.Context) {
ctx.Redirect(http.StatusPermanentRedirect, "/docs/index.html")
})
return r, nil
}
// NewRouter creates a new router with the given options
func NewRouter(options ...Option) error {
// Default configuration
cfg := &RouterConfig{
port: 8080, // Default port
context: nil,
shutdownTimeout: 5 * time.Second,
}
// Apply options
for _, option := range options {
option(cfg)
}
// Validate configuration
if cfg.context == nil {
return errors.New("context is required")
}
if cfg.devClient == nil {
return errors.New("dev client is required")
}
routes, err := setupRoutes(cfg)
if err != nil {
return err
}
log.Info("Starting server")
return runServer(cfg.context.Context, fmt.Sprint(":", cfg.port), routes, cfg.shutdownTimeout)
}
func runServer(ctx context.Context, addr string, r *gin.Engine, shutdownTimeout time.Duration) error {
srv := &http.Server{
Addr: addr,
Handler: r,
}
// Run the server in a separate goroutine
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.WithError(err).Fatal("web server exit")
}
}()
// Wait for a shutdown signal
<-ctx.Done()
// Shutdown server gracefully
ctxShutdown, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
return srv.Shutdown(ctxShutdown)
}