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