User login

main
Drew Bednar 2 months ago
parent 202b8457b5
commit a50cb265cb

@ -12,10 +12,9 @@ import (
rdb "git.runcible.io/learning/ratchet/internal/database"
"git.runcible.io/learning/ratchet/internal/logging"
"git.runcible.io/learning/ratchet/internal/server"
"github.com/alexedwards/scs/sqlite3store"
"github.com/alexedwards/scs/v2"
_ "github.com/mattn/go-sqlite3"
// "git.runcible.io/learning/ratchet"
// ratchethttp "git.runcible.io/learning/ratchet/internal"
)
// var (
@ -56,6 +55,7 @@ func main() {
// SessionManager
sm := scs.New()
sm.Store = sqlite3store.New(db)
// ratchet.Version = strings.TrimPrefix(version, "")
// ratchet.Commit = commit
app := server.NewRatchetApp(logger, tc, db, sm)

@ -2,6 +2,7 @@ package model
import (
"database/sql"
"errors"
"fmt"
"log/slog"
"time"
@ -57,7 +58,29 @@ func (u *UserService) Insert(name, email, password string) (int, error) {
}
func (u *UserService) Authenticate(email, password string) (int, error) {
return 0, nil
var id int
var hashedPassword []byte
stmt := `SELECT id, hashed_password FROM users WHERE email == ?`
err := u.DB.QueryRow(stmt, email).Scan(&id, &hashedPassword)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, ErrInvalidCredentials
} else {
return 0, err
}
}
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(password))
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return 0, ErrInvalidCredentials
} else {
return 0, err
}
}
return id, nil
}
func (u *UserService) Exists(id int) (bool, error) {

@ -39,6 +39,12 @@ type userSignupForm struct {
validator.Validator `form:"-"`
}
type userLoginForm struct {
Email string `form:"email"`
Password string `form:"password"`
validator.Validator `form:"-"`
}
func decodePostForm(r *http.Request, fd *form.Decoder, dst any) error {
err := r.ParseForm()
if err != nil {

@ -346,15 +346,76 @@ func handleUserSignupPost(logger *slog.Logger, tc *TemplateCache, fd *form.Decod
})
}
func handleUserLoginGet() http.Handler {
func handleUserLoginGet(tc *TemplateCache, sm *scs.SessionManager) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Displaying loging form for user")
data := newTemplateData(r, sm)
data.Form = userLoginForm{}
renderTemplate(w, r, tc, http.StatusOK, "login.go.tmpl", data)
})
}
func handleUserLoginPost() http.Handler {
func handleUserLoginPost(logger *slog.Logger, tc *TemplateCache, sm *scs.SessionManager, fd *form.Decoder, userService *model.UserService) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Authenticate and login user")
// parse form
err := r.ParseForm()
if err != nil {
logger.Error("Failed to parse login form")
clientError(w, http.StatusBadRequest)
return
}
form := userLoginForm{}
err = decodePostForm(r, fd, &form)
if err != nil {
logger.Error("Failed to decode login form")
clientError(w, http.StatusBadRequest)
return
}
form.CheckField(validator.NotBlank(form.Email), "email", "This field cannot be blank")
form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "This field must be a valid email")
form.CheckField(validator.NotBlank(form.Password), "password", "This field cannot be blank")
if !form.Valid() {
logger.Info("An invalid form was submitted")
data := newTemplateData(r, sm)
data.Form = form
renderTemplate(w, r, tc, http.StatusUnprocessableEntity, "login.go.tmpl", data)
return
}
id, err := userService.Authenticate(form.Email, form.Password)
if err != nil {
if errors.Is(err, model.ErrInvalidCredentials) {
form.AddNonFieldError("Email or password is incorrect")
data := newTemplateData(r, sm)
data.Form = form
renderTemplate(w, r, tc, http.StatusUnprocessableEntity, "login.go.tmpl", data)
} else {
serverError(w, r, err)
}
return
}
// Use the RenewToken() method on the current session to change the session ID.
// It's good practice to generate a new session ID when the authentication state
// or privilege levels change for the user (e.g. login and logout operations)
// This changes the ID of the current users session but retain any data
// associated with the session
// see https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Session_Management_Cheat_Sheet.md#renew-the-session-id-after-any-privilege-level-change
err = sm.RenewToken(r.Context())
if err != nil {
serverError(w, r, err)
return
}
// Add the ID of the current user to the session, so that they are now "logged in"
sm.Put(r.Context(), "authenticatedUserID", id)
http.Redirect(w, r, "/snippet/create", http.StatusSeeOther)
})
}

@ -33,8 +33,8 @@ func addRoutes(mux *http.ServeMux,
mux.Handle("GET /user/signup", sm.LoadAndSave(handleUserSignupGet(tc, sm)))
mux.Handle("POST /user/signup", sm.LoadAndSave(handleUserSignupPost(logger, tc, fd, sm, userService)))
mux.Handle("GET /user/login", sm.LoadAndSave(handleUserLoginGet()))
mux.Handle("POST /user/login", sm.LoadAndSave(handleUserLoginPost()))
mux.Handle("GET /user/login", sm.LoadAndSave(handleUserLoginGet(tc, sm)))
mux.Handle("POST /user/login", sm.LoadAndSave(handleUserLoginPost(logger, tc, sm, fd, userService)))
mux.Handle("POST /user/logout", sm.LoadAndSave(handleUserLogoutPost()))
return mux

@ -1,5 +1,10 @@
// See also https://www.alexedwards.net/blog/validation-snippets-for-go
// for more validation snippets
//
// A NonFieldError for example would be "Your email or password is incorrect".
// more secure because it does not leak which field was in error. Used in the login
// form
package validator
import (
@ -10,12 +15,17 @@ import (
)
type Validator struct {
FieldErrors map[string]string
NonFieldErrors []string
FieldErrors map[string]string
}
// Valid() returns true if the FieldErrors map doesn't contain any entries.
func (v *Validator) Valid() bool {
return len(v.FieldErrors) == 0
return len(v.FieldErrors) == 0 && len(v.NonFieldErrors) == 0
}
func (v *Validator) AddNonFieldError(message string) {
v.NonFieldErrors = append(v.NonFieldErrors, message)
}
// AddFieldError() adds an error message to the FieldErrors map (so long as no

@ -0,0 +1,28 @@
{{define "title"}}Login{{end}}
{{define "main"}}
<form action='/user/login' method='POST' novalidate>
<!-- Notice that here we are looping over the NonFieldErrors and displaying
them, if any exist -->
{{range .Form.NonFieldErrors}}
<div class='error'>{{.}}</div>
{{end}}
<div>
<label>Email:</label>
{{with .Form.FieldErrors.email}}
<label class='error'>{{.}}</label>
{{end}}
<input type='email' name='email' value='{{.Form.Email}}'>
</div>
<div>
<label>Password:</label>
{{with .Form.FieldErrors.password}}
<label class='error'>{{.}}</label>
{{end}}
<input type='password' name='password'>
</div>
<div>
<input type='submit' value='Login'>
</div>
</form>
{{end}}
Loading…
Cancel
Save