From c0f532df4e99c9f58d247a8aeca2704f389ade20 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Tue, 8 Aug 2023 20:09:57 +0200 Subject: [PATCH] Add the pages section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For now, this is almost identical to the campsite types, but this section is for purely informational pages that have no other relation to the database than “belongs to the same company”. Part of #33. --- deploy/add_page.sql | 22 +++ deploy/edit_page.sql | 24 ++++ deploy/page.sql | 57 ++++++++ pkg/app/admin.go | 5 + pkg/app/public.go | 9 +- pkg/page/admin.go | 183 +++++++++++++++++++++++ pkg/page/public.go | 72 ++++++++++ po/ca.po | 74 ++++++++-- po/es.po | 72 ++++++++-- revert/add_page.sql | 7 + revert/edit_page.sql | 7 + revert/page.sql | 7 + sqitch.plan | 3 + test/add_page.sql | 56 ++++++++ test/edit_page.sql | 60 ++++++++ test/page.sql | 199 ++++++++++++++++++++++++++ verify/add_page.sql | 7 + verify/edit_page.sql | 7 + verify/page.sql | 19 +++ web/templates/admin/page/form.gohtml | 66 +++++++++ web/templates/admin/page/index.gohtml | 31 ++++ web/templates/public/page.gohtml | 14 ++ 22 files changed, 974 insertions(+), 27 deletions(-) create mode 100644 deploy/add_page.sql create mode 100644 deploy/edit_page.sql create mode 100644 deploy/page.sql create mode 100644 pkg/page/admin.go create mode 100644 pkg/page/public.go create mode 100644 revert/add_page.sql create mode 100644 revert/edit_page.sql create mode 100644 revert/page.sql create mode 100644 test/add_page.sql create mode 100644 test/edit_page.sql create mode 100644 test/page.sql create mode 100644 verify/add_page.sql create mode 100644 verify/edit_page.sql create mode 100644 verify/page.sql create mode 100644 web/templates/admin/page/form.gohtml create mode 100644 web/templates/admin/page/index.gohtml create mode 100644 web/templates/public/page.gohtml diff --git a/deploy/add_page.sql b/deploy/add_page.sql new file mode 100644 index 0000000..ae64cac --- /dev/null +++ b/deploy/add_page.sql @@ -0,0 +1,22 @@ +-- Deploy camper:add_page to pg +-- requires: roles +-- requires: schema_camper +-- requires: page + +begin; + +set search_path to camper, public; + +create or replace function add_page(company integer, title text, content text) returns uuid as +$$ + insert into page (company_id, title, content) + values (company, title, xmlparse (content content)) + returning slug; +$$ + language sql +; + +revoke execute on function add_page(integer, text, text) from public; +grant execute on function add_page(integer, text, text) to admin; + +commit; diff --git a/deploy/edit_page.sql b/deploy/edit_page.sql new file mode 100644 index 0000000..4a423be --- /dev/null +++ b/deploy/edit_page.sql @@ -0,0 +1,24 @@ +-- Deploy camper:edit_page to pg +-- requires: roles +-- requires: schema_camper +-- requires: page + +begin; + +set search_path to camper, public; + +create or replace function edit_page(slug uuid, title text, content text) returns uuid as +$$ + update page + set title = edit_page.title + , content = xmlparse(content edit_page.content) + where slug = edit_page.slug + returning slug; +$$ + language sql +; + +revoke execute on function edit_page(uuid, text, text) from public; +grant execute on function edit_page(uuid, text, text) to admin; + +commit; diff --git a/deploy/page.sql b/deploy/page.sql new file mode 100644 index 0000000..028c350 --- /dev/null +++ b/deploy/page.sql @@ -0,0 +1,57 @@ +-- Deploy camper:page to pg +-- requires: roles +-- requires: schema_camper +-- requires: company +-- requires: user_profile + +begin; + +set search_path to camper, public; + +create table page ( + page_id serial primary key, + company_id integer not null references company, + slug uuid not null unique default gen_random_uuid(), + title text not null constraint title_not_empty check(length(trim(title)) > 0), + content xml not null default '' +); + +grant select on table page to guest; +grant select on table page to employee; +grant select, insert, update, delete on table page to admin; + +grant usage on sequence page_page_id_seq to admin; + +alter table page enable row level security; + +create policy guest_ok +on page +for select +using (true) +; + +create policy insert_to_company +on page +for insert +with check ( + company_id in (select company_id from user_profile) +) +; + +create policy update_company +on page +for update +using ( + company_id in (select company_id from user_profile) +) +; + +create policy delete_from_company +on page +for delete +using ( + company_id in (select company_id from user_profile) +) +; + +commit; diff --git a/pkg/app/admin.go b/pkg/app/admin.go index a2ec2ce..2165f90 100644 --- a/pkg/app/admin.go +++ b/pkg/app/admin.go @@ -12,16 +12,19 @@ import ( "dev.tandem.ws/tandem/camper/pkg/campsite" "dev.tandem.ws/tandem/camper/pkg/database" httplib "dev.tandem.ws/tandem/camper/pkg/http" + "dev.tandem.ws/tandem/camper/pkg/page" "dev.tandem.ws/tandem/camper/pkg/template" ) type adminHandler struct { campsite *campsite.AdminHandler + page *page.AdminHandler } func newAdminHandler() *adminHandler { return &adminHandler{ campsite: campsite.NewAdminHandler(), + page: page.NewAdminHandler(), } } @@ -43,6 +46,8 @@ func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *data switch head { case "campsites": h.campsite.Handler(user, company, conn).ServeHTTP(w, r) + case "pages": + h.page.Handler(user, company, conn).ServeHTTP(w, r) case "": switch r.Method { case http.MethodGet: diff --git a/pkg/app/public.go b/pkg/app/public.go index b17fc51..37a3255 100644 --- a/pkg/app/public.go +++ b/pkg/app/public.go @@ -12,16 +12,19 @@ import ( "dev.tandem.ws/tandem/camper/pkg/campsite" "dev.tandem.ws/tandem/camper/pkg/database" httplib "dev.tandem.ws/tandem/camper/pkg/http" + "dev.tandem.ws/tandem/camper/pkg/page" "dev.tandem.ws/tandem/camper/pkg/template" ) type publicHandler struct { campsite *campsite.PublicHandler + page *page.PublicHandler } func newPublicHandler() *publicHandler { return &publicHandler{ campsite: campsite.NewPublicHandler(), + page: page.NewPublicHandler(), } } @@ -31,10 +34,12 @@ func (h *publicHandler) Handler(user *auth.User, company *auth.Company, conn *da head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { case "": - page := newHomePage() - page.MustRender(w, r, user, company, conn) + home := newHomePage() + home.MustRender(w, r, user, company, conn) case "campsites": h.campsite.Handler(user, company, conn).ServeHTTP(w, r) + case "pages": + h.page.Handler(user, company, conn).ServeHTTP(w, r) default: http.NotFound(w, r) } diff --git a/pkg/page/admin.go b/pkg/page/admin.go new file mode 100644 index 0000000..c512d8c --- /dev/null +++ b/pkg/page/admin.go @@ -0,0 +1,183 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package page + +import ( + "context" + "net/http" + + "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" + "dev.tandem.ws/tandem/camper/pkg/uuid" +) + +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 "new": + switch r.Method { + case http.MethodGet: + f := newPageForm() + f.MustRender(w, r, user, company) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) + } + case "": + switch r.Method { + case http.MethodGet: + servePageIndex(w, r, user, company, conn) + case http.MethodPost: + f := newPageForm() + f.Handle(w, r, user, company, func(ctx context.Context) { + conn.MustExec(ctx, "select add_page($1, $2, $3)", company.ID, f.Title, f.Content) + }) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) + } + default: + if !uuid.Valid(head) { + http.NotFound(w, r) + return + } + f := newPageForm() + if err := f.FillFromDatabase(r.Context(), conn, head); err != nil { + if database.ErrorIsNotFound(err) { + http.NotFound(w, r) + return + } + panic(err) + } + switch r.Method { + case http.MethodGet: + f.MustRender(w, r, user, company) + case http.MethodPut: + f.Handle(w, r, user, company, func(ctx context.Context) { + conn.MustExec(ctx, "select edit_page($1, $2, $3)", f.Slug, f.Title, f.Content) + }) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) + } + } + }) +} + +func servePageIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + pages, err := collectPageEntries(r.Context(), company, conn) + if err != nil { + panic(err) + } + page := &pageIndex{ + Pages: pages, + } + page.MustRender(w, r, user, company) +} + +type pageEntry struct { + Slug string + Title string +} + +func collectPageEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*pageEntry, error) { + rows, err := conn.Query(ctx, "select slug, title from page where company_id = $1", company.ID) + if err != nil { + return nil, err + } + defer rows.Close() + + var types []*pageEntry + for rows.Next() { + entry := &pageEntry{} + if err = rows.Scan(&entry.Slug, &entry.Title); err != nil { + return nil, err + } + types = append(types, entry) + } + + return types, nil +} + +type pageIndex struct { + Pages []*pageEntry +} + +func (index *pageIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "page/index.gohtml", index) +} + +type pageForm struct { + Slug string + Title *form.Input + Content *form.Input +} + +func newPageForm() *pageForm { + return &pageForm{ + Title: &form.Input{ + Name: "title", + }, + Content: &form.Input{ + Name: "content", + }, + } +} + +func (f *pageForm) FillFromDatabase(ctx context.Context, conn *database.Conn, slug string) error { + f.Slug = slug + row := conn.QueryRow(ctx, "select title, content from page where slug = $1", slug) + return row.Scan(&f.Title.Val, &f.Content.Val) +} + +func (f *pageForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.Title.FillValue(r) + f.Content.FillValue(r) + return nil +} + +func (f *pageForm) Valid(l *locale.Locale) bool { + v := form.NewValidator(l) + v.CheckRequired(f.Title, l.GettextNoop("Title can not be empty.")) + return v.AllOK +} + +func (f *pageForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "page/form.gohtml", f) +} + +func (f *pageForm) Handle(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, exec func(ctx context.Context)) { + 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 !f.Valid(user.Locale) { + if !httplib.IsHTMxRequest(r) { + w.WriteHeader(http.StatusUnprocessableEntity) + } + f.MustRender(w, r, user, company) + return + } + exec(r.Context()) + httplib.Redirect(w, r, "/admin/pages", http.StatusSeeOther) +} diff --git a/pkg/page/public.go b/pkg/page/public.go new file mode 100644 index 0000000..851dd0e --- /dev/null +++ b/pkg/page/public.go @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package page + +import ( + "context" + gotemplate "html/template" + "net/http" + + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/database" + httplib "dev.tandem.ws/tandem/camper/pkg/http" + "dev.tandem.ws/tandem/camper/pkg/template" + "dev.tandem.ws/tandem/camper/pkg/uuid" +) + +type PublicHandler struct { +} + +func NewPublicHandler() *PublicHandler { + return &PublicHandler{} +} + +func (h *PublicHandler) 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 r.Method { + case http.MethodGet: + if !uuid.Valid(head) { + http.NotFound(w, r) + return + } + page, err := newPublicPage(r.Context(), conn, head) + if database.ErrorIsNotFound(err) { + http.NotFound(w, r) + } else if err != nil { + panic(err) + } + page.MustRender(w, r, user, company, conn) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) + } + }) +} + +type publicPage struct { + *template.PublicPage + Title string + Content gotemplate.HTML +} + +func newPublicPage(ctx context.Context, conn *database.Conn, slug string) (*publicPage, error) { + page := &publicPage{ + PublicPage: template.NewPublicPage(), + } + row := conn.QueryRow(ctx, "select title, content::text from page where slug = $1", slug) + if err := row.Scan(&page.Title, &page.Content); err != nil { + return nil, err + } + + return page, nil +} + +func (p *publicPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + p.Setup(r, user, company, conn) + template.MustRenderPublic(w, r, user, company, "page.gohtml", p) +} diff --git a/po/ca.po b/po/ca.po index b67c1e2..903c9c9 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-08-08 02:43+0200\n" +"POT-Creation-Date: 2023-08-08 20:04+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -35,6 +35,60 @@ msgstr "Salta al contingut principal" msgid "Singular Lodges" msgstr "Allotjaments singulars" +#: web/templates/admin/page/form.gohtml:16 +#: web/templates/admin/page/form.gohtml:33 +msgctxt "title" +msgid "Edit Page" +msgstr "Edició de pàgina" + +#: web/templates/admin/page/form.gohtml:18 +#: web/templates/admin/page/form.gohtml:35 +msgctxt "title" +msgid "New Page" +msgstr "Nova pàgina" + +#: web/templates/admin/page/form.gohtml:42 +msgctxt "input" +msgid "Title" +msgstr "Títol" + +#: web/templates/admin/page/form.gohtml:50 +msgctxt "input" +msgid "Content" +msgstr "Contingut" + +#: web/templates/admin/page/form.gohtml:59 +#: web/templates/admin/campsite/type/new.gohtml:59 +msgctxt "action" +msgid "Update" +msgstr "Actualitza" + +#: web/templates/admin/page/form.gohtml:61 +#: web/templates/admin/campsite/type/new.gohtml:61 +msgctxt "action" +msgid "Add" +msgstr "Afegeix" + +#: web/templates/admin/page/index.gohtml:6 +#: web/templates/admin/page/index.gohtml:12 +msgctxt "title" +msgid "Pages" +msgstr "Pàgines" + +#: web/templates/admin/page/index.gohtml:11 +msgctxt "action" +msgid "Add Page" +msgstr "Afegeix pàgina" + +#: web/templates/admin/page/index.gohtml:17 +msgctxt "header" +msgid "Title" +msgstr "Títol" + +#: web/templates/admin/page/index.gohtml:29 +msgid "No pages added yet." +msgstr "No s’ha afegit cap pàgina encara." + #: web/templates/admin/campsite/type/new.gohtml:16 #: web/templates/admin/campsite/type/new.gohtml:33 msgctxt "title" @@ -58,16 +112,6 @@ msgctxt "input" msgid "Description" msgstr "Descripció" -#: web/templates/admin/campsite/type/new.gohtml:59 -msgctxt "action" -msgid "Update" -msgstr "Actualitza" - -#: web/templates/admin/campsite/type/new.gohtml:61 -msgctxt "action" -msgid "Add" -msgstr "Afegeix" - #: web/templates/admin/campsite/type/index.gohtml:6 #: web/templates/admin/campsite/type/index.gohtml:12 msgctxt "title" @@ -155,9 +199,13 @@ msgctxt "action" msgid "Logout" msgstr "Surt" +#: pkg/page/admin.go:157 +msgid "Title can not be empty." +msgstr "No podeu deixar el títol en blanc." + #: pkg/app/login.go:56 pkg/app/user.go:246 msgid "Email can not be empty." -msgstr "No podeu deixar el correu en blanc." +msgstr "No podeu deixar el correu-e en blanc." #: pkg/app/login.go:57 pkg/app/user.go:247 msgid "This email is not valid. It should be like name@domain.com." @@ -192,7 +240,7 @@ msgstr "L’idioma escollit no és vàlid." msgid "File must be a valid PNG or JPEG image." msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida." -#: pkg/app/admin.go:37 +#: pkg/app/admin.go:40 msgid "Access forbidden" msgstr "Accés prohibit" diff --git a/po/es.po b/po/es.po index 510854d..ec39984 100644 --- a/po/es.po +++ b/po/es.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-08-08 02:43+0200\n" +"POT-Creation-Date: 2023-08-08 20:04+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -35,6 +35,60 @@ msgstr "Saltar al contenido principal" msgid "Singular Lodges" msgstr "Alojamientos singulares" +#: web/templates/admin/page/form.gohtml:16 +#: web/templates/admin/page/form.gohtml:33 +msgctxt "title" +msgid "Edit Page" +msgstr "Edición de página" + +#: web/templates/admin/page/form.gohtml:18 +#: web/templates/admin/page/form.gohtml:35 +msgctxt "title" +msgid "New Page" +msgstr "Nueva página" + +#: web/templates/admin/page/form.gohtml:42 +msgctxt "input" +msgid "Title" +msgstr "Título" + +#: web/templates/admin/page/form.gohtml:50 +msgctxt "input" +msgid "Content" +msgstr "Contenido" + +#: web/templates/admin/page/form.gohtml:59 +#: web/templates/admin/campsite/type/new.gohtml:59 +msgctxt "action" +msgid "Update" +msgstr "Actualitzar" + +#: web/templates/admin/page/form.gohtml:61 +#: web/templates/admin/campsite/type/new.gohtml:61 +msgctxt "action" +msgid "Add" +msgstr "Añadir" + +#: web/templates/admin/page/index.gohtml:6 +#: web/templates/admin/page/index.gohtml:12 +msgctxt "title" +msgid "Pages" +msgstr "Páginas" + +#: web/templates/admin/page/index.gohtml:11 +msgctxt "action" +msgid "Add Page" +msgstr "Añadir página" + +#: web/templates/admin/page/index.gohtml:17 +msgctxt "header" +msgid "Title" +msgstr "Título" + +#: web/templates/admin/page/index.gohtml:29 +msgid "No pages added yet." +msgstr "No se ha añadido ninguna página todavía." + #: web/templates/admin/campsite/type/new.gohtml:16 #: web/templates/admin/campsite/type/new.gohtml:33 msgctxt "title" @@ -58,16 +112,6 @@ msgctxt "input" msgid "Description" msgstr "Descripción" -#: web/templates/admin/campsite/type/new.gohtml:59 -msgctxt "action" -msgid "Update" -msgstr "Actualitzar" - -#: web/templates/admin/campsite/type/new.gohtml:61 -msgctxt "action" -msgid "Add" -msgstr "Añadir" - #: web/templates/admin/campsite/type/index.gohtml:6 #: web/templates/admin/campsite/type/index.gohtml:12 msgctxt "title" @@ -155,6 +199,10 @@ msgctxt "action" msgid "Logout" msgstr "Salir" +#: pkg/page/admin.go:157 +msgid "Title can not be empty." +msgstr "No podéis dejar el título en blanco." + #: pkg/app/login.go:56 pkg/app/user.go:246 msgid "Email can not be empty." msgstr "No podéis dejar el correo-e en blanco." @@ -192,7 +240,7 @@ msgstr "El idioma escogido no es válido." msgid "File must be a valid PNG or JPEG image." msgstr "El archivo tiene que ser una imagen PNG o JPEG válida." -#: pkg/app/admin.go:37 +#: pkg/app/admin.go:40 msgid "Access forbidden" msgstr "Acceso prohibido" diff --git a/revert/add_page.sql b/revert/add_page.sql new file mode 100644 index 0000000..9551278 --- /dev/null +++ b/revert/add_page.sql @@ -0,0 +1,7 @@ +-- Revert camper:add_page from pg + +begin; + +drop function if exists camper.add_page(integer, text, text); + +commit; diff --git a/revert/edit_page.sql b/revert/edit_page.sql new file mode 100644 index 0000000..3e513d4 --- /dev/null +++ b/revert/edit_page.sql @@ -0,0 +1,7 @@ +-- Revert camper:edit_page from pg + +begin; + +drop function if exists camper.edit_page(uuid, text, text); + +commit; diff --git a/revert/page.sql b/revert/page.sql new file mode 100644 index 0000000..7ea87ff --- /dev/null +++ b/revert/page.sql @@ -0,0 +1,7 @@ +-- Revert camper:page from pg + +begin; + +drop table if exists camper.page; + +commit; diff --git a/sqitch.plan b/sqitch.plan index c932fb2..52c4447 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -42,3 +42,6 @@ change_password [roles schema_auth schema_camper user] 2023-07-21T23:54:52Z jord campsite_type [roles schema_camper company user_profile] 2023-07-31T11:20:29Z jordi fita mas # Add relation of campsite type add_campsite_type [roles schema_camper campsite_type company] 2023-08-04T16:14:48Z jordi fita mas # Add function to create campsite types edit_campsite_type [roles schema_camper campsite_type company] 2023-08-07T22:21:34Z jordi fita mas # Add function to edit campsite types +page [roles schema_camper company user_profile] 2023-08-08T15:50:26Z jordi fita mas # Add relation for public page +add_page [roles schema_camper page] 2023-08-08T17:20:15Z jordi fita mas # Add function to create pages +edit_page [roles schema_camper page] 2023-08-08T17:26:51Z jordi fita mas # Add function to edit pages diff --git a/test/add_page.sql b/test/add_page.sql new file mode 100644 index 0000000..26cd2bf --- /dev/null +++ b/test/add_page.sql @@ -0,0 +1,56 @@ +-- Test add_page +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +set search_path to camper, public; + +select plan(12); + +select has_function('camper', 'add_page', array ['integer', 'text', 'text']); +select function_lang_is('camper', 'add_page', array ['integer', 'text', 'text'], 'sql'); +select function_returns('camper', 'add_page', array ['integer', 'text', 'text'], 'uuid'); +select isnt_definer('camper', 'add_page', array ['integer', 'text', 'text']); +select volatility_is('camper', 'add_page', array ['integer', 'text', 'text'], 'volatile'); +select function_privs_are('camper', 'add_page', array ['integer', 'text', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'add_page', array ['integer', 'text', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'add_page', array ['integer', 'text', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'add_page', array ['integer', 'text', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate page cascade; +truncate company cascade; +reset client_min_messages; + + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca') + , (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca') +; + +select lives_ok( + $$ select add_page(1, 'Page A', '

