بخش دوم از آموزش فعال سازی از طریق ایمیل در ASP.NET Core ، برای یک برنامه ی وب ASP.NET Core 2.2 است که به کاربر اجازه می دهد تا یک ایمیل تأیید شده را بروزرسانی کند. در اینجا مراحل مجاز کردن کاربر جهت بروزرسانی ایمیل خود آورده شده است.

اگرقسمت اول  آموزش فعال سازی از طریق ایمیل در ASP.NET Core را نخوانده‏ اید همواره می‏توانید آن را در لینک‏ زیر بخوانید.

نیاز به ایمیل تأیید شده در ASP.NET Core 2.2  بخش اول

پیش نیازها

ویژوال استودیو نسخه ی ۲۰۱۷، ۱۵.۹ یا بالاتر
ویژوال استودیو برای Mac نسخه ی ۷.۷ یا بالاتر
افزونه ی سی شارپ ویژوال استودیو کد (VS Code) نسخه ی ۱.۱۷.۱ یا بالاتر

می توانید پروژه ی VS 2017 را دانلود کرده یا این مراحل را برای تغییر پروژه ی خود پس از کامل کردن مراحل در بخش اول دنبال کنید.

مرحله ۱ : افزودن ویژگی UnconfirmedEmail به IdentityUser

کلاس جدیدی به نام ApplicationUser در پوشه ی Entities اضافه کنید:

using Microsoft.AspNetCore.Identity;

namespace <YourProjectName>.Entities
{
    public class ApplicationUser : IdentityUser
    {
        [PersonalData]
        public string UnconfirmedEmail { get; set; }
    }
}

از Find and Replace برای جایگزینی <IdentityUser> با <ApplicationUser> در Current Project (پروژه ی فعلی) استفاده کنید.

Startup.cs > ConfigureServices را برای استفاده از ApllicationUser تصحیح کنید:

services.AddIdentity<ApplicationUser, IdentityRole>

Areas\Identity\Pages\Account\Manage\EnableAuthenticator.cshtml.cs را تصحیح کنید:

private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user)

Areas\Identity\Pages\Account\Manage\DownloadPersonalData.cshtml.cs را تصحیح کنید:

var personalDataProps = typeof(ApplicationUser).GetProperties().Where(
                        prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));

Areas\Identity\Pages\Account\ExternalLogin.cshtml.cs را تصحیح کنید:

var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };

Register.cshtml.cs را تصحیح کنید:

var user = new ApplicationUser { UserName = Input.UserName, Email = Input.Email };

مسائل و مشکلات فضای نام را در جایی که IdentityUser را جایگزین کردید حل کنید.

using <YourProjectName>.Entities;

یا برای cshtml:

@using <YourProjectName>.Entities;

پروژه را ساخته و وجود خطاها را بررسی کنید.

مرحله ۲ : بروزرسانی پایگاه داده

ApplicationDbContext در پوشه ی Data را تصحیح کرده، ApplicationUser را اضافه کنید:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>

فرمان “Add-Migration UnconfirmedEmail” را از Package Manager Console در VS 2017 اجرا کنید.

فرمان “Update-Database” را اجرا کنید.

مرحله ۳ : افزودن صفحه ی تغییر ایمیل در ASP.NET Core

ManageNavPages.cs را تصحیح کرده، ویژگی های ChangePassword فوق را اضافه کنید:

public static string ChangeEmail => "ChangeEmail";

و:

public static string ChangeEmailNavClass(ViewContext viewContext) =>
                                         PageNavClass(viewContext, ChangeEmail);

_ManageNav.cshtml را تصحیح کرده، آیتم Profile زیر را اضافه کنید:

<li class="nav-item">
<a class="nav-link @ManageNavPages.ChangeEmailNavClass(ViewContext)"
 id="change-email" asp-page="./ChangeEmail">Email</a></li>

یک صفحه ی razor جدید به نام ChangeEmail در Areas\Identity\Pages\Account\Manage ایجاد کنید.

ChangeEmail.cshtml را تصحیح کنید:

@page
@model ChangeEmailModel
@{
    ViewData["Title"] = "Change Email";
    ViewData["ActivePage"] = ManageNavPages.ChangeEmail;
}

<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage" />

<div class="row">
    <div class="col-md-6">
        <form id="change-email-form" method="post">
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Email"></label>
                <input asp-for="Email" class="form-control" disabled />
            </div>

            <h5>New email needs to be verified.</h5>
            <div class="form-group">
                <label asp-for="Input.Email"></label>
                <input asp-for="Input.Email" class="form-control" />
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-primary">Update Email</button>
        </form>
    </div>
</div>

@section Scripts {
<partial name="_ValidationScriptsPartial" />

}

ChangeEmail.cshtml.cs را تصحیح کنید:

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using <YourProjectName>.Services;
using <YourProjectName>.Entities;

