Skip to content

Commit

Permalink
Merge pull request #142 from FS-FAST-TRACK/feature/quizmaster-fronten…
Browse files Browse the repository at this point in the history
…d-profile-maintenance

Feature/quizmaster frontend profile maintenance
  • Loading branch information
jaymar921 authored Jan 11, 2024
2 parents 399251e + 86a103f commit a26b536
Show file tree
Hide file tree
Showing 22 changed files with 1,038 additions and 9 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ packages/
/WebApp/backend/QuizMaster.API.Media/wwwroot/Images
/WebApp/backend/QuizMaster.API.QuizSession/QuizSession.db
/WebApp/backend/QuizMaster.API.QuizSession/QuizSession.db
WebApp/backend/QuizMaster.API.QuizSession/sessionDb
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ public class ApplicationSettings
public string RabbitMq_Account_RequestQueueName { get; set; }
public string RabbitMq_Account_ResponseQueueName { get; set; }
public string RabbitMq_Hostname { get; set; }
public string SMTP_EMAIL { get; set; }
public string SMTP_PASSWORD { get; set; }
}
}
7 changes: 5 additions & 2 deletions WebApp/backend/QuizMaster.API.Account/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@ public static void Main(string[] args)
var channel = GrpcChannel.ForAddress(builder.Configuration["ApplicationSettings:Service_MonitoringGRPC"], new GrpcChannelOptions { HttpHandler = handler });
return new AuditService.AuditServiceClient(channel);
});