This is what, exactly?

Dunno

') $$, + 'Should be able to add a page to the first company' +); + +select lives_ok( + $$ select add_page(2, 'Page B', '') $$, + 'Should be able to add a page to the second company' +); + +select bag_eq( + $$ select company_id, title, content::text from page $$, + $$ values (1, 'Page A', '

This is what, exactly?

Dunno

') + , (2, 'Page B', '') + $$, + 'Should have added all two pages' +); + + +select * +from finish(); + +rollback; diff --git a/test/edit_page.sql b/test/edit_page.sql new file mode 100644 index 0000000..19a331b --- /dev/null +++ b/test/edit_page.sql @@ -0,0 +1,60 @@ +-- Test edit_page +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +set search_path to camper, public; + +select plan(12); + +select has_function('camper', 'edit_page', array ['uuid', 'text', 'text']); +select function_lang_is('camper', 'edit_page', array ['uuid', 'text', 'text'], 'sql'); +select function_returns('camper', 'edit_page', array ['uuid', 'text', 'text'], 'uuid'); +select isnt_definer('camper', 'edit_page', array ['uuid', 'text', 'text']); +select volatility_is('camper', 'edit_page', array ['uuid', 'text', 'text'], 'volatile'); +select function_privs_are('camper', 'edit_page', array ['uuid', 'text', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'edit_page', array ['uuid', 'text', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'edit_page', array ['uuid', 'text', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'edit_page', array ['uuid', 'text', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate page cascade; +truncate company cascade; +reset client_min_messages; + + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca') +; + +insert into page (company_id, slug, title, content) +values (1, '87452b88-b48f-48d3-bb6c-0296de64164e', 'Page A', '

A

') + , (1, '9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Page B', '

B

') +; + +select lives_ok( + $$ select edit_page('87452b88-b48f-48d3-bb6c-0296de64164e', 'Page 1', '

1

') $$, + 'Should be able to edit the first type' +); + +select lives_ok( + $$ select edit_page('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Page 2', '

2

') $$, + 'Should be able to edit the second type' +); + +select bag_eq( + $$ select slug::text, title, content::text from page $$, + $$ values ('87452b88-b48f-48d3-bb6c-0296de64164e', 'Page 1', '

1

') + , ('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Page 2', '

2

') + $$, + 'Should have updated all pages.' +); + + +select * +from finish(); + +rollback; diff --git a/test/page.sql b/test/page.sql new file mode 100644 index 0000000..f502263 --- /dev/null +++ b/test/page.sql @@ -0,0 +1,199 @@ +-- Test page +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(51); + +set search_path to camper, public; + +select has_table('page'); +select has_pk('page'); +select table_privs_are('page', 'guest', array['SELECT']); +select table_privs_are('page', 'employee', array['SELECT']); +select table_privs_are('page', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('page', 'authenticator', array[]::text[]); + +select has_sequence('page_page_id_seq'); +select sequence_privs_are('page_page_id_seq', 'guest', array[]::text[]); +select sequence_privs_are('page_page_id_seq', 'employee', array[]::text[]); +select sequence_privs_are('page_page_id_seq', 'admin', array['USAGE']); +select sequence_privs_are('page_page_id_seq', 'authenticator', array[]::text[]); + +select has_column('page', 'page_id'); +select col_is_pk('page', 'page_id'); +select col_type_is('page', 'page_id', 'integer'); +select col_not_null('page', 'page_id'); +select col_has_default('page', 'page_id'); +select col_default_is('page', 'page_id', 'nextval(''page_page_id_seq''::regclass)'); + +select has_column('page', 'company_id'); +select col_is_fk('page', 'company_id'); +select fk_ok('page', 'company_id', 'company', 'company_id'); +select col_type_is('page', 'company_id', 'integer'); +select col_not_null('page', 'company_id'); +select col_hasnt_default('page', 'company_id'); + +select has_column('page', 'slug'); +select col_is_unique('page', 'slug'); +select col_type_is('page', 'slug', 'uuid'); +select col_not_null('page', 'slug'); +select col_has_default('page', 'slug'); +select col_default_is('page', 'slug', 'gen_random_uuid()'); + +select has_column('page', 'title'); +select col_type_is('page', 'title', 'text'); +select col_not_null('page', 'title'); +select col_hasnt_default('page', 'title'); + +select has_column('page', 'content'); +select col_type_is('page', 'content', 'xml'); +select col_not_null('page', 'content'); +select col_has_default('page', 'content'); +--select col_default_is('page', 'description', ''); + + +set client_min_messages to warning; +truncate page cascade; +truncate company_host cascade; +truncate company_user cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + +insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month') + , (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month') +; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca') +; + +insert into company_user (company_id, user_id, role) +values (2, 1, 'admin') + , (4, 5, 'admin') +; + +insert into company_host (company_id, host) +values (2, 'co2') + , (4, 'co4') +; + +insert into page (company_id, title) +values (2, 'Front') + , (4, 'Back') +; + +prepare page_data as +select company_id, title +from page +order by company_id, title; + +set role guest; +select bag_eq( + 'page_data', + $$ values (2, 'Front') + , (4, 'Back') + $$, + 'Everyone should be able to list all pages across all companies' +); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select lives_ok( + $$ insert into page(company_id, title) values (2, 'Another page' ) $$, + 'Admin from company 2 should be able to insert a new page to that company.' +); + +select bag_eq( + 'page_data', + $$ values (2, 'Front') + , (2, 'Another page') + , (4, 'Back') + $$, + 'The new row should have been added' +); + +select lives_ok( + $$ update page set title = 'Another' where company_id = 2 and title = 'Another page' $$, + 'Admin from company 2 should be able to update pages of that company.' +); + +select bag_eq( + 'page_data', + $$ values (2, 'Front') + , (2, 'Another') + , (4, 'Back') + $$, + 'The row should have been updated.' +); + +select lives_ok( + $$ delete from page where company_id = 2 and title = 'Another' $$, + 'Admin from company 2 should be able to delete pages from that company.' +); + +select bag_eq( + 'page_data', + $$ values (2, 'Front') + , (4, 'Back') + $$, + 'The row should have been deleted.' +); + +select throws_ok( + $$ insert into page (company_id, title) values (4, 'Another page' ) $$, + '42501', 'new row violates row-level security policy for table "page"', + 'Admin from company 2 should NOT be able to insert new pages to company 4.' +); + +select lives_ok( + $$ update page set title = 'Nope' where company_id = 4 $$, + 'Admin from company 2 should not be able to update new pages of company 4, but no error if company_id is not changed.' +); + +select bag_eq( + 'page_data', + $$ values (2, 'Front') + , (4, 'Back') + $$, + 'No row should have been changed.' +); + +select throws_ok( + $$ update page set company_id = 4 where company_id = 2 $$, + '42501', 'new row violates row-level security policy for table "page"', + 'Admin from company 2 should NOT be able to move pages to company 4' +); + +select lives_ok( + $$ delete from page where company_id = 4 $$, + 'Admin from company 2 should NOT be able to delete pages from company 4, but not error is thrown' +); + +select bag_eq( + 'page_data', + $$ values (2, 'Front') + , (4, 'Back') + $$, + 'No row should have been changed' +); + +select throws_ok( + $$ insert into page (company_id, title) values (2, ' ' ) $$, + '23514', 'new row for relation "page" violates check constraint "title_not_empty"', + 'Should not be able to insert pages with a blank title.' +); + + +reset role; +select * +from finish(); + +rollback; + diff --git a/verify/add_page.sql b/verify/add_page.sql new file mode 100644 index 0000000..36bc9ee --- /dev/null +++ b/verify/add_page.sql @@ -0,0 +1,7 @@ +-- Verify camper:add_page on pg + +begin; + +select has_function_privilege('camper.add_page(integer, text, text)', 'execute'); + +rollback; diff --git a/verify/edit_page.sql b/verify/edit_page.sql new file mode 100644 index 0000000..016d310 --- /dev/null +++ b/verify/edit_page.sql @@ -0,0 +1,7 @@ +-- Verify camper:edit_page on pg + +begin; + +select has_function_privilege('camper.edit_page(uuid, text, text)', 'execute'); + +rollback; diff --git a/verify/page.sql b/verify/page.sql new file mode 100644 index 0000000..342dc21 --- /dev/null +++ b/verify/page.sql @@ -0,0 +1,19 @@ +-- Verify camper:page on pg + +begin; + +select page_id + , company_id + , slug + , title + , content +from camper.page +where false; + +select 1 / count(*) from pg_class where oid = 'camper.page'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.page'::regclass; +select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.page'::regclass; +select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.page'::regclass; +select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.page'::regclass; + +rollback; diff --git a/web/templates/admin/page/form.gohtml b/web/templates/admin/page/form.gohtml new file mode 100644 index 0000000..8a737a7 --- /dev/null +++ b/web/templates/admin/page/form.gohtml @@ -0,0 +1,66 @@ + +{{ define "head" -}} + + + + + +{{- end }} + +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/page.pageForm*/ -}} + {{ if .Slug}} + {{( pgettext "Edit Page" "title" )}} + {{ else }} + {{( pgettext "New Page" "title" )}} + {{ end }} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/page.pageForm*/ -}} +
+

