Get user from database based on cookie and serve login if not logged in

To get the user from the database i have to set the cookie first, that
was already done in database.MustAcquire, but i thought they were too
far apart, even thought they are so related.  So, the cookie, and thus
the role, is set when getting the user, that is actually the first thing
to do once the connection is acquired.  However, that way the database
package has no knowledge of cookies, and the code that sets the cookie
and retrieves the user are next to each other.

I applied the same logic to the changes of locale.Match: it has not
business knowing that the accept language string comes from a request;
it only needs the actual string.  Also, the TODO comment about getting
the user’s locale made no sense, now, because app already knows that
locale, so there is no need to pass the user to the locale package.

Getting the locale is done after retrieving the user from the database,
for the same reason the connection is Acquired as far up as possible:
almost every request will need this value, together with the user and
the database connection.

I am a bit affraid that i will end up with functions that always expect
these three values.  Maybe i can put the locale inside user, as it is
the user’s locale, after all, no matter if it came from the database or
the user agent, but connection and user must be separate, i think.

We’ll see.
This commit is contained in:
jordi fita mas 2023-07-26 01:50:39 +02:00
parent 9fccd5f81d
commit 1ef6dcc4cf
8 changed files with 139 additions and 61 deletions

View File

@ -6,14 +6,17 @@
package app package app
import ( import (
"golang.org/x/text/language" "context"
"net/http" "net/http"
"path" "path"
"strings" "strings"
"golang.org/x/text/language"
"dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/database"
httplib "dev.tandem.ws/tandem/camper/pkg/http" httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/locale" "dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/template"
) )
func shiftPath(p string) (head, tail string) { func shiftPath(p string) (head, tail string) {
@ -55,28 +58,43 @@ func New(db *database.DB) http.Handler {
} }
func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
requestURL := r.URL.Path
var head string var head string
head, r.URL.Path = shiftPath(r.URL.Path) head, r.URL.Path = shiftPath(r.URL.Path)
if head == "static" { if head == "static" {
h.fileHandler.ServeHTTP(w, r) h.fileHandler.ServeHTTP(w, r)
} else { } else {
cookie := getSessionCookie(r) conn, err := h.db.Acquire(r.Context())
conn := h.db.MustAcquire(r.Context(), cookie) if err != nil {
panic(err)
}
defer conn.Release() defer conn.Release()
cookie := getSessionCookie(r)
user, err := h.getUser(r.Context(), conn, cookie)
if err != nil {
panic(err)
}
l := h.matchLocale(r, user)
if head == "login" { if head == "login" {
switch r.Method { switch r.Method {
case http.MethodPost: case http.MethodPost:
h.handleLogin(w, r, conn) h.handleLogin(w, r, l, conn)
default: default:
methodNotAllowed(w, r, http.MethodPost) methodNotAllowed(w, r, http.MethodPost)
} }
} else { } else {
if !user.LoggedIn {
h.serveLoginForm(w, r, l, requestURL)
return
}
switch head { switch head {
case "": case "":
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
h.handleGet(w, r) h.serveDashboard(w, r, l)
default: default:
methodNotAllowed(w, r, http.MethodGet) methodNotAllowed(w, r, http.MethodGet)
} }
@ -86,3 +104,51 @@ 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)
}

View File

@ -24,6 +24,7 @@ const (
type loginForm struct { type loginForm struct {
Email *form.Input Email *form.Input
Password *form.Input Password *form.Input
Redirect *form.Input
Error error Error error
} }
@ -35,6 +36,9 @@ func newLoginForm() *loginForm {
Password: &form.Input{ Password: &form.Input{
Name: "password", Name: "password",
}, },
Redirect: &form.Input{
Name: "redirect",
},
} }
} }
@ -44,6 +48,10 @@ func (f *loginForm) Parse(r *http.Request) error {
} }
f.Email.FillValue(r) f.Email.FillValue(r)
f.Password.FillValue(r) f.Password.FillValue(r)
f.Redirect.FillValue(r)
if f.Redirect.Val == "" {
f.Redirect.Val = "/"
}
return nil return nil
} }
@ -56,28 +64,24 @@ func (f *loginForm) Valid(l *locale.Locale) bool {
return v.AllOK return v.AllOK
} }
func (h *App) handleGet(w http.ResponseWriter, r *http.Request) { func (h *App) serveLoginForm(w http.ResponseWriter, _ *http.Request, l *locale.Locale, requestURL string) {
l := h.matchLocale(r)
login := newLoginForm() login := newLoginForm()
login.Redirect.Val = requestURL
w.WriteHeader(http.StatusUnauthorized)
template.MustRender(w, l, "login.gohtml", login) template.MustRender(w, l, "login.gohtml", login)
} }
func (h *App) matchLocale(r *http.Request) *locale.Locale { func (h *App) handleLogin(w http.ResponseWriter, r *http.Request, l *locale.Locale, conn *database.Conn) {
return locale.Match(r, h.locales, h.defaultLocale, h.languageMatcher)
}
func (h *App) handleLogin(w http.ResponseWriter, r *http.Request, conn *database.Conn) {
login := newLoginForm() login := newLoginForm()
if err := login.Parse(r); err != nil { if err := login.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
l := h.matchLocale(r)
if login.Valid(l) { if login.Valid(l) {
cookie := conn.MustGetText(r.Context(), "select login($1, $2, $3)", login.Email, login.Password, httplib.RemoteAddr(r)) cookie := conn.MustGetText(r.Context(), "select login($1, $2, $3)", login.Email, login.Password, httplib.RemoteAddr(r))
if cookie != "" { if cookie != "" {
setSessionCookie(w, cookie) setSessionCookie(w, cookie)
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, login.Redirect.Val, http.StatusSeeOther)
return return
} }
login.Error = errors.New(l.Gettext("Invalid user or password.")) login.Error = errors.New(l.Gettext("Invalid user or password."))

View File

@ -53,17 +53,6 @@ func (db *DB) Acquire(ctx context.Context) (*Conn, error) {
return &Conn{conn}, nil return &Conn{conn}, nil
} }
func (db *DB) MustAcquire(ctx context.Context, cookie string) *Conn {
conn, err := db.Acquire(ctx)
if err != nil {
panic(err)
}
if _, err = conn.Exec(ctx, "select set_cookie($1)", cookie); err != nil {
panic(false)
}
return conn
}
type Conn struct { type Conn struct {
*pgxpool.Conn *pgxpool.Conn
} }

View File

@ -7,8 +7,6 @@ package locale
import ( import (
"context" "context"
"net/http"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
"golang.org/x/text/language" "golang.org/x/text/language"
@ -60,23 +58,18 @@ func (l *Locale) GettextNoop(str string) string {
return str return str
} }
func Match(r *http.Request, locales Locales, defaultLocale *Locale, matcher language.Matcher) *Locale { func Match(acceptLanguage string, locales Locales, matcher language.Matcher) *Locale {
var locale *Locale t, _, err := language.ParseAcceptLanguage(acceptLanguage)
// TODO: find user locale if err != nil {
if locale == nil { return nil
t, _, err := language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))
if err == nil {
tag, _, _ := matcher.Match(t...)
var ok bool
locale, ok = locales[tag]
for !ok && !tag.IsRoot() {
tag = tag.Parent()
locale, ok = locales[tag]
}
}
} }
if locale == nil { var locale *Locale
locale = defaultLocale tag, _, _ := matcher.Match(t...)
var ok bool
locale, ok = locales[tag]
for !ok && !tag.IsRoot() {
tag = tag.Parent()
locale, ok = locales[tag]
} }
return locale return locale
} }

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: camper\n" "Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-07-24 17:04+0200\n" "POT-Creation-Date: 2023-07-26 01:33+0200\n"
"PO-Revision-Date: 2023-07-22 23:45+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n" "Language-Team: Catalan <ca@dodds.net>\n"
@ -18,22 +18,27 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: web/templates/login.gohtml:2 web/templates/login.gohtml:8 #: web/templates/dashboard.gohtml:2 web/templates/dashboard.gohtml:6
msgctxt "title"
msgid "Dashboard"
msgstr "Tauler"
#: web/templates/login.gohtml:2 web/templates/login.gohtml:9
msgctxt "title" msgctxt "title"
msgid "Login" msgid "Login"
msgstr "Entrada" msgstr "Entrada"
#: web/templates/login.gohtml:17 #: web/templates/login.gohtml:18
msgctxt "input" msgctxt "input"
msgid "Email" msgid "Email"
msgstr "Correu-e" msgstr "Correu-e"
#: web/templates/login.gohtml:26 #: web/templates/login.gohtml:27
msgctxt "input" msgctxt "input"
msgid "Password" msgid "Password"
msgstr "Contrasenya" msgstr "Contrasenya"
#: web/templates/login.gohtml:35 #: web/templates/login.gohtml:36
msgctxt "action" msgctxt "action"
msgid "Login" msgid "Login"
msgstr "Entra" msgstr "Entra"
@ -42,18 +47,18 @@ msgstr "Entra"
msgid "Skip to main content" msgid "Skip to main content"
msgstr "Salta al contingut principal" msgstr "Salta al contingut principal"
#: pkg/app/login.go:50 #: pkg/app/login.go:60
msgid "Email can not be empty." msgid "Email can not be empty."
msgstr "No podeu deixar el correu en blanc." msgstr "No podeu deixar el correu en blanc."
#: pkg/app/login.go:51 #: pkg/app/login.go:61
msgid "This email is not valid. It should be like name@domain.com." 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." msgstr "Aquest correu-e no és vàlid. Hauria de ser similar a nom@domini.com."
#: pkg/app/login.go:53 #: pkg/app/login.go:63
msgid "Password can not be empty." msgid "Password can not be empty."
msgstr "No podeu deixar la contrasenya en blanc." msgstr "No podeu deixar la contrasenya en blanc."
#: pkg/app/login.go:82 #: pkg/app/login.go:86
msgid "Invalid user or password." msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes." msgstr "Nom dusuari o contrasenya incorrectes."

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: camper\n" "Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-07-24 17:04+0200\n" "POT-Creation-Date: 2023-07-26 01:33+0200\n"
"PO-Revision-Date: 2023-07-22 23:46+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n" "Language-Team: Spanish <es@tp.org.es>\n"
@ -18,22 +18,27 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: web/templates/login.gohtml:2 web/templates/login.gohtml:8 #: web/templates/dashboard.gohtml:2 web/templates/dashboard.gohtml:6
msgctxt "title"
msgid "Dashboard"
msgstr "Panel"
#: web/templates/login.gohtml:2 web/templates/login.gohtml:9
msgctxt "title" msgctxt "title"
msgid "Login" msgid "Login"
msgstr "Entrada" msgstr "Entrada"
#: web/templates/login.gohtml:17 #: web/templates/login.gohtml:18
msgctxt "input" msgctxt "input"
msgid "Email" msgid "Email"
msgstr "Correo-e" msgstr "Correo-e"
#: web/templates/login.gohtml:26 #: web/templates/login.gohtml:27
msgctxt "input" msgctxt "input"
msgid "Password" msgid "Password"
msgstr "Contraseña" msgstr "Contraseña"
#: web/templates/login.gohtml:35 #: web/templates/login.gohtml:36
msgctxt "action" msgctxt "action"
msgid "Login" msgid "Login"
msgstr "Entrar" msgstr "Entrar"
@ -42,18 +47,18 @@ msgstr "Entrar"
msgid "Skip to main content" msgid "Skip to main content"
msgstr "Saltar al contenido principal" msgstr "Saltar al contenido principal"
#: pkg/app/login.go:50 #: pkg/app/login.go:60
msgid "Email can not be empty." msgid "Email can not be empty."
msgstr "No podéis dejar el correo-e en blanco." msgstr "No podéis dejar el correo-e en blanco."
#: pkg/app/login.go:51 #: pkg/app/login.go:61
msgid "This email is not valid. It should be like name@domain.com." 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." msgstr "Este correo-e no es válido. Tiene que ser parecido a nombre@dominio.com."
#: pkg/app/login.go:53 #: pkg/app/login.go:63
msgid "Password can not be empty." msgid "Password can not be empty."
msgstr "No podéis dejar la contraseña en blanco." msgstr "No podéis dejar la contraseña en blanco."
#: pkg/app/login.go:82 #: pkg/app/login.go:86
msgid "Invalid user or password." msgid "Invalid user or password."
msgstr "Usuario o contraseña incorrectos." msgstr "Usuario o contraseña incorrectos."

View File

@ -0,0 +1,11 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Dashboard" "title" )}}
{{- end }}
{{ define "content" -}}
<h2>{{( pgettext "Dashboard" "title" )}}</h2>
{{- end }}

View File

@ -1,3 +1,7 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}} {{ define "title" -}}
{{( pgettext "Login" "title" )}} {{( pgettext "Login" "title" )}}
{{- end }} {{- end }}
@ -5,6 +9,7 @@
{{ define "content" -}} {{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/app.loginForm */ -}} {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/app.loginForm */ -}}
<form method="post" action="/login"> <form method="post" action="/login">
<input type="hidden" name="{{ .Redirect.Name}}" value="{{ .Redirect.Val }}">
<h2>{{( pgettext "Login" "title" )}}</h2> <h2>{{( pgettext "Login" "title" )}}</h2>
{{ if .Error -}} {{ if .Error -}}
<div class="error" role="alert"> <div class="error" role="alert">