Merge branch 'main' into fix/issue-13

This commit is contained in:
2026-02-16 05:55:17 +01:00
18 changed files with 165 additions and 50 deletions

View File

@@ -29,8 +29,8 @@ const (
func (h *Handlers) HandleAppNew() http.HandlerFunc {
tmpl := templates.GetParsed()
return func(writer http.ResponseWriter, _ *http.Request) {
data := h.addGlobals(map[string]any{})
return func(writer http.ResponseWriter, request *http.Request) {
data := h.addGlobals(map[string]any{}, request)
err := tmpl.ExecuteTemplate(writer, "app_new.html", data)
if err != nil {
@@ -57,12 +57,12 @@ func (h *Handlers) HandleAppCreate() http.HandlerFunc {
branch := request.FormValue("branch")
dockerfilePath := request.FormValue("dockerfile_path")
data := map[string]any{
data := h.addGlobals(map[string]any{
"Name": name,
"RepoURL": repoURL,
"Branch": branch,
"DockerfilePath": dockerfilePath,
}
}, request)
if name == "" || repoURL == "" {
data["Error"] = "Name and repository URL are required"
@@ -150,7 +150,7 @@ func (h *Handlers) HandleAppDetail() http.HandlerFunc {
"WebhookURL": webhookURL,
"DeployKey": deployKey,
"Success": request.URL.Query().Get("success"),
})
}, request)
err := tmpl.ExecuteTemplate(writer, "app_detail.html", data)
if err != nil {
@@ -183,7 +183,7 @@ func (h *Handlers) HandleAppEdit() http.HandlerFunc {
data := h.addGlobals(map[string]any{
"App": application,
})
}, request)
err := tmpl.ExecuteTemplate(writer, "app_edit.html", data)
if err != nil {
@@ -241,10 +241,10 @@ func (h *Handlers) HandleAppUpdate() http.HandlerFunc {
if saveErr != nil {
h.log.Error("failed to update app", "error", saveErr)
data := map[string]any{
data := h.addGlobals(map[string]any{
"App": application,
"Error": "Failed to update app",
}
}, request)
_ = tmpl.ExecuteTemplate(writer, "app_edit.html", data)
return
@@ -337,7 +337,7 @@ func (h *Handlers) HandleAppDeployments() http.HandlerFunc {
data := h.addGlobals(map[string]any{
"App": application,
"Deployments": deployments,
})
}, request)
err := tmpl.ExecuteTemplate(writer, "deployments.html", data)
if err != nil {

View File

@@ -10,8 +10,8 @@ import (
func (h *Handlers) HandleLoginGET() http.HandlerFunc {
tmpl := templates.GetParsed()
return func(writer http.ResponseWriter, _ *http.Request) {
data := h.addGlobals(map[string]any{})
return func(writer http.ResponseWriter, request *http.Request) {
data := h.addGlobals(map[string]any{}, request)
err := tmpl.ExecuteTemplate(writer, "login.html", data)
if err != nil {
@@ -38,7 +38,7 @@ func (h *Handlers) HandleLoginPOST() http.HandlerFunc {
data := h.addGlobals(map[string]any{
"Username": username,
})
}, request)
if username == "" || password == "" {
data["Error"] = "Username and password are required"

View File

@@ -67,7 +67,7 @@ func (h *Handlers) HandleDashboard() http.HandlerFunc {
data := h.addGlobals(map[string]any{
"AppStats": appStats,
})
}, request)
execErr := tmpl.ExecuteTemplate(writer, "dashboard.html", data)
if execErr != nil {

View File

@@ -3,9 +3,11 @@ package handlers
import (
"encoding/json"
"html/template"
"log/slog"
"net/http"
"github.com/gorilla/csrf"
"go.uber.org/fx"
"git.eeqj.de/sneak/upaas/internal/database"
@@ -64,11 +66,18 @@ func New(_ fx.Lifecycle, params Params) (*Handlers, error) {
}, nil
}
// addGlobals adds version info to template data map.
func (h *Handlers) addGlobals(data map[string]any) map[string]any {
// addGlobals adds version info and CSRF token to template data map.
func (h *Handlers) addGlobals(
data map[string]any,
request *http.Request,
) map[string]any {
data["Version"] = h.globals.Version
data["Appname"] = h.globals.Appname
if request != nil {
data["CSRFField"] = template.HTML(csrf.TemplateField(request)) //nolint:gosec // csrf.TemplateField produces safe HTML
}
return data
}

View File

@@ -15,8 +15,8 @@ const (
func (h *Handlers) HandleSetupGET() http.HandlerFunc {
tmpl := templates.GetParsed()
return func(writer http.ResponseWriter, _ *http.Request) {
data := h.addGlobals(map[string]any{})
return func(writer http.ResponseWriter, request *http.Request) {
data := h.addGlobals(map[string]any{}, request)
err := tmpl.ExecuteTemplate(writer, "setup.html", data)
if err != nil {
@@ -54,13 +54,14 @@ func validateSetupForm(formData setupFormData) string {
func (h *Handlers) renderSetupError(
tmpl *templates.TemplateExecutor,
writer http.ResponseWriter,
request *http.Request,
username string,
errorMsg string,
) {
data := h.addGlobals(map[string]any{
"Username": username,
"Error": errorMsg,
})
}, request)
_ = tmpl.ExecuteTemplate(writer, "setup.html", data)
}
@@ -83,7 +84,7 @@ func (h *Handlers) HandleSetupPOST() http.HandlerFunc {
}
if validationErr := validateSetupForm(formData); validationErr != "" {
h.renderSetupError(tmpl, writer, formData.username, validationErr)
h.renderSetupError(tmpl, writer, request, formData.username, validationErr)
return
}
@@ -95,7 +96,7 @@ func (h *Handlers) HandleSetupPOST() http.HandlerFunc {
)
if createErr != nil {
h.log.Error("failed to create user", "error", createErr)
h.renderSetupError(tmpl, writer, formData.username, "Failed to create user")
h.renderSetupError(tmpl, writer, request, formData.username, "Failed to create user")
return
}
@@ -106,6 +107,7 @@ func (h *Handlers) HandleSetupPOST() http.HandlerFunc {
h.renderSetupError(
tmpl,
writer,
request,
formData.username,
"Failed to create session",
)

View File

@@ -10,6 +10,7 @@ import (
"github.com/99designs/basicauth-go"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/gorilla/csrf"
"go.uber.org/fx"
"git.eeqj.de/sneak/upaas/internal/config"
@@ -152,6 +153,15 @@ func (m *Middleware) SessionAuth() func(http.Handler) http.Handler {
}
}
// CSRF returns CSRF protection middleware using gorilla/csrf.
func (m *Middleware) CSRF() func(http.Handler) http.Handler {
return csrf.Protect(
[]byte(m.params.Config.SessionSecret),
csrf.Secure(false), // Allow HTTP for development; reverse proxy handles TLS
csrf.Path("/"),
)
}
// SetupRequired returns middleware that redirects to setup if no user exists.
func (m *Middleware) SetupRequired() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {

View File

@@ -37,18 +37,22 @@ func (s *Server) SetupRoutes() {
http.FileServer(http.FS(static.Static)),
))
// Public routes
s.router.Get("/login", s.handlers.HandleLoginGET())
s.router.Post("/login", s.handlers.HandleLoginPOST())
s.router.Get("/setup", s.handlers.HandleSetupGET())
s.router.Post("/setup", s.handlers.HandleSetupPOST())
// Webhook endpoint (uses secret for auth, not session)
// Webhook endpoint (uses secret for auth, not session — no CSRF)
s.router.Post("/webhook/{secret}", s.handlers.HandleWebhook())
// Protected routes (require session auth)
// All HTML-serving routes get CSRF protection
s.router.Group(func(r chi.Router) {
r.Use(s.mw.SessionAuth())
r.Use(s.mw.CSRF())
// Public routes
r.Get("/login", s.handlers.HandleLoginGET())
r.Post("/login", s.handlers.HandleLoginPOST())
r.Get("/setup", s.handlers.HandleSetupGET())
r.Post("/setup", s.handlers.HandleSetupPOST())
// Protected routes (require session auth)
r.Group(func(r chi.Router) {
r.Use(s.mw.SessionAuth())
// Dashboard
r.Get("/", s.handlers.HandleDashboard())
@@ -90,6 +94,7 @@ func (s *Server) SetupRoutes() {
// Ports
r.Post("/apps/{id}/ports", s.handlers.HandlePortAdd())
r.Post("/apps/{id}/ports/{portID}/delete", s.handlers.HandlePortDelete())
})
})
// Metrics endpoint (optional, with basic auth)