Ejercicio: personalización de Identity

Completado

En la unidad anterior, ha aprendido cómo funciona la personalización en Identity de ASP.NET Core. En esta unidad, se amplía el modelo de datos de Identity y se realizan los cambios correspondientes de la interfaz de usuario.

Personalización de los datos de la cuenta de usuario

En esta sección, va a crear y personalizar los archivos de la interfaz de usuario de Identity que se usarán en lugar de la biblioteca de clases de Razor predeterminada.

  1. Agregue los archivos de registro del usuario que se van a modificar en el proyecto:

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

    En el comando anterior:

    • La opción --dbContext proporciona a la herramienta conocimientos de la clase derivada de DbContext existente llamada RazorPagesPizzaAuth.
    • La opción --files especifica una lista delimitada por signos de punto y coma de archivos únicos que se van a agregar al área de Identity.
      • Account.Manage.Index es la página de administración de perfiles. Esta página se modifica más adelante en esta unidad.
      • Account.Register es la página de registro de usuarios. Esta página también se modifica en esta unidad.
      • Se aplica scaffolding a Account.Manage.EnableAuthenticator y Account.ConfirmEmail, pero no se modifican en esta unidad.

    Sugerencia

    Ejecute el comando siguiente desde la raíz del proyecto para ver los valores válidos de la opción --files: dotnet aspnet-codegenerator identity --listFiles

    Los archivos siguientes se agregan al directorio 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

Extienda IdentityUser

Se le da un nuevo requisito para almacenar los nombres de los usuarios. Dado que la clase IdentityUser predeterminada no contiene propiedades para los nombres y apellidos, debe extender la clase RazorPagesPizzaUser.

Realice los siguientes cambios en Areas/Identity/Data/RazorPagesPizzaUser.cs:

  1. Agregue las propiedades FirstName y LastName:

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

    Las propiedades del fragmento anterior representan columnas adicionales que se van a crear en la tabla subyacente AspNetUsers. Ambas propiedades son necesarias y, por tanto, se anotan con el atributo [Required]. Además, el atributo [MaxLength] indica que se permite una longitud máxima de 100 caracteres. El tipo de datos de la columna de la tabla subyacente se define en consecuencia. Se asigna un valor predeterminado de string.Empty porque el contexto que admite un valor NULL está habilitado en este proyecto y las propiedades son cadenas que no aceptan valores NULL.

  2. Agregue la instrucción using siguiente en la parte superior del archivo.

    using System.ComponentModel.DataAnnotations;
    

    El código anterior resuelve los atributos de anotación de datos aplicados a las propiedades FirstName y LastName.

Actualización de la base de datos

Ahora que se realizaron los cambios del modelo, se deben realizar cambios complementarios en la base de datos.

  1. Asegúrese de haber guardado todos los cambios.

  2. Cree y aplique una migración de EF Core para actualizar el almacén de datos subyacente:

    dotnet ef migrations add UpdateUser
    dotnet ef database update
    

    La migración de EF Core UpdateUser ha aplicado un script de cambios de DDL al esquema de la tabla AspNetUsers. En concreto, se han agregado las columnas FirstName y LastName, tal y como se muestra en el fragmento siguiente de resultado de migración:

    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 la base de datos para analizar el efecto de la migración de EF Core UpdateUser en el esquema de la tabla AspNetUsers.

    En el panel SQL Server, expanda el nodo Columnas de la tabla dbo.AspNetUsers.

    Captura de pantalla del esquema de la tabla AspNetUsers.

    Las propiedades FirstName y LastName de la clase RazorPagesPizzaUser se corresponden a las columnas FirstName y LastName de la imagen anterior. Se ha asignado un tipo de datos nvarchar(100) a cada una de las dos columnas debido a los atributos [MaxLength(100)]. Se ha agregado la restricción no NULL porque en la clase, FirstName y LastName son cadenas que no aceptan valores NULL. Las filas existentes muestran cadenas vacías en las nuevas columnas.

Personalización del formulario de registro de usuarios

Ha agregado nuevas columnas para FirstName y LastName. Ahora debe editar la interfaz de usuario para mostrar los campos coincidentes en el formulario de registro.

  1. En Areas/Identity/Pages/Account/Register.cshtml, agregue el marcado resaltado siguiente:

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

    Con el marcado anterior, se agregan los cuadros de texto Nombre de pila y Apellidos al formulario de registro del usuario.

  2. En Areas/Identity/Pages/Account/Register.cshtml.cs, agregue compatibilidad para los cuadros de texto de nombre.

    1. Agregue las propiedades FirstName y LastName a la clase anidada 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; }
      

      Los atributos [Display] definen el texto de la etiqueta que se va a asociar con los cuadros de texto.

    2. Modifique el método OnPostAsync para establecer las propiedades FirstName y LastName en el objeto RazorPagesPizza. Agregue las líneas resaltadas siguientes:

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

      El cambio anterior establece las propiedades FirstName y LastName en la entrada del usuario del formulario de registro.

Personalización del encabezado del sitio

Actualice Pages/Shared/_LoginPartial.cshtml para mostrar el nombre de pila y los apellidos recopilados durante el registro del usuario. Se necesitan las líneas resaltadas en el fragmento de código siguiente:

<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) devuelve un objeto RazorPagesPizzaUser que admite un valor NULL. El operador condicional null ?. se usa para acceder a las propiedades FirstName y LastName solo si el objeto RazorPagesPizzaUser no es NULL.

