پیادهسازی ویژگی 'تغییر رمز عبور' (Implementing a Change Password Feature)
هدف شما در این تمرین اضافه کردن امکان تغییر رمز عبور (Change Password) برای کاربر احراز هویت شده (Authenticated User) است، با استفاده از فرم (Form) که شبیه به این است:
در طول این تمرین باید مطمئن شوید که:
- از کاربر رمز عبور فعلی (Current Password) او را بپرسید و تأیید کنید که با رمز عبور هش شده (Hashed Password) در جدول (Table)
usersمطابقت دارد (تا تأیید شود که واقعاً خود او درخواست را انجام میدهد). - رمز عبور جدید او را هش (Hash) کنید قبل از اینکه جدول
usersرا بهروزرسانی کنید.
مرحله 1 (Step 1)
دو مسیر (Route) و هندلر (Handler) جدید ایجاد کنید:
GET /account/password/updateکه به یک هندلر جدیدaccountPasswordUpdateنگاشت میشود.POST /account/password/updateکه به یک هندلر جدیدaccountPasswordUpdatePostنگاشت میشود.
هر دو مسیر باید فقط برای کاربران احراز هویت شده محدود شوند.
مرحله 2 (Step 2)
یک فایل قالب (Template File) جدید ui/html/pages/password.tmpl ایجاد کنید که شامل فرم تغییر رمز عبور باشد. این فرم باید:
- سه فیلد (Field) داشته باشد:
currentPassword،newPasswordوnewPasswordConfirmation. - دادههای فرم (Form Data) را به
/account/password/updateارسال کند وقتی که ارسال شد. - خطاها (Errors) را برای هر یک از فیلدها در صورت بروز خطای اعتبارسنجی (Validation Error) نمایش دهد.
- رمزهای عبور را در صورت بروز خطای اعتبارسنجی دوباره نمایش ندهد.
نکته: ممکن است بخواهید از کاری که روی فرم ثبتنام کاربر انجام دادیم به عنوان راهنما استفاده کنید.
سپس فایل cmd/web/handlers.go را بهروزرسانی کنید تا شامل یک ساختار جدید accountPasswordUpdateForm باشد که میتوانید دادههای فرم را در آن تجزیه کنید، و هندلر accountPasswordUpdate را بهروزرسانی کنید تا این فرم خالی را نمایش دهد.
وقتی به https://localhost:4000/account/password/update به عنوان یک کاربر احراز هویت شده مراجعه میکنید، باید شبیه به این باشد:
مرحله 3 (Step 3)
هندلر accountPasswordUpdatePost را بهروزرسانی کنید تا بررسیهای اعتبارسنجی (Validation Checks) فرم زیر را انجام دهد، و فرم را با پیامهای خطا (Error Messages) مربوطه در صورت بروز هرگونه خطا دوباره نمایش دهد.
- هر سه فیلد الزامی (Required) هستند.
- مقدار
newPasswordباید حداقل 8 کاراکتر (Character) باشد. - مقادیر
newPasswordوnewPasswordConfirmationباید مطابقت (Match) داشته باشند.
مرحله 4 (Step 4)
در فایل internal/models/users.go خود یک متد (Method) جدید UserModel.PasswordUpdate() با امضا (Signature) زیر ایجاد کنید:
func (m *UserModel) PasswordUpdate(id int, currentPassword, newPassword string) error
در این متد:
- جزئیات کاربر را برای کاربری با شناسه داده شده توسط پارامتر
idاز پایگاه داده بازیابی کنید. - بررسی کنید که مقدار
currentPasswordبا رمز عبور هش شده برای کاربر مطابقت دارد. اگر مطابقت ندارد، یک خطایErrInvalidCredentialsبرگردانید. - در غیر این صورت، مقدار
newPasswordرا هش کنید و ستونhashed_passwordرا در جدولusersبرای کاربر مربوطه بهروزرسانی کنید.
همچنین نوع رابط UserModelInterface را بهروزرسانی کنید تا متد PasswordUpdate() را که بهتازگی ایجاد کردهاید، شامل شود.
مرحله 5
هندلر accountPasswordUpdatePost را بهروزرسانی کنید تا اگر فرم معتبر است، متد UserModel.PasswordUpdate() را فراخوانی کند (به یاد داشته باشید، شناسه کاربر باید در دادههای جلسه باشد).
در صورت بروز خطای models.ErrInvalidCredentials، به کاربر اطلاع دهید که مقدار اشتباهی را در فیلد فرم currentPassword وارد کرده است. در غیر این صورت، یک پیام فلش به جلسه کاربر اضافه کنید که میگوید رمز عبور او با موفقیت تغییر کرده است و او را به صفحه حساب کاربریاش هدایت کنید.
مرحله 6
حساب کاربری را بهروزرسانی کنید تا یک لینک به فرم تغییر رمز عبور اضافه کنید، شبیه به این:
مرحله 7
سعی کنید تستهای برنامه را اجرا کنید. باید یک شکست دریافت کنید زیرا نوع mocks.UserModel دیگر رابط مشخص شده در ساختار models.UserModelInterface را برآورده نمیکند. این مشکل را با اضافه کردن متد PasswordUpdate() مناسب به ماک برطرف کنید و مطمئن شوید که تستها پاس میشوند.
کد پیشنهادی
کد پیشنهادی برای مرحله 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}}
واژهنامه اصطلاحات فنی
| اصطلاح فارسی | معادل انگلیسی | توضیح |
|---|---|---|
| تغییر رمز عبور | Change Password | قابلیت تغییر رمز عبور کاربر |
| کاربر احراز هویت شده | Authenticated User | کاربری که وارد سیستم شده است |
| فرم | Form | فرم ورود اطلاعات در صفحه |
| رمز عبور فعلی | Current Password | رمز عبور کنونی کاربر |
| رمز عبور هش شده | Hashed Password | رمز عبور رمزنگاری شده |
| جدول | Table | جدول در پایگاه داده |
| هش | Hash | تبدیل داده به فرمت رمزنگاری شده |
| مسیر | Route | آدرس دسترسی به صفحه |
| هندلر | Handler | تابع پردازش درخواست |
| فایل قالب | Template File | فایل حاوی ساختار صفحه |
| فیلد | Field | بخش ورود اطلاعات در فرم |
| دادههای فرم | Form Data | اطلاعات وارد شده در فرم |
| خطاها | Errors | پیامهای خطا در برنامه |
| خطای اعتبارسنجی | Validation Error | خطا در بررسی صحت دادهها |
| بررسیهای اعتبارسنجی | Validation Checks | بررسی صحت دادههای ورودی |
| پیامهای خطا | Error Messages | پیامهای نمایش خطا به کاربر |
| الزامی | Required | فیلد اجباری در فرم |
| کاراکتر | Character | نویسه یا حرف |
| مطابقت | Match | یکسان بودن دو مقدار |
| متد | Method | تابع عضو یک کلاس |
| امضا | Signature | تعریف پارامترها و نوع برگشتی تابع |