Use HTTP Host to establish the request’s company
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.
This commit is contained in:
parent
f65110824e
commit
0d2812acc5
5
demo.sql
5
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)
|
||||
|
|
|
@ -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;
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
||||
* 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)
|
||||
}
|
|
@ -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()
|
||||
},
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert camper:company_host from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table if exists public.company_host;
|
||||
|
||||
commit;
|
|
@ -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 <jordi@tandem.blog> # Add relation for company
|
||||
company_user [roles schema_camper user company] 2023-07-29T02:08:07Z jordi fita mas <jordi@tandem.blog> # Add relation of company user
|
||||
campsite_type [roles schema_camper company] 2023-07-31T11:20:29Z jordi fita mas <jordi@tandem.blog> # Add relation of campsite type
|
||||
company_host [roles schema_public] 2023-08-03T17:46:45Z jordi fita mas <jordi@tandem.blog> # Add relation of DNS domain and company
|
||||
|
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
-- Verify camper:company_host on pg
|
||||
|
||||
begin;
|
||||
|
||||
select host
|
||||
, company_id
|
||||
from public.company_host
|
||||
where false;
|
||||
|
||||
rollback;
|
|
@ -40,7 +40,7 @@
|
|||
<nav>
|
||||
<ul role="menu">
|
||||
<li role="presentation">
|
||||
<a role="menuitem" href="{{ companyURL }}/">{{( pgettext "Dashboard" "title" )}}</a>
|
||||
<a role="menuitem" href="/">{{( pgettext "Dashboard" "title" )}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
Loading…
Reference in New Issue