Exercício – Personalizar o Identity
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 a interface do usuário 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.
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"
No comando anterior:
- A opção
--dbContext
fornece à ferramenta o conhecimento da classe derivada deDbContext
existente chamadaRazorPagesPizzaAuth
. - A opção
--files
especifica uma lista delimitada por ponto e vírgula de arquivos exclusivos a serem adicionados à área Identity.Account.Manage.Index
é a página de gerenciamento de perfil. Esta página será modificada posteriormente nesta unidade.Account.Register
é a página de registro do usuário. Esta página também será modificada nesta unidade.Account.Manage.EnableAuthenticator
eAccount.ConfirmEmail
são estruturados, mas não são modificados nesta unidade.
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:
- 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
- A opção
Estender IdentityUser
Você recebeu um novo requisito para armazenar os nomes dos usuários. Como a classe padrão IdentityUser
não contém propriedades para nomes e sobrenomes, você precisa estender a classe RazorPagesPizzaUser
.
Faça as alterações a seguir em Areas/Identity/Data/RazorPagesPizzaUser.cs:
Adicione as propriedades
FirstName
eLastName
:using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Identity; namespace RazorPagesPizza.Areas.Identity.Data; 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 valorstring.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.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
eLastName
.
Atualizar o banco de dados
Agora que as alterações de modelo estão feitas, as alterações que acompanham devem ser feitas no banco de dados.
Verifique se todas as alterações foram salvas.
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 tabelaAspNetUsers
. Especificamente, as colunasFirstName
eLastName
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'';
Examine o banco de dados para analisar o impacto da migração do
UpdateUser
EF Core no esquema da tabelaAspNetUsers
.No painel SQL Server, expanda o nó Colunas na tabela dbo.AspNetUsers.
As propriedades
FirstName
eLastName
na classeRazorPagesPizzaUser
correspondem às colunasFirstName
eLastName
na imagem anterior. Um tipo de dadosnvarchar(100)
foi atribuído a cada uma das duas colunas devido aos atributos[MaxLength(100)]
. A restrição não nula foi adicionada porqueFirstName
eLastName
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.
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" role="alert"></div> <div class="form-floating mb-3"> <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 mb-3"> <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 mb-3"> <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" /> <label asp-for="Input.Email">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.
Em Areas/Identity/Pages/Account/Register.cshtml.cs, adicione suporte às caixas de texto de nome.
Adicione as propriedades
FirstName
eLastName
à classe aninhadaInputModel
: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.Modifique o método
OnPostAsync
para definir as propriedadesFirstName
eLastName
no objetoRazorPagesPizza
. 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
eLastName
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>
UserManager.GetUserAsync(User)
retorna um objeto RazorPagesPizzaUser
anulável. O operador condicional nulo ?.
é usado para acessar as propriedades FirstName
e LastName
somente se o objeto RazorPagesPizzaUser
não for nulo.
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.
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" role="alert"></div> <div class="form-floating mb-3"> <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 mb-3"> <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 mb-3"> <input asp-for="Username" class="form-control" disabled /> <label asp-for="Username" class="form-label"></label> </div>
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.
Adicione as propriedades
FirstName
eLastName
à classe aninhadaInputModel
: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> [Phone] [Display(Name = "Phone number")] public string PhoneNumber { get; set; } }
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.
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
Na primeira vez que testou o aplicativo, você registrou um usuário e, em seguida, clicou em um link para simular a confirmação do endereço de email do usuário. Para enviar uma email de confirmação real, você precisa criar uma implementação de IEmailSender e registrá-la no sistema de injeção de dependência. Para manter as coisas simples, sua implementação nesta unidade não envia emails para um servidor de protocolo SMTP (Simple Mail Transfer Protocol). Ela só gravará o conteúdo de email no console.
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}");
No painel Explorer, clique com o botão direito do mouse na pasta RazorPagesPizza\Services 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.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 umIEmailSender
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.
Verifique se salvou todas as alterações.
No painel do terminal, crie o projeto e execute o aplicativo com
dotnet run
.No navegador, navegue até o aplicativo. Selecione Logoff se você estiver conectado.
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
eLastName
deInputModel
.Após o registro, você é 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
.Selecione Logon e entre com o novo usuário. O cabeçalho do aplicativo agora contém Olá, [nome] [sobrenome]!.
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
eLastName
ao esquema. Portanto, o registro da tabelaAspNetUsers
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.
No aplicativo Web, entre com o primeiro usuário criado.
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 paraFirstName
eLastName
.Insira valores válidos para Nome e Sobrenome. Selecione Salvar.
O cabeçalho do aplicativo é atualizado para Olá, [nome] [sobrenome]!.
Para interromper o aplicativo, pressione Ctrl+C no painel do terminal no VS Code.
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.