Exercício – Personalizar o Identity

Concluído

Na unidade anterior, você aprendeu como funciona a personalização na Identidade do ASP.NET Core. Nesta unidade, você estenderá o modelo de dados do Identity e fará as alterações correspondentes na interface do usuário.

Personalizar os dados da conta de usuário

Nesta seção, você vai criar e personalizar os arquivos de interface do usuário de identidade a serem usados em vez da Biblioteca de Classes Razor padrão.

  1. Adicione ao projeto os arquivos de registro do usuário a serem modificados:

    dotnet aspnet-codegenerator identity --dbContext RazorPagesPizzaAuth --files "Account.Manage.EnableAuthenticator;Account.Manage.Index;Account.Register;Account.ConfirmEmail" --userClass RazorPagesPizzaUser --force
    

    No comando anterior:

    • A opção --dbContext fornece à ferramenta o conhecimento da classe derivada de DbContext existente chamada RazorPagesPizzaAuth.
    • A opção --files especifica uma lista delimitada por ponto e vírgula de arquivos exclusivos a serem adicionados à área Identity.
    • A opção --userClass resulta na criação de uma classe derivada de IdentityUser chamada RazorPagesPizzaUser.
    • A opção --force faz com que os arquivos existentes na área Identity sejam substituídos.

    Dica

    Execute o seguinte comando na raiz do projeto a fim de exibir valores válidos para a opção --files: dotnet aspnet-codegenerator identity --listFiles

    Os seguintes arquivos são adicionados ao diretório Areas/Identity:

    • Data/
      • RazorPagesPizzaUser.cs
    • Pages/
      • _ViewImports.cshtml
      • Account/
        • _ViewImports.cshtml
        • ConfirmEmail.cshtml
        • ConfirmEmail.cshtml.cs
        • Register.cshtml
        • Register.cshtml.cs
        • Manage/
          • _ManageNav.cshtml
          • _ViewImports.cshtml
          • EnableAuthenticator.cshtml
          • EnableAuthenticator.cshtml.cs
          • Index.cshtml
          • Index.cshtml.cs
          • ManageNavPages.cs

    Além disso, o arquivo Data/RazorPagesPizzaAuth.cs, que existia antes da execução do comando anterior, foi substituído porque a opção --force foi usada. A declaração de classe RazorPagesPizzaAuth agora referencia o tipo de usuário recém-criado RazorPagesPizzaUser:

    public class RazorPagesPizzaAuth : IdentityDbContext<RazorPagesPizzaUser>
    

    As Razor Pages EnableAuthenticator e ConfirmEmail foram gerada por scaffolding, mas só serão modificadas mais adiante no módulo.

  2. Em Program.cs, a chamada a AddDefaultIdentity precisa estar ciente do novo tipo de usuário do Identity. Incorpore as alterações realçadas a seguir. (Exemplo reformatado para legibilidade.)

    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    using RazorPagesPizza.Areas.Identity.Data;
    
    var builder = WebApplication.CreateBuilder(args);
    var connectionString = builder.Configuration.GetConnectionString("RazorPagesPizzaAuthConnection");
    builder.Services.AddDbContext<RazorPagesPizzaAuth>(options => options.UseSqlServer(connectionString)); 
    builder.Services.AddDefaultIdentity<RazorPagesPizzaUser>(options => options.SignIn.RequireConfirmedAccount = true)
          .AddEntityFrameworkStores<RazorPagesPizzaAuth>();
    
    // Add services to the container.
    builder.Services.AddRazorPages();
    
  3. Atualize Pages/Shared/_LoginPartial.cshtml para incorporar as alterações realçadas a seguir no topo. Salve as alterações.

    @using Microsoft.AspNetCore.Identity
    @using RazorPagesPizza.Areas.Identity.Data
    @inject SignInManager<RazorPagesPizzaUser> SignInManager
    @inject UserManager<RazorPagesPizzaUser> UserManager
    
    <ul class="navbar-nav">
    

    As alterações anteriores atualizam o tipo de usuário passado para SignInManager<T> e UserManager<T> e nas diretivas @inject. Em vez do tipo IdentityUser padrão, o usuário RazorPagesPizzaUser agora é referenciado. A diretiva @using foi adicionada para resolver as referências de RazorPagesPizzaUser.

    Pages/Shared/_LoginPartial.cshtml está fisicamente localizado fora da área Identity. Portanto, o arquivo não foi atualizado automaticamente pela ferramenta de scaffolding. Foi necessário fazer as alterações apropriadas manualmente.

    Dica

    Como alternativa à edição manual do arquivo _LoginPartial.cshtml, ele pode ser excluído antes da execução da ferramenta de scaffolding. O arquivo _LoginPartial.cshtml será recriado com referências à nova classe RazorPagesPizzaUser.

  4. Atualize Areas/Identity/Data/RazorPagesPizzaUser.cs para dar suporte ao armazenamento e à recuperação dos dados adicionais de perfil do usuário. Faça as seguintes alterações:

    1. Adicione as propriedades FirstName e LastName:

      public class RazorPagesPizzaUser : IdentityUser
      {
          [Required]
          [MaxLength(100)]
          public string FirstName { get; set; } = string.Empty;
      
          [Required]
          [MaxLength(100)]
          public string LastName { get; set; } = string.Empty;
      }
      

      As propriedades no snippet anterior representam colunas adicionais a serem criadas na tabela AspNetUsers subjacente. Ambas as propriedades são necessárias e, portanto, são anotadas com o atributo [Required]. Além disso, o atributo [MaxLength] indica que um tamanho máximo de 100 caracteres é permitido. O tipo de dados da coluna da tabela subjacente é definido de acordo. Um valor string.Empty padrão é atribuído, uma vez que o contexto anulável está habilitado neste projeto e as propriedades são cadeias de caracteres não anuláveis.

    2. Adicione a instrução using a seguir ao início do arquivo.

      using System.ComponentModel.DataAnnotations;
      

      O código anterior resolve os atributos de anotação de dados aplicados às propriedades FirstName e LastName.

