Files
AbstractWizard/server/server.go
zhi 047f0b8422 feat: add CORS support via CORS_ORIGINS env var
- 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
2026-03-11 10:07:32 +00:00

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