.NET Core  

HTML Pages in .NET 9 Web API [GamesCatalog] 22

Previous part: Authenticated Requests in .NET 9 Web API [GamesCatalog] 21

Step 1. Let's create the route that will send the user's password recovery email. In the Services project, under the Functions folder, we will create the SendRecoverPasswordEmailService.cs.

SendRecoverPasswordEmailService

Code

using System.Net;
using System.Net.Mail;

namespace Services.Functions
{
    public interface ISendRecoverPasswordEmailService
    {
        Task SendEmail(string recipientEmail, string token);
    }

    public class SendRecoverPasswordEmailService(string senderEmail, string url, string senderPassword, string host) : ISendRecoverPasswordEmailService
    {
        public async Task SendEmail(string recipientEmail, string token)
        {
            MailMessage mail = new(senderEmail, recipientEmail)
            {
                Subject = "GamesCatalog - Password Recovery",
                Body = $"<h2><a href='{url}/User/RecoverPassword/{token}'>Follow this link to change your Password</a></h2>",
                IsBodyHtml = true
            };

            SmtpClient smtpClient = new(host)
            {
                Port = 587,
                EnableSsl = true,
                Credentials = new NetworkCredential(senderEmail, senderPassword)
            };

            try
            {
                // Send the email
                await smtpClient.SendMailAsync(mail);
                Console.WriteLine("Email sent successfully!");
            }
            catch
            {
                throw;
            }
        }
    }
}

In this code, we assemble and send an email with a link that the user can use to access a page where they will have the option to change their password.

Step 2. In appsettings.json, create the SendEmailKeys with valid credentials from an email account for this kind of sending.

 SendEmailKeys

Step 2.1. And use the keys in the DI of SendRecoverPasswordEmailService.

SendRecoverPasswordEmailService

Code

string emailUrl;

if (builder.Environment.IsDevelopment())
    emailUrl = builder.Configuration["SendEmailKeys:UrlLocal"];
else emailUrl = builder.Configuration["SendEmailKeys:UrlLocal"];

builder.Services.AddScoped<ISendRecoverPasswordEmailService, SendRecoverPasswordEmailService>(p
    => new SendRecoverPasswordEmailService(
    builder.Configuration["SendEmailKeys:SenderEmail"],
    emailUrl,
    builder.Configuration["SendEmailKeys:SenderPassword"],
    builder.Configuration["SendEmailKeys:Host"]
    ));

Step 3. In UserService, add the use of ISendRecoverPasswordEmailService in the function call.

UserService

Step 3.1. And create the function SendRecoverPasswordEmailAsync.

public async Task<BaseResp> SendRecoverPasswordEmailAsync(ReqUserEmail reqUserEmail)
{
    string? validateError = reqUserEmail.Validate();

    if (!string.IsNullOrEmpty(validateError))
        return new BaseResp(null, validateError);

    UserDTO? userResp = await userRepo.GetByEmailAsync(reqUserEmail.Email);

    if (userResp != null)
    {
        string token = jwtTokenService.GenerateToken(
            userResp.Id,
            userResp.Email,
            DateTime.UtcNow.AddHours(1)
        );

        try
        {
            _ = sendRecoverPasswordEmailService.SendEmail(userResp.Email, token);
        }
        catch
        {
            return new BaseResp(null, "A error occurred!");
        }
    }

    return new BaseResp("Email Sent.");
}

Define the function in the interface.

Step 4. Create the route in UserController.

[Route("RecoverPassword")]
[HttpPost]
public async Task<IActionResult> SendRecoverPasswordEmail(ReqUserEmail reqUserEmail) =>
    BuildResponse(await userService.SendRecoverPasswordEmailAsync(reqUserEmail));

Step 5. Create the test in GamesCatalogAPI.http.

POST {{GamesCatalogAPI_HostAddress}}/User/RecoverPassword
Content-Type: application/json

{
  "email": "[email protected]"
}
### 

Step 6. Create a user and execute the POST request to the /User/RecoverPassword route, so that the user's email address receives an email with the link to the password reset URL.

POST request

Step 7. Now, let's create the routes related to password change. They will return static HTML pages. We will create two .html files: Index and PasswordUpdated.

HTML pages

Step 8. Index.html will use Bootstrap. It will be a page for the user to enter the new password.

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Games Catalog</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
    <style>
        .containerCenter {
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;
        }
    </style>
    <script>
        function ShowPassword(InputId) {
            var x = document.getElementById(InputId);
            if (x.type === "password") {
                x.type = "text";
            } else {
                x.type = "password";
            }
        }
    </script>
</head>
<body style="background: #1B2D3E">
    <div class="container containerCenter">
        <div class="row">
            <div class="col align-self-center">
                <div class="card">
                    <div class="card-body">
                        <h5 class="card-title">Games Catalog</h5>
                        <h6 class="card-subtitle mb-2 text-body-secondary">Password Recovery</h6>
                        <form method="POST" action="/User/RecoverPassword/{{token}}">
                            <p class="card-text">
                                <div class="mb-12">
                                    <label for="Password" class="form-label">Password</label>
                                    <input type="password" name="Password" id="Password" class="form-control" maxlength="20" aria-describedby="passwordHelpBlock" required>
                                    <div id="passwordHelpBlock" class="form-text">
                                        Your password must be 4-20 characters long.
                                    </div>
                                    <input type="checkbox" onclick="ShowPassword('Password')"> Show Password
                                </div>
                                <br />
                                <div class="mb-12">
                                    <label for="PasswordConfirmation" class="form-label">Password Confirmation</label>
                                    <input type="password" name="PasswordConfirmation" id="PasswordConfirmation" class="form-control">
                                    <input type="checkbox" onclick="ShowPassword('PasswordConfirmation')"> Show Password
                                </div>
                            </p>
                            <input type="submit" class="btn btn-success" value="Update Password" />
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm" crossorigin="anonymous"></script>
</body>
</html>

