upaas/internal/handlers/setup.go
clawbot b1dc8fcc4e Add CSRF protection to state-changing POST endpoints
Add gorilla/csrf middleware to protect all HTML-serving routes against
cross-site request forgery attacks. The webhook endpoint is excluded
since it uses secret-based authentication.

Changes:
- Add gorilla/csrf v1.7.3 dependency
- Add CSRF() middleware method using session secret as key
- Apply CSRF middleware to all HTML route groups in routes.go
- Pass CSRF token to all templates via addGlobals helper
- Add {{ .CSRFField }} / {{ $.CSRFField }} hidden inputs to all forms

Closes #11
2026-02-15 14:17:55 -08:00

121 lines
2.9 KiB
Go

package handlers
import (
"net/http"
"git.eeqj.de/sneak/upaas/templates"
)
const (
// minPasswordLength is the minimum required password length.
minPasswordLength = 8
)
// HandleSetupGET returns the setup page handler.
func (h *Handlers) HandleSetupGET() http.HandlerFunc {
tmpl := templates.GetParsed()
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 {
h.log.Error("template execution failed", "error", err)
http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
}
}
}
// setupFormData holds form data for the setup page.
type setupFormData struct {
username string
password string
passwordConfirm string
}
// validateSetupForm validates the setup form and returns an error message if invalid.
func validateSetupForm(formData setupFormData) string {
if formData.username == "" || formData.password == "" {
return "Username and password are required"
}
if len(formData.password) < minPasswordLength {
return "Password must be at least 8 characters"
}
if formData.password != formData.passwordConfirm {
return "Passwords do not match"
}
return ""
}
// renderSetupError renders the setup page with an error message.
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)
}
// HandleSetupPOST handles the setup form submission.
func (h *Handlers) HandleSetupPOST() http.HandlerFunc {
tmpl := templates.GetParsed()
return func(writer http.ResponseWriter, request *http.Request) {
parseErr := request.ParseForm()
if parseErr != nil {
http.Error(writer, "Bad Request", http.StatusBadRequest)
return
}
formData := setupFormData{
username: request.FormValue("username"),
password: request.FormValue("password"),
passwordConfirm: request.FormValue("password_confirm"),
}
if validationErr := validateSetupForm(formData); validationErr != "" {
h.renderSetupError(tmpl, writer, request, formData.username, validationErr)
return
}
user, createErr := h.auth.CreateUser(
request.Context(),
formData.username,
formData.password,
)
if createErr != nil {
h.log.Error("failed to create user", "error", createErr)
h.renderSetupError(tmpl, writer, request, formData.username, "Failed to create user")
return
}
sessionErr := h.auth.CreateSession(writer, request, user)
if sessionErr != nil {
h.log.Error("failed to create session", "error", sessionErr)
h.renderSetupError(
tmpl,
writer,
request,
formData.username,
"Failed to create session",
)
return
}
http.Redirect(writer, request, "/", http.StatusSeeOther)
}
}