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 }}