diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index 7fbf19e..bd94c1f 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -4,11 +4,14 @@ import ( "bytes" "errors" "fmt" + "io/fs" "net/http" "strconv" + "strings" "gitea.local.lab/Lbenedar/snippetbox/internal/models" "gitea.local.lab/Lbenedar/snippetbox/internal/validator" + "gitea.local.lab/Lbenedar/snippetbox/ui" "github.com/julienschmidt/httprouter" ) @@ -32,6 +35,20 @@ type userLoginForm struct { validator.Validator `form:"-"` } +type accountViewForm struct { + Name string `form:"name"` + Email string `form:"email"` + Joined string `form:"joined"` + validator.Validator `form:"-"` +} + +type passwordChangeForm struct { + CurrentPassword string `form:"curr_pass"` + NewPassword string `form:"new_pass"` + ConfirmPassword string `form:"conf_pass"` + validator.Validator `form:"-"` +} + func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) { ts, ok := app.templateCache[page] if !ok { @@ -210,7 +227,11 @@ func (app *application) userLoginPost(w http.ResponseWriter, r *http.Request) { } app.sessionManager.Put(r.Context(), "authenticatedUserID", id) - + redirectPath := app.sessionManager.PopString(r.Context(), "redirectParent") + if redirectPath != "" { + http.Redirect(w, r, redirectPath, http.StatusSeeOther) + return + } http.Redirect(w, r, "/snippet/create", http.StatusSeeOther) } @@ -241,3 +262,73 @@ func (app *application) userLogin(w http.ResponseWriter, r *http.Request) { func ping(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) } + +func (app *application) about(w http.ResponseWriter, r *http.Request) { + data := app.newTemplateData(r) + text, err := fs.ReadFile(ui.Files, "static/text/about.txt") + if err != nil { + app.serverError(w, err) + return + } + data.AboutText = string(text) + app.render(w, http.StatusOK, "about.tmpl", data) +} + +func (app *application) accountView(w http.ResponseWriter, r *http.Request) { + data := app.newTemplateData(r) + id := app.sessionManager.Get(r.Context(), "authenticatedUserID").(int) + + user, err := app.users.GetById(id) + if err != nil { + app.serverError(w, err) + return + } + data.Form = accountViewForm{Name: user.Name, Email: user.Email, Joined: humanDate(user.Created)} + app.render(w, http.StatusOK, "account.tmpl", data) +} + +func (app *application) accountChangePassword(w http.ResponseWriter, r *http.Request) { + data := app.newTemplateData(r) + data.Form = passwordChangeForm{} + app.render(w, http.StatusOK, "password.tmpl", data) +} + +func (app *application) accountChangePasswordPost(w http.ResponseWriter, r *http.Request) { + form := passwordChangeForm{} + err := app.decodePostForm(r, &form) + if err != nil { + app.clientError(w, http.StatusBadRequest) + return + } + + form.CheckField(validator.NotBlank(form.CurrentPassword), "curr_pass", "This field cannot be blank") + form.CheckField(validator.NotBlank(form.NewPassword), "new_pass", "This field cannot be blank") + form.CheckField(validator.MinChars(form.NewPassword, 8), "new_pass", "Minimum password length is 8 symbols") + form.CheckField(validator.NotBlank(form.ConfirmPassword), "conf_pass", "This field cannot be blank") + form.CheckField(strings.Compare(form.NewPassword, form.ConfirmPassword) == 0, "conf_pass", "Confirm password does not match") + + if !form.Valid() { + data := app.newTemplateData(r) + data.Form = form + app.render(w, http.StatusUnprocessableEntity, "password.tmpl", data) + return + } + + id := app.sessionManager.Get(r.Context(), "authenticatedUserID").(int) + err = app.users.ChangePassword(id, form.CurrentPassword, form.NewPassword) + if err != nil { + if errors.Is(err, models.ErrInvalidCredentials) { + form.AddNonFieldError("Password is incorrect") + + data := app.newTemplateData(r) + data.Form = form + app.render(w, http.StatusUnprocessableEntity, "password.tmpl", data) + } else { + app.serverError(w, err) + } + return + } + + app.sessionManager.Put(r.Context(), "flash", "Password successfully changed!") + http.Redirect(w, r, "/account/view", http.StatusSeeOther) +} diff --git a/cmd/web/handlers_test.go b/cmd/web/handlers_test.go index 42574e7..3fdc55d 100644 --- a/cmd/web/handlers_test.go +++ b/cmd/web/handlers_test.go @@ -192,3 +192,37 @@ func TestUserSignup(t *testing.T) { }) } } + +func TestSnippetCreate(t *testing.T) { + app := newTestApplication(t) + ts := newTestServer(t, app.routes()) + defer ts.Close() + + const ( + validPassword = "validPa$$word" + validEmail = "bob@example.com" + ) + + t.Run("Unauthenticated", func(t *testing.T) { + code, head, _ := ts.get(t, "/snippet/create") + assert.Equal(t, code, http.StatusSeeOther) + assert.Equal(t, head.Get("Location"), "/user/login") + }) + + t.Run("Authenticated", func(t *testing.T) { + _, _, body := ts.get(t, "/user/login") + validCsrfToken := extractCSRFToken(t, body) + t.Logf("CsrfToken: %s", validCsrfToken) + + form := url.Values{} + form.Add("email", validEmail) + form.Add("password", validPassword) + form.Add("csrf_token", validCsrfToken) + code, _, _ := ts.postForm(t, "/user/login", form) + t.Logf("Auth result: %d", code) + + code, _, body = ts.get(t, "/snippet/create") + assert.Equal(t, code, http.StatusOK) + assert.StringContains(t, body, "
+{{end}} \ No newline at end of file diff --git a/ui/html/pages/password.tmpl b/ui/html/pages/password.tmpl new file mode 100644 index 0000000..951e10d --- /dev/null +++ b/ui/html/pages/password.tmpl @@ -0,0 +1,35 @@ +{{define "title"}}Change Password{{end}} + +{{define "main"}} +