Merge branch 'main' into fix/issue-13
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user