From 202b8457b5f2ccc1fed85de5657c1bc128417d02 Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Sat, 8 Feb 2025 09:11:24 -0500 Subject: [PATCH] User signup --- go.mod | 1 + go.sum | 2 ++ internal/model/user.go | 36 +++++++++++++++++++++++++++++++++++- internal/server/handlers.go | 22 ++++++++++++++++++++-- internal/server/routes.go | 3 ++- internal/server/server.go | 2 +- 6 files changed, 61 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index b6067a5..b9f0978 100644 --- a/go.mod +++ b/go.mod @@ -8,4 +8,5 @@ require ( github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885 // indirect github.com/alexedwards/scs/v2 v2.8.0 // indirect github.com/go-playground/form/v4 v4.2.1 // indirect + golang.org/x/crypto v0.32.0 // indirect ) diff --git a/go.sum b/go.sum index eb987a7..97670f1 100644 --- a/go.sum +++ b/go.sum @@ -8,3 +8,5 @@ github.com/go-playground/form/v4 v4.2.1/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIh github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= diff --git a/internal/model/user.go b/internal/model/user.go index ba5527b..1d86f19 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -2,7 +2,12 @@ package model import ( "database/sql" + "fmt" + "log/slog" "time" + + "github.com/mattn/go-sqlite3" + "golang.org/x/crypto/bcrypt" ) type User struct { @@ -14,12 +19,41 @@ type User struct { UpdatedAt time.Time } +// TODD add logger to service type UserService struct { DB *sql.DB } func (u *UserService) Insert(name, email, password string) (int, error) { - return 0, nil + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12) + if err != nil { + return 0, err + } + + stmt := `INSERT INTO users (name, email, hashed_password) + VALUES (?,?,?)` + + result, err := u.DB.Exec(stmt, name, email, string(hashedPassword)) + if err != nil { + slog.Debug(fmt.Sprintf("Error encounters on insert: %s", err.Error())) + // This is a assertion that err is of type sqlite3.Error. If it is ok is true. + if serr, ok := err.(sqlite3.Error); ok { + slog.Debug("Error is sqlite3.Error type.") + if serr.ExtendedCode == sqlite3.ErrConstraintUnique { + slog.Debug("Error is a unique contraint violation.") + return 0, ErrDuplicateEmail + } + } + return 0, err + } + + lastId, err := result.LastInsertId() + if err != nil { + slog.Debug("An error occured when retrieving insert result id.") + return 0, err + } + slog.Debug(fmt.Sprintf("Inserted new user. User pk: %d", int(lastId))) + return int(lastId), nil } func (u *UserService) Authenticate(email, password string) (int, error) { diff --git a/internal/server/handlers.go b/internal/server/handlers.go index fc8765b..7287aef 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -291,7 +291,7 @@ func handleUserSignupGet(tc *TemplateCache, sm *scs.SessionManager) http.Handler }) } -func handleUserSignupPost(logger *slog.Logger, tc *TemplateCache, fd *form.Decoder, sm *scs.SessionManager) http.Handler { +func handleUserSignupPost(logger *slog.Logger, tc *TemplateCache, fd *form.Decoder, sm *scs.SessionManager, userService *model.UserService) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check that the provided name, email address and password are not blank. @@ -319,12 +319,30 @@ func handleUserSignupPost(logger *slog.Logger, tc *TemplateCache, fd *form.Decod form.CheckField(validator.NotBlank(form.Password), "password", "this field cannot be blank") form.CheckField(validator.MinChars(form.Password, 8), "password", "this field must be at least 8 characters long") + // Todo Email allready in use validation + if !form.Valid() { data := newTemplateData(r, sm) data.Form = form renderTemplate(w, r, tc, http.StatusUnprocessableEntity, "signup.go.tmpl", data) } - fmt.Fprintln(w, "Creating new user") + + _, err = userService.Insert(form.Name, form.Email, form.Password) + if err != nil { + if errors.Is(err, model.ErrDuplicateEmail) { + form.AddFieldError("email", "Email is already in use") + + data := newTemplateData(r, sm) + data.Form = form + renderTemplate(w, r, tc, http.StatusUnprocessableEntity, "signup.go.tmpl", data) + } else { + serverError(w, r, err) + } + return + } + + sm.Put(r.Context(), "flash", "Your signup was successful. Please log in.") + http.Redirect(w, r, "/user/login", http.StatusSeeOther) }) } diff --git a/internal/server/routes.go b/internal/server/routes.go index ab570b0..e9230ef 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -16,6 +16,7 @@ func addRoutes(mux *http.ServeMux, db *sql.DB, fd *form.Decoder, sm *scs.SessionManager, + userService *model.UserService, snippetService *model.SnippetService) http.Handler { // /{$} is used to prevent subtree path patterns from acting like a wildcard @@ -31,7 +32,7 @@ func addRoutes(mux *http.ServeMux, // mux.Handle("/", http.NotFoundHandler()) mux.Handle("GET /user/signup", sm.LoadAndSave(handleUserSignupGet(tc, sm))) - mux.Handle("POST /user/signup", sm.LoadAndSave(handleUserSignupPost(logger, tc, fd, 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("POST /user/logout", sm.LoadAndSave(handleUserLogoutPost())) diff --git a/internal/server/server.go b/internal/server/server.go index 4b1ba1a..bb12d30 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -39,7 +39,7 @@ func NewRatchetApp(logger *slog.Logger, tc *TemplateCache, db *sql.DB, sm *scs.S // Mux Router implements the Handler interface. AKA it has a ServeHTTP receiver. // SEE we can really clean things up by moving this into routes.go and handlers.go - wrappedMux := addRoutes(router, rs.logger, rs.templateCache, db, rs.formDecoder, sm, rs.snippetService) + wrappedMux := addRoutes(router, rs.logger, rs.templateCache, db, rs.formDecoder, sm, rs.userService, rs.snippetService) rs.Handler = CommonHeaderMiddleware(wrappedMux) rs.Handler = RequestLoggingMiddleware(rs.Handler, logger) rs.Handler = RecoveryMiddleware(rs.Handler)