From 2f3fc8812d7a20f7be0ef906e779d1e39cb35d19 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Wed, 26 Jul 2023 12:08:59 +0200 Subject: [PATCH] Include the locale inside the User struct The locale is completely dependent on the user, as much as its email or CSRF token, so it does not make much sense to have it in a separate variable: for different users we might have to use different locales. Also, this means one less variable to pass to handlers, that most of them will need the user at some point or another (i.e., to render its profile icon). The thing is that i can not import `app.User` from the template package because it would create an import cycle. I created the `auth` package just because of that. I thought that the login code would be better moved to the auth package as well, but of course then i would reintroduce the import cycle between auth and template. --- pkg/app/app.go | 60 +++++------------------------------------- pkg/app/login.go | 42 ++++++----------------------- pkg/app/user.go | 49 ++++++++++++++++++++++++++++++++++ pkg/auth/session.go | 50 +++++++++++++++++++++++++++++++++++ pkg/template/render.go | 14 +++++----- po/ca.po | 20 +++++++------- po/es.po | 20 +++++++------- 7 files changed, 141 insertions(+), 114 deletions(-) create mode 100644 pkg/app/user.go create mode 100644 pkg/auth/session.go diff --git a/pkg/app/app.go b/pkg/app/app.go index b4097ae..ed3a2c7 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -6,13 +6,13 @@ package app import ( - "context" "net/http" "path" "strings" "golang.org/x/text/language" + "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/locale" @@ -70,23 +70,21 @@ func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { } defer conn.Release() - cookie := getSessionCookie(r) - user, err := h.getUser(r.Context(), conn, cookie) + user, err := h.getUser(r, conn) if err != nil { panic(err) } - l := h.matchLocale(r, user) if head == "login" { switch r.Method { case http.MethodPost: - h.handleLogin(w, r, l, conn) + handleLogin(w, r, user, conn) default: methodNotAllowed(w, r, http.MethodPost) } } else { if !user.LoggedIn { - h.serveLoginForm(w, r, l, requestURL) + serveLoginForm(w, r, user, requestURL) return } @@ -94,7 +92,7 @@ func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { case "": switch r.Method { case http.MethodGet: - h.serveDashboard(w, r, l) + h.serveDashboard(w, r, user) default: methodNotAllowed(w, r, http.MethodGet) } @@ -105,50 +103,6 @@ func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -type User struct { - Email string - LoggedIn bool - Role string - Language language.Tag - CsrfToken string -} - -func (h *App) getUser(ctx context.Context, conn *database.Conn, cookie string) (*User, error) { - if _, err := conn.Exec(ctx, "select set_cookie($1)", cookie); err != nil { - conn.Release() - return nil, err - } - - user := &User{ - Email: "", - LoggedIn: false, - Role: "guest", - } - row := conn.QueryRow(ctx, "select coalesce(email, ''), email is not null, role, lang_tag, csrf_token from user_profile") - var langTag string - if err := row.Scan(&user.Email, &user.LoggedIn, &user.Role, &langTag, &user.CsrfToken); err != nil { - return nil, err - } - if lang, err := language.Parse(langTag); err == nil { - user.Language = lang - } else { - return nil, err - } - - return user, nil -} - -func (h *App) matchLocale(r *http.Request, user *User) *locale.Locale { - l := h.locales[user.Language] - if l == nil { - l = locale.Match(r.Header.Get("Accept-Language"), h.locales, h.languageMatcher) - if l == nil { - l = h.defaultLocale - } - } - return l -} - -func (h *App) serveDashboard(w http.ResponseWriter, _ *http.Request, l *locale.Locale) { - template.MustRender(w, l, "dashboard.gohtml", nil) +func (h *App) serveDashboard(w http.ResponseWriter, _ *http.Request, user *auth.User) { + template.MustRender(w, user, "dashboard.gohtml", nil) } diff --git a/pkg/app/login.go b/pkg/app/login.go index 4d73e0f..99f1b79 100644 --- a/pkg/app/login.go +++ b/pkg/app/login.go @@ -8,8 +8,8 @@ package app import ( "errors" "net/http" - "time" + "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" @@ -17,10 +17,6 @@ import ( "dev.tandem.ws/tandem/camper/pkg/template" ) -const ( - sessionCookie = "camper-session" -) - type loginForm struct { Email *form.Input Password *form.Input @@ -64,52 +60,30 @@ func (f *loginForm) Valid(l *locale.Locale) bool { return v.AllOK } -func (h *App) serveLoginForm(w http.ResponseWriter, _ *http.Request, l *locale.Locale, requestURL string) { +func serveLoginForm(w http.ResponseWriter, _ *http.Request, user *auth.User, requestURL string) { login := newLoginForm() login.Redirect.Val = requestURL w.WriteHeader(http.StatusUnauthorized) - template.MustRender(w, l, "login.gohtml", login) + template.MustRender(w, user, "login.gohtml", login) } -func (h *App) handleLogin(w http.ResponseWriter, r *http.Request, l *locale.Locale, conn *database.Conn) { +func handleLogin(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) { login := newLoginForm() if err := login.Parse(r); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - if login.Valid(l) { + if login.Valid(user.Locale) { cookie := conn.MustGetText(r.Context(), "select login($1, $2, $3)", login.Email, login.Password, httplib.RemoteAddr(r)) if cookie != "" { - setSessionCookie(w, cookie) + auth.SetSessionCookie(w, cookie) http.Redirect(w, r, login.Redirect.Val, http.StatusSeeOther) return } - login.Error = errors.New(l.Gettext("Invalid user or password.")) + login.Error = errors.New(user.Locale.Gettext("Invalid user or password.")) w.WriteHeader(http.StatusUnauthorized) } else { w.WriteHeader(http.StatusUnprocessableEntity) } - template.MustRender(w, l, "login.gohtml", login) -} - -func setSessionCookie(w http.ResponseWriter, cookie string) { - http.SetCookie(w, createSessionCookie(cookie, 8766*24*time.Hour)) -} - -func getSessionCookie(r *http.Request) string { - if cookie, err := r.Cookie(sessionCookie); err == nil { - return cookie.Value - } - return "" -} - -func createSessionCookie(value string, duration time.Duration) *http.Cookie { - return &http.Cookie{ - Name: sessionCookie, - Value: value, - Path: "/", - Expires: time.Now().Add(duration), - HttpOnly: true, - SameSite: http.SameSiteLaxMode, - } + template.MustRender(w, user, "login.gohtml", login) } diff --git a/pkg/app/user.go b/pkg/app/user.go new file mode 100644 index 0000000..f778420 --- /dev/null +++ b/pkg/app/user.go @@ -0,0 +1,49 @@ +package app + +import ( + "net/http" + + "golang.org/x/text/language" + + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/database" + "dev.tandem.ws/tandem/camper/pkg/locale" +) + +func (h *App) getUser(r *http.Request, conn *database.Conn) (*auth.User, error) { + cookie := auth.GetSessionCookie(r) + if _, err := conn.Exec(r.Context(), "select set_cookie($1)", cookie); err != nil { + return nil, err + } + + user := &auth.User{ + Email: "", + LoggedIn: false, + Role: "guest", + } + row := conn.QueryRow(r.Context(), "select coalesce(email, ''), email is not null, role, lang_tag, csrf_token from user_profile") + var langTag string + if err := row.Scan(&user.Email, &user.LoggedIn, &user.Role, &langTag, &user.CsrfToken); err != nil { + return nil, err + } + if lang, err := language.Parse(langTag); err == nil { + user.Language = lang + } else { + return nil, err + } + + user.Locale = h.locales[user.Language] + if user.Locale == nil { + user.Locale = h.matchLocale(r) + } + + return user, nil +} + +func (h *App) matchLocale(r *http.Request) *locale.Locale { + l := locale.Match(r.Header.Get("Accept-Language"), h.locales, h.languageMatcher) + if l == nil { + l = h.defaultLocale + } + return l +} diff --git a/pkg/auth/session.go b/pkg/auth/session.go new file mode 100644 index 0000000..df29c55 --- /dev/null +++ b/pkg/auth/session.go @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package auth + +import ( + "net/http" + "time" + + "golang.org/x/text/language" + + "dev.tandem.ws/tandem/camper/pkg/locale" +) + +const ( + sessionCookie = "camper-session" +) + +type User struct { + Email string + LoggedIn bool + Role string + Language language.Tag + CsrfToken string + Locale *locale.Locale +} + +func SetSessionCookie(w http.ResponseWriter, cookie string) { + http.SetCookie(w, createSessionCookie(cookie, 8766*24*time.Hour)) +} + +func GetSessionCookie(r *http.Request) string { + if cookie, err := r.Cookie(sessionCookie); err == nil { + return cookie.Value + } + return "" +} + +func createSessionCookie(value string, duration time.Duration) *http.Cookie { + return &http.Cookie{ + Name: sessionCookie, + Value: value, + Path: "/", + Expires: time.Now().Add(duration), + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + } +} diff --git a/pkg/template/render.go b/pkg/template/render.go index d7153d0..b1affd9 100644 --- a/pkg/template/render.go +++ b/pkg/template/render.go @@ -10,25 +10,25 @@ import ( "io" "net/http" - "dev.tandem.ws/tandem/camper/pkg/locale" + "dev.tandem.ws/tandem/camper/pkg/auth" ) func templateFile(name string) string { return "web/templates/" + name } -func MustRender(w io.Writer, locale *locale.Locale, filename string, data interface{}) { +func MustRender(w io.Writer, user *auth.User, filename string, data interface{}) { layout := "layout.gohtml" - mustRenderLayout(w, locale, layout, filename, data) + mustRenderLayout(w, user, layout, filename, data) } -func mustRenderLayout(w io.Writer, locale *locale.Locale, layout string, filename string, data interface{}) { +func mustRenderLayout(w io.Writer, user *auth.User, layout string, filename string, data interface{}) { t := template.New(filename) t.Funcs(template.FuncMap{ - "gettext": locale.Get, - "pgettext": locale.GetC, + "gettext": user.Locale.Get, + "pgettext": user.Locale.GetC, "currentLocale": func() string { - return locale.Language.String() + return user.Locale.Language.String() }, }) if _, err := t.ParseFiles(templateFile(layout), templateFile(filename)); err != nil { diff --git a/po/ca.po b/po/ca.po index 68e3ef4..83d7117 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-07-26 01:33+0200\n" +"POT-Creation-Date: 2023-07-26 11:57+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -18,27 +18,27 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: web/templates/dashboard.gohtml:2 web/templates/dashboard.gohtml:6 +#: web/templates/dashboard.gohtml:6 web/templates/dashboard.gohtml:10 msgctxt "title" msgid "Dashboard" msgstr "Tauler" -#: web/templates/login.gohtml:2 web/templates/login.gohtml:9 +#: web/templates/login.gohtml:6 web/templates/login.gohtml:13 msgctxt "title" msgid "Login" msgstr "Entrada" -#: web/templates/login.gohtml:18 +#: web/templates/login.gohtml:22 msgctxt "input" msgid "Email" msgstr "Correu-e" -#: web/templates/login.gohtml:27 +#: web/templates/login.gohtml:31 msgctxt "input" msgid "Password" msgstr "Contrasenya" -#: web/templates/login.gohtml:36 +#: web/templates/login.gohtml:40 msgctxt "action" msgid "Login" msgstr "Entra" @@ -47,18 +47,18 @@ msgstr "Entra" msgid "Skip to main content" msgstr "Salta al contingut principal" -#: pkg/app/login.go:60 +#: pkg/app/login.go:58 msgid "Email can not be empty." msgstr "No podeu deixar el correu en blanc." -#: pkg/app/login.go:61 +#: pkg/app/login.go:59 msgid "This email is not valid. It should be like name@domain.com." msgstr "Aquest correu-e no és vàlid. Hauria de ser similar a nom@domini.com." -#: pkg/app/login.go:63 +#: pkg/app/login.go:61 msgid "Password can not be empty." msgstr "No podeu deixar la contrasenya en blanc." -#: pkg/app/login.go:86 +#: pkg/app/login.go:85 msgid "Invalid user or password." msgstr "Nom d’usuari o contrasenya incorrectes." diff --git a/po/es.po b/po/es.po index 3eb1bea..c334f6b 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-07-26 01:33+0200\n" +"POT-Creation-Date: 2023-07-26 11:57+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -18,27 +18,27 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: web/templates/dashboard.gohtml:2 web/templates/dashboard.gohtml:6 +#: web/templates/dashboard.gohtml:6 web/templates/dashboard.gohtml:10 msgctxt "title" msgid "Dashboard" msgstr "Panel" -#: web/templates/login.gohtml:2 web/templates/login.gohtml:9 +#: web/templates/login.gohtml:6 web/templates/login.gohtml:13 msgctxt "title" msgid "Login" msgstr "Entrada" -#: web/templates/login.gohtml:18 +#: web/templates/login.gohtml:22 msgctxt "input" msgid "Email" msgstr "Correo-e" -#: web/templates/login.gohtml:27 +#: web/templates/login.gohtml:31 msgctxt "input" msgid "Password" msgstr "Contraseña" -#: web/templates/login.gohtml:36 +#: web/templates/login.gohtml:40 msgctxt "action" msgid "Login" msgstr "Entrar" @@ -47,18 +47,18 @@ msgstr "Entrar" msgid "Skip to main content" msgstr "Saltar al contenido principal" -#: pkg/app/login.go:60 +#: pkg/app/login.go:58 msgid "Email can not be empty." msgstr "No podéis dejar el correo-e en blanco." -#: pkg/app/login.go:61 +#: pkg/app/login.go:59 msgid "This email is not valid. It should be like name@domain.com." msgstr "Este correo-e no es válido. Tiene que ser parecido a nombre@dominio.com." -#: pkg/app/login.go:63 +#: pkg/app/login.go:61 msgid "Password can not be empty." msgstr "No podéis dejar la contraseña en blanco." -#: pkg/app/login.go:86 +#: pkg/app/login.go:85 msgid "Invalid user or password." msgstr "Usuario o contraseña incorrectos."