Files
dev-service/pkg/api/router.go
itzaname 9a913ab90b
All checks were successful
continuous-integration/drone/push Build is passing
Attempt adding admin dashboard
2026-02-25 23:07:31 -05:00

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)
}