223 lines
5.9 KiB
Go
223 lines
5.9 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"git.itzana.me/StrafesNET/dev-service/pkg/api/handlers"
|
|
"git.itzana.me/StrafesNET/dev-service/pkg/api/middleware"
|
|
"git.itzana.me/StrafesNET/dev-service/pkg/authz"
|
|
"git.itzana.me/StrafesNET/dev-service/pkg/datastore"
|
|
"git.itzana.me/StrafesNET/dev-service/pkg/ratelimit"
|
|
"git.itzana.me/StrafesNET/dev-service/web"
|
|
"github.com/gin-gonic/gin"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/urfave/cli/v2"
|
|
"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
|
|
context *cli.Context
|
|
store datastore.Datastore
|
|
auth *authz.Service
|
|
limiter *ratelimit.RateLimit
|
|
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
|
|
}
|
|
}
|
|
|
|
// WithDatastore sets the datastore for the router
|
|
func WithDatastore(store datastore.Datastore) Option {
|
|
return func(cfg *RouterConfig) {
|
|
cfg.store = store
|
|
}
|
|
}
|
|
|
|
// WithShutdownTimeout sets the graceful shutdown timeout
|
|
func WithShutdownTimeout(timeout time.Duration) Option {
|
|
return func(cfg *RouterConfig) {
|
|
cfg.shutdownTimeout = timeout
|
|
}
|
|
}
|
|
|
|
// WithRateLimit sets the rate limiter for the router
|
|
func WithRateLimit(limiter *ratelimit.RateLimit) Option {
|
|
return func(cfg *RouterConfig) {
|
|
cfg.limiter = limiter
|
|
}
|
|
}
|
|
|
|
// WithAuth sets the auth for the router
|
|
func WithAuth(auth *authz.Service) Option {
|
|
return func(cfg *RouterConfig) {
|
|
cfg.auth = auth
|
|
}
|
|
}
|
|
|
|
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.WithDatastore(cfg.store),
|
|
handlers.WithAuthService(cfg.auth),
|
|
handlers.WithRatelimiter(cfg.limiter),
|
|
}
|
|
|
|
userHandler, err := handlers.NewUserHandler(handlerOptions...)
|
|
if err != nil {
|
|
log.WithError(err).Fatal("failed to create user handler")
|
|
return nil, err
|
|
}
|
|
|
|
applicationHandler, err := handlers.NewApplicationHandler(handlerOptions...)
|
|
if err != nil {
|
|
log.WithError(err).Fatal("failed to create application handler")
|
|
return nil, err
|
|
}
|
|
|
|
configHandler, err := handlers.NewConfigHandler(handlerOptions...)
|
|
if err != nil {
|
|
log.WithError(err).Fatal("failed to create config handler")
|
|
return nil, err
|
|
}
|
|
|
|
v1 := r.Group("/api")
|
|
{
|
|
// Middleware
|
|
v1.Use(middleware.UserSession(cfg.auth))
|
|
|
|
// Config endpoints
|
|
v1.GET("/config", configHandler.GetConfig)
|
|
|
|
// User endpoints
|
|
v1.GET("/user", userHandler.GetCurrentUser)
|
|
v1.GET("/user/roles", userHandler.GetCurrentRoles)
|
|
|
|
// Application endpoints
|
|
v1.GET("/application", applicationHandler.GetMyApplications)
|
|
v1.POST("/application", applicationHandler.CreateApplication)
|
|
v1.PUT("/application/:id", applicationHandler.UpdateApplication)
|
|
v1.DELETE("/application/:id", applicationHandler.DeleteApplication)
|
|
|
|
// Admin endpoints (protected by middleware)
|
|
adminHandler, err := handlers.NewAdminHandler(handlerOptions...)
|
|
if err != nil {
|
|
log.WithError(err).Fatal("failed to create admin handler")
|
|
return nil, err
|
|
}
|
|
|
|
admin := v1.Group("/admin", middleware.RequireAdmin(cfg.auth))
|
|
{
|
|
// User management
|
|
admin.GET("/user", adminHandler.ListUsers)
|
|
admin.GET("/user/:id", adminHandler.GetUser)
|
|
admin.PUT("/user/:id", adminHandler.UpdateUser)
|
|
admin.POST("/user/:id/permission/:permId", adminHandler.AddUserPermission)
|
|
admin.DELETE("/user/:id/permission/:permId", adminHandler.RemoveUserPermission)
|
|
admin.GET("/user/:id/application", adminHandler.GetUserApplications)
|
|
|
|
// Permission management
|
|
admin.GET("/permission", adminHandler.ListPermissions)
|
|
admin.POST("/permission", adminHandler.CreatePermission)
|
|
admin.PUT("/permission/:id", adminHandler.UpdatePermission)
|
|
admin.DELETE("/permission/:id", adminHandler.DeletePermission)
|
|
admin.PATCH("/permission/:id/default", adminHandler.SetPermissionDefault)
|
|
|
|
// Rate limit management
|
|
admin.GET("/rate-limit", adminHandler.ListRateLimits)
|
|
admin.POST("/rate-limit", adminHandler.CreateRateLimit)
|
|
admin.PUT("/rate-limit/:id", adminHandler.UpdateRateLimit)
|
|
admin.DELETE("/rate-limit/:id", adminHandler.DeleteRateLimit)
|
|
}
|
|
}
|
|
|
|
r.Use(middleware.RedirectIfNotLoggedIn(cfg.auth))
|
|
web.AddRoutes(r)
|
|
|
|
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.store == nil {
|
|
return errors.New("datastore is required")
|
|
}
|
|
|
|
if cfg.auth == nil {
|
|
return errors.New("auth service is required")
|
|
}
|
|
|
|
if cfg.limiter == nil {
|
|
return errors.New("rate limiter 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)
|
|
}
|