From 4f2dc018da9d0cf7edc6559d34ae7282282219c0 Mon Sep 17 00:00:00 2001 From: tamsin johnson Date: Wed, 7 Feb 2024 11:07:29 -0800 Subject: [PATCH] lets-go:11.0 user signup without peeking deliberately skipping salting and encrypting passwords at this point. intending to approach this in tandem with authenication. also starting to want db migrations, but trying not to get distracted. --- snippetbox/cmd/web/handlers.go | 47 ++++++++++++++++++++++++++ snippetbox/cmd/web/main.go | 2 ++ snippetbox/cmd/web/routes.go | 4 +++ snippetbox/db/initdb.d/init.sql | 8 +++++ snippetbox/internal/models/users.go | 50 ++++++++++++++++++++++++++++ snippetbox/ui/html/pages/signup.tmpl | 22 ++++++++++++ 6 files changed, 133 insertions(+) create mode 100644 snippetbox/internal/models/users.go create mode 100644 snippetbox/ui/html/pages/signup.tmpl diff --git a/snippetbox/cmd/web/handlers.go b/snippetbox/cmd/web/handlers.go index bc2736c..a7921c2 100644 --- a/snippetbox/cmd/web/handlers.go +++ b/snippetbox/cmd/web/handlers.go @@ -101,3 +101,50 @@ func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther) } + +type userSignupForm struct { + Username string `form:"username"` + Password string `form:"password"` + validator.Validator `form:"-"` +} + +// userSignup ... +func (app *application) userSignup(w http.ResponseWriter, r *http.Request) { + data := app.newTemplateData(r) + + data.Form = userSignupForm{} + + app.render(w, r, http.StatusOK, "signup.tmpl", data) +} + + +// userSignup ... +func (app *application) userSignupPost(w http.ResponseWriter, r *http.Request) { + var form userSignupForm + + err := app.decodePostForm(r, &form) + if err != nil { + app.clientError(w, http.StatusBadRequest) + return + } + + form.CheckField(validator.NotBlank(form.Username), "username", "This field cannot be blank") + form.CheckField(validator.NotBlank(form.Password), "password", "This field cannot be blank") + + if !form.Valid() { + data := app.newTemplateData(r) + data.Form = form + app.render(w, r, http.StatusUnprocessableEntity, "signup.tmpl", data) + return + } + + id, err := app.users.Insert(form.Username, form.Password) + if err != nil { + app.serverError(w, r, err) + return + } + + app.sessionManager.Put(r.Context(), "flash", fmt.Sprintf("CREATED A USER! (%d)", id)) + + http.Redirect(w, r, "/", http.StatusSeeOther) +} diff --git a/snippetbox/cmd/web/main.go b/snippetbox/cmd/web/main.go index 16c7862..708ffe6 100644 --- a/snippetbox/cmd/web/main.go +++ b/snippetbox/cmd/web/main.go @@ -23,6 +23,7 @@ import ( type application struct { logger *slog.Logger snippets *models.SnippetModel + users *models.UserModel templateCache map[string]*template.Template formDecoder *form.Decoder sessionManager *scs.SessionManager @@ -63,6 +64,7 @@ func main() { app := &application{ logger: logger, snippets: &models.SnippetModel{DB: db}, + users: &models.UserModel{DB: db}, templateCache: templateCache, formDecoder: formDecoder, sessionManager: sessionManager, diff --git a/snippetbox/cmd/web/routes.go b/snippetbox/cmd/web/routes.go index d2cb8c5..a5fc868 100644 --- a/snippetbox/cmd/web/routes.go +++ b/snippetbox/cmd/web/routes.go @@ -29,6 +29,10 @@ func (app *application) routes() http.Handler { router.Handler(http.MethodGet, "/snippet/view/:id", dynamic.ThenFunc(app.snippetView)) router.Handler(http.MethodGet, "/snippet/create", dynamic.ThenFunc(app.snippetCreate)) router.Handler(http.MethodPost, "/snippet/create", dynamic.ThenFunc(app.snippetCreatePost)) + router.Handler(http.MethodGet, "/user/signup", dynamic.ThenFunc(app.userSignup)) + router.Handler(http.MethodPost, "/user/signup", dynamic.ThenFunc(app.userSignupPost)) + // router.Handler(http.MethodGet, "/user/login", dynamic.ThenFunc(app.userLogin)) + // router.Handler(http.MethodPost, "/user/login", dynamic.ThenFunc(app.userLoginPost)) standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders) diff --git a/snippetbox/db/initdb.d/init.sql b/snippetbox/db/initdb.d/init.sql index e7f49e3..eb83090 100644 --- a/snippetbox/db/initdb.d/init.sql +++ b/snippetbox/db/initdb.d/init.sql @@ -20,3 +20,11 @@ CREATE TABLE sessions ( ); CREATE INDEX sessions_expiry_idx on sessions(expiry); + +CREATE TABLE users ( + id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, + username VARCHAR(20) NOT NULL, + password VARCHAR(20) NOT NULL, + created DATETIME NOT NULL, + UNIQUE (username) +) diff --git a/snippetbox/internal/models/users.go b/snippetbox/internal/models/users.go new file mode 100644 index 0000000..b12fe4d --- /dev/null +++ b/snippetbox/internal/models/users.go @@ -0,0 +1,50 @@ +package models + +import ( + "database/sql" + "errors" +) + +type User struct { + Username string +} + +type UserModel struct { + DB *sql.DB +} + +// Insert +func (m *UserModel) Insert(username string, password string) (int, error) { + stmt := `INSERT INTO users (username, password, created) + VALUES(?, ?, UTC_TIMESTAMP())` + + result, err := m.DB.Exec(stmt, username, password) + if err != nil { + return 0, err + } + + id, err := result.LastInsertId() + if err != nil { + return 0, err + } + + return int(id), nil +} + +// Get +func (m *UserModel) Get(id int) (User, error) { + stmt := "SELECT id, username FROM users WHERE id = ?" + + var u User + + err := m.DB.QueryRow(stmt, id).Scan(&u.Username) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return User{}, ErrNoRecord + } else { + return User{}, err + } + } + + return u, nil +} diff --git a/snippetbox/ui/html/pages/signup.tmpl b/snippetbox/ui/html/pages/signup.tmpl new file mode 100644 index 0000000..14d2e00 --- /dev/null +++ b/snippetbox/ui/html/pages/signup.tmpl @@ -0,0 +1,22 @@ +{{define "title"}}User Signup{{end}} + +{{define "main"}} +
+
+ + {{with .Form.FieldErrors.username}} + + {{end}} + +
+
+ + {{with .Form.FieldErrors.password}} + + {{end}} + +
+
+ +
+{{end}}