All checks were successful
continuous-integration/drone/push Build is passing
Closes #3 Reviewed-on: #14 Co-authored-by: itzaname <me@sliving.io> Co-committed-by: itzaname <me@sliving.io>
188 lines
4.2 KiB
Go
188 lines
4.2 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"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"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// 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
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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(handlerOptions...)
|
|
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"
|
|
v1 := r.Group("/api/v1")
|
|
{
|
|
// Auth middleware
|
|
v1.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)
|
|
|
|
// Users
|
|
v1.GET("/user", usersHandler.List)
|
|
v1.GET("/user/:id", usersHandler.Get)
|
|
v1.GET("/user/:id/rank", usersHandler.GetRank)
|
|
|
|
// Maps
|
|
v1.GET("/map", mapsHandler.List)
|
|
v1.GET("/map/:id", mapsHandler.Get)
|
|
|
|
// Rank
|
|
v1.GET("/rank", rankHandler.List)
|
|
|
|
}
|
|
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)
|
|
}
|