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, loginPathForRequest(r), 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 }