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.
This commit is contained in:
jordi fita mas 2023-07-23 20:49:26 +02:00
parent 25bf186a83
commit 2300735030
3 changed files with 115 additions and 10 deletions

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -5,13 +5,24 @@
{{ define "content" -}}
<form method="post" action="/login">
<h2>{{( pgettext "Login" "title" )}}</h2>
{{ if . -}}
<div class="message" role="alert">
{{ range . -}}
<p>{{ . }}</p>
{{- end }}
</div>
{{- end }}
<fieldset>
<label for="email">{{( pgettext "Email" "input" )}}</label>
<input id="email" type="email" name="username" autocomplete="username" required autofocus>
<br>
<label for="current-password">{{( pgettext "Password" "input" )}}</label>
<input id="current-password" type="password" name="password" autocomplete="current-password" required>
<label>
{{( pgettext "Email" "input" )}}<br>
<input type="email" name="email" autocomplete="username" required autofocus><br>
</label><label>
{{( pgettext "Password" "input" )}}<br>
<input type="password" name="password" autocomplete="current-password" required><br>
</label>
</fieldset>
<button type="submit">{{( pgettext "Login" "action" )}}</button>
<footer>
<button type="submit">{{( pgettext "Login" "action" )}}</button>
</footer>
</form>
{{- end }}