Atualizar o banco de dados

Agora que as alterações de modelo foram feitas, as alterações que acompanham devem ser feitas no banco de dados.

  1. Verifique se todas as alterações foram salvas.

  2. Crie e aplique uma migração do EF Core para atualizar o armazenamento de dados subjacente:

    dotnet ef migrations add UpdateUser
    dotnet ef database update
    

    A migração do EF Core UpdateUser aplicou um script de alteração de DDL ao esquema da tabela AspNetUsers. Especificamente, as colunas FirstName e LastName foram adicionadas, conforme visto no seguinte trecho de resultado da migração:

    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
        Executed DbCommand (37ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
        ALTER TABLE [AspNetUsers] ADD [FirstName] nvarchar(100) NOT NULL DEFAULT N'';
    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
        Executed DbCommand (36ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
        ALTER TABLE [AspNetUsers] ADD [LastName] nvarchar(100) NOT NULL DEFAULT N'';
    
  3. Examine o banco de dados para analisar o impacto da migração do UpdateUser EF Core no esquema da tabela AspNetUsers.

    No painel SQL Server, expanda o nó Colunas na tabela dbo.AspNetUsers.

    Captura de tela do esquema da tabela AspNetUsers.

    As propriedades FirstName e LastName na classe RazorPagesPizzaUser correspondem às colunas FirstName e LastName na imagem anterior. Um tipo de dados nvarchar(100) foi atribuído a cada uma das duas colunas devido aos atributos [MaxLength(100)]. A restrição não nula foi adicionada porque FirstName e LastName na classe são cadeias de caracteres não anuláveis. As linhas existentes mostram cadeias de caracteres vazias nas novas colunas.

Personalizar o formulário de registro de usuário

Você adicionou novas colunas para FirstName e LastName. Agora você precisa editar a interface do usuário para exibir campos correspondentes no formulário de registro.

  1. Em Areas/Identity/Pages/Account/Register.cshtml, adicione a seguinte marcação realçada:

    <form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
        <h2>Create a new account.</h2>
        <hr />
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="form-floating">
            <input asp-for="Input.FirstName" class="form-control" />
            <label asp-for="Input.FirstName"></label>
            <span asp-validation-for="Input.FirstName" class="text-danger"></span>
        </div>
        <div class="form-floating">
            <input asp-for="Input.LastName" class="form-control" />
            <label asp-for="Input.LastName"></label>
            <span asp-validation-for="Input.LastName" class="text-danger"></span>
        </div>
        <div class="form-floating">
            <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
            <label asp-for="Input.Email"></label>
            <span asp-validation-for="Input.Email" class="text-danger"></span>
        </div>
    

    Com a marcação anterior, as caixas de texto Nome e Sobrenome são adicionadas ao formulário de registro de usuário.

  2. Em Areas/Identity/Pages/Account/Register.cshtml.cs, adicione suporte às caixas de texto de nome.

    1. Adicione as propriedades FirstName e LastName à classe aninhada InputModel:

      public class InputModel
      {
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "First name")]
          public string FirstName { get; set; }
      
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "Last name")]
          public string LastName { get; set; }
      
          /// <summary>
          ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
          ///     directly from your code. This API may change or be removed in future releases.
          /// </summary>
          [Required]
          [EmailAddress]
          [Display(Name = "Email")]
          public string Email { get; set; }
      

      Os atributos [Display] definem o texto do rótulo a ser associado às caixas de texto.

    2. Modifique o método OnPostAsync para definir as propriedades FirstName e LastName no objeto RazorPagesPizza. Adicione as seguintes linhas realçadas:

      public async Task<IActionResult> OnPostAsync(string returnUrl = null)
      {
          returnUrl ??= Url.Content("~/");
          ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
          if (ModelState.IsValid)
          {
              var user = CreateUser();
      
              user.FirstName = Input.FirstName;
              user.LastName = Input.LastName;
              
              await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
              await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
              var result = await _userManager.CreateAsync(user, Input.Password);
      
      

      A alteração anterior define as propriedades FirstName e LastName como a entrada do usuário no formulário de registro.

Personalizar o cabeçalho do site

Atualize Pages/Shared/_LoginPartial.cshtml para exibir o nome e o sobrenome coletados durante o registro de usuário. As linhas realçadas no seguinte snippet são necessárias:

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    RazorPagesPizzaUser user = await UserManager.GetUserAsync(User);
    var fullName = $"{user.FirstName} {user.LastName}";

    <li class="nav-item">
        <a id="manage" class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello, @fullName!</a>
    </li>

Personalizar o formulário de gerenciamento de perfil

Você adicionou os novos campos ao formulário de registro do usuário, mas também deve adicioná-los ao formulário de gerenciamento de perfil para que os usuários possam editá-los.

  1. Em Areas/Identity/Pages/Account/Manage/Index.cshtml, adicione a marcação realçada a seguir. Salve suas alterações.

    <form id="profile-form" method="post">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="form-floating">
            <input asp-for="Input.FirstName" class="form-control" />
            <label asp-for="Input.FirstName"></label>
            <span asp-validation-for="Input.FirstName" class="text-danger"></span>
        </div>
        <div class="form-floating">
            <input asp-for="Input.LastName" class="form-control" />
            <label asp-for="Input.LastName"></label>
            <span asp-validation-for="Input.LastName" class="text-danger"></span>
        </div>
        <div class="form-floating">
            <input asp-for="Username" class="form-control" disabled />
            <label asp-for="Username" class="form-label"></label>
        </div>
    
  2. Em Areas/Identity/Pages/Account/Manage/Index.cshtml.cs, faça as alterações a seguir para dar suporte às caixas de texto de nome.

    1. Adicione as propriedades FirstName e LastName à classe aninhada InputModel:

      public class InputModel
      {
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "First name")]
          public string FirstName { get; set; }
      
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "Last name")]
          public string LastName { get; set; }
      
          [Phone]
          [Display(Name = "Phone number")]
          public string PhoneNumber { get; set; }
      }
      
    2. Incorpore as alterações realçadas no método LoadAsync:

      private async Task LoadAsync(RazorPagesPizzaUser user)
      {
          var userName = await _userManager.GetUserNameAsync(user);
          var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
      
          Username = userName;
      
          Input = new InputModel
          {
              PhoneNumber = phoneNumber,
              FirstName = user.FirstName,
              LastName = user.LastName
          };
      }
      

      O código anterior dá suporte à recuperação do nome e do sobrenome para exibição nas caixas de texto correspondentes do formulário de gerenciamento de perfil.

    3. Incorpore as alterações realçadas no método OnPostAsync. Salve as alterações.

      public async Task<IActionResult> OnPostAsync()
      {
          var user = await _userManager.GetUserAsync(User);
          if (user == null)
          {
              return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
          }
      
          if (!ModelState.IsValid)
          {
              await LoadAsync(user);
              return Page();
          }
      
          user.FirstName = Input.FirstName;
          user.LastName = Input.LastName;
          await _userManager.UpdateAsync(user);
      
          var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
          if (Input.PhoneNumber != phoneNumber)
          {
              var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
              if (!setPhoneResult.Succeeded)
              {
                  StatusMessage = "Unexpected error when trying to set phone number.";
                  return RedirectToPage();
              }
          }
      
          await _signInManager.RefreshSignInAsync(user);
          StatusMessage = "Your profile has been updated";
          return RedirectToPage();
      }
      

      O código anterior dá suporte à atualização do nome e do sobrenome na tabela AspNetUsers do banco de dados.

