پیادهسازی قابلیت «تغییر رمز عبور»
هدف شما در این تمرین، افزودن امکان تغییر رمز عبور برای کاربر احراز هویت شده است، با استفاده از فرمی که شبیه به این است:
در طول این تمرین باید مطمئن شوید که:
- از کاربر رمز عبور فعلی را بخواهید و تأیید کنید که با رمز عبور هش شده در جدول
usersمطابقت دارد (تا تأیید شود که واقعاً خود کاربر درخواست را ارسال کرده است). - رمز عبور جدید را قبل از بهروزرسانی جدول
usersهش کنید.
مرحله 1
دو route و handler جدید ایجاد کنید:
GET /account/password/update→ handleraccountPasswordUpdatePOST /account/password/update→ handleraccountPasswordUpdatePost
هر دو مسیر باید فقط برای کاربران احراز هویت شده محدود شوند.
مرحله 2
یک فایل جدید ui/html/pages/password.tmpl ایجاد کنید که شامل فرم تغییر رمز عبور است. این فرم باید:
- سه فیلد داشته باشد:
currentPassword،newPasswordوnewPasswordConfirmation. - دادههای فرم را هنگام ارسال به
/account/password/updateبا روشPOSTارسال کند. - در صورت خطای اعتبارسنجی (validation error)، خطاها را برای هر یک از فیلدها نمایش دهد.
- در صورت خطای اعتبارسنجی، رمزهای عبور را دوباره نمایش ندهد.
راهنما: ممکن است بخواهید از کاری که روی فرم ثبتنام کاربر انجام دادیم به عنوان راهنما استفاده کنید.
سپس فایل cmd/web/handlers.go را بهروزرسانی کنید تا شامل یک struct جدید accountPasswordUpdateForm باشد که بتوانید دادههای فرم را در آن پارس کنید، و handler accountPasswordUpdate را بهروزرسانی کنید تا این فرم خالی را نمایش دهد.
وقتی به عنوان یک کاربر احراز هویت شده به https://localhost:4000/account/password/update مراجعه میکنید، باید شبیه به این باشد:
مرحله 3
handler accountPasswordUpdatePost را بهروزرسانی کنید تا بررسیهای اعتبارسنجی فرم زیر را انجام دهد، و در صورت هرگونه خطا، فرم را با پیامهای خطای مربوطه دوباره نمایش دهد.
- هر سه فیلد الزامی هستند.
- مقدار
newPasswordباید حداقل 8 کاراکتر باشد. - مقادیر
newPasswordوnewPasswordConfirmationباید مطابقت داشته باشند.
مرحله 4
در فایل internal/models/users.go خود یک متد جدید UserModel.PasswordUpdate() با امضای زیر ایجاد کنید:
func (m *UserModel) PasswordUpdate(id int, currentPassword, newPassword string) error
در این متد:
- جزئیات کاربر را برای کاربری با شناسه داده شده توسط پارامتر
idاز پایگاه داده بازیابی کنید. - بررسی کنید که مقدار
currentPasswordبا رمز عبور هش شده کاربر مطابقت دارد. اگر مطابقت نداشت، یک خطایErrInvalidCredentialsبرگردانید. - در غیر این صورت، مقدار
newPasswordرا هش کنید و ستونhashed_passwordدر جدولusersرا برای کاربر مربوطه بهروزرسانی کنید.
همچنین نوع interface UserModelInterface را بهروزرسانی کنید تا متد PasswordUpdate() که تازه ایجاد کردید را شامل شود.
مرحله 5
handler accountPasswordUpdatePost را بهروزرسانی کنید تا اگر فرم معتبر باشد، متد UserModel.PasswordUpdate() را فراخوانی کند (به یاد داشته باشید، شناسه کاربر باید در دادههای نشست باشد).
در صورت بروز خطای models.ErrInvalidCredentials، به کاربر اطلاع دهید که مقدار اشتباهی در فیلد فرم currentPassword وارد کرده است. در غیر این صورت، یک پیام flash به نشست کاربر اضافه کنید که میگوید رمز عبور آنها با موفقیت تغییر کرده است و آنها را به صفحه حساب کاربریشان هدایت کنید.
مرحله 6
صفحه حساب کاربری را بهروزرسانی کنید تا شامل لینکی به فرم تغییر رمز عبور باشد، مشابه این:
مرحله 7
سعی کنید تستهای برنامه را اجرا کنید. باید یک خطا دریافت کنید چون نوع mocks.UserModel دیگر interface مشخص شده در struct models.UserModelInterface را برآورده نمیکند. این را با افزودن متد مناسب PasswordUpdate() به mock برطرف کنید و مطمئن شوید که تستها پاس میشوند.
کد پیشنهادی
کد پیشنهادی برای مرحله 1
package main ... func (app *application) accountPasswordUpdate(w http.ResponseWriter, r *http.Request) { // Some code will go here later... } func (app *application) accountPasswordUpdatePost(w http.ResponseWriter, r *http.Request) { // Some code will go here later... }
package main ... func (app *application) routes() http.Handler { mux := http.NewServeMux() mux.Handle("GET /static/", http.FileServerFS(ui.Files)) mux.HandleFunc("GET /ping", ping) dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate) mux.Handle("GET /{$}", dynamic.ThenFunc(app.home)) mux.Handle("GET /about", dynamic.ThenFunc(app.about)) mux.Handle("GET /snippet/view/{id}", dynamic.ThenFunc(app.snippetView)) mux.Handle("GET /user/signup", dynamic.ThenFunc(app.userSignup)) mux.Handle("POST /user/signup", dynamic.ThenFunc(app.userSignupPost)) mux.Handle("GET /user/login", dynamic.ThenFunc(app.userLogin)) mux.Handle("POST /user/login", dynamic.ThenFunc(app.userLoginPost)) protected := dynamic.Append(app.requireAuthentication) mux.Handle("GET /snippet/create", protected.ThenFunc(app.snippetCreate)) mux.Handle("POST /snippet/create", protected.ThenFunc(app.snippetCreatePost)) mux.Handle("GET /account/view", protected.ThenFunc(app.accountView)) // Add the two new routes, restricted to authenticated users only. mux.Handle("GET /account/password/update", protected.ThenFunc(app.accountPasswordUpdate)) mux.Handle("POST /account/password/update", protected.ThenFunc(app.accountPasswordUpdatePost)) mux.Handle("POST /user/logout", protected.ThenFunc(app.userLogoutPost)) standard := alice.New(app.recoverPanic, app.logRequest, commonHeaders) return standard.Then(mux) }
کد پیشنهادی برای مرحله 2
{{define "title"}}Change Password{{end}}
{{define "main"}}
<h2>Change Password</h2>
<form action='/account/password/update' method='POST' novalidate>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<div>
<label>Current password:</label>
{{with .Form.FieldErrors.currentPassword}}
<label class='error'>{{.}}</label>
{{end}}
<input type='password' name='currentPassword'>
</div>
<div>
<label>New password:</label>
{{with .Form.FieldErrors.newPassword}}
<label class='error'>{{.}}</label>
{{end}}
<input type='password' name='newPassword'>
</div>
<div>
<label>Confirm new password:</label>
{{with .Form.FieldErrors.newPasswordConfirmation}}
<label class='error'>{{.}}</label>
{{end}}
<input type='password' name='newPasswordConfirmation'>
</div>
<div>
<input type='submit' value='Change password'>
</div>
</form>
{{end}}
package main ... type accountPasswordUpdateForm struct { CurrentPassword string `form:"currentPassword"` NewPassword string `form:"newPassword"` NewPasswordConfirmation string `form:"newPasswordConfirmation"` validator.Validator `form:"-"` } func (app *application) accountPasswordUpdate(w http.ResponseWriter, r *http.Request) { data := app.newTemplateData(r) data.Form = accountPasswordUpdateForm{} app.render(w, r, http.StatusOK, "password.tmpl", data) } ...
کد پیشنهادی برای مرحله 3
package main ... func (app *application) accountPasswordUpdatePost(w http.ResponseWriter, r *http.Request) { var form accountPasswordUpdateForm err := app.decodePostForm(r, &form) if err != nil { app.clientError(w, http.StatusBadRequest) return } form.CheckField(validator.NotBlank(form.CurrentPassword), "currentPassword", "This field cannot be blank") form.CheckField(validator.NotBlank(form.NewPassword), "newPassword", "This field cannot be blank") form.CheckField(validator.MinChars(form.NewPassword, 8), "newPassword", "This field must be at least 8 characters long") form.CheckField(validator.NotBlank(form.NewPasswordConfirmation), "newPasswordConfirmation", "This field cannot be blank") form.CheckField(form.NewPassword == form.NewPasswordConfirmation, "newPasswordConfirmation", "Passwords do not match") if !form.Valid() { data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "password.tmpl", data) return } }
کد پیشنهادی برای مرحله 4
package models ... type UserModelInterface interface { Insert(name, email, password string) error Authenticate(email, password string) (int, error) Exists(id int) (bool, error) Get(id int) (User, error) PasswordUpdate(id int, currentPassword, newPassword string) error } ... func (m *UserModel) PasswordUpdate(id int, currentPassword, newPassword string) error { var currentHashedPassword []byte stmt := "SELECT hashed_password FROM users WHERE id = ?" err := m.DB.QueryRow(stmt, id).Scan(¤tHashedPassword) if err != nil { return err } err = bcrypt.CompareHashAndPassword(currentHashedPassword, []byte(currentPassword)) if err != nil { if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { return ErrInvalidCredentials } else { return err } } newHashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), 12) if err != nil { return err } stmt = "UPDATE users SET hashed_password = ? WHERE id = ?" _, err = m.DB.Exec(stmt, string(newHashedPassword), id) return err }
کد پیشنهادی برای مرحله 5
package main ... func (app *application) accountPasswordUpdatePost(w http.ResponseWriter, r *http.Request) { var form accountPasswordUpdateForm err := app.decodePostForm(r, &form) if err != nil { app.clientError(w, http.StatusBadRequest) return } form.CheckField(validator.NotBlank(form.CurrentPassword), "currentPassword", "This field cannot be blank") form.CheckField(validator.NotBlank(form.NewPassword), "newPassword", "This field cannot be blank") form.CheckField(validator.MinChars(form.NewPassword, 8), "newPassword", "This field must be at least 8 characters long") form.CheckField(validator.NotBlank(form.NewPasswordConfirmation), "newPasswordConfirmation", "This field cannot be blank") form.CheckField(form.NewPassword == form.NewPasswordConfirmation, "newPasswordConfirmation", "Passwords do not match") if !form.Valid() { data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "password.tmpl", data) return } userID := app.sessionManager.GetInt(r.Context(), "authenticatedUserID") err = app.users.PasswordUpdate(userID, form.CurrentPassword, form.NewPassword) if err != nil { if errors.Is(err, models.ErrInvalidCredentials) { form.AddFieldError("currentPassword", "Current password is incorrect") data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "password.tmpl", data) } else { app.serverError(w, r, err) } return } app.sessionManager.Put(r.Context(), "flash", "Your password has been updated!") http.Redirect(w, r, "/account/view", http.StatusSeeOther) }
کد پیشنهادی برای مرحله 6
{{define "title"}}Your Account{{end}}
{{define "main"}}
<h2>Your Account</h2>
{{with .User}}
<table>
<tr>
<th>Name</th>
<td>{{.Name}}</td>
</tr>
<tr>
<th>Email</th>
<td>{{.Email}}</td>
</tr>
<tr>
<th>Joined</th>
<td>{{humanDate .Created}}</td>
</tr>
<tr>
<!-- Add a link to the change password form -->
<th>Password</th>
<td><a href="/account/password/update">Change password</a></td>
</tr>
</table>
{{end }}
{{end}}
کد پیشنهادی برای مرحله 7
$ go test ./...
# snippetbox.alexedwards.net/cmd/web [snippetbox.alexedwards.net/cmd/web.test]
cmd/web/testutils_test.go:48:19: cannot use &mocks.UserModel{} (value of type *mocks.UserModel) as type models.UserModelInterface in struct literal:
*mocks.UserModel does not implement models.UserModelInterface (missing PasswordUpdate method)
FAIL snippetbox.alexedwards.net/cmd/web [build failed]
ok snippetbox.alexedwards.net/internal/models 1.099s
? snippetbox.alexedwards.net/internal/models/mocks [no test files]
? snippetbox.alexedwards.net/internal/validator [no test files]
? snippetbox.alexedwards.net/ui [no test files]
FAIL
package mocks ... func (m *UserModel) PasswordUpdate(id int, currentPassword, newPassword string) error { if id == 1 { if currentPassword != "pa$$word" { return models.ErrInvalidCredentials } return nil } return models.ErrNoRecord }
$ go test ./... ok snippetbox.alexedwards.net/cmd/web 0.026s ok snippetbox.alexedwards.net/internal/models (cached) ? snippetbox.alexedwards.net/internal/models/mocks [no test files] ? snippetbox.alexedwards.net/internal/validator [no test files] ? snippetbox.alexedwards.net/ui [no test files]