From 0d2812acc5ee8d39dfbf16f05f3ab0e74a61f2a5 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Thu, 3 Aug 2023 20:21:21 +0200 Subject: [PATCH] =?UTF-8?q?Use=20HTTP=20Host=20to=20establish=20the=20requ?= =?UTF-8?q?est=E2=80=99s=20company?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We made the decision that this application will also serve the public pages to guests and customers, to avoid the overhead of having to synchronize all data between this application and a bespoke WordPress plugin. That means that i no longer can have a /company/slug in the URL to know which company the request is for, not only because it looks ugly but because guest users do not have a “main company”—or any company whatsoever. Since the public-facing web is going to be served through a valid DNS domain, and all companies are going to have a different domain, i realized this is enough: i only had to add a relation of company and their hosts. The same company can have many hosts for staging servers or to separate the administration and public parts, for instance. With change, the company is already known from the first handler, and can pass it down to all the others, not only the handlers under /company/slug/whatever. And i no longer need the companyURL function, as there is no more explicit company in the URL. Even though template technically does not need the template, as it only contains the ID —the rest of the data is in a relation inaccessible to guests for now—, but i left the parameter just in case later on i need the decimal digits or currency symbol for whatever reason. --- demo.sql | 5 +++ deploy/company_host.sql | 20 ++++++++++++ pkg/app/app.go | 37 +++++++++++---------- pkg/app/login.go | 12 +++---- pkg/app/user.go | 18 +++++------ pkg/auth/company.go | 37 ++++----------------- pkg/company/http.go | 64 ------------------------------------- pkg/template/render.go | 5 ++- revert/company_host.sql | 7 ++++ sqitch.plan | 1 + test/company_host.sql | 37 +++++++++++++++++++++ verify/company_host.sql | 10 ++++++ web/templates/layout.gohtml | 2 +- 13 files changed, 125 insertions(+), 130 deletions(-) create mode 100644 deploy/company_host.sql delete mode 100644 pkg/company/http.go create mode 100644 revert/company_host.sql create mode 100644 test/company_host.sql create mode 100644 verify/company_host.sql diff --git a/demo.sql b/demo.sql index 7aa2de1..ef480ea 100644 --- a/demo.sql +++ b/demo.sql @@ -12,6 +12,11 @@ alter sequence company_company_id_seq restart with 52; insert into company (slug, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag, legal_disclaimer) values ('09184122-b276-4be2-9553-e4bbcbafe40d', 'Càmping les mines, S.L.U.', 'ESB17616756', 'Pescamines', parse_packed_phone_number('972 50 60 70', 'ES'), 'info@lesmines.cat', 'https://lesmines.cat/', 'C/ de l’Hort', 'Castelló d’Empúries', 'Girona', '17486', 'ES', 'EUR', 'ca', 'Càmping les mines, S.L.U. és responsable del tractament de les seves dades d’acord amb el RGPD i la LOPDGDD, i les tracta per a mantenir una relació mercantil/comercial amb vostè. Les conservarà mentre es mantingui aquesta relació i no es comunicaran a tercers. Pot exercir els drets d’accés, rectificació, portabilitat, supressió, limitació i oposició a Càmping les mines, S.L.U., amb domicili Carrer de l’Hort 71, 17486 Castelló d’Empúries o enviant un correu electrònic a info@lesmines.cat. Per a qualsevol reclamació pot acudir a agpd.es. Per a més informació pot consultar la nostra política de privacitat a lesmines.cat.'); +insert into company_host (company_id, host) +values (52, 'localhost:8080') + , (52, 'camper.tandem.ws') +; + insert into company_user (company_id, user_id) values (52, 42) , (52, 43) diff --git a/deploy/company_host.sql b/deploy/company_host.sql new file mode 100644 index 0000000..cc2cc1c --- /dev/null +++ b/deploy/company_host.sql @@ -0,0 +1,20 @@ +-- Deploy camper:company_host to pg +-- requires: roles +-- requires: schema_public + +begin; + +set search_path to public, camper; + +create table company_host ( + host text primary key, + company_id integer not null references company +); + +comment on column company_host.host is 'This should be a value from the HTTP Host header.'; + +grant select on table company_host to guest; +grant select on table company_host to employee; +grant select on table company_host to admin; + +commit; diff --git a/pkg/app/app.go b/pkg/app/app.go index 514fcc4..228ca11 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -11,17 +11,18 @@ import ( "golang.org/x/text/language" "dev.tandem.ws/tandem/camper/pkg/auth" - "dev.tandem.ws/tandem/camper/pkg/company" + "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/locale" + "dev.tandem.ws/tandem/camper/pkg/template" ) type App struct { db *database.DB fileHandler http.Handler profile *profileHandler - company *company.Handler + campsite *campsite.Handler locales locale.Locales defaultLocale *locale.Locale languageMatcher language.Matcher @@ -38,7 +39,7 @@ func New(db *database.DB, avatarsDir string) (http.Handler, error) { db: db, fileHandler: static, profile: profile, - company: company.NewHandler(), + campsite: campsite.NewHandler(), locales: locales, defaultLocale: locales[language.Catalan], languageMatcher: language.NewMatcher(locales.Tags()), @@ -72,31 +73,39 @@ func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { panic(err) } + company, err := auth.CompanyByHost(r.Context(), conn, r.Host) + if database.ErrorIsNotFound(err) { + http.NotFound(w, r) + return + } else if err != nil { + panic(err) + } + if head == "login" { switch r.Method { case http.MethodGet: - serveLoginForm(w, r, user, "/") + serveLoginForm(w, r, user, company, "/") case http.MethodPost: - handleLogin(w, r, user, conn) + handleLogin(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodPost, http.MethodGet) } } else { if !user.LoggedIn { w.WriteHeader(http.StatusUnauthorized) - serveLoginForm(w, r, user, requestPath) + serveLoginForm(w, r, user, company, requestPath) return } switch head { case "me": - h.profile.Handler(user, conn).ServeHTTP(w, r) - case "company": - h.company.Handler(user, conn).ServeHTTP(w, r) + h.profile.Handler(user, company, conn).ServeHTTP(w, r) + case "campsites": + h.campsite.Handler(user, company, conn).ServeHTTP(w, r) case "": switch r.Method { case http.MethodGet: - redirectToMainCompany(w, r, conn) + serveDashboard(w, r, user, company) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } @@ -107,10 +116,6 @@ func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -func redirectToMainCompany(w http.ResponseWriter, r *http.Request, conn *database.Conn) { - co, err := auth.QueryMainCompany(r.Context(), conn) - if err != nil { - panic(err) - } - httplib.Relocate(w, r, co.URL(), http.StatusFound) +func serveDashboard(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRender(w, r, user, company, "dashboard.gohtml", nil) } diff --git a/pkg/app/login.go b/pkg/app/login.go index 6cd6834..dd2a524 100644 --- a/pkg/app/login.go +++ b/pkg/app/login.go @@ -60,17 +60,17 @@ func (f *loginForm) Valid(l *locale.Locale) bool { return v.AllOK } -func (f *loginForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User) { - template.MustRender(w, r, user, nil, "login.gohtml", f) +func (f *loginForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRender(w, r, user, company, "login.gohtml", f) } -func serveLoginForm(w http.ResponseWriter, r *http.Request, user *auth.User, redirectPath string) { +func serveLoginForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, redirectPath string) { login := newLoginForm() login.Redirect.Val = redirectPath - login.MustRender(w, r, user) + login.MustRender(w, r, user, company) } -func handleLogin(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) { +func handleLogin(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { login := newLoginForm() if err := login.Parse(r); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -88,7 +88,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request, user *auth.User, conn * } else { w.WriteHeader(http.StatusUnprocessableEntity) } - login.MustRender(w, r, user) + login.MustRender(w, r, user, company) } func handleLogout(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) { diff --git a/pkg/app/user.go b/pkg/app/user.go index da28d8d..f80302f 100644 --- a/pkg/app/user.go +++ b/pkg/app/user.go @@ -80,7 +80,7 @@ func newProfileHandler(static http.Handler, avatarsDir string) (*profileHandler, return handler, nil } -func (h *profileHandler) Handler(user *auth.User, conn *database.Conn) http.HandlerFunc { +func (h *profileHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var head string head, r.URL.Path = httplib.ShiftPath(r.URL.Path) @@ -103,9 +103,9 @@ func (h *profileHandler) Handler(user *auth.User, conn *database.Conn) http.Hand case "": switch r.Method { case http.MethodGet: - serveProfileForm(w, r, user, conn) + serveProfileForm(w, r, user, company, conn) case http.MethodPut: - h.updateProfile(w, r, user, conn) + h.updateProfile(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) } @@ -130,15 +130,15 @@ func (h *profileHandler) avatarPath(user *auth.User) string { return filepath.Join(h.avatarsDir, strconv.Itoa(user.ID)) } -func serveProfileForm(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) { +func serveProfileForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { profile := newProfileForm(r.Context(), user.Locale, conn) profile.Name.Val = conn.MustGetText(r.Context(), "select name from user_profile") profile.Email.Val = user.Email profile.Language.Selected = []string{user.Language.String()} - profile.MustRender(w, r, user) + profile.MustRender(w, r, user, company) } -func (h *profileHandler) updateProfile(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) { +func (h *profileHandler) updateProfile(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { profile := newProfileForm(r.Context(), user.Locale, conn) if err := profile.Parse(w, r); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -153,7 +153,7 @@ func (h *profileHandler) updateProfile(w http.ResponseWriter, r *http.Request, u if !httplib.IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity) } - profile.MustRender(w, r, user) + profile.MustRender(w, r, user, company) return } //goland:noinspection SqlWithoutWhere @@ -248,8 +248,8 @@ func (f *profileForm) Valid(l *locale.Locale) bool { return v.AllOK } -func (f *profileForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User) { - template.MustRender(w, r, user, nil, "profile.gohtml", f) +func (f *profileForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRender(w, r, user, company, "profile.gohtml", f) } func (f *profileForm) HasAvatarFile() bool { diff --git a/pkg/auth/company.go b/pkg/auth/company.go index d653645..fc40a06 100644 --- a/pkg/auth/company.go +++ b/pkg/auth/company.go @@ -12,44 +12,19 @@ import ( ) type Company struct { - ID int - CurrencySymbol string - DecimalDigits int - Slug string + ID int } -func QueryMainCompany(ctx context.Context, conn *database.Conn) (*Company, error) { - slug, err := conn.GetText(ctx, "select slug::text from company order by company_id limit 1") - if err != nil { - return nil, err - } - return QueryBySlug(ctx, conn, slug) -} - -func QueryBySlug(ctx context.Context, conn *database.Conn, slug string) (*Company, error) { - company := &Company{ - Slug: slug, - } +func CompanyByHost(ctx context.Context, conn *database.Conn, host string) (*Company, error) { + company := &Company{} if err := conn.QueryRow(ctx, ` select company_id - , currency_symbol - , decimal_digits - from company - join currency using (currency_code) - where slug = $1 - `, company.Slug).Scan( + from company_host + where host = $1 + `, host).Scan( &company.ID, - &company.CurrencySymbol, - &company.DecimalDigits, ); err != nil { return nil, err } return company, nil } - -func (c *Company) URL() string { - if c == nil { - return "" - } - return "/company/" + c.Slug -} diff --git a/pkg/company/http.go b/pkg/company/http.go deleted file mode 100644 index 2758d46..0000000 --- a/pkg/company/http.go +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 jordi fita mas - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package company - -import ( - "dev.tandem.ws/tandem/camper/pkg/auth" - "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/template" - "dev.tandem.ws/tandem/camper/pkg/uuid" - "net/http" -) - -type Handler struct { - campsite *campsite.Handler -} - -func NewHandler() *Handler { - return &Handler{ - campsite: campsite.NewHandler(), - } -} - -func (h *Handler) Handler(user *auth.User, conn *database.Conn) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var slug string - slug, r.URL.Path = httplib.ShiftPath(r.URL.Path) - if !uuid.Valid(slug) { - http.NotFound(w, r) - return - } - company, err := auth.QueryBySlug(r.Context(), conn, slug) - if database.ErrorIsNotFound(err) { - http.NotFound(w, r) - return - } else if err != nil { - panic(err) - } - - var head string - head, r.URL.Path = httplib.ShiftPath(r.URL.Path) - switch head { - case "campsites": - h.campsite.Handler(user, company, conn).ServeHTTP(w, r) - case "": - switch r.Method { - case http.MethodGet: - serveDashboard(w, r, user, company) - default: - httplib.MethodNotAllowed(w, r, http.MethodGet) - } - default: - http.NotFound(w, r) - } - }) -} - -func serveDashboard(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { - template.MustRender(w, r, user, company, "dashboard.gohtml", nil) -} diff --git a/pkg/template/render.go b/pkg/template/render.go index e21139d..89e869b 100644 --- a/pkg/template/render.go +++ b/pkg/template/render.go @@ -30,9 +30,8 @@ func MustRender(w io.Writer, r *http.Request, user *auth.User, company *auth.Com func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, layout string, filename string, data interface{}) { t := template.New(filename) t.Funcs(template.FuncMap{ - "gettext": user.Locale.Get, - "pgettext": user.Locale.GetC, - "companyURL": company.URL, + "gettext": user.Locale.Get, + "pgettext": user.Locale.GetC, "currentLocale": func() string { return user.Locale.Language.String() }, diff --git a/revert/company_host.sql b/revert/company_host.sql new file mode 100644 index 0000000..1e549a5 --- /dev/null +++ b/revert/company_host.sql @@ -0,0 +1,7 @@ +-- Revert camper:company_host from pg + +begin; + +drop table if exists public.company_host; + +commit; diff --git a/sqitch.plan b/sqitch.plan index ac5b9ea..d7d2108 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -37,3 +37,4 @@ available_countries [schema_camper country country_i18n] 2023-07-29T01:48:40Z jo company [roles schema_camper extension_vat email extension_pg_libphonenumber extension_uri currency_code currency country_code country language] 2023-07-29T01:56:41Z jordi fita mas # Add relation for company company_user [roles schema_camper user company] 2023-07-29T02:08:07Z jordi fita mas # Add relation of company user campsite_type [roles schema_camper company] 2023-07-31T11:20:29Z jordi fita mas # Add relation of campsite type +company_host [roles schema_public] 2023-08-03T17:46:45Z jordi fita mas # Add relation of DNS domain and company diff --git a/test/company_host.sql b/test/company_host.sql new file mode 100644 index 0000000..581eec1 --- /dev/null +++ b/test/company_host.sql @@ -0,0 +1,37 @@ +-- Test company_host +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(17); + +set search_path to camper, public; + +select has_table('company_host'); +select has_pk('company_host' ); +select table_privs_are('company_host', 'guest', array ['SELECT']); +select table_privs_are('company_host', 'employee', array ['SELECT']); +select table_privs_are('company_host', 'admin', array ['SELECT']); +select table_privs_are('company_host', 'authenticator', array []::text[]); + +select has_column('company_host', 'host'); +select col_is_pk('company_host', 'host'); +select col_type_is('company_host', 'host', 'text'); +select col_not_null('company_host', 'host'); +select col_hasnt_default('company_host', 'host'); + +select has_column('company_host', 'company_id'); +select col_is_fk('company_host', 'company_id'); +select fk_ok('company_host', 'company_id', 'company', 'company_id'); +select col_type_is('company_host', 'company_id', 'integer'); +select col_not_null('company_host', 'company_id'); +select col_hasnt_default('company_host', 'company_id'); + + +select * +from finish(); + +rollback; + diff --git a/verify/company_host.sql b/verify/company_host.sql new file mode 100644 index 0000000..0bc07e8 --- /dev/null +++ b/verify/company_host.sql @@ -0,0 +1,10 @@ +-- Verify camper:company_host on pg + +begin; + +select host + , company_id +from public.company_host +where false; + +rollback; diff --git a/web/templates/layout.gohtml b/web/templates/layout.gohtml index 96d0b0a..92f640e 100644 --- a/web/templates/layout.gohtml +++ b/web/templates/layout.gohtml @@ -40,7 +40,7 @@