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