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."