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:
parent
9fccd5f81d
commit
1ef6dcc4cf
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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."))
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
23
po/ca.po
23
po/ca.po
|
@ -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 d’usuari o contrasenya incorrectes."
|
msgstr "Nom d’usuari o contrasenya incorrectes."
|
||||||
|
|
23
po/es.po
23
po/es.po
|
@ -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."
|
||||||
|
|
|
@ -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 }}
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in New Issue