@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user