namespace <YourProjectName>.Areas.Identity.Pages.Account.Manage
{
    public class ChangeEmailModel : PageModel
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly ILogger<ChangeEmailModel> _logger;
        private readonly IEmailSender _emailSender;

        public ChangeEmailModel(
            UserManager<ApplicationUser> userManager,
            SignInManager<ApplicationUser> signInManager,
            ILogger<ChangeEmailModel> logger,
            IEmailSender emailSender)
        {
            _userManager = userManager;
            _signInManager = signInManager;
            _logger = logger;
            _emailSender = emailSender;
        }

        [BindProperty]
        public InputModel Input { get; set; }

        [TempData]
        [Display(Name = "Verified Email")]
        public string Email { get; set; }

        [TempData]
        public string StatusMessage { get; set; }

        public class InputModel
        {
            [Required]
            [EmailAddress]
            [Display(Name = "New Email")]
            public string Email { get; set; }
        }

        public async Task<IActionResult> OnGetAsync()
        {
            var user = await _userManager.GetUserAsync(User);
            if (user == null)
            {
                return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
            }

            var email = await _userManager.GetEmailAsync(user);

            Email = email;

            return Page();
        }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            var user = await _userManager.GetUserAsync(User);
            if (user == null)
            {
                return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
            }

            var email = await _userManager.GetEmailAsync(user);
            if (Input.Email != email)
            {
                var errors = new List<IdentityError>();
                if (_userManager.Options.User.RequireUniqueEmail)
                {
                    var owner = await _userManager.FindByEmailAsync(Input.Email);
                    if (owner != null && !string.Equals
                       (await _userManager.GetUserIdAsync(owner), 
                        await _userManager.GetUserIdAsync(user)))
                    {
                        ModelState.AddModelError(string.Empty, 
                        new IdentityErrorDescriber().DuplicateEmail(Input.Email).Description);
                        return Page();
                    }
                }

                var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email);
                if (!setEmailResult.Succeeded)
                {
                    var userId = await _userManager.GetUserIdAsync(user);
                    throw new InvalidOperationException($"Unexpected error occurred 
                                       setting email for user with ID '{userId}'.");
                }

                if (Input.Email.ToUpper() != email.ToUpper())
                {
                    var result = await _userManager.UpdateSecurityStampAsync(user);
                    if (!result.Succeeded)
                    {
                        foreach (var error in result.Errors)
                        {
                            ModelState.AddModelError(string.Empty, error.Description);
                            return Page();
                        }
                    }

                    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);

                    var callbackUrl = Url.Page(
                        "/Account/ConfirmEmail",
                        pageHandler: null,
                        values: new { userId = user.Id, code = code },
                        protocol: Request.Scheme);

                    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                        $"Please confirm your account by 
                        <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                    _logger.LogInformation("User updated their UnconfirmedEmail.");
                    StatusMessage = "Please check your inbox to confirm the new email.";

                }
                else
                {
                    _logger.LogInformation("User updated their Email.");
                    StatusMessage = "Your email has been updated.";
                }
            }

            return RedirectToPage();
        }
    }
}

مرحله ۴ : تغییر Profile

Index.cshtml.cs در Areas\Identity\Pages\Account\Manage را برای استفاده از صفحه ی ChangeEmail جدید تصحیح کنید.
این مورد را افزوده:

public string Email { get; set; }

این مورد را حذف کنید:

public bool IsEmailConfirmed { get; set; }

از InputModel این بخش را حذف کنید:

[Required]
[EmailAddress]
public string Email { get; set; }

این بخش را از OnGetAsync > Input حذف کنید:

Email = email,

از OnGetAsync این قسمت را حذف کنید:

IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);

این قسمت را از OnPostAsync حذف کنید:

var email = await _userManager.GetEmailAsync(user);
if (Input.Email != email)
{
    var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email);
    if (!setEmailResult.Succeeded)
    {
        var userId = await _userManager.GetUserIdAsync(user);
        throw new InvalidOperationException($"Unexpected error occurred
                     setting email for user with ID '{userId}'.");
     }
 }

این مورد را حذف کنید:

public async Task<IActionResult> OnPostSendVerificationEmailAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var user = await _userManager.GetUserAsync(User);
    if (user == null)
    {
        return NotFound($"Unable to load user with ID '
        {_userManager.GetUserId(User)}'.");
    }


    var userId = await _userManager.GetUserIdAsync(user);
    var email = await _userManager.GetEmailAsync(user);
    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
    var callbackUrl = Url.Page(
        "/Account/ConfirmEmail",
        pageHandler: null,
        values: new { userId = userId, code = code },
        protocol: Request.Scheme);
    await _emailSender.SendEmailAsync(
        email,
        "Confirm your email",
        $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode
                                         (callbackUrl)}' >clicking here</a>.");

    StatusMessage = "Verification email sent. Please check your email.";
    return RedirectToPage();
}

Index.cshtml را تصحیح کنید.این مورد را جایگزین کنید:

