Files
nexus/cmd/server/main.go
2026-07-14 05:53:25 -05:00

125 lines
3.3 KiB
Go

package server
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"git.arcline.it/ArclineIT/nexus/internal/config"
"git.arcline.it/ArclineIT/nexus/internal/handler"
"git.arcline.it/ArclineIT/nexus/internal/middleware"
)
// Run starts the Nexus HTTP server and blocks until shutdown.
func Run() error {
cfg := config.Load()
logger := setupLogger(cfg)
authHandler := handler.NewAuthHandler(cfg)
uiHandler, err := handler.NewUIHandler(cfg)
if err != nil {
return fmt.Errorf("initializing UI handler: %w", err)
}
mux := http.NewServeMux()
// Public API routes
mux.HandleFunc("GET /health", handler.Ready())
mux.HandleFunc("GET /ready", handler.Ready())
mux.HandleFunc("POST /auth/login", authHandler.Login())
mux.HandleFunc("POST /auth/refresh", authHandler.Refresh())
// Protected API routes (Bearer token)
mux.Handle("GET /auth/me", middleware.Authenticate(cfg)(authHandler.Me()))
// Public web UI routes
mux.HandleFunc("GET /{$}", uiHandler.Root())
mux.HandleFunc("GET /login", uiHandler.LoginPage())
mux.HandleFunc("POST /login", uiHandler.LoginSubmit())
mux.HandleFunc("GET /signup", uiHandler.SignupPage())
mux.HandleFunc("POST /signup", uiHandler.SignupSubmit())
mux.HandleFunc("GET /forgot-password", uiHandler.ForgotPasswordPage())
mux.HandleFunc("POST /forgot-password", uiHandler.ForgotPasswordSubmit())
mux.HandleFunc("GET /reset-password", uiHandler.ResetPasswordPage())
mux.HandleFunc("POST /reset-password", uiHandler.ResetPasswordSubmit())
// Protected web UI routes (cookie or Bearer)
mux.Handle("GET /dashboard", middleware.WebAuth(cfg)(uiHandler.DashboardPage()))
mux.Handle("POST /logout", middleware.WebAuth(cfg)(uiHandler.Logout()))
// Apply global middleware — outermost first
var h http.Handler = mux
h = middleware.CORS(h)
h = middleware.RequestID(h)
h = middleware.Logger(logger)(h)
h = middleware.Recoverer(logger)(h)
addr := fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port)
srv := &http.Server{
Addr: addr,
Handler: h,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
}
// Graceful shutdown
errCh := make(chan error, 1)
go func() {
logger.Info("nexus control panel starting", slog.String("addr", addr))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- err
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
select {
case sig := <-quit:
logger.Info("shutting down", slog.String("signal", sig.String()))
case err := <-errCh:
logger.Error("server error", slog.Any("error", err))
}
ctx, cancel := context.WithTimeout(context.Background(), cfg.Server.ShutdownTimeout)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
return fmt.Errorf("shutting down server: %w", err)
}
logger.Info("server stopped gracefully")
return nil
}
func setupLogger(cfg *config.Config) *slog.Logger {
var level slog.Level
switch cfg.Logging.Level {
case "debug":
level = slog.LevelDebug
case "warn":
level = slog.LevelWarn
case "error":
level = slog.LevelError
default:
level = slog.LevelInfo
}
opts := &slog.HandlerOptions{Level: level}
var h slog.Handler
if cfg.Logging.Format == "json" {
h = slog.NewJSONHandler(os.Stdout, opts)
} else {
h = slog.NewTextHandler(os.Stdout, opts)
}
return slog.New(h)
}