Let's Go Further پیوست‌ها › ایجاد توکن‌های فعال‌سازی اضافی
قبلی · فهرست مطالب · بعدی
فصل ۲۱.۲.

ایجاد توکن‌های فعال‌سازی اضافی

ممکن است بخواهید یک endpoint مستقل برای تولید توکن‌های فعال‌سازی به API خود اضافه کنید. این کار می‌تواند در سناریوهایی مفید باشد که کاربر حساب خود را به‌موقع فعال نمی‌کند یا ایمیل خوش‌آمدگویی را دریافت نمی‌کند.

در این بخش، کد مربوط به این کار را به‌سرعت بررسی می‌کنیم و endpoint زیر را اضافه خواهیم کرد:

متد الگوی URL Handler عملیات
POST /v1/tokens/activation createActivationTokenHandler تولید یک توکن فعال‌سازی جدید

کد handler مربوط به createActivationTokenHandler باید به شکل زیر باشد:

فایل: cmd/api/tokens.go
...

func (app *application) createActivationTokenHandler(w http.ResponseWriter, r *http.Request) {
    // Parse and validate the user's email address.
    var input struct {
        Email string `json:"email"`
    }

    err := app.readJSON(w, r, &input)
    if err != nil {
        app.badRequestResponse(w, r, err)
        return
    }

    v := validator.New()

    if data.ValidateEmail(v, input.Email); !v.Valid() {
        app.failedValidationResponse(w, r, v.Errors)
        return
    }

    // Try to retrieve the corresponding user record for the email address. If it can't
    // be found, return an error message to the client.
    user, err := app.models.Users.GetByEmail(input.Email)
    if err != nil {
        switch {
        case errors.Is(err, data.ErrRecordNotFound):
            v.AddError("email", "no matching email address found")
            app.failedValidationResponse(w, r, v.Errors)
        default:
            app.serverErrorResponse(w, r, err)
        }
        return
    }

    // Return an error if the user has already been activated.
    if user.Activated {
        v.AddError("email", "user has already been activated")
        app.failedValidationResponse(w, r, v.Errors)
        return
    }

    // Otherwise, create a new activation token.
    token, err := app.models.Tokens.New(user.ID, 3*24*time.Hour, data.ScopeActivation)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }

    // Email the user with their additional activation token.
    app.background(func() {
        data := map[string]any{
            "activationToken": token.Plaintext,
        }

        // Since email addresses MAY be case sensitive, notice that we are sending this 
        // email using the address stored in our database for the user --- not to the 
        // input.Email address provided by the client in this request.
        err := app.mailer.Send(user.Email, "token_activation.tmpl", data)
        if err != nil {
            app.logger.Error(err.Error())
        }
    })

    // Send a 202 Accepted response and confirmation message to the client.
    env := envelope{"message": "an email will be sent to you containing activation instructions"}

    err = app.writeJSON(w, http.StatusAccepted, env, nil)
    if err != nil {
        app.serverErrorResponse(w, r, err)
    }
}

همچنین نیاز دارید یک فایل internal/mailer/templates/token_activation.tmpl شامل قالب‌های ایمیل مورد نیاز ایجاد کنید، مشابه نمونه زیر:

فایل: internal/mailer/templates/token_activation.tmpl
{{define "subject"}}Activate your Greenlight account{{end}}

{{define "plainBody"}}
Hi,

Please send a `PUT /v1/users/activated` request with the following JSON body to activate your account:

{"token": "{{.activationToken}}"}

Please note that this is a one-time use token and it will expire in 3 days.

Thanks,

The Greenlight Team
{{end}}

{{define "htmlBody"}}
<!doctype html>
<html>
  <head>
    <meta name="viewport" content="width=device-width" />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
  <body>
    <p>Hi,</p>
    <p>Please send a <code>PUT /v1/users/activated</code> request with the following JSON body to activate your account:</p>
    <pre><code>
    {"token": "{{.activationToken}}"}
    </code></pre> 
    <p>Please note that this is a one-time use token and it will expire in 3 days.</p>
    <p>Thanks,</p>
    <p>The Greenlight Team</p>
  </body>
</html>
{{end}}

و فایل cmd/api/routes.go را به‌روزرسانی کنید تا endpoint جدید را شامل شود:

فایل: cmd/api/routes.go
package main

...

func (app *application) routes() http.Handler {
    router := httprouter.New()

    router.NotFound = http.HandlerFunc(app.notFoundResponse)
    router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)

    router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)

    router.HandlerFunc(http.MethodGet, "/v1/movies", app.requirePermission("movies:read", app.listMoviesHandler))
    router.HandlerFunc(http.MethodPost, "/v1/movies", app.requirePermission("movies:write", app.createMovieHandler))
    router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.requirePermission("movies:read", app.showMovieHandler))
    router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.requirePermission("movies:write", app.updateMovieHandler))
    router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.requirePermission("movies:write", app.deleteMovieHandler))

    router.HandlerFunc(http.MethodPost, "/v1/users", app.registerUserHandler)
    router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUserHandler)
    router.HandlerFunc(http.MethodPut, "/v1/users/password", app.updateUserPasswordHandler)

    router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler)
    // Add the POST /v1/tokens/activation endpoint.
    router.HandlerFunc(http.MethodPost, "/v1/tokens/activation", app.createActivationTokenHandler)
    router.HandlerFunc(http.MethodPost, "/v1/tokens/password-reset", app.createPasswordResetTokenHandler)

    router.Handler(http.MethodGet, "/debug/vars", expvar.Handler())

    return app.metrics(app.recoverPanic(app.enableCORS(app.rateLimit(app.authenticate(router)))))
}

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

$ curl -X POST -d '{"email": "bob@example.com"}' localhost:4000/v1/tokens/activation
{
    "message": "an email will be sent to you containing activation instructions"
}

توکن از طریق ایمیل برای کاربر ارسال خواهد شد و سپس کاربر می‌تواند توکن را به endpoint PUT /v1/users/activated ارسال کند تا حساب خود را فعال کند، دقیقاً به همان شکلی که توکن را از ایمیل خوش‌آمدگویی دریافت کرده است.

اگر endpointی مانند این پیاده‌سازی کنید، مهم است توجه داشته باشید که این کار به کاربران اجازه می‌دهد در هر زمانی چندین توکن فعال‌سازی معتبر «فعال» داشته باشند. این موضوع اشکالی ندارد — اما باید مطمئن شوید که پس از فعال‌سازی موفق، تمام توکن‌های فعال‌سازی یک کاربر را حذف می‌کنید (نه فقط توکنی که استفاده کرده است).

و باز هم، اگر API شما backend یک وب‌سایت باشد، می‌توانید ایمیل‌ها و فرآیند کاری را با استفاده از الگوهای مشابهی که قبلاً درباره آن‌ها صحبت کردیم، بهینه‌سازی کنید تا تجربه کاربری ساده‌تری ایجاد شود.