Personalización del formulario de administración de perfiles

Ha agregado los campos nuevos al formulario de registro de usuario, pero también debe agregarlos al formulario de administración de perfiles para que los usuarios existentes puedan editarlos.

  1. En Areas/Identity/Pages/Account/Manage/Index.cshtml, agregue el marcado resaltado siguiente. Guarde los cambios.

    <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>
    
  2. En Areas/Identity/Pages/Account/Manage/Index.cshtml.cs, realice los cambios siguientes para admitir los cuadros de texto de nombre.

    1. Agregue las propiedades FirstName y LastName a la clase anidada 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>
          [Phone]
          [Display(Name = "Phone number")]
          public string PhoneNumber { get; set; }
      }
      
    2. Incorpore los cambios resaltados en el 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
          };
      }
      

      El código anterior admite la recuperación del nombre de pila y los apellidos que se muestran en los cuadros de texto correspondientes del formulario de administración de perfiles.

    3. Incorpore los cambios resaltados en el método OnPostAsync. Guarde los cambios.

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

      El código anterior admite la actualización del nombre de pila y los apellidos de la tabla AspNetUsers de la base de datos.

Configuración del remitente del correo electrónico de confirmación

La primera vez que probó la aplicación, registró un usuario y, después, hizo clic en un vínculo para simular la confirmación de la dirección de correo electrónico del usuario. Para enviar el correo electrónico de confirmación real, debe crear una implementación de IEmailSender y registrarla en el sistema de inserción de dependencias. Para simplificar las cosas, la implementación de esta unidad no envía realmente correo electrónico a un servidor de Protocolo simple de transferencia de correo (SMTP). Solo escribe el contenido del correo electrónico en la consola.

  1. Como va a ver el correo electrónico en texto sin formato en la consola, debe cambiar el mensaje generado para excluir el texto codificado en HTML. En Areas/Identity/Pages/Account/Register.cshtml.cs, busque el código siguiente:

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

    Cámbielo por esto:

    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
        $"Please confirm your account by visiting the following URL:\r\n\r\n{callbackUrl}");
    
  2. En el panel Explorador, haga clic con el botón derecho en la carpeta RazorPagesPizza\Services y cree un archivo denominado EmailSender.cs. Abra el archivo y agregue el código siguiente:

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

    En el código anterior se crea una implementación de IEmailSender que escribe el contenido del mensaje en la consola. En una implementación real, SendEmailAsync se conectaría a un servicio de correo externo o a alguna otra acción para enviar correo electrónico.

  3. En Program.cs, agregue las líneas resaltadas:

    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();
    

    Lo anterior registra EmailSender como IEmailSender en el sistema de inserción de dependencias.

Prueba de los cambios en el formulario de registro

Eso es todo. Ahora se probarán los cambios en el formulario de registro y el correo electrónico de confirmación.

  1. Asegúrese de guardar todos los cambios.

  2. En el panel de terminal, compile el proyecto y ejecute la aplicación con dotnet run.

  3. En el explorador, vaya a la aplicación. Seleccione Cerrar sesión si tiene la sesión iniciada.

  4. Seleccione Registrar y use el formulario actualizado para registrar un nuevo usuario.

    Nota:

    Las restricciones de validación en los campos Nombre de pila y Apellidos reflejan las anotaciones de datos en las propiedades FirstName y LastName de InputModel.

  5. Después de registrarse, se le redirigirá a la pantalla Registrar confirmación. En el panel de terminal, desplácese hacia arriba para buscar la salida de la consola similar a la siguiente:

    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>
    

    Presione Ctrl+clic para ir a la dirección URL. Se muestra la pantalla de confirmación.

    Nota

    Si usa GitHub Codespaces, puede que tenga que agregar -7192 a la primera parte de la dirección URL reenviada. Por ejemplo, scaling-potato-5gr4j4-7192.preview.app.github.dev.

  6. Seleccione Iniciar sesión e inicie sesión con el nuevo usuario. El encabezado de la aplicación ahora contiene Hola, [Nombre de pila] [Apellidos].

  7. En el panel SQL Server de VS Code, haga clic con el botón derecho en la base de datos RazorPagesPizza y seleccione Nueva consulta. En la pestaña que aparece, escriba la siguiente consulta y presione Ctrl+Mayús+E para ejecutarla.

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

    Aparece una pestaña con resultados similares a los siguientes:

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

    Primer usuario registrado antes de agregar FirstName y LastName al esquema. Por lo tanto, el registro de la tabla AspNetUsers asociada no tiene datos en esas columnas.

Prueba de los cambios en el formulario de administración de perfiles

También debe probar los cambios realizados en el formulario de administración de perfiles.

  1. En la aplicación web, inicie sesión con el primer usuario que ha creado.

  2. Seleccione el vínculo Hola para ir al formulario de administración de perfiles.

    Nota:

    El vínculo no se muestra correctamente porque la fila de la tabla AspNetUsers para este usuario no contiene valores para FirstName y LastName.

  3. Escriba valores válidos para los campos Nombre de pila y Apellidos. Seleccione Guardar.

    El encabezado de la aplicación se actualiza a Hola, [Nombre de pila] [Apellidos].

  4. Para detener la aplicación, presione Ctrl+C en el panel de terminal de VS Code.

Resumen

En esta unidad, ha personalizado Identity para almacenar información de usuario personalizada. También ha personalizado el correo electrónico de confirmación. En la siguiente unidad, aprenderá a implementar la autenticación multifactor en Identity.