diff --git a/pkg/form.go b/pkg/form.go index bfebf97..bfe734a 100644 --- a/pkg/form.go +++ b/pkg/form.go @@ -2,16 +2,27 @@ 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 - Errors []error + 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 { @@ -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 { diff --git a/pkg/login.go b/pkg/login.go index 6522e58..f4bd486 100644 --- a/pkg/login.go +++ b/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" { - conn := getConn(r) - cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", page.Email, page.Password, remoteAddr(r)) - if cookie != "" { - setSessionCookie(w, cookie) - http.Redirect(w, r, "/", http.StatusSeeOther) + if err := form.Parse(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) return } - w.WriteHeader(http.StatusUnauthorized) - page.LoginError = true - } else { - w.WriteHeader(http.StatusOK) + if form.Validate() { + conn := getConn(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) + } else { + 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() diff --git a/pkg/profile.go b/pkg/profile.go index 7a9fb1b..27c447e 100644 --- a/pkg/profile.go +++ b/pkg/profile.go @@ -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) diff --git a/po/ca.po b/po/ca.po index 7cf7c48..03c1cbc 100644 --- a/po/ca.po +++ b/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 \n" "Language-Team: Catalan \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." diff --git a/po/es.po b/po/es.po index d7c5b8d..7ba5e79 100644 --- a/po/es.po +++ b/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 \n" "Language-Team: Spanish \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." diff --git a/web/template/form.gohtml b/web/template/form.gohtml index d66827d..b0e632d 100644 --- a/web/template/form.gohtml +++ b/web/template/form.gohtml @@ -1,15 +1,16 @@ {{ define "input-field" -}}
+ {{- range $attribute := .Attributes }} {{$attribute.Key}}="{{$attribute.Val}}" {{ end -}} + {{ if .Required }}required="required"{{ end }} value="{{ .Value }}" placeholder="{{ .Label }}"> - {{ if .Errors }} + {{- if .Errors }}
    {{- range $error := .Errors }}
  • {{ . }}
  • {{- end }}
- {{ end }} + {{- end }}
{{- end }} diff --git a/web/template/login.gohtml b/web/template/login.gohtml index 383cde3..283e83d 100644 --- a/web/template/login.gohtml +++ b/web/template/login.gohtml @@ -1,28 +1,22 @@ {{ define "title" -}} -{{( pgettext "Login" "title" )}} + {{( pgettext "Login" "title" )}} {{- end }} {{ define "content" }} -

Numerus

- {{ if .LoginError -}} - - {{- end }} -
-

{{( pgettext "Login" "title" )}}

-
-
- - -
- -
- - -
- - -
-
+

Numerus

+ {{ if .Errors -}} + + {{- end }} +
+

{{( pgettext "Login" "title" )}}

+
+ {{ template "input-field" .Email }} + {{ template "input-field" .Password }} + +
+
{{- end }}