diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index e54a161..d7cc3e7 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -19,6 +19,13 @@ type snippetCreateForm struct { validator.Validator `form:"-"` } +type userSignupForm struct { + Name string `form:"name"` + Email string `form:"email"` + Password string `form:"password"` + validator.Validator `form:"-"` +} + func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) { ts, ok := app.templateCache[page] if !ok { @@ -119,7 +126,41 @@ func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request } func (app *application) userSignupPost(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "Create a new user...") + var form userSignupForm + err := app.decodePostForm(r, &form) + if err != nil { + app.clientError(w, http.StatusBadRequest) + return + } + + form.CheckField(validator.NotBlank(form.Name), "name", "This field cannot be blank") + 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 address") + 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 character long") + + if !form.Valid() { + data := app.newTemplateData(r) + data.Form = form + app.render(w, http.StatusUnprocessableEntity, "signup.tmpl", data) + return + } + + err = app.users.Insert(form.Name, form.Email, form.Password) + if err != nil { + if errors.Is(err, models.ErrDuplicateEmail) { + form.AddFieldError("email", "Email address already in use") + + data := app.newTemplateData(r) + data.Form = form + app.render(w, http.StatusUnprocessableEntity, "signup.tmpl", data) + } else { + app.serverError(w, err) + } + return + } + app.sessionManager.Put(r.Context(), "flash", "Your singup was successful. Please log in.") + http.Redirect(w, r, "/user/login", http.StatusSeeOther) } func (app *application) userLoginPost(w http.ResponseWriter, r *http.Request) { @@ -131,7 +172,9 @@ func (app *application) userLogoutPost(w http.ResponseWriter, r *http.Request) { } func (app *application) userSignup(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "Display a HTML for signing up a new user...") + data := app.newTemplateData(r) + data.Form = userSignupForm{} + app.render(w, http.StatusOK, "signup.tmpl", data) } func (app *application) userLogin(w http.ResponseWriter, r *http.Request) { diff --git a/cmd/web/helpers.go b/cmd/web/helpers.go index 9c9ff5e..b356b49 100644 --- a/cmd/web/helpers.go +++ b/cmd/web/helpers.go @@ -24,7 +24,7 @@ func (app *application) notFound(w http.ResponseWriter) { app.clientError(w, http.StatusNotFound) } -func (app *application) decodePostForm(r *http.Request, snippetForm *snippetCreateForm) error { +func (app *application) decodePostForm(r *http.Request, snippetForm any) error { err := r.ParseForm() if err != nil { return err diff --git a/go.mod b/go.mod index b98643a..6f85f10 100644 --- a/go.mod +++ b/go.mod @@ -10,4 +10,5 @@ require ( github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/justinas/alice v1.2.0 // indirect + golang.org/x/crypto v0.48.0 // indirect ) diff --git a/go.sum b/go.sum index e2e115f..644b54c 100644 --- a/go.sum +++ b/go.sum @@ -13,3 +13,5 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo= github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= diff --git a/internal/models/users.go b/internal/models/users.go index 61b4e2b..295cf7d 100644 --- a/internal/models/users.go +++ b/internal/models/users.go @@ -2,7 +2,12 @@ package models import ( "database/sql" + "errors" + "strings" "time" + + "github.com/go-sql-driver/mysql" + "golang.org/x/crypto/bcrypt" ) type User struct { @@ -18,6 +23,24 @@ type UserModel struct { } func (m *UserModel) Insert(name, email, password string) error { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12) + if err != nil { + return err + } + + stmt := `INSERT INTO users (name, email, hashed_password, created) + VALUES(?, ?, ?, UTC_TIMESTAMP())` + + _, err = m.DB.Exec(stmt, name, email, string(hashedPassword)) + if err != nil { + var mySQLError *mysql.MySQLError + if errors.As(err, &mySQLError) { + if mySQLError.Number == 1062 && strings.Contains(mySQLError.Message, "users_uc_email") { + return ErrDuplicateEmail + } + } + return err + } return nil } diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 0248f44..5d3956a 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -1,6 +1,7 @@ package validator import ( + "regexp" "strings" "unicode/utf8" ) @@ -44,3 +45,13 @@ func PermittedInt(value int, permittedValues ...int) bool { } return false } + +var EmailRX = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + +func MinChars(value string, n int) bool { + return utf8.RuneCountInString(value) >= n +} + +func Matches(value string, rx *regexp.Regexp) bool { + return rx.MatchString(value) +} diff --git a/ui/html/pages/signup.tmpl b/ui/html/pages/signup.tmpl new file mode 100644 index 0000000..e883343 --- /dev/null +++ b/ui/html/pages/signup.tmpl @@ -0,0 +1,30 @@ +{{define "title"}}Signup{{end}} + +{{define "main"}} +
+{{end}} \ No newline at end of file