Configurar o remetente de email de confirmação

Para enviar o email de confirmação, você precisa criar uma implementação de IEmailSender e registrá-la no sistema injeção de dependência. Para simplificar, a implementação não enviará realmente um email para um servidor SMTP. Ela só gravará o conteúdo de email no console.

  1. Como você exibirá o email em texto sem formatação no console, altere a mensagem gerada para excluir o texto codificado em HTML. Em Areas/Identity/Pages/Account/Register.cshtml.cs, localize o seguinte código:

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

    Mude-a para:

    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
        $"Please confirm your account by visiting the following URL:\r\n\r\n{callbackUrl}");
    
  2. No painel Explorer, clique com o botão direito do mouse na pasta Serviços e crie um novo arquivo chamado EmailSender.cs. Abra o arquivo e adicione o seguinte código:

    using Microsoft.AspNetCore.Identity.UI.Services;
    namespace RazorPagesPizza.Services;
    
    public class EmailSender : IEmailSender
    {
        public EmailSender() {}
    
        public Task SendEmailAsync(string email, string subject, string htmlMessage)
        {
            Console.WriteLine();
            Console.WriteLine("Email Confirmation Message");
            Console.WriteLine("--------------------------");
            Console.WriteLine($"TO: {email}");
            Console.WriteLine($"SUBJECT: {subject}");
            Console.WriteLine($"CONTENTS: {htmlMessage}");
            Console.WriteLine();
    
            return Task.CompletedTask;
        }
    }
    

    O código anterior cria uma implementação de IEmailSender que grava o conteúdo da mensagem no console. Em uma implementação do mundo real, SendEmailAsync se conectaria a um serviço de email externo ou a alguma outra ação para enviar email.

  3. Em Program.cs, adicione as linhas realçadas:

    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    using RazorPagesPizza.Areas.Identity.Data;
    using Microsoft.AspNetCore.Identity.UI.Services;
    using RazorPagesPizza.Services;
    
    var builder = WebApplication.CreateBuilder(args);
    var connectionString = builder.Configuration.GetConnectionString("RazorPagesPizzaAuthConnection");
    builder.Services.AddDbContext<RazorPagesPizzaAuth>(options => options.UseSqlServer(connectionString)); 
    builder.Services.AddDefaultIdentity<RazorPagesPizzaUser>(options => options.SignIn.RequireConfirmedAccount = true)
          .AddEntityFrameworkStores<RazorPagesPizzaAuth>();
    
    // Add services to the container.
    builder.Services.AddRazorPages();
    builder.Services.AddTransient<IEmailSender, EmailSender>();
    
    var app = builder.Build();
    

    O anterior registra EmailSender como um IEmailSender no sistema de injeção de dependência.

