Use a “proper” struct for the login form
Similar to the profile form, the login form now parses and validates itself, with the InputField structs that the templates expect. I realized that i was doing more work than necessary when parsing fields fro the profile form because i was repeating the operation and the field name, so now it is a function of InputField. This time i needed extra attributes for the login form. I am not sure that the Go source code needs to know about HTML attributes, but it was the easiest way to pass them to the template.
This commit is contained in:
parent
75fd12bf1c
commit
ff5b76b4f5
15
pkg/form.go
15
pkg/form.go
|
@ -2,18 +2,29 @@ package pkg
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Attribute struct {
|
||||
Key, Val string
|
||||
}
|
||||
|
||||
type InputField struct {
|
||||
Name string
|
||||
Label string
|
||||
Type string
|
||||
Value string
|
||||
Required bool
|
||||
Attributes []*Attribute
|
||||
Errors []error
|
||||
}
|
||||
|
||||
func (field *InputField) FillValue(r *http.Request) {
|
||||
field.Value = strings.TrimSpace(r.FormValue(field.Name))
|
||||
}
|
||||
|
||||
func (field *InputField) Equals(other *InputField) bool {
|
||||
return field.Value == other.Value
|
||||
}
|
||||
|
@ -40,6 +51,10 @@ type SelectField struct {
|
|||
Errors []error
|
||||
}
|
||||
|
||||
func (field *SelectField) FillValue(r *http.Request) {
|
||||
field.Selected = r.FormValue(field.Name)
|
||||
}
|
||||
|
||||
func (field *SelectField) HasValidOption() bool {
|
||||
for _, option := range field.Options {
|
||||
if option.Value == field.Selected {
|
||||
|
|
96
pkg/login.go
96
pkg/login.go
|
@ -2,6 +2,7 @@ package pkg
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
@ -17,17 +18,66 @@ const (
|
|||
defaultRole = "guest"
|
||||
)
|
||||
|
||||
type LoginPage struct {
|
||||
LoginError bool
|
||||
Email string
|
||||
Password string
|
||||
type loginForm struct {
|
||||
locale *Locale
|
||||
Errors []error
|
||||
Email *InputField
|
||||
Password *InputField
|
||||
Valid bool
|
||||
}
|
||||
|
||||
type AppUser struct {
|
||||
Email string
|
||||
LoggedIn bool
|
||||
Role string
|
||||
Language language.Tag
|
||||
func newLoginForm(locale *Locale) *loginForm {
|
||||
return &loginForm{
|
||||
locale: locale,
|
||||
Email: &InputField{
|
||||
Name: "email",
|
||||
Label: pgettext("input", "Email", locale),
|
||||
Type: "email",
|
||||
Required: true,
|
||||
Attributes: []*Attribute{
|
||||
{"autofocus", "autofocus"},
|
||||
{"autocomplete", "username"},
|
||||
{"autocapitalize", "none"},
|
||||
},
|
||||
},
|
||||
Password: &InputField{
|
||||
Name: "password",
|
||||
Label: pgettext("input", "Password", locale),
|
||||
Type: "password",
|
||||
Required: true,
|
||||
Attributes: []*Attribute{
|
||||
{"autocomplete", "current-password"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (form *loginForm) Parse(r *http.Request) error {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
form.Email.FillValue(r)
|
||||
form.Password.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (form *loginForm) Validate() bool {
|
||||
form.Valid = true
|
||||
if form.Email.IsEmpty() {
|
||||
form.AppendInputError(form.Email, errors.New(gettext("Email can not be empty.", form.locale)))
|
||||
} else if !form.Email.HasValidEmail() {
|
||||
form.AppendInputError(form.Email, errors.New(gettext("This value is not a valid email. It should be like name@domain.com.", form.locale)))
|
||||
}
|
||||
if form.Password.IsEmpty() {
|
||||
form.AppendInputError(form.Password, errors.New(gettext("Password can not be empty.", form.locale)))
|
||||
}
|
||||
return form.Valid
|
||||
}
|
||||
|
||||
func (form *loginForm) AppendInputError(field *InputField, err error) {
|
||||
field.Errors = append(field.Errors, err)
|
||||
form.Valid = false
|
||||
}
|
||||
|
||||
func LoginHandler() http.Handler {
|
||||
|
@ -37,25 +87,28 @@ func LoginHandler() http.Handler {
|
|||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
r.ParseForm()
|
||||
page := LoginPage{
|
||||
Email: r.FormValue("email"),
|
||||
Password: r.FormValue("password"),
|
||||
}
|
||||
locale := getLocale(r)
|
||||
form := newLoginForm(locale)
|
||||
if r.Method == "POST" {
|
||||
if err := form.Parse(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if form.Validate() {
|
||||
conn := getConn(r)
|
||||
cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", page.Email, page.Password, remoteAddr(r))
|
||||
cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", form.Email.Value, form.Password.Value, remoteAddr(r))
|
||||
if cookie != "" {
|
||||
setSessionCookie(w, cookie)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
form.Errors = append(form.Errors, errors.New(gettext("Invalid user or password.", locale)))
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
page.LoginError = true
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
}
|
||||
mustRenderWebTemplate(w, r, "login.gohtml", page)
|
||||
}
|
||||
mustRenderWebTemplate(w, r, "login.gohtml", form)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -91,6 +144,13 @@ func createSessionCookie(value string, duration time.Duration) *http.Cookie {
|
|||
}
|
||||
}
|
||||
|
||||
type AppUser struct {
|
||||
Email string
|
||||
LoggedIn bool
|
||||
Role string
|
||||
Language language.Tag
|
||||
}
|
||||
|
||||
func CheckLogin(db *Db, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var ctx = r.Context()
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LanguageOption struct {
|
||||
|
@ -13,6 +12,7 @@ type LanguageOption struct {
|
|||
}
|
||||
|
||||
type profileForm struct {
|
||||
locale *Locale
|
||||
Name *InputField
|
||||
Email *InputField
|
||||
Password *InputField
|
||||
|
@ -25,6 +25,7 @@ func newProfileForm(ctx context.Context, conn *Conn, locale *Locale) *profileFor
|
|||
automaticOption := pgettext("language option", "Automatic", locale)
|
||||
languages := MustGetOptions(ctx, conn, "select 'und', $1 union all select lang_tag, endonym from language where selectable", automaticOption)
|
||||
return &profileForm{
|
||||
locale: locale,
|
||||
Name: &InputField{
|
||||
Name: "name",
|
||||
Label: pgettext("input", "User name", locale),
|
||||
|
@ -59,29 +60,29 @@ func (form *profileForm) Parse(r *http.Request) error {
|
|||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
form.Email.Value = strings.TrimSpace(r.FormValue("email"))
|
||||
form.Name.Value = strings.TrimSpace(r.FormValue("name"))
|
||||
form.Password.Value = strings.TrimSpace(r.FormValue("password"))
|
||||
form.PasswordConfirm.Value = strings.TrimSpace(r.FormValue("password_confirm"))
|
||||
form.Language.Selected = r.FormValue("language")
|
||||
form.Email.FillValue(r)
|
||||
form.Name.FillValue(r)
|
||||
form.Password.FillValue(r)
|
||||
form.PasswordConfirm.FillValue(r)
|
||||
form.Language.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (form *profileForm) Validate(locale *Locale) bool {
|
||||
func (form *profileForm) Validate() bool {
|
||||
form.Valid = true
|
||||
if form.Email.IsEmpty() {
|
||||
form.AppendInputError(form.Email, errors.New(gettext("Email can not be empty.", locale)))
|
||||
form.AppendInputError(form.Email, errors.New(gettext("Email can not be empty.", form.locale)))
|
||||
} else if !form.Email.HasValidEmail() {
|
||||
form.AppendInputError(form.Email, errors.New(gettext("This value is not a valid email. It should be like name@domain.com.", locale)))
|
||||
form.AppendInputError(form.Email, errors.New(gettext("This value is not a valid email. It should be like name@domain.com.", form.locale)))
|
||||
}
|
||||
if form.Name.IsEmpty() {
|
||||
form.AppendInputError(form.Name, errors.New(gettext("Name can not be empty.", locale)))
|
||||
form.AppendInputError(form.Name, errors.New(gettext("Name can not be empty.", form.locale)))
|
||||
}
|
||||
if !form.PasswordConfirm.Equals(form.Password) {
|
||||
form.AppendInputError(form.PasswordConfirm, errors.New(gettext("Confirmation does not match password.", locale)))
|
||||
form.AppendInputError(form.PasswordConfirm, errors.New(gettext("Confirmation does not match password.", form.locale)))
|
||||
}
|
||||
if !form.Language.HasValidOption() {
|
||||
form.AppendSelectError(form.Language, errors.New(gettext("Selected language is not valid.", locale)))
|
||||
form.AppendSelectError(form.Language, errors.New(gettext("Selected language is not valid.", form.locale)))
|
||||
}
|
||||
return form.Valid
|
||||
}
|
||||
|
@ -107,7 +108,7 @@ func ProfileHandler() http.Handler {
|
|||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if ok := form.Validate(locale); ok {
|
||||
if ok := form.Validate(); ok {
|
||||
//goland:noinspection SqlWithoutWhere
|
||||
cookie := conn.MustGetText(r.Context(), "", "update user_profile set name = $1, email = $2, lang_tag = $3 returning build_cookie()", form.Name.Value, form.Email.Value, form.Language.Selected)
|
||||
setSessionCookie(w, cookie)
|
||||
|
|
68
po/ca.po
68
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: numerus\n"
|
||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||
"POT-Creation-Date: 2023-02-01 10:11+0100\n"
|
||||
"POT-Creation-Date: 2023-02-01 10:52+0100\n"
|
||||
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||
|
@ -47,27 +47,12 @@ msgctxt "nav"
|
|||
msgid "Contacts"
|
||||
msgstr "Contactes"
|
||||
|
||||
#: web/template/login.gohtml:2 web/template/login.gohtml:13
|
||||
#: web/template/login.gohtml:2 web/template/login.gohtml:15
|
||||
msgctxt "title"
|
||||
msgid "Login"
|
||||
msgstr "Entrada"
|
||||
|
||||
#: web/template/login.gohtml:9
|
||||
msgid "Invalid user or password"
|
||||
msgstr "Nom d’usuari o contrasenya incorrectes"
|
||||
|
||||
#: web/template/login.gohtml:17 web/template/tax-details.gohtml:27
|
||||
#: web/template/contacts-new.gohtml:27 pkg/profile.go:36
|
||||
msgctxt "input"
|
||||
msgid "Email"
|
||||
msgstr "Correu-e"
|
||||
|
||||
#: web/template/login.gohtml:22 pkg/profile.go:42
|
||||
msgctxt "input"
|
||||
msgid "Password"
|
||||
msgstr "Contrasenya"
|
||||
|
||||
#: web/template/login.gohtml:25
|
||||
#: web/template/login.gohtml:19
|
||||
msgctxt "action"
|
||||
msgid "Login"
|
||||
msgstr "Entra"
|
||||
|
@ -156,6 +141,12 @@ msgctxt "input"
|
|||
msgid "Phone"
|
||||
msgstr "Telèfon"
|
||||
|
||||
#: web/template/tax-details.gohtml:27 web/template/contacts-new.gohtml:27
|
||||
#: pkg/login.go:34 pkg/profile.go:37
|
||||
msgctxt "input"
|
||||
msgid "Email"
|
||||
msgstr "Correu-e"
|
||||
|
||||
#: web/template/tax-details.gohtml:31 web/template/contacts-new.gohtml:31
|
||||
msgctxt "input"
|
||||
msgid "Web"
|
||||
|
@ -230,43 +221,56 @@ msgctxt "title"
|
|||
msgid "New Contact"
|
||||
msgstr "Nou contacte"
|
||||
|
||||
#: pkg/login.go:45 pkg/profile.go:43
|
||||
msgctxt "input"
|
||||
msgid "Password"
|
||||
msgstr "Contrasenya"
|
||||
|
||||
#: pkg/login.go:68 pkg/profile.go:74
|
||||
msgid "Email can not be empty."
|
||||
msgstr "No podeu deixar el correu-e en blanc."
|
||||
|
||||
#: pkg/login.go:70 pkg/profile.go:76
|
||||
msgid "This value is not a valid email. It should be like name@domain.com."
|
||||
msgstr "Aquest valor no és un correu-e vàlid. Hauria de ser similar a nom@domini.cat."
|
||||
|
||||
#: pkg/login.go:73
|
||||
msgid "Password can not be empty."
|
||||
msgstr "No podeu deixar la contrasenya en blanc."
|
||||
|
||||
#: pkg/login.go:105
|
||||
msgid "Invalid user or password."
|
||||
msgstr "Nom d’usuari o contrasenya incorrectes."
|
||||
|
||||
#: pkg/profile.go:25
|
||||
msgctxt "language option"
|
||||
msgid "Automatic"
|
||||
msgstr "Automàtic"
|
||||
|
||||
#: pkg/profile.go:30
|
||||
#: pkg/profile.go:31
|
||||
msgctxt "input"
|
||||
msgid "User name"
|
||||
msgstr "Nom d’usuari"
|
||||
|
||||
#: pkg/profile.go:47
|
||||
#: pkg/profile.go:48
|
||||
msgctxt "input"
|
||||
msgid "Password Confirmation"
|
||||
msgstr "Confirmació contrasenya"
|
||||
|
||||
#: pkg/profile.go:52
|
||||
#: pkg/profile.go:53
|
||||
msgctxt "input"
|
||||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: pkg/profile.go:73
|
||||
msgid "Email can not be empty."
|
||||
msgstr "No podeu deixar el correu-e en blanc."
|
||||
|
||||
#: pkg/profile.go:75
|
||||
msgid "This value is not a valid email. It should be like name@domain.com."
|
||||
msgstr "Aquest valor no és un correu-e vàlid. Hauria de ser similar a nom@domini.cat."
|
||||
|
||||
#: pkg/profile.go:78
|
||||
#: pkg/profile.go:79
|
||||
msgid "Name can not be empty."
|
||||
msgstr "No podeu deixar el nom en blanc."
|
||||
|
||||
#: pkg/profile.go:81
|
||||
#: pkg/profile.go:82
|
||||
msgid "Confirmation does not match password."
|
||||
msgstr "La confirmació no és igual a la contrasenya."
|
||||
|
||||
#: pkg/profile.go:84
|
||||
#: pkg/profile.go:85
|
||||
msgid "Selected language is not valid."
|
||||
msgstr "Heu seleccionat un idioma que no és vàlid."
|
||||
|
||||
|
|
68
po/es.po
68
po/es.po
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: numerus\n"
|
||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||
"POT-Creation-Date: 2023-02-01 10:11+0100\n"
|
||||
"POT-Creation-Date: 2023-02-01 10:52+0100\n"
|
||||
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||
|
@ -47,27 +47,12 @@ msgctxt "nav"
|
|||
msgid "Contacts"
|
||||
msgstr "Contactos"
|
||||
|
||||
#: web/template/login.gohtml:2 web/template/login.gohtml:13
|
||||
#: web/template/login.gohtml:2 web/template/login.gohtml:15
|
||||
msgctxt "title"
|
||||
msgid "Login"
|
||||
msgstr "Entrada"
|
||||
|
||||
#: web/template/login.gohtml:9
|
||||
msgid "Invalid user or password"
|
||||
msgstr "Nombre de usuario o contraseña inválido"
|
||||
|
||||
#: web/template/login.gohtml:17 web/template/tax-details.gohtml:27
|
||||
#: web/template/contacts-new.gohtml:27 pkg/profile.go:36
|
||||
msgctxt "input"
|
||||
msgid "Email"
|
||||
msgstr "Correo-e"
|
||||
|
||||
#: web/template/login.gohtml:22 pkg/profile.go:42
|
||||
msgctxt "input"
|
||||
msgid "Password"
|
||||
msgstr "Contraseña"
|
||||
|
||||
#: web/template/login.gohtml:25
|
||||
#: web/template/login.gohtml:19
|
||||
msgctxt "action"
|
||||
msgid "Login"
|
||||
msgstr "Entrar"
|
||||
|
@ -156,6 +141,12 @@ msgctxt "input"
|
|||
msgid "Phone"
|
||||
msgstr "Teléfono"
|
||||
|
||||
#: web/template/tax-details.gohtml:27 web/template/contacts-new.gohtml:27
|
||||
#: pkg/login.go:34 pkg/profile.go:37
|
||||
msgctxt "input"
|
||||
msgid "Email"
|
||||
msgstr "Correo-e"
|
||||
|
||||
#: web/template/tax-details.gohtml:31 web/template/contacts-new.gohtml:31
|
||||
msgctxt "input"
|
||||
msgid "Web"
|
||||
|
@ -230,43 +221,56 @@ msgctxt "title"
|
|||
msgid "New Contact"
|
||||
msgstr "Nuevo contacto"
|
||||
|
||||
#: pkg/login.go:45 pkg/profile.go:43
|
||||
msgctxt "input"
|
||||
msgid "Password"
|
||||
msgstr "Contraseña"
|
||||
|
||||
#: pkg/login.go:68 pkg/profile.go:74
|
||||
msgid "Email can not be empty."
|
||||
msgstr "No podéis dejar el correo-e en blanco."
|
||||
|
||||
#: pkg/login.go:70 pkg/profile.go:76
|
||||
msgid "This value is not a valid email. It should be like name@domain.com."
|
||||
msgstr "Este valor no es un correo-e válido. Tiene que ser parecido a nombre@dominio.es."
|
||||
|
||||
#: pkg/login.go:73
|
||||
msgid "Password can not be empty."
|
||||
msgstr "No podéis dejar la contaseña en blanco."
|
||||
|
||||
#: pkg/login.go:105
|
||||
msgid "Invalid user or password."
|
||||
msgstr "Nombre de usuario o contraseña inválido."
|
||||
|
||||
#: pkg/profile.go:25
|
||||
msgctxt "language option"
|
||||
msgid "Automatic"
|
||||
msgstr "Automático"
|
||||
|
||||
#: pkg/profile.go:30
|
||||
#: pkg/profile.go:31
|
||||
msgctxt "input"
|
||||
msgid "User name"
|
||||
msgstr "Nombre de usuario"
|
||||
|
||||
#: pkg/profile.go:47
|
||||
#: pkg/profile.go:48
|
||||
msgctxt "input"
|
||||
msgid "Password Confirmation"
|
||||
msgstr "Confirmación contrasenya"
|
||||
|
||||
#: pkg/profile.go:52
|
||||
#: pkg/profile.go:53
|
||||
msgctxt "input"
|
||||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: pkg/profile.go:73
|
||||
msgid "Email can not be empty."
|
||||
msgstr "No podéis dejar el correo-e en blanco."
|
||||
|
||||
#: pkg/profile.go:75
|
||||
msgid "This value is not a valid email. It should be like name@domain.com."
|
||||
msgstr "Este valor no es un correo-e válido. Tiene que ser parecido a nombre@dominio.es."
|
||||
|
||||
#: pkg/profile.go:78
|
||||
#: pkg/profile.go:79
|
||||
msgid "Name can not be empty."
|
||||
msgstr "No podéis dejar el nombre en blanco."
|
||||
|
||||
#: pkg/profile.go:81
|
||||
#: pkg/profile.go:82
|
||||
msgid "Confirmation does not match password."
|
||||
msgstr "La confirmación no corresponde con la contraseña."
|
||||
|
||||
#: pkg/profile.go:84
|
||||
#: pkg/profile.go:85
|
||||
msgid "Selected language is not valid."
|
||||
msgstr "Habéis escogido un idioma que no es válido."
|
||||
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
{{ define "input-field" -}}
|
||||
<div class="input {{ if .Errors }}has-errors{{ end }}">
|
||||
<input type="{{ .Type }}" name="{{ .Name }}" id="{{ .Name }}-field"
|
||||
{{- range $attribute := .Attributes }} {{$attribute.Key}}="{{$attribute.Val}}" {{ end -}}
|
||||
{{ if .Required }}required="required"{{ end }} value="{{ .Value }}" placeholder="{{ .Label }}">
|
||||
<label for="{{ .Name }}-field">{{ .Label }}</label>
|
||||
{{ if .Errors }}
|
||||
{{- if .Errors }}
|
||||
<ul>
|
||||
{{- range $error := .Errors }}
|
||||
<li>{{ . }}</li>
|
||||
{{- end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end }}
|
||||
|
||||
|
|
|
@ -1,27 +1,21 @@
|
|||
{{ define "title" -}}
|
||||
{{( pgettext "Login" "title" )}}
|
||||
{{( pgettext "Login" "title" )}}
|
||||
{{- end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<h1><img src="/static/numerus.svg" alt="Numerus" width="620" height="77"></h1>
|
||||
{{ if .LoginError -}}
|
||||
{{ if .Errors -}}
|
||||
<div class="error" role="alert">
|
||||
<p>{{( gettext "Invalid user or password" )}}</p>
|
||||
{{ range $error := .Errors -}}
|
||||
<p>{{ $error }}</p>
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end }}
|
||||
<section id="login">
|
||||
<h2>{{( pgettext "Login" "title" )}}</h2>
|
||||
<form method="POST" action="/login">
|
||||
<div class="input">
|
||||
<input id="user_email" type="email" required autofocus name="email" autocapitalize="none" value="{{ .Email }}" placeholder="{{( pgettext "Email" "input" )}}">
|
||||
<label for="user_email">{{( pgettext "Email" "input" )}}</label>
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<input id="user_password" type="password" required name="password" autocomplete="current-password" value="{{ .Password }}" placeholder="{{( pgettext "Password" "input" )}}">
|
||||
<label for="user_password">{{( pgettext "Password" "input" )}}</label>
|
||||
</div>
|
||||
|
||||
{{ template "input-field" .Email }}
|
||||
{{ template "input-field" .Password }}
|
||||
<button type="submit">{{( pgettext "Login" "action" )}}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
|
Loading…
Reference in New Issue