builder.Services.AddLogging();
// configure strongly typed app settings object
builder.Services.Configure<ApplicationSettings>(builder.Configuration.GetSection("ApplicationSettings"));
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddScoped<EmailSenderService>();
builder.Services.AddSingleton<PasswordHandler>();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<AccountDbContext>(
dbContextOptions => dbContextOptions.UseSqlServer(
Expand Down
12 changes: 12 additions & 0 deletions WebApp/backend/QuizMaster.API.Account/Proto/accounts.proto
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ service AccountService {
rpc DeleteAccount(DeleteAccountRequest) returns (DeleteAccountReply);
rpc UpdateAccount(UpdateAccountRequest) returns (UpdateAccountReply);
rpc SetAdminAccount(SetAccountAdminRequest) returns (SetAccountAdminResponse);
rpc UpdateUserPassword(UpdatePasswordRequest) returns (SetAccountAdminResponse);
rpc UpdateUserPasswordConfirm(ConfirmUpdatePasswordRequest) returns (SetAccountAdminResponse);
}

// Get account by id
Expand Down Expand Up @@ -120,4 +122,14 @@ message SetAccountAdminRequest{
message SetAccountAdminResponse{
int32 code = 1;
string message = 2;
}

message UpdatePasswordRequest{
int32 id = 1;
string currentPassword = 2;
string newPassword = 3;
}

message ConfirmUpdatePasswordRequest{
string confirmationToken = 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Microsoft.Extensions.Options;
using QuizMaster.API.Account.Configuration;
using System.Net.Mail;
using System.Net;

namespace QuizMaster.API.Account.Service
{
public class EmailSenderService
{
private readonly ApplicationSettings _settings;
public EmailSenderService(IOptions<ApplicationSettings> appsettings)
{
_settings = appsettings.Value;
}

public void SendEmail(string email, string firstname, string token)
{
MailMessage mailMessage = new MailMessage();
mailMessage.From = new MailAddress(_settings.SMTP_EMAIL, "🔎QuizMaster@no-reply", System.Text.Encoding.UTF8);
mailMessage.To.Add(email);
mailMessage.Subject = "Quiz Master Password Reset";
mailMessage.Body = @$"
<html><body>
<div style=""width: 400px"">
<img
style=""width: 400px; height: 84px""
src=""https://github.com/jaymar921/Public-Repo/blob/main/wave_fs_vector_1.png?raw=true""
/>
<h3 style=""text-align: center; color: #18a44c"">
Reset Password Confirmation
</h3>
<img
style=""width: 400px; height: 300px""
src=""https://github.com/jaymar921/Public-Repo/blob/main/reset_pass_fs.png?raw=true""
/>
<h3 style=""font-size: 18px; padding: 5px 20px 5px 20px"">Hello {firstname}</h3>
<p style=""font-size: 14px; padding: 5px 20px 5px 20px"">
You are receiving this email because you requested a change on your
password. Click the button below to reset your password.
</p>
<div style=""padding: 5px 20px 5px 20px; text-align: center"">
<a
style=""
background-color: #18a44c;
color: white;
padding: 10px;
font-size: 14px;
outline: none;
border: none;
border-radius: 4px;
width: 500px;
cursor: pointer;
text-decoration: none;
""
href='https://localhost:7081/gateway/api/account/update_password/{token}'
>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Confirm Reset
Password&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
</a>
</div>
<p style=""padding: 0px 20px 0px 20px; font-size: 12px; color: gray"">
If you did not request a password reset, you can ignore this email.
</p>
<hr />
<p style=""padding: 0px 20px 0px 20px; font-size: 12px; color: gray"">
Copyright 2023 Ⓒ QuizMaster
</p>
</div>
</body></html>
";
mailMessage.IsBodyHtml = true;

// Create the credentials to login to the gmail account associated with my custom domain
string sendEmailsFrom = _settings.SMTP_EMAIL;
string sendEmailsFromPassword = _settings.SMTP_PASSWORD;
NetworkCredential cred = new NetworkCredential(sendEmailsFrom, sendEmailsFromPassword);

SmtpClient mailClient = new SmtpClient("smtp.gmail.com", 587);
mailClient.EnableSsl = true;
mailClient.DeliveryMethod = SmtpDeliveryMethod.Network;
mailClient.UseDefaultCredentials = false;
mailClient.Timeout = 20000;
mailClient.Credentials = cred;
mailClient.Send(mailMessage);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ public class InformationService : AccountService.AccountServiceBase
private readonly UserManager<UserAccount> _userManager;
private readonly IMapper _mapper;
private readonly AuditService.AuditServiceClient _auditServiceClient;
private readonly EmailSenderService _emailSenderService;
private readonly PasswordHandler _passwordHandler;

public InformationService(UserManager<UserAccount> userManager, IMapper mapper, AuditService.AuditServiceClient auditServiceClient)
public InformationService(UserManager<UserAccount> userManager, IMapper mapper, AuditService.AuditServiceClient auditServiceClient, EmailSenderService emailSenderService, PasswordHandler passwordHandler)
{
_userManager = userManager;
_mapper = mapper;
_auditServiceClient = auditServiceClient;
_emailSenderService = emailSenderService;
_passwordHandler = passwordHandler;
}

public override async Task<AccountOrNotFound> GetAccountById(GetAccountByIdRequest request, ServerCallContext context)
Expand Down Expand Up @@ -536,5 +540,85 @@ private void LogSetAdminEvent(bool setAdmin, Dictionary<string, string> oldValue
}
}

public override async Task<SetAccountAdminResponse> UpdateUserPasswordConfirm(ConfirmUpdatePasswordRequest request, ServerCallContext context)
{
var reply = new SetAccountAdminResponse();

var (userId, currentPassword, newPassword) = _passwordHandler.GetPassword(request.ConfirmationToken);

if (string.IsNullOrEmpty(userId))
userId = "-1";
// Find the existing user to capture old values
var existingUser = await _userManager.FindByIdAsync(userId);

if (existingUser == null)
{
reply.Code = 404;
reply.Message = "Account not found";
return await Task.FromResult(reply);
}

var updatePasswordResult = await _userManager.ChangePasswordAsync(existingUser, currentPassword, newPassword);

if(!updatePasswordResult.Succeeded)
{
reply.Code = 400;
reply.Message = "Failed to update password";
}
else
{
reply.Code = 200;
reply.Message = "Password was updated successfully";
}


return await Task.FromResult(reply);
}

// Update Password
public override async Task<SetAccountAdminResponse> UpdateUserPassword(UpdatePasswordRequest request, ServerCallContext context)
{
var reply = new SetAccountAdminResponse();

// Find the existing user to capture old values
var existingUser = await _userManager.FindByIdAsync(request.Id.ToString());

if (existingUser == null)
{
reply.Code = 404;
reply.Message = "Account not found";
return await Task.FromResult(reply);
}

if (request.CurrentPassword.Equals(request.NewPassword))
{
reply.Code = 400;
reply.Message = "New Password should not be the same as Current Password";
return await Task.FromResult(reply);
}

PasswordHasher<UserAccount> hasher = new();

// check if password is correct
var passwordVerification = hasher.VerifyHashedPassword(existingUser, existingUser.PasswordHash, request.CurrentPassword);
if (PasswordVerificationResult.Success != passwordVerification)
{
reply.Code = 400;
reply.Message = "Incorrect Password";


return await Task.FromResult(reply);
}

string token = _passwordHandler.GenerateToken(request.Id.ToString(), request.CurrentPassword, request.NewPassword);
Task.Run(() => { _emailSenderService.SendEmail(existingUser.Email, existingUser.FirstName, token); });

reply.Code = 200;
reply.Message = "A confirmation email was sent to your account.";


return await Task.FromResult(reply);
}

}
}
31 changes: 31 additions & 0 deletions WebApp/backend/QuizMaster.API.Account/Service/PasswordHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace QuizMaster.API.Account.Service
{
public class PasswordHandler
{
private readonly IDictionary<string, IDictionary<string, string>> _passwordHolder;
public PasswordHandler() {
_passwordHolder = new Dictionary<string, IDictionary<string, string>>();
}

public string GenerateToken(string userId,string currentPassword, string newPassword)
{
string token = Guid.NewGuid().ToString();
IDictionary<string, string> Passwords = new Dictionary<string, string>();
Passwords["userId"] = userId;
Passwords["currentPassword"] = currentPassword;
Passwords["newPassword"] = newPassword;
_passwordHolder[token] = Passwords;
return token;
}

public (string, string, string) GetPassword(string guid)
{
if(_passwordHolder.ContainsKey(guid))
{
var map = _passwordHolder[guid];
return (map["userId"], map["currentPassword"], map["newPassword"]);
}
return (string.Empty, string.Empty, string.Empty);
}
}
}
4 changes: 3 additions & 1 deletion WebApp/backend/QuizMaster.API.Account/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"RabbitMq_Account_RequestQueueName": "QuizMasterRequestQueue",
"RabbitMq_Account_ResponseQueueName": "QuizMasterResponseQueue",
"RabbitMq_Hostname": "localhost",
"Service_MonitoringGRPC": "https://localhost:7065"
"Service_MonitoringGRPC": "https://localhost:7065",
"SMTP_EMAIL": "jabejar@fullscale.io", /* TEMP, WILL DELETE THE CRED IN FUTURE */
"SMTP_PASSWORD": "dzsw pzja rmzu axyq" /* TEMP, WILL DELETE THE CRED IN FUTURE */
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public async Task<AuthResponse> Authenticate(AuthRequest authRequest)
if (userAccount.Id == -1) { userAccount = repository.GetUserByEmail(authRequest.Email); }
if (userAccount.Id == -1) { return new() { Token = null }; };
*/
if (retrieveUserInformation.Account.Id == -1) { return new() { Token = null }; };
if (retrieveUserInformation.Account.Id == -1 || !retrieveUserInformation.Account.ActiveData) { return new() { Token = null }; };

// attributes to store in the JWT token
Dictionary<string, string> keyValuePairs = new Dictionary<string, string>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using QuizMaster.API.Gateway.Attributes;
using QuizMaster.Library.Common.Entities.Accounts;
using QuizMaster.Library.Common.Models;
using QuizMaster.Library.Common.Models.Accounts;

namespace QuizMaster.API.Gatewway.Controllers
{
Expand Down Expand Up @@ -361,7 +362,39 @@ public async Task<IActionResult> Update(int id, JsonPatchDocument<AccountCreateD
return Ok(new ResponseDto { Type = "Success", Message = updateReply.Message });
}

//[QuizMasterAdminAuthorization]
[QuizMasterAuthorization]
[HttpPost]
[Route("account/{id}/update_password")]
public async Task<IActionResult> UpdatePassword(int id, [FromBody] ChangePasswordDTO passwordDTO)
{
var updatePasswordRequest = new UpdatePasswordRequest() { CurrentPassword = passwordDTO.CurrentPassword, Id = id, NewPassword = passwordDTO.NewPassword};

var response = await _channelClient.UpdateUserPasswordAsync(updatePasswordRequest);

if(response.Code != 200)
{
return BadRequest(new ResponseDto { Type = "Error", Message = response.Message });
}
return Ok(new ResponseDto { Type = "Success", Message = response.Message});
}

[HttpGet]
[Route("account/update_password/{token}")]
public async Task<IActionResult> ConfirmUpdatePassword(string token)
{

var updatePasswordRequest = new ConfirmUpdatePasswordRequest () { ConfirmationToken = token };

var response = await _channelClient.UpdateUserPasswordConfirmAsync(updatePasswordRequest);

if (response.Code != 200)
{
return BadRequest(new ResponseDto { Type = "Error", Message = response.Message });
}
return Ok(new ResponseDto { Type = "Success", Message = response.Message });
}

[QuizMasterAdminAuthorization]
[HttpPost]
[Route("account/set_admin/{username}")]
public async Task<IActionResult> SetAdmin(string username, [FromQuery] bool setAdmin = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

<ItemGroup>
<Folder Include="Migrations\" />
<Folder Include="sessionDb\" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
}
},
"ConnectionStrings": {
"QuizMasterQuizSessionDBConnectionString": "Data Source=QuizSession.db"
"QuizMasterQuizSessionDBConnectionString": "Data Source=sessionDb\\QuizSession.db"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace QuizMaster.Library.Common.Models.Accounts
{
public class ChangePasswordDTO
{
public string CurrentPassword { get; set; } = string.Empty;
public string NewPassword { get; set; } = string.Empty;
}
}
2 changes: 2 additions & 0 deletions WebApp/frontend/quiz-master/api/api-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const QUIZMASTER_ACCOUNT_POST=`${QUIZMASTER_ACCOUNT}/create`
const QUIZMASTER_ACCOUNT_POST_PARTIAL=`${QUIZMASTER_ACCOUNT}/create_partial`
const QUIZMASTER_ACCOUNT_DELETE=`${QUIZMASTER_ACCOUNT}/delete/`
const QUIZMASTER_ACCOUNT_PATCH=`${QUIZMASTER_ACCOUNT}/update/`
const QUIZMASTER_ACCOUNT_PASSWORD_RESET_POST= (id: Number) => {return `${QUIZMASTER_ACCOUNT}/${id}/update_password`}
const QUIZMASTER_ACCOUNT_POST_SET_ADMIN=`${QUIZMASTER_ACCOUNT}/set_admin/`
//#endregion

Expand Down Expand Up @@ -87,6 +88,7 @@ export {
QUIZMASTER_ACCOUNT_POST_PARTIAL,
QUIZMASTER_ACCOUNT_DELETE,
QUIZMASTER_ACCOUNT_PATCH,
QUIZMASTER_ACCOUNT_PASSWORD_RESET_POST,
QUIZMASTER_ACCOUNT_POST_SET_ADMIN,
QUIZMASTER_AUTH_POST_LOGIN,
QUIZMASTER_AUTH_POST_PARTIAL_LOGIN,
Expand Down
Loading

0 comments on commit a26b536

Please sign in to comment.