- New CORSMiddleware in server/cors.go - Reads comma-separated origins from CORS_ORIGINS env - Empty or "*" allows all origins - Handles preflight OPTIONS requests - Wraps existing LoggingMiddleware chain
139 lines
2.9 KiB
Go
139 lines
2.9 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"AbstractWizard/audit"
|
|
)
|
|
|
|
// Mode represents the server operating mode.
|
|
type Mode int
|
|
|
|
const (
|
|
ModeInit Mode = iota // init: read/write allowed
|
|
ModeReadOnly // readonly: only reads allowed
|
|
)
|
|
|
|
func (m Mode) String() string {
|
|
switch m {
|
|
case ModeInit:
|
|
return "init"
|
|
case ModeReadOnly:
|
|
return "readonly"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// ParseMode converts a string to a Mode.
|
|
func ParseMode(s string) (Mode, bool) {
|
|
switch s {
|
|
case "init":
|
|
return ModeInit, true
|
|
case "readonly":
|
|
return ModeReadOnly, true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
// AppConfig holds environment-based configuration.
|
|
type AppConfig struct {
|
|
ConfigDir string
|
|
ListenAddr string
|
|
MaxBackups int
|
|
CORSOrigins []string
|
|
}
|
|
|
|
// Server is the main HTTP server.
|
|
type Server struct {
|
|
cfg AppConfig
|
|
audit *audit.Logger
|
|
mode Mode
|
|
modeMu sync.RWMutex
|
|
srv *http.Server
|
|
}
|
|
|
|
// New creates a new Server.
|
|
func New(cfg AppConfig, auditLog *audit.Logger) *Server {
|
|
s := &Server{
|
|
cfg: cfg,
|
|
audit: auditLog,
|
|
mode: ModeInit,
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
s.registerRoutes(mux)
|
|
|
|
s.srv = &http.Server{
|
|
Addr: cfg.ListenAddr,
|
|
Handler: CORSMiddleware(cfg.CORSOrigins, LoggingMiddleware(mux)),
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 30 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// registerRoutes sets up all API routes.
|
|
func (s *Server) registerRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /health", s.handleHealth)
|
|
mux.HandleFunc("GET /api/v1/config/{path...}", s.handleGetConfig)
|
|
mux.HandleFunc("PUT /api/v1/config/{path...}", s.handlePutConfig)
|
|
mux.HandleFunc("PATCH /api/v1/config/{path...}", s.handlePatchConfig)
|
|
mux.HandleFunc("GET /api/v1/backups/{path...}", s.handleListBackups)
|
|
mux.HandleFunc("POST /api/v1/rollback/{path...}", s.handleRollback)
|
|
mux.HandleFunc("GET /api/v1/mode", s.handleGetMode)
|
|
mux.HandleFunc("PUT /api/v1/mode", s.handleSetMode)
|
|
}
|
|
|
|
// GetMode returns the current server mode.
|
|
func (s *Server) GetMode() Mode {
|
|
s.modeMu.RLock()
|
|
defer s.modeMu.RUnlock()
|
|
return s.mode
|
|
}
|
|
|
|
// SetMode changes the server mode.
|
|
func (s *Server) SetMode(m Mode) {
|
|
s.modeMu.Lock()
|
|
defer s.modeMu.Unlock()
|
|
s.mode = m
|
|
}
|
|
|
|
// ListenAndServe starts the HTTP server and blocks until shutdown.
|
|
func (s *Server) ListenAndServe() error {
|
|
stop := make(chan os.Signal, 1)
|
|
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
log.Printf("listening on %s", s.cfg.ListenAddr)
|
|
if err := s.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
errCh <- err
|
|
}
|
|
close(errCh)
|
|
}()
|
|
|
|
select {
|
|
case sig := <-stop:
|
|
log.Printf("received signal %v, shutting down", sig)
|
|
case err := <-errCh:
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
return s.srv.Shutdown(ctx)
|
|
}
|