160 lines
4.2 KiB
Go
160 lines
4.2 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type Options struct {
|
|
UIServeDir string
|
|
BigscreenDir string
|
|
TemplateDir string
|
|
StaticDir string
|
|
EnableCORS bool
|
|
AuthSecret string
|
|
}
|
|
|
|
func NewRouter(opts Options) *gin.Engine {
|
|
r := gin.New()
|
|
r.Use(gin.Logger())
|
|
r.Use(gin.Recovery())
|
|
|
|
if opts.EnableCORS {
|
|
r.Use(func(c *gin.Context) {
|
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
|
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
if c.Request.Method == http.MethodOptions {
|
|
c.AbortWithStatus(http.StatusNoContent)
|
|
return
|
|
}
|
|
c.Next()
|
|
})
|
|
}
|
|
|
|
if strings.TrimSpace(opts.StaticDir) != "" {
|
|
r.Static("/static", opts.StaticDir)
|
|
}
|
|
// Do not render legacy templates; keep core frontend under /ui
|
|
|
|
api := r.Group("/api")
|
|
{
|
|
api.GET("/health", handleHealth)
|
|
api.GET("/system/status", handleSystemStatus)
|
|
api.GET("/stations", handleStations)
|
|
api.GET("/data", handleData)
|
|
api.GET("/forecast", handleForecast)
|
|
api.GET("/forecast/perf", handleForecastPerf)
|
|
api.GET("/radar/times", handleRadarTimes)
|
|
api.GET("/radar/tiles_at", handleRadarTilesAt)
|
|
api.GET("/rain/times", handleRainTimes)
|
|
api.GET("/rain/tiles_at", handleRainTilesAt)
|
|
}
|
|
|
|
hasUI := strings.TrimSpace(opts.UIServeDir) != ""
|
|
if hasUI {
|
|
// Serve built Angular assets under /ui for static files
|
|
r.Static("/ui", opts.UIServeDir)
|
|
// Serve Angular index.html at root
|
|
r.GET("/", func(c *gin.Context) {
|
|
c.File(filepath.Join(opts.UIServeDir, "index.html"))
|
|
})
|
|
}
|
|
|
|
hasBigscreen := strings.TrimSpace(opts.BigscreenDir) != ""
|
|
var bigscreenIndex string
|
|
if hasBigscreen {
|
|
bigscreenDir := filepath.Clean(opts.BigscreenDir)
|
|
bigscreenIndex = filepath.Join(bigscreenDir, "index.html")
|
|
// auth guard
|
|
requireAuth := func(c *gin.Context) bool {
|
|
if strings.TrimSpace(opts.AuthSecret) == "" {
|
|
// no secret configured -> deny by default
|
|
c.Redirect(http.StatusFound, "/admin/login")
|
|
c.Abort()
|
|
return false
|
|
}
|
|
cookie, err := c.Cookie("core_session")
|
|
if err != nil || cookie == "" {
|
|
c.Redirect(http.StatusFound, "/admin/login")
|
|
c.Abort()
|
|
return false
|
|
}
|
|
if _, ok := validateSession(cookie, opts.AuthSecret); !ok {
|
|
c.Redirect(http.StatusFound, "/admin/login")
|
|
c.Abort()
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
serveBigscreenIndex := func(c *gin.Context) {
|
|
if !requireAuth(c) {
|
|
return
|
|
}
|
|
c.File(bigscreenIndex)
|
|
}
|
|
r.GET("/bigscreen", serveBigscreenIndex)
|
|
r.GET("/bigscreen/*filepath", func(c *gin.Context) {
|
|
if !requireAuth(c) {
|
|
return
|
|
}
|
|
rel := strings.TrimPrefix(c.Param("filepath"), "/")
|
|
if rel == "" {
|
|
serveBigscreenIndex(c)
|
|
return
|
|
}
|
|
full := filepath.Join(bigscreenDir, filepath.FromSlash(rel))
|
|
if !strings.HasPrefix(full, bigscreenDir+string(os.PathSeparator)) && full != bigscreenDir {
|
|
c.AbortWithStatus(http.StatusBadRequest)
|
|
return
|
|
}
|
|
if info, err := os.Stat(full); err == nil && !info.IsDir() {
|
|
c.File(full)
|
|
return
|
|
}
|
|
serveBigscreenIndex(c)
|
|
})
|
|
}
|
|
|
|
// Admin login routes
|
|
r.GET("/admin/login", func(c *gin.Context) {
|
|
if strings.TrimSpace(opts.TemplateDir) != "" {
|
|
c.File(filepath.Join(opts.TemplateDir, "login.html"))
|
|
return
|
|
}
|
|
c.String(http.StatusOK, "login page not configured")
|
|
})
|
|
r.POST("/admin/login", handleLogin(opts))
|
|
r.GET("/admin/logout", handleLogout(opts))
|
|
|
|
// Optional SPA fallback: serve index.html for non-API, non-static routes
|
|
r.NoRoute(func(c *gin.Context) {
|
|
p := c.Request.URL.Path
|
|
if strings.HasPrefix(p, "/api/") || strings.HasPrefix(p, "/static/") {
|
|
c.AbortWithStatus(http.StatusNotFound)
|
|
return
|
|
}
|
|
if hasBigscreen && strings.HasPrefix(p, "/bigscreen") {
|
|
c.File(bigscreenIndex)
|
|
return
|
|
}
|
|
if hasUI {
|
|
c.File(filepath.Join(opts.UIServeDir, "index.html"))
|
|
return
|
|
}
|
|
c.AbortWithStatus(http.StatusNotFound)
|
|
})
|
|
|
|
return r
|
|
}
|
|
|
|
// validateSession bridges to auth package without importing at top to avoid circular style concerns.
|
|
func validateSession(token, secret string) (string, bool) {
|
|
// local shim to avoid exposing auth from here
|
|
return parseToken(token, []byte(secret))
|
|
}
|