/* * SPDX-FileCopyrightText: 2023 jordi fita mas * SPDX-License-Identifier: AGPL-3.0-only */ package surroundings import ( "context" "net/http" "strconv" "github.com/jackc/pgx/v4" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/form" httplib "dev.tandem.ws/tandem/camper/pkg/http" "dev.tandem.ws/tandem/camper/pkg/locale" "dev.tandem.ws/tandem/camper/pkg/template" ) type AdminHandler struct { } func NewAdminHandler() *AdminHandler { return &AdminHandler{} } func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var head string head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { case "": switch r.Method { case http.MethodGet: serveHighlightsIndex(w, r, user, company, conn) case http.MethodPost: addHighlight(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) } case "new": switch r.Method { case http.MethodGet: f := newHighlightForm(company) f.MustRender(w, r, user, company) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } case "order": switch r.Method { case http.MethodPost: orderHighlights(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodPost) } default: id, err := strconv.Atoi(head) if err != nil { http.NotFound(w, r) return } f := newHighlightForm(company) if err := f.FillFromDatabase(r.Context(), conn, id); err != nil { if database.ErrorIsNotFound(err) { http.NotFound(w, r) return } panic(err) } head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { case "": switch r.Method { case http.MethodGet: f.MustRender(w, r, user, company) case http.MethodPut: editHighlight(w, r, user, company, conn, f) case http.MethodDelete: deleteHighlight(w, r, user, conn, id) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut, http.MethodDelete) } default: http.NotFound(w, r) } } }) } func orderHighlights(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } input := r.PostForm["highlight_id"] if len(input) > 0 { var ids []int for _, s := range input { if id, err := strconv.Atoi(s); err == nil { ids = append(ids, id) } else { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } } if err := conn.OrderSurroundingsHighlights(r.Context(), ids); err != nil { panic(err) } } serveHighlightsIndex(w, r, user, company, conn) } func serveHighlightsIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { highlights, err := collectHighlightEntries(r.Context(), company, conn) if err != nil { panic(err) } page := &highlightIndex{ Highlights: highlights, } page.MustRender(w, r, user, company) } type highlightIndex struct { Highlights []*highlightEntry } func (page *highlightIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "surroundings/index.gohtml", page) } type highlightEntry struct { ID int URL string Media string Name string } func collectHighlightEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*highlightEntry, error) { rows, err := conn.Query(ctx, ` select surroundings_highlight_id , '/admin/surroundings/' || surroundings_highlight_id , media.path , name from surroundings_highlight as highlight join media using (media_id) where highlight.company_id = $1 order by position , name `, company.ID) if err != nil { return nil, err } defer rows.Close() var highlights []*highlightEntry for rows.Next() { entry := &highlightEntry{} if err = rows.Scan(&entry.ID, &entry.URL, &entry.Media, &entry.Name); err != nil { return nil, err } highlights = append(highlights, entry) } return highlights, nil } func addHighlight(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { f := newHighlightForm(company) processHighlightForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { var err error if f.ID, err = tx.AddSurroundingsHighlight(ctx, company.ID, f.Media.Int(), f.Name[f.DefaultLang].Val, f.Description[f.DefaultLang].Val); err != nil { return err } return translateHighlight(ctx, tx, company, f) }) } func processHighlightForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *highlightForm, act func(context.Context, *database.Tx) error) { if err := f.Parse(r); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil { panic(err) } else if !ok { if !httplib.IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity) } f.MustRender(w, r, user, company) return } tx := conn.MustBegin(r.Context()) if err := act(r.Context(), tx); err == nil { tx.MustCommit(r.Context()) } else { if rErr := tx.Rollback(r.Context()); rErr != nil { err = rErr } panic(err) } httplib.Redirect(w, r, "/admin/surroundings", http.StatusSeeOther) } func translateHighlight(ctx context.Context, tx *database.Tx, company *auth.Company, f *highlightForm) error { for lang := range company.Locales { l := lang.String() if l == f.DefaultLang { continue } if err := tx.TranslateSurroundingsHighlight(ctx, f.ID, lang, f.Name[l].Val, f.Description[l].Val); err != nil { return err } } return nil } func editHighlight(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *highlightForm) { processHighlightForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { if err := tx.EditSurroundingsHighlight(ctx, f.ID, f.Media.Int(), f.Name[f.DefaultLang].Val, f.Description[f.DefaultLang].Val); err != nil { return err } return translateHighlight(ctx, tx, company, f) }) httplib.Redirect(w, r, "/admin/surroundings", http.StatusSeeOther) } func deleteHighlight(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn, id int) { if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } if err := conn.RemoveSurroundingsHighlight(r.Context(), id); err != nil { panic(err) } httplib.Redirect(w, r, "/admin/surroundings", http.StatusSeeOther) } type highlightForm struct { DefaultLang string ID int Media *form.Media Name form.I18nInput Description form.I18nInput } func newHighlightForm(company *auth.Company) *highlightForm { return &highlightForm{ DefaultLang: company.DefaultLanguage.String(), Media: &form.Media{ Input: &form.Input{ Name: "media", }, Label: locale.PgettextNoop("Highlight image", "input"), Prompt: locale.PgettextNoop("Set highlight image", "action"), }, Name: form.NewI18nInput(company.Locales, "name"), Description: form.NewI18nInput(company.Locales, "description"), } } func (f *highlightForm) FillFromDatabase(ctx context.Context, conn *database.Conn, id int) error { f.ID = id var name database.RecordArray var description database.RecordArray row := conn.QueryRow(ctx, ` select media_id::text , highlight.name , highlight.description::text , array_agg((lang_tag, i18n.name)) , array_agg((lang_tag, i18n.description::text)) from surroundings_highlight as highlight left join surroundings_highlight_i18n as i18n using (surroundings_highlight_id) where surroundings_highlight_id = $1 group by media_id::text , highlight.name , highlight.description::text `, pgx.QueryResultFormats{pgx.BinaryFormatCode}, id) if err := row.Scan(&f.Media.Val, &f.Name[f.DefaultLang].Val, &f.Description[f.DefaultLang].Val, &name, &description); err != nil { return err } if err := f.Name.FillArray(name); err != nil { return err } if err := f.Description.FillArray(description); err != nil { return err } return nil } func (f *highlightForm) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } f.Media.FillValue(r) f.Name.FillValue(r) f.Description.FillValue(r) return nil } func (f *highlightForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) { v := form.NewValidator(l) if v.CheckRequired(f.Media.Input, l.GettextNoop("Slide image can not be empty.")) { if _, err := v.CheckImageMedia(ctx, conn, f.Media.Input, l.GettextNoop("Slide image must be an image media type.")); err != nil { return false, err } } v.CheckRequired(f.Name[f.DefaultLang], l.GettextNoop("Name can not be empty.")) return v.AllOK, nil } func (f *highlightForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "surroundings/form.gohtml", f) }