Testar as alterações ao formulário de registro

Isso é tudo. Vamos testar as alterações no formulário de registro e no email de confirmação.

  1. Verifique se você salvou todas as alterações.

  2. No painel do terminal, crie o projeto e execute o aplicativo com dotnet run.

  3. No navegador, navegue até o aplicativo. Selecione Logoff se ainda estiver conectado.

  4. Selecione Registrar e use o formulário atualizado para registrar um novo usuário.

    Observação

    As restrições de validação nos campos Nome e Sobrenome refletem as anotações de dados nas propriedades FirstName e LastName de InputModel.

  5. Após o registro, você será redirecionado para a tela de Confirmação de registro. No painel do terminal, role para cima para encontrar a saída do console semelhante à seguinte:

    Email Confirmation Message
    --------------------------
    TO: jana.heinrich@contoso.com
    SUBJECT: Confirm your email
    CONTENTS: Please confirm your account by visiting the following URL:
    
    https://localhost:7192/Identity/Account/ConfirmEmail?<query string removed>
    

    Navegue até a URL com Ctrl+clique. A tela de confirmação é exibida.

    Observação

    Se você estiver usando o GitHub Codespaces, talvez seja necessário adicionar -7192 à primeira parte da URL encaminhada. Por exemplo, scaling-potato-5gr4j4-7192.preview.app.github.dev.

  6. Selecione Logon e entre com o novo usuário. O cabeçalho do aplicativo agora contém Olá, [nome] [sobrenome]!.

  7. No painel SQL Server no VS Code, clique com o botão direito do mouse no banco de dados RazorPagesPizza e selecione Nova consulta. Na guia exibida, insira a consulta a seguir e pressione Ctrl+Shift+E para executá-la.

    SELECT UserName, Email, FirstName, LastName
    FROM dbo.AspNetUsers
    

    Uma guia com resultados semelhantes aos seguintes aparece:

    UserName Email Nome LastName
    kai.klein@contoso.com kai.klein@contoso.com
    jana.heinrich@contoso.com jana.heinrich@contoso.com Jana Heinrich

    O primeiro usuário registrado antes de adicionar FirstName e LastName ao esquema. Portanto, o registro da tabela AspNetUsers associado não tem dados nessas colunas.

Testar as alterações no formulário de gerenciamento de perfil

Você também deve testar as alterações feitas no formulário de gerenciamento de perfil.

  1. No aplicativo Web, entre com o primeiro usuário criado.

  2. Clique no link Olá, ! para navegar até o formulário de gerenciamento de perfil.

    Observação

    O link não é exibido corretamente porque a linha da tabela AspNetUsers desse usuário não contém valores para FirstName e LastName.

  3. Insira valores válidos para Nome e Sobrenome. Selecione Salvar.

    O cabeçalho do aplicativo é atualizado para Olá, [nome] [sobrenome]!.

  4. Pressione Ctrl+C no painel do terminal no VS Code para interromper o aplicativo.

Resumo

Nesta unidade, você personalizou a Identidade para armazenar informações personalizadas do usuário. Você também personalizou o email de confirmação. Na próxima unidade, você vai aprender a implementar a autenticação multifator em Identidade.