@if (Model.IsEmailConfirmed)
{
<div class="input-group">
<input asp-for="Input.Email" class="form-control" />

<span class="input-group-addon" aria-hidden="true">
<span class="glyphicon glyphicon-ok text-success"></span></span>
</div>
}
else
{
<input asp-for="Input.Email" class="form-control" />

<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" 

class="btn btn-link">Send verification email</button>
}
<span asp-validation-for="Input.Email" class="text-danger"></span>

با:

<input asp-for="Email" class="form-control" disabled />

مرحله ۵ : بازنویسی UserManager

کلاسی جدید به نام ApplicationUserManager در پوشه ی Services اضافه کنید:

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using <YourProjectName>.Entities;

namespace <YourProjectName>.Services
{
    public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        public ApplicationUserManager(IUserStore<ApplicationUser> store,
            IOptions<IdentityOptions> optionsAccessor,
            IPasswordHasher<ApplicationUser> passwordHasher,
            IEnumerable<IUserValidator<ApplicationUser>> userValidators,
            IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators,
            ILookupNormalizer keyNormalizer,
            IdentityErrorDescriber errors,
            IServiceProvider services,
            ILogger<UserManager<ApplicationUser>> logger)
            : base(store, optionsAccessor, passwordHasher, userValidators,
                  passwordValidators, keyNormalizer, errors, services, logger)
        {
        }

        /// <summary>
        /// Sets the <paramref name="email"/> address for a <paramref name="user"/>.
        /// </summary>
        /// <param name="user">The user whose email should be set.</param>
        /// <param name="email">The email to set.</param>
        /// <returns>
        /// The <see cref="Task"/> that represents the asynchronous operation, 
        /// containing the <see cref="IdentityResult"/>
        /// of the operation.
        /// </returns>
        public override async Task<IdentityResult> SetEmailAsync(ApplicationUser user, string email)
        {
            ThrowIfDisposed();
            if (user == null)
            {
                throw new ArgumentNullException(nameof(user));
            }

            if (user.EmailConfirmed && user.Email.ToUpper() != email.ToUpper())
                user.UnconfirmedEmail = email;
            else
                user.Email = email;

            return await UpdateUserAsync(user);
        }

        /// <summary>
        /// Validates that an email confirmation token matches the specified 
        /// <paramref name="user"/> and if successful sets
        /// EmailConfirmed to true and if UnconfirmedEmail is not NULL or Empty, 
        /// copies the user's UnconfirmedEmail to user's
        /// Email and sets UnconfirmedEmail to NULL.
        /// </summary>
        /// <param name="user">The user to validate the token against.</param>
        /// <param name="token">The email confirmation token to validate.</param>
        /// <returns>
        /// The <see cref="Task"/> that represents the asynchronous operation, 
        /// containing the <see cref="IdentityResult"/>
        /// of the operation.
        /// </returns>
        public override async Task<IdentityResult> 
                  ConfirmEmailAsync(ApplicationUser user, string token)
        {
            ThrowIfDisposed();
            if (user == null)
            {
                throw new ArgumentNullException(nameof(user));
            }

            IdentityResult result;
            var provider = Options.Tokens.EmailConfirmationTokenProvider;
            var isValidToken = await base.VerifyUserTokenAsync
                               (user, provider, "EmailConfirmation", token);

            if (isValidToken)
            {
                if (!string.IsNullOrEmpty(user.UnconfirmedEmail))
                {
                    user.Email = user.UnconfirmedEmail;
                    user.UnconfirmedEmail = null;
                }
                user.EmailConfirmed = true;
                result = await UpdateUserAsync(user);
            }
            else
            {
                result = IdentityResult.Failed(new IdentityErrorDescriber().InvalidToken());
            }

            return result;
        }
    }
}

Startup.cs > ConfigureServices را تصحیح کرده، .AddUserManager<ApplicationUserManager>() را اضافه کنید:

services.AddIdentity<ApplicationUser, IdentityRole>(config =>
    {
        config.SignIn.RequireConfirmedEmail = true;
        config.User.RequireUniqueEmail = true;
    })
    .AddDefaultUI(UIFramework.Bootstrap4)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddUserManager<ApplicationUserManager>()
    .AddDefaultTokenProviders();

پروژه را ساخته و آزمایش کنید.

نکات جالب

اطمینان ندارم، باید “Override all files” (بازنویسی تمام فایل ها) را انجام می دادم هنگامیکه Identity را چارچوب بندی می کردم، اما ترجیح می دهم بررسی کرده و کنترل کاملی بر روی تجربه ی کاربر داشته باشم.

توجه به UpdateSecurityStampAsync پیش از GenerateEmailConfigurationTokenAsync.

این مورد هر کد قبلی ارسال شده به کاربر را نامعتبر می کند.

زهره سلطانیان

نوشته‌های مرتبط

دیدگاه‌ها

*
*

این سایت از اکیسمت برای کاهش هرزنامه استفاده می کند. بیاموزید که چگونه اطلاعات دیدگاه های شما پردازش می‌شوند.