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.
This commit is contained in:
jordi fita mas 2023-07-26 12:08:59 +02:00
parent 1ef6dcc4cf
commit 2f3fc8812d
7 changed files with 141 additions and 114 deletions

View File

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

View File

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

49
pkg/app/user.go Normal file
View File

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

50
pkg/auth/session.go Normal file
View File

@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* 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,
}
}

View File

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

View File

@ -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 <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\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 dusuari o contrasenya incorrectes."

View File

@ -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 <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\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."