Initial commit
CI / test (push) Canceled after 0s

This commit is contained in:
2026-09-12 22:22:17 +02:00
commit 904d14b64c
314 changed files with 31884 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
package web
import (
"bytes"
"errors"
"fmt"
"net/http"
"runtime/debug"
"strconv"
"strings"
"time"
"gardomatic.kleiax.de/lib/client"
"github.com/go-playground/form/v4"
"github.com/julienschmidt/httprouter"
"github.com/justinas/nosurf"
)
func (app *application) serverError(w http.ResponseWriter, err error) {
trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
app.logger.Error("request failed", "error", trace)
app.renderErrorPage(w, http.StatusInternalServerError, "Etwas ist schiefgelaufen", "Die Anfrage konnte nicht verarbeitet werden. Bitte versuche es später noch einmal.")
}
func (app *application) clientError(w http.ResponseWriter, status int) {
title, message := http.StatusText(status), "Die Anfrage konnte nicht verarbeitet werden."
switch status {
case http.StatusBadRequest:
title, message = "Ungültige Anfrage", "Die übermittelten Daten konnten nicht gelesen werden."
case http.StatusForbidden:
title, message = "Zugriff nicht erlaubt", "Du hast für diese Aktion nicht die erforderliche Berechtigung."
case http.StatusNotFound:
title, message = "Seite nicht gefunden", "Die gewünschte Seite oder der Eintrag existiert nicht."
case http.StatusRequestEntityTooLarge:
title, message = "Datei zu groß", "Mindestens eine hochgeladene Datei überschreitet die erlaubte Größe."
}
app.renderErrorPage(w, status, title, message)
}
func (app *application) renderErrorPage(w http.ResponseWriter, status int, title, message string) {
ts, ok := app.templateCache["error.tmpl"]
if !ok {
http.Error(w, title, status)
return
}
data := &templateData{commonTemplateData: commonTemplateData{CurrentYear: time.Now().Year(), ErrorStatus: status, ErrorTitle: title, ErrorMessage: message}}
buf := new(bytes.Buffer)
if err := ts.ExecuteTemplate(buf, "base", data.pageView("error.tmpl")); err != nil {
http.Error(w, title, status)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
_, _ = buf.WriteTo(w)
}
func (app *application) loadOptionalGarden(w http.ResponseWriter, r *http.Request, data *templateData) bool {
rawID := strings.TrimSpace(r.URL.Query().Get("garden"))
if rawID == "" {
return true
}
gardenID, err := strconv.Atoi(rawID)
if err != nil || gardenID < 1 {
app.notFound(w)
return false
}
garden, _, err := client.FromContext(r.Context()).Garden(r.Context(), gardenID)
if err != nil {
app.handleAPIError(w, r, err)
return false
}
data.Garden = &garden
return true
}
func gardenQuerySuffix(r *http.Request) string {
gardenID, err := strconv.Atoi(r.URL.Query().Get("garden"))
if err != nil || gardenID < 1 {
return ""
}
return "?garden=" + strconv.Itoa(gardenID)
}
func gardenAwarePath(path string, garden *client.Garden) string {
if garden == nil {
return path
}
return pathWithQuery(path, "garden", garden.ID)
}
func adminPath(r *http.Request, fragment string, pairs ...any) string {
if gardenID, err := strconv.Atoi(r.URL.Query().Get("garden")); err == nil && gardenID > 0 {
pairs = append(pairs, "garden", gardenID)
}
return pathWithQuery(webPath("admin"), pairs...) + fragment
}
func (app *application) notFound(w http.ResponseWriter) {
app.clientError(w, http.StatusNotFound)
}
func (app *application) newTemplateData(r *http.Request) *templateData {
data := &templateData{
commonTemplateData: commonTemplateData{
CurrentYear: time.Now().Year(),
IsAuthenticated: app.isAuthenticated(r),
CSRFToken: nosurf.Token(r),
},
adminTemplateData: adminTemplateData{TaskPriorities: []client.TaskPriority{{Name: "Niedrig", Value: -5, Active: true}, {Name: "Normal", Value: 0, Active: true}, {Name: "Erhöht", Value: 3, Active: true}, {Name: "Hoch", Value: 5, Active: true}}},
}
if user, ok := userFromContext(r.Context()); ok {
data.CurrentUser = &user
data.IsActivated = user.Activated
}
return data
}
func (app *application) loadTagSuggestions(r *http.Request, gardenID int) []string {
tags, _, err := client.FromContext(r.Context()).GardenTags(r.Context(), gardenID)
if err != nil {
app.logger.Warn("could not load optional tag suggestions", "garden_id", gardenID, "error", err)
return nil
}
return tags
}
func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) {
app.renderTemplate(w, status, page, "base", data)
}
func (app *application) renderTemplate(w http.ResponseWriter, status int, page, name string, data *templateData) {
ts, ok := app.templateCache[page]
if !ok {
err := fmt.Errorf("the template %s does not exist", page)
app.serverError(w, err)
return
}
buf := new(bytes.Buffer)
err := ts.ExecuteTemplate(buf, name, data.pageView(page))
if err != nil {
app.serverError(w, err)
return
}
w.WriteHeader(status)
_, _ = buf.WriteTo(w)
}
func (app *application) decodePostForm(r *http.Request, dst any) error {
err := r.ParseForm()
if err != nil {
return err
}
err = app.formDecoder.Decode(dst, r.PostForm)
if err != nil {
var invalidDecoderError *form.InvalidDecoderError
if errors.As(err, &invalidDecoderError) {
panic(err)
}
return err
}
return nil
}
func (app *application) readPathID(r *http.Request, name string) (int, error) {
value := httprouter.ParamsFromContext(r.Context()).ByName(name)
id, err := strconv.Atoi(value)
if err != nil || id < 1 {
return 0, fmt.Errorf("invalid %s path parameter", name)
}
return id, nil
}
func (app *application) handleAPIError(w http.ResponseWriter, r *http.Request, err error) {
var apiError *client.APIError
if errors.As(err, &apiError) {
switch apiError.StatusCode {
case http.StatusUnauthorized:
http.Redirect(w, r, webPath("login"), http.StatusSeeOther)
return
case http.StatusForbidden:
if user, ok := userFromContext(r.Context()); ok && !user.Activated {
http.Redirect(w, r, webPath("activate"), http.StatusSeeOther)
return
}
app.clientError(w, http.StatusForbidden)
return
case http.StatusNotFound:
app.notFound(w)
return
}
}
app.serverError(w, err)
}
func (app *application) isAuthenticated(r *http.Request) bool {
isAuthenticated, ok := r.Context().Value(isAuthenticatedContextKey).(bool)
if !ok {
return false
}
return isAuthenticated
}