Let's Go تمرین‌های راهنمایی › پیاده‌سازی ویژگی ‘تغییر رمز عبور’
قبلی · فهرست · بعدی ›
فصل 16.6.

پیاده‌سازی ویژگی 'تغییر رمز عبور' (Implementing a Change Password Feature)

هدف شما در این تمرین اضافه کردن امکان تغییر رمز عبور (Change Password) برای کاربر احراز هویت شده (Authenticated User) است، با استفاده از فرم (Form) که شبیه به این است:

16.06-01.png

در طول این تمرین باید مطمئن شوید که:

مرحله 1 (Step 1)

دو مسیر (Route) و هندلر (Handler) جدید ایجاد کنید:

هر دو مسیر باید فقط برای کاربران احراز هویت شده محدود شوند.

نمایش کد پیشنهادی

مرحله 2 (Step 2)

یک فایل قالب (Template File) جدید ui/html/pages/password.tmpl ایجاد کنید که شامل فرم تغییر رمز عبور باشد. این فرم باید:

نکته: ممکن است بخواهید از کاری که روی فرم ثبت‌نام کاربر انجام دادیم به عنوان راهنما استفاده کنید.

سپس فایل cmd/web/handlers.go را به‌روزرسانی کنید تا شامل یک ساختار جدید accountPasswordUpdateForm باشد که می‌توانید داده‌های فرم را در آن تجزیه کنید، و هندلر accountPasswordUpdate را به‌روزرسانی کنید تا این فرم خالی را نمایش دهد.

وقتی به https://localhost:4000/account/password/update به عنوان یک کاربر احراز هویت شده مراجعه می‌کنید، باید شبیه به این باشد:

16.06-02.png

نمایش کد پیشنهادی

مرحله 3 (Step 3)

هندلر accountPasswordUpdatePost را به‌روزرسانی کنید تا بررسی‌های اعتبارسنجی (Validation Checks) فرم زیر را انجام دهد، و فرم را با پیام‌های خطا (Error Messages) مربوطه در صورت بروز هرگونه خطا دوباره نمایش دهد.

16.06-03.png

نمایش کد پیشنهادی

مرحله 4 (Step 4)

در فایل internal/models/users.go خود یک متد (Method) جدید UserModel.PasswordUpdate() با امضا (Signature) زیر ایجاد کنید:

func (m *UserModel) PasswordUpdate(id int, currentPassword, newPassword string) error

در این متد:

  1. جزئیات کاربر را برای کاربری با شناسه داده شده توسط پارامتر id از پایگاه داده بازیابی کنید.
  2. بررسی کنید که مقدار currentPassword با رمز عبور هش شده برای کاربر مطابقت دارد. اگر مطابقت ندارد، یک خطای ErrInvalidCredentials برگردانید.
  3. در غیر این صورت، مقدار newPassword را هش کنید و ستون hashed_password را در جدول users برای کاربر مربوطه به‌روزرسانی کنید.

همچنین نوع رابط UserModelInterface را به‌روزرسانی کنید تا متد PasswordUpdate() را که به‌تازگی ایجاد کرده‌اید، شامل شود.

نمایش کد پیشنهادی

مرحله 5

هندلر accountPasswordUpdatePost را به‌روزرسانی کنید تا اگر فرم معتبر است، متد UserModel.PasswordUpdate() را فراخوانی کند (به یاد داشته باشید، شناسه کاربر باید در داده‌های جلسه باشد).

در صورت بروز خطای models.ErrInvalidCredentials، به کاربر اطلاع دهید که مقدار اشتباهی را در فیلد فرم currentPassword وارد کرده است. در غیر این صورت، یک پیام فلش به جلسه کاربر اضافه کنید که می‌گوید رمز عبور او با موفقیت تغییر کرده است و او را به صفحه حساب کاربری‌اش هدایت کنید.

نمایش کد پیشنهادی

مرحله 6

حساب کاربری را به‌روزرسانی کنید تا یک لینک به فرم تغییر رمز عبور اضافه کنید، شبیه به این:

16.06-04.png

نمایش کد پیشنهادی

مرحله 7

سعی کنید تست‌های برنامه را اجرا کنید. باید یک شکست دریافت کنید زیرا نوع mocks.UserModel دیگر رابط مشخص شده در ساختار models.UserModelInterface را برآورده نمی‌کند. این مشکل را با اضافه کردن متد PasswordUpdate() مناسب به ماک برطرف کنید و مطمئن شوید که تست‌ها پاس می‌شوند.

نمایش کد پیشنهادی

کد پیشنهادی

کد پیشنهادی برای مرحله 1

File: cmd/web/handlers.go
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...
}
File: cmd/web/routes.go
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

File: ui/html/pages/password.tmpl
{{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}}
File: cmd/web/handlers.go
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

File: cmd/web/handlers.go
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

File: internal/models/users.go
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(&currentHashedPassword)
    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

File: cmd/web/handlers.go
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

File: ui/html/pages/account.tmpl
{{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 تعریف پارامترها و نوع برگشتی تابع