+ {{ if .Slug }} + {{( pgettext "Edit Page" "title" )}} + {{ else }} + {{( pgettext "New Page" "title" )}} + {{ end }} +

+ {{ CSRFInput }} +
+ {{ with .Title -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Content -}} + + {{- end }} +
+
+ +
+
+{{- end }} diff --git a/web/templates/admin/page/index.gohtml b/web/templates/admin/page/index.gohtml new file mode 100644 index 0000000..5178eeb --- /dev/null +++ b/web/templates/admin/page/index.gohtml @@ -0,0 +1,31 @@ + +{{ define "title" -}} + {{( pgettext "Pages" "title" )}} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/page.pageIndex*/ -}} + {{( pgettext "Add Page" "action" )}} +

{{( pgettext "Pages" "title" )}}

+ {{ if .Pages -}} + + + + + + + + {{ range .Pages -}} + + + + {{- end }} + +
{{( pgettext "Title" "header" )}}
{{ .Title }}
+ {{ else -}} +

{{( gettext "No pages added yet." )}}

+ {{- end }} +{{- end }} diff --git a/web/templates/public/page.gohtml b/web/templates/public/page.gohtml new file mode 100644 index 0000000..63d45cd --- /dev/null +++ b/web/templates/public/page.gohtml @@ -0,0 +1,14 @@ + +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/page.publicPage*/ -}} + {{ .Title }} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/page.publicPage*/ -}} +

{{ .Title }}

+ {{ .Content }} +{{- end }}