Let's Go تمرین‌های راهنما › افزودن صفحه «حساب کاربری» به برنامه
قبلی · فهرست · بعدی
فصل ۱۶.۴.

افزودن صفحه ‘حساب کاربری’ به برنامه

هدف شما برای این تمرین افزودن یک صفحه جدید ‘حساب کاربری شما’ به برنامه است. باید به یک route جدید GET /account/view نگاشت شود و نام، آدرس ایمیل و تاریخ signup برای کاربر authenticated فعلی را نمایش دهد، مشابه این:

16.04-01.png

مرحله 1

در فایل internal/models/users.go یک متد جدید UserModel.Get() ایجاد کنید. این باید ID یک کاربر را به عنوان پارامتر بپذیرد، و یک struct User حاوی همه اطلاعات این کاربر را برگرداند (به جز hashed password آن‌ها، که نیاز نداریم). اگر کاربری با این ID پیدا نشد، باید یک خطای ErrNoRecord برگرداند.

همچنین، نوع UserModelInterface را به‌روزرسانی کنید تا این متد جدید Get() را شامل شود، و یک متد متناظر به mock mocks.UserModel اضافه کنید تا همچنان interface را برآورده کند.

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

مرحله 2

یک route GET /account/view ایجاد کنید که به یک handler جدید accountView نگاشت شود. route باید فقط به کاربران authenticated محدود شود.

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

مرحله 3

handler accountView را به‌روزرسانی کنید تا "authenticatedUserID" را از نشست دریافت کند، جزئیات کاربر مربوط را از پایگاه داده واکشی کند (با استفاده از متد جدید UserModel.Get())، و آن‌ها را در یک پاسخ HTTP plain text dump کند. اگر کاربری مطابق با "authenticatedUserID" از نشست پیدا نشد، کلاینت را به GET /user/login هدایت کنید تا احراز هویت مجدد را مجبور کند.

وقتی به عنوان یک کاربر authenticated از https://localhost:4000/account/view در مرورگر خود بازدید می‌کنید، باید یک پاسخ مشابه این دریافت کنید:

16.04-02.png

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

مرحله 4

یک فایل جدید ui/html/pages/account.tmpl ایجاد کنید که اطلاعات کاربر را در یک جدول نمایش می‌دهد. سپس handler accountView را به‌روزرسانی کنید تا این template جدید را render کند، و جزئیات کاربر را از طریق struct templateData عبور دهد.

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

مرحله 5

علاوه بر این، نوار navigation اصلی سایت را به‌روزرسانی کنید تا یک لینک به صفحه مشاهده حساب کاربری شامل شود (فقط برای کاربران authenticated قابل مشاهده). سپس بررسی کنید که صفحه جدید و navigation به درستی کار می‌کند با بازدید از https://localhost:4000/account/view در مرورگر خود در حالی که logged in هستید.

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

کد پیشنهادی

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

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)
}

...

func (m *UserModel) Get(id int) (User, error) {
    var user User

    stmt := `SELECT id, name, email, created FROM users WHERE id = ?`

    err := m.DB.QueryRow(stmt, id).Scan(&user.ID, &user.Name, &user.Email, &user.Created)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return User{}, ErrNoRecord
        } else {
            return User{}, err
        }
    }

    return user, nil
}
File: internal/models/mocks/users.go
package mocks

import (
    "time" // New import

    "snippetbox.alexedwards.net/internal/models"
)

...

func (m *UserModel) Get(id int) (models.User, error) {
    if id == 1 {
        u := models.User{
            ID:      1,
            Name:    "Alice",
            Email:   "alice@example.com",
            Created: time.Now(),
        }

        return u, nil
    }

    return models.User{}, models.ErrNoRecord
}

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

File: cmd/web/handlers.go
...

func (app *application) accountView(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))
    // Add the view account route, using the protected middleware chain.
    mux.Handle("GET /account/view", protected.ThenFunc(app.accountView))
    mux.Handle("POST /user/logout", protected.ThenFunc(app.userLogoutPost))

    standard := alice.New(app.recoverPanic, app.logRequest, commonHeaders)
    return standard.Then(mux)
}

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

File: cmd/web/handlers.go
...

func (app *application) accountView(w http.ResponseWriter, r *http.Request) {
    userID := app.sessionManager.GetInt(r.Context(), "authenticatedUserID")

    user, err := app.users.Get(userID)
    if err != nil {
        if errors.Is(err, models.ErrNoRecord) {
            http.Redirect(w, r, "/user/login", http.StatusSeeOther)
        } else {
            app.serverError(w, r, err)
        }
        return
    }

    fmt.Fprintf(w, "%+v", user)
}

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

File: cmd/web/templates.go
...

type templateData struct {
    CurrentYear     int
    Snippet         models.Snippet
    Snippets        []models.Snippet
    Form            any
    Flash           string
    IsAuthenticated bool
    CSRFToken       string
    User            models.User
}

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

func (app *application) accountView(w http.ResponseWriter, r *http.Request) {
    userID := app.sessionManager.GetInt(r.Context(), "authenticatedUserID")

    user, err := app.users.Get(userID)
    if err != nil {
        if errors.Is(err, models.ErrNoRecord) {
            http.Redirect(w, r, "/user/login", http.StatusSeeOther)
        } else {
            app.serverError(w, r, err)
        }
        return
    }

    data := app.newTemplateData(r)
    data.User = user

    app.render(w, r, http.StatusOK, "account.tmpl", data)
}

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

File: ui/html/partials/nav.tmpl
{{define "nav"}}
<nav>
    <div>
        <a href='/'>Home</a>
        <a href='/about'>About</a>
         {{if .IsAuthenticated}}
            <a href='/snippet/create'>Create snippet</a>
        {{end}}
    </div>
    <div>
        {{if .IsAuthenticated}}
            <!-- Add the view account link for authenticated users -->
            <a href='/account/view'>Account</a>
            <form action='/user/logout' method='POST'>
                <input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
                <button>Logout</button>
            </form>
        {{else}}
            <a href='/user/signup'>Signup</a>
            <a href='/user/login'>Login</a>
        {{end}}
    </div>
</nav>
{{end}}