From 2300735030e29db890956ad6869008fc7ae84d02 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Sun, 23 Jul 2023 20:49:26 +0200 Subject: [PATCH] Handle the login form I now actually handle the /login URL and check whether the email and password are valid, creating the session cookie if correct, but doing nothing else with that cookie, for now. The validation is done by hand for now, because i do not yet how i will actually do it without so much duplication. --- pkg/app/app.go | 74 +++++++++++++++++++++++++++++++++++--- pkg/database/db.go | 28 +++++++++++++++ web/templates/login.gohtml | 23 ++++++++---- 3 files changed, 115 insertions(+), 10 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index dda379f..56afa90 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -6,18 +6,25 @@ package app import ( + "errors" "net/http" + "net/mail" "path" "strings" + "time" "golang.org/x/text/language" "dev.tandem.ws/tandem/camper/pkg/database" - middleware "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/template" ) +const ( + sessionCookie = "camper-session" +) + func shiftPath(p string) (head, tail string) { p = path.Clean("/" + p) if i := strings.IndexByte(p[1:], '/') + 1; i <= 0 { @@ -51,8 +58,8 @@ func New(db *database.DB) http.Handler { } var handler http.Handler = app - handler = middleware.RecoverPanic(handler) - handler = middleware.LogRequest(handler) + handler = httplib.RecoverPanic(handler) + handler = httplib.LogRequest(handler) return handler } @@ -62,6 +69,13 @@ func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch head { case "static": h.fileHandler.ServeHTTP(w, r) + case "login": + switch r.Method { + case http.MethodPost: + h.handleLogin(w, r) + default: + methodNotAllowed(w, r, http.MethodPost) + } case "": switch r.Method { case http.MethodGet: @@ -75,6 +89,58 @@ func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (h *App) handleGet(w http.ResponseWriter, r *http.Request) { - l := locale.Match(r, h.locales, h.defaultLocale, h.languageMatcher) + 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/database/db.go b/pkg/database/db.go index 1d82fe9..3f8fe25 100644 --- a/pkg/database/db.go +++ b/pkg/database/db.go @@ -44,3 +44,31 @@ func New(ctx context.Context, connString string) (*DB, error) { type DB struct { *pgxpool.Pool } + +func (db *DB) Acquire(ctx context.Context) (*Conn, error) { + conn, err := db.Pool.Acquire(ctx) + if err != nil { + return nil, err + } + return &Conn{conn}, nil +} + +func (db *DB) MustAcquire(ctx context.Context) *Conn { + conn, err := db.Acquire(ctx) + if err != nil { + panic(err) + } + return conn +} + +type Conn struct { + *pgxpool.Conn +} + +func (c *Conn) MustGetText(ctx context.Context, sql string, args ...interface{}) string { + var result string + if err := c.QueryRow(ctx, sql, args...).Scan(&result); err != nil { + panic(err) + } + return result +} diff --git a/web/templates/login.gohtml b/web/templates/login.gohtml index cf1208d..09b861e 100644 --- a/web/templates/login.gohtml +++ b/web/templates/login.gohtml @@ -5,13 +5,24 @@ {{ define "content" -}}

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

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