Files
yovision/Sense/server/internal/platform/app.go
T

104 lines
3.0 KiB
Go

package platform
import (
"database/sql"
"io/fs"
"log/slog"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
type App struct {
config Config
database *sql.DB
logger *slog.Logger
mux *http.ServeMux
migrations []Migration
}
func NewApp(cfg Config, database *sql.DB, logger *slog.Logger) *App {
app := &App{config: cfg, database: database, logger: logger, mux: http.NewServeMux()}
app.registerPlatformRoutes()
return app
}
func (a *App) Config() Config { return a.config }
func (a *App) Database() *sql.DB { return a.database }
func (a *App) Logger() *slog.Logger { return a.logger }
func (a *App) Handle(pattern string, handler http.Handler) {
a.mux.Handle(pattern, handler)
}
func (a *App) HandleFunc(pattern string, handler http.HandlerFunc) {
a.mux.HandleFunc(pattern, handler)
}
func (a *App) Handler() http.Handler {
return requestSecurityHeaders(a.mux)
}
func (a *App) registerPlatformRoutes() {
a.mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok", "product": "sense"})
})
a.mux.HandleFunc("GET /readyz", a.ready)
a.mux.HandleFunc("GET /api/v1/system/info", func(w http.ResponseWriter, _ *http.Request) {
WriteJSON(w, http.StatusOK, map[string]any{
"product": "Sense",
"database": a.config.DatabaseMode,
"modules": []string{},
})
})
// Deliberately do not register upstream demo/code-generation routes.
if a.config.StaticDir != "" {
a.registerStaticFiles(a.config.StaticDir)
}
}
func (a *App) ready(w http.ResponseWriter, r *http.Request) {
if a.database == nil {
WriteJSON(w, http.StatusOK, map[string]string{"status": "ready", "database": "memory"})
return
}
ctx, cancel := timeLimitedContext(r, 2*time.Second)
defer cancel()
if err := a.database.PingContext(ctx); err != nil {
WriteJSON(w, http.StatusServiceUnavailable, map[string]string{"status": "not_ready", "database": "unavailable"})
return
}
WriteJSON(w, http.StatusOK, map[string]string{"status": "ready", "database": "postgres"})
}
func (a *App) registerStaticFiles(directory string) {
absolute, err := filepath.Abs(directory)
if err != nil {
a.logger.Warn("invalid UI static directory", "error", err)
return
}
fileSystem := os.DirFS(absolute)
a.mux.Handle("GET /assets/", http.StripPrefix("/", http.FileServer(http.FS(fileSystem))))
a.mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(filepath.ToSlash(filepath.Clean(r.URL.Path)), "/")
if path == "." || path == "" {
path = "index.html"
}
if _, err := fs.Stat(fileSystem, path); err != nil {
path = "index.html"
}
http.ServeFileFS(w, r, fileSystem, path)
})
}
func requestSecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Referrer-Policy", "no-referrer")
next.ServeHTTP(w, r)
})
}