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
import (
"golang.org/x/text/language"
"context"
"net/http"
"path"
"strings"
"golang.org/x/text/language"
"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"
)
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) {
requestURL := r.URL.Path
var head string
head, r.URL.Path = shiftPath(r.URL.Path)
if head == "static" {
h.fileHandler.ServeHTTP(w, r)
} else {
cookie := getSessionCookie(r)
conn := h.db.MustAcquire(r.Context(), cookie)
conn, err := h.db.Acquire(r.Context())
if err != nil {
panic(err)
}
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" {
switch r.Method {
case http.MethodPost:
h.handleLogin(w, r, conn)
h.handleLogin(w, r, l, conn)
default:
methodNotAllowed(w, r, http.MethodPost)
}
} else {
if !user.LoggedIn {
h.serveLoginForm(w, r, l, requestURL)
return
}
switch head {
case "":
switch r.Method {
case http.MethodGet:
h.handleGet(w, r)
h.serveDashboard(w, r, l)
default:
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 {
Email *form.Input
Password *form.Input
Redirect *form.Input
Error error
}
@ -35,6 +36,9 @@ func newLoginForm() *loginForm {
Password: &form.Input{
Name: "password",
},
Redirect: &form.Input{
Name: "redirect",
},
}
}
@ -44,6 +48,10 @@ func (f *loginForm) Parse(r *http.Request) error {
}
f.Email.FillValue(r)
f.Password.FillValue(r)
f.Redirect.FillValue(r)
if f.Redirect.Val == "" {
f.Redirect.Val = "/"
}
return nil
}
@ -56,28 +64,24 @@ func (f *loginForm) Valid(l *locale.Locale) bool {
return v.AllOK
}
func (h *App) handleGet(w http.ResponseWriter, r *http.Request) {
l := h.matchLocale(r)
func (h *App) serveLoginForm(w http.ResponseWriter, _ *http.Request, l *locale.Locale, requestURL string) {
login := newLoginForm()
login.Redirect.Val = requestURL
w.WriteHeader(http.StatusUnauthorized)
template.MustRender(w, l, "login.gohtml", login)
}
func (h *App) matchLocale(r *http.Request) *locale.Locale {
return locale.Match(r, h.locales, h.defaultLocale, h.languageMatcher)
}
func (h *App) handleLogin(w http.ResponseWriter, r *http.Request, conn *database.Conn) {
func (h *App) handleLogin(w http.ResponseWriter, r *http.Request, l *locale.Locale, conn *database.Conn) {
login := newLoginForm()
if err := login.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
l := h.matchLocale(r)
if login.Valid(l) {
cookie := conn.MustGetText(r.Context(), "select login($1, $2, $3)", login.Email, login.Password, httplib.RemoteAddr(r))
if cookie != "" {
setSessionCookie(w, cookie)
http.Redirect(w, r, "/", http.StatusSeeOther)
http.Redirect(w, r, login.Redirect.Val, http.StatusSeeOther)
return
}
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
}
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 {
*pgxpool.Conn
}

View File

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

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-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"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n"
@ -18,22 +18,27 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\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"
msgid "Login"
msgstr "Entrada"
#: web/templates/login.gohtml:17
#: web/templates/login.gohtml:18
msgctxt "input"
msgid "Email"
msgstr "Correu-e"
#: web/templates/login.gohtml:26
#: web/templates/login.gohtml:27
msgctxt "input"
msgid "Password"
msgstr "Contrasenya"
#: web/templates/login.gohtml:35
#: web/templates/login.gohtml:36
msgctxt "action"
msgid "Login"
msgstr "Entra"
@ -42,18 +47,18 @@ msgstr "Entra"
msgid "Skip to main content"
msgstr "Salta al contingut principal"
#: pkg/app/login.go:50
#: pkg/app/login.go:60
msgid "Email can not be empty."
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."
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."
msgstr "No podeu deixar la contrasenya en blanc."
#: pkg/app/login.go:82
#: pkg/app/login.go:86
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-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"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n"
@ -18,22 +18,27 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\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"
msgid "Login"
msgstr "Entrada"
#: web/templates/login.gohtml:17
#: web/templates/login.gohtml:18
msgctxt "input"
msgid "Email"
msgstr "Correo-e"
#: web/templates/login.gohtml:26
#: web/templates/login.gohtml:27
msgctxt "input"
msgid "Password"
msgstr "Contraseña"
#: web/templates/login.gohtml:35
#: web/templates/login.gohtml:36
msgctxt "action"
msgid "Login"
msgstr "Entrar"
@ -42,18 +47,18 @@ msgstr "Entrar"
msgid "Skip to main content"
msgstr "Saltar al contenido principal"
#: pkg/app/login.go:50
#: pkg/app/login.go:60
msgid "Email can not be empty."
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."
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."
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."
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" -}}
{{( pgettext "Login" "title" )}}
{{- end }}
@ -5,6 +9,7 @@
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/app.loginForm */ -}}
<form method="post" action="/login">
<input type="hidden" name="{{ .Redirect.Name}}" value="{{ .Redirect.Val }}">
<h2>{{( pgettext "Login" "title" )}}</h2>
{{ if .Error -}}
<div class="error" role="alert">