diff --git a/pkg/app/app.go b/pkg/app/app.go index 56afa90..c80b0b2 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -6,23 +6,14 @@ package app import ( - "errors" + "golang.org/x/text/language" "net/http" - "net/mail" "path" "strings" - "time" - - "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" -) - -const ( - sessionCookie = "camper-session" ) func shiftPath(p string) (head, tail string) { @@ -87,60 +78,3 @@ func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) } } - -func (h *App) handleGet(w http.ResponseWriter, r *http.Request) { - l := h.matchLocale(r) - template.MustRender(w, l, "login.gohtml", nil) -} - -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) { - if err := r.ParseForm(); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - var errs []error - l := h.matchLocale(r) - email := strings.TrimSpace(r.FormValue("email")) - if email == "" { - errs = append(errs, errors.New(l.Get("Email can not be empty."))) - } else if _, err := mail.ParseAddress(email); err != nil { - errs = append(errs, errors.New(l.Get("This email is not valid. It should be like name@domain.com."))) - } - password := strings.TrimSpace(r.FormValue("password")) - if password == "" { - errs = append(errs, errors.New(l.Get("Password can not be empty."))) - } - if errs == nil { - conn := h.db.MustAcquire(r.Context()) - cookie := conn.MustGetText(r.Context(), "select login($1, $2, $3)", email, password, httplib.RemoteAddr(r)) - if cookie != "" { - setSessionCookie(w, cookie) - http.Redirect(w, r, "/", http.StatusSeeOther) - return - } - errs = append(errs, errors.New(l.Get("Invalid user or password."))) - w.WriteHeader(http.StatusUnauthorized) - } else { - w.WriteHeader(http.StatusUnprocessableEntity) - } - template.MustRender(w, l, "login.gohtml", errs) -} - -func setSessionCookie(w http.ResponseWriter, cookie string) { - http.SetCookie(w, createSessionCookie(cookie, 8766*24*time.Hour)) -} - -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/app/login.go b/pkg/app/login.go new file mode 100644 index 0000000..3f24ea5 --- /dev/null +++ b/pkg/app/login.go @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package app + +import ( + "errors" + "net/http" + "time" + + "dev.tandem.ws/tandem/camper/pkg/form" + httplib "dev.tandem.ws/tandem/camper/pkg/http" + "dev.tandem.ws/tandem/camper/pkg/locale" + "dev.tandem.ws/tandem/camper/pkg/template" +) + +const ( + sessionCookie = "camper-session" +) + +type loginForm struct { + Email *form.Input + Password *form.Input + Error error +} + +func newLoginForm() *loginForm { + return &loginForm{ + Email: &form.Input{ + Name: "email", + }, + Password: &form.Input{ + Name: "password", + }, + } +} + +func (f *loginForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.Email.FillValue(r) + f.Password.FillValue(r) + return nil +} + +func (f *loginForm) Valid(l *locale.Locale) bool { + v := form.NewValidator(l) + if v.CheckRequired(f.Email, l.GettextNoop("Email can not be empty.")) { + v.CheckValidEmail(f.Email, l.GettextNoop("This email is not valid. It should be like name@domain.com.")) + } + v.CheckRequired(f.Password, l.GettextNoop("Password can not be empty.")) + return v.AllOK +} + +func (h *App) handleGet(w http.ResponseWriter, r *http.Request) { + l := h.matchLocale(r) + login := newLoginForm() + 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) { + 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) { + conn := h.db.MustAcquire(r.Context()) + 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) + return + } + login.Error = errors.New(l.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 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/form/input.go b/pkg/form/input.go new file mode 100644 index 0000000..4f98595 --- /dev/null +++ b/pkg/form/input.go @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package form + +import ( + "database/sql/driver" + "net/http" + "strings" +) + +type Input struct { + Name string + Val string + Error error +} + +func (input *Input) FillValue(r *http.Request) { + input.Val = strings.TrimSpace(r.FormValue(input.Name)) +} +func (input *Input) Value() (driver.Value, error) { + return input.Val, nil +} diff --git a/pkg/form/validator.go b/pkg/form/validator.go new file mode 100644 index 0000000..07d9840 --- /dev/null +++ b/pkg/form/validator.go @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package form + +import ( + "dev.tandem.ws/tandem/camper/pkg/locale" + "errors" + "net/mail" +) + +type Validator struct { + l *locale.Locale + AllOK bool +} + +func NewValidator(l *locale.Locale) *Validator { + return &Validator{ + l: l, + AllOK: true, + } +} + +func (v *Validator) CheckRequired(input *Input, message string) bool { + return v.check(input, input.Val != "", message) +} + +func (v *Validator) CheckValidEmail(input *Input, message string) bool { + _, err := mail.ParseAddress(input.Val) + return v.check(input, err == nil, message) +} + +func (v *Validator) check(field *Input, ok bool, message string) bool { + if !ok { + field.Error = errors.New(v.l.Get(message)) + v.AllOK = false + } + return ok +} diff --git a/po/ca.po b/po/ca.po index 253eb01..f31fc43 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-22 23:45+0200\n" +"POT-Creation-Date: 2023-07-24 17:04+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -18,22 +18,22 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: web/templates/login.gohtml:2 web/templates/login.gohtml:7 +#: web/templates/login.gohtml:2 web/templates/login.gohtml:8 msgctxt "title" msgid "Login" msgstr "Entrada" -#: web/templates/login.gohtml:9 +#: web/templates/login.gohtml:17 msgctxt "input" msgid "Email" msgstr "Correu-e" -#: web/templates/login.gohtml:12 +#: web/templates/login.gohtml:26 msgctxt "input" msgid "Password" msgstr "Contrasenya" -#: web/templates/login.gohtml:15 +#: web/templates/login.gohtml:35 msgctxt "action" msgid "Login" msgstr "Entra" @@ -41,3 +41,19 @@ msgstr "Entra" #: web/templates/layout.gohtml:15 msgid "Skip to main content" msgstr "Salta al contingut principal" + +#: pkg/app/login.go:50 +msgid "Email can not be empty." +msgstr "No podeu deixar el correu en blanc." + +#: pkg/app/login.go:51 +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 +msgid "Password can not be empty." +msgstr "No podeu deixar la contrasenya en blanc." + +#: pkg/app/login.go:82 +msgid "Invalid user or password." +msgstr "Nom d’usuari o contrasenya incorrectes." diff --git a/po/es.po b/po/es.po index ae28e74..5ccfb31 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-22 23:45+0200\n" +"POT-Creation-Date: 2023-07-24 17:04+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -18,22 +18,22 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: web/templates/login.gohtml:2 web/templates/login.gohtml:7 +#: web/templates/login.gohtml:2 web/templates/login.gohtml:8 msgctxt "title" msgid "Login" msgstr "Entrada" -#: web/templates/login.gohtml:9 +#: web/templates/login.gohtml:17 msgctxt "input" msgid "Email" msgstr "Correo-e" -#: web/templates/login.gohtml:12 +#: web/templates/login.gohtml:26 msgctxt "input" msgid "Password" msgstr "Contraseña" -#: web/templates/login.gohtml:15 +#: web/templates/login.gohtml:35 msgctxt "action" msgid "Login" msgstr "Entrar" @@ -41,3 +41,19 @@ msgstr "Entrar" #: web/templates/layout.gohtml:15 msgid "Skip to main content" msgstr "Saltar al contenido principal" + +#: pkg/app/login.go:50 +msgid "Email can not be empty." +msgstr "No podéis dejar el correo-e en blanco." + +#: pkg/app/login.go:51 +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 +msgid "Password can not be empty." +msgstr "No podéis dejar la contraseña en blanco." + +#: pkg/app/login.go:82 +msgid "Invalid user or password." +msgstr "Usuario o contraseña incorrectos." diff --git a/web/templates/layout.gohtml b/web/templates/layout.gohtml index 4b34760..3665ba9 100644 --- a/web/templates/layout.gohtml +++ b/web/templates/layout.gohtml @@ -20,3 +20,10 @@ + +{{ define "error-attrs" }}{{ if .Error }}aria-invalid="true" aria-errormessage="{{ .Name }}-error"{{ end }}{{ end }} +{{ define "error-message" -}} + {{ if .Error -}} + {{ .Error }}
+ {{- end }} +{{- end }} diff --git a/web/templates/login.gohtml b/web/templates/login.gohtml index 09b861e..f4642e9 100644 --- a/web/templates/login.gohtml +++ b/web/templates/login.gohtml @@ -3,23 +3,33 @@ {{- end }} {{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/app.loginForm */ -}}

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

- {{ if . -}} -