Code for PasswordUpdated.html, which will return the confirmation of the password update.

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Games Catalog</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
    <style>
        .containerCenter {
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;
            background-color: var(--background-color);
        }
    </style>
</head>
<body style="background: #1B2D3E ">
    <div class="container containerCenter">
        <div class="row">
            <div class="col align-self-center">
                <div class="card">
                    <div class="card-body">
                        <h5 class="card-title">Games Catalog</h5>
                        <h6 class="card-subtitle mb-2 text-body-secondary">Recover Password</h6>
                        <div class="h4 pb-2 mb-4 text-success border-top border-success">
                            <p class="fs-2">{{ReturnMessage}}</p>
                        </div>

                    </div>
                </div>
            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm" crossorigin="anonymous"></script>
</body>
</html>

Step 9. Let's create the routes in UserController. Add the dependencies for IHostEnvironment and IJwtTokenService.

Code

 public class UserController(IUserService userService, IHostEnvironment hostingEnvironment, IJwtTokenService jwtTokenService) : BaseController

Step 9.1. Let's create the route that returns the HTML page for entering the new password.

[ApiExplorerSettings(IgnoreApi = true)]
[Route("RecoverPassword/{token}")]
[HttpGet]
public IActionResult RecoverPasswordBody(string token)
{
    string html = System.IO.File.ReadAllText(
        Path.Combine(
            hostingEnvironment.ContentRootPath,
            "StaticFiles",
            "RecoverPassword",
            "Index.html"
        )
    );

    html = html.Replace("{{token}}", token);

    return base.Content(html, "text/html");
}

We receive the GET request and return the page with the token.

Step 9.2. For the route that performs the password update, we will create a model to receive the new password.

GET request

Code

using System.ComponentModel.DataAnnotations;

namespace Models.Reqs.User;

public record ReqRecoverPassword : ReqBaseModel
{
    [Display(Name = "Password")]
    [DataType(DataType.Password)]
    [Required(ErrorMessage = "Password is required")]
    [StringLength(30, MinimumLength = 4)]
    public required string Password { get; init; }

    [Display(Name = "PasswordConfirmation")]
    [DataType(DataType.Password)]
    [Required(ErrorMessage = "Password Confirmation is required")]
    [StringLength(30, MinimumLength = 4)]
    public required string PasswordConfirmation { get; init; }
}

Step 9.3. In UserRepo, let's create a function to update the password.

public async Task UpdatePasswordAsync(int uid, string password)
{
    using var context = DbCtx.CreateDbContext();

    context.User
        .Where(x => x.Id == uid)
        .ExecuteUpdate(y => y.SetProperty(z => z.Password, password));

    await context.SaveChangesAsync();
}

Define the function in the interface.

Step 9.4. In UserService, let's create UpdatePasswordAsync and define it in the interface.

public async Task<BaseResp> UpdatePasswordAsync(ReqRecoverPassword reqRecoverPassword, int uid)
{
    string? validateError = reqRecoverPassword.Validate();

    if (string.IsNullOrEmpty(validateError) &&
        reqRecoverPassword.Password != reqRecoverPassword.PasswordConfirmation)
    {
        validateError = "Invalid password Confirmation";
    }

    if (!string.IsNullOrEmpty(validateError))
        return new BaseResp(null, validateError);

    UserDTO? user = await userRepo.GetByIdAsync(uid);

    if (user != null)
    {
        await userRepo.UpdatePasswordAsync(user.Id, encryptionService.Encrypt(reqRecoverPassword.Password));
        return new BaseResp("Password Updated.");
    }
    else
    {
        return new BaseResp(null, "Invalid User");
    }
}

Step 9.5. In UserController, let's create the RecoverPassword route, which will be used by Index.html.

[ApiExplorerSettings(IgnoreApi = true)]
[Route("RecoverPassword/{token}")]
[HttpPost]
public async Task<IActionResult> RecoverPassword(string token, [FromForm] ReqRecoverPassword reqRecoverPassword)
{
    string html = System.IO.File.ReadAllText(
        Path.Combine(
            hostingEnvironment.ContentRootPath,
            "StaticFiles",
            "RecoverPassword",
            "PasswordUpdated.html"
        )
    );

    try
    {
        int? uid = jwtTokenService.GetUidFromToken(token);

        html = html.Replace("{{token}}", token);

        if (uid == null)
        {
            html = html.Replace("{{ReturnMessage}}", "User Not Found");
        }
        else
        {
            BaseResp bLLResponse = await userService.UpdatePasswordAsync(reqRecoverPassword, Convert.ToInt32(uid));

            if (bLLResponse.Success)
                html = html.Replace("{{ReturnMessage}}", bLLResponse.Content?.ToString());
            else
                html = html.Replace("{{ReturnMessage}}", bLLResponse.Error?.Message?.ToString());
        }
    }
    catch (SecurityTokenExpiredException)
    {
        html = html.Replace("{{ReturnMessage}}", "Email link expired.");
    }

    return base.Content(html, "text/html");
}

Step 10. Thus, when we click the link sent by email, we are directed to the password update screen.

Games Catalog

When we enter the new password and confirm, the user's password is updated, and we are directed to the confirmation screen.

Password updated

In the next part, we will create the function that retrieves or reuses the user's token from the IGDB API to query the games on the server side.

Code on git: GamesCatalogBackEnd