From 95d20a30a111cb1b463436f2e724900935b3289f Mon Sep 17 00:00:00 2001 From: JayMar921 Date: Tue, 9 Jan 2024 14:26:54 +0800 Subject: [PATCH 1/5] Feature: Added Profile Maintenance UI --- .../quiz-master/app/profile/layout.tsx | 45 +++++ .../frontend/quiz-master/app/profile/page.tsx | 103 +++++++++++ .../components/Commons/navbars/UserNavBar.tsx | 6 +- .../components/Commons/profile/EditField.tsx | 30 ++++ .../Commons/profile/EditFieldWithButton.tsx | 166 ++++++++++++++++++ .../Commons/profile/SaveCancelButton.tsx | 26 +++ 6 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 WebApp/frontend/quiz-master/app/profile/layout.tsx create mode 100644 WebApp/frontend/quiz-master/app/profile/page.tsx create mode 100644 WebApp/frontend/quiz-master/components/Commons/profile/EditField.tsx create mode 100644 WebApp/frontend/quiz-master/components/Commons/profile/EditFieldWithButton.tsx create mode 100644 WebApp/frontend/quiz-master/components/Commons/profile/SaveCancelButton.tsx diff --git a/WebApp/frontend/quiz-master/app/profile/layout.tsx b/WebApp/frontend/quiz-master/app/profile/layout.tsx new file mode 100644 index 00000000..c6216bf5 --- /dev/null +++ b/WebApp/frontend/quiz-master/app/profile/layout.tsx @@ -0,0 +1,45 @@ +import Link from "next/link"; +import { ChevronLeftIcon, UserIcon } from "@heroicons/react/24/outline"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( +
+
+
+
+ + +

+ Back +

+ +
+

+ User profile management +

+
+
+
+ +

+ Account Details +

+
+
+
+
+
+
+ {children} +
+
+ ); +} diff --git a/WebApp/frontend/quiz-master/app/profile/page.tsx b/WebApp/frontend/quiz-master/app/profile/page.tsx new file mode 100644 index 00000000..a7ff448d --- /dev/null +++ b/WebApp/frontend/quiz-master/app/profile/page.tsx @@ -0,0 +1,103 @@ +"use client"; +import EditField from "@/components/Commons/profile/EditField"; +import EditFieldWithButton from "@/components/Commons/profile/EditFieldWithButton"; +import SaveCancelButton from "@/components/Commons/profile/SaveCancelButton"; +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; +import { useState } from "react"; + +export default function Page() { + const [firstName, setFirstName] = useState("Jay"); + const [lastName, setLastName] = useState("Abejar"); + const [userName, setUserName] = useState("jaymar921"); + const [email, setEmail] = useState("jay@gmail.com"); + const [editToggled, setEditToggled] = useState(false); + return ( + <> +
+
+

+ Profile Details +

+
+
+

User Details

+ {!editToggled && ( + + )} +
+
+
+ + + + {}} + /> + {editToggled && ( + {}} + onCancel={() => { + setEditToggled(false); + }} + /> + )} +
+
+

Account Details

+
+
+
+ + +
+
+

Delete Account

+
+
+
+ +

+ Warning: Deleting an account will be permanent and + cannot be undone. +

+
+ +
+ + ); +} diff --git a/WebApp/frontend/quiz-master/components/Commons/navbars/UserNavBar.tsx b/WebApp/frontend/quiz-master/components/Commons/navbars/UserNavBar.tsx index 257e15c5..849c7196 100644 --- a/WebApp/frontend/quiz-master/components/Commons/navbars/UserNavBar.tsx +++ b/WebApp/frontend/quiz-master/components/Commons/navbars/UserNavBar.tsx @@ -12,8 +12,10 @@ export default function UserNavBar() { return (
-
{user?.username}
-
{user?.email}
+ +
{user?.username}
+
{user?.email}
+
diff --git a/WebApp/frontend/quiz-master/components/Commons/profile/EditField.tsx b/WebApp/frontend/quiz-master/components/Commons/profile/EditField.tsx new file mode 100644 index 00000000..50d9c678 --- /dev/null +++ b/WebApp/frontend/quiz-master/components/Commons/profile/EditField.tsx @@ -0,0 +1,30 @@ +import { Dispatch, SetStateAction } from "react"; + +export default function EditField({ + editting, + title, + value, + onInput, +}: { + editting: Boolean; + title: string; + value: string; + onInput: Dispatch>; +}) { + return ( +
+

{title}

+ {!editting && ( +

{value}

+ )} + {editting && ( + onInput(e.target.value)} + value={`${value}`} + /> + )} +
+ ); +} diff --git a/WebApp/frontend/quiz-master/components/Commons/profile/EditFieldWithButton.tsx b/WebApp/frontend/quiz-master/components/Commons/profile/EditFieldWithButton.tsx new file mode 100644 index 00000000..1d584b32 --- /dev/null +++ b/WebApp/frontend/quiz-master/components/Commons/profile/EditFieldWithButton.tsx @@ -0,0 +1,166 @@ +"use client"; +import { Dispatch, SetStateAction, useState } from "react"; +import SaveCancelButton from "./SaveCancelButton"; +export default function EditFieldWithButton({ + title, + value, + onInput, + changeBtnTitle, + inputType, + onSave, + onCancel, +}: { + title: string; + value: string; + onInput?: Dispatch>; + changeBtnTitle?: string; + inputType?: string; + onSave?: (s: Array) => void; + onCancel?: () => void; +}) { + const [editting, setEditting] = useState(false); + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + return ( +
+

{title}

+
+ {!editting && value !== "" && ( +

{value}

+ )} + {editting && ( +
+
+ { + onInput && onInput(e.target.value); + if (inputType === "password") { + setCurrentPassword(e.target.value); + } + }} + value={`${ + inputType !== "password" + ? value + : currentPassword + }`} + /> +
+
+ {inputType === "password" && ( + + setNewPassword(e.target.value) + } + value={`${newPassword}`} + /> + )} + {inputType === "password" && ( + + setConfirmPassword(e.target.value) + } + value={`${confirmPassword}`} + /> + )} +
+
+ )} + {!editting && ( + + )} + {editting && ( + <> +
+ { + if (inputType === "password") { + if (currentPassword === "") { + alert("Current password is empty"); + return; + } + if (currentPassword === newPassword) { + alert( + "Old password cannot be the same as new password" + ); + return; + } + if (newPassword !== confirmPassword) { + alert( + "New Password and Confirm Password must match" + ); + return; + } + onSave && + onSave([ + currentPassword, + newPassword, + ]); + } else { + value; + } + }} + onCancel={() => { + setEditting(false); + setNewPassword(""); + setConfirmPassword(""); + setCurrentPassword(""); + onCancel && onCancel(); + }} + /> + {inputType === "password" && ( +

+ After clicking the save changes, a + confirmation email will be sent to you +

+ )} +
+ + )} +
+
+ ); +} diff --git a/WebApp/frontend/quiz-master/components/Commons/profile/SaveCancelButton.tsx b/WebApp/frontend/quiz-master/components/Commons/profile/SaveCancelButton.tsx new file mode 100644 index 00000000..0a8d1969 --- /dev/null +++ b/WebApp/frontend/quiz-master/components/Commons/profile/SaveCancelButton.tsx @@ -0,0 +1,26 @@ +export default function SaveCancelButton({ + onSave, + onCancel, + className, +}: { + onSave: () => void; + onCancel?: () => void; + className?: string; +}) { + return ( +
+ + +
+ ); +} From 3426e94f2c7ecd0158cd48fc1b87f06868f8d734 Mon Sep 17 00:00:00 2001 From: JayMar921 Date: Tue, 9 Jan 2024 23:00:37 +0800 Subject: [PATCH 2/5] Feature: Update Profile Functionality 1/2 - Details can now be updated both in frontend and backend - Password update is now working in the backend, it will send an email, but not yet implemented in the frontend --- .../Configuration/ApplicationSettings.cs | 2 + .../backend/QuizMaster.API.Account/Program.cs | 7 +- .../Proto/accounts.proto | 12 +++ .../Service/EmailSenderService.cs | 39 +++++++ .../Service/InformationService.cs | 73 ++++++++++++- .../Service/PasswordHandler.cs | 31 ++++++ .../QuizMaster.API.Account/appsettings.json | 4 +- .../Controllers/AccountGatewayController.cs | 35 +++++- .../Models/Accounts/ChangePasswordDTO.cs | 8 ++ .../frontend/quiz-master/app/profile/page.tsx | 102 ++++++++++++++++-- .../components/Commons/profile/EditField.tsx | 3 +- .../Commons/profile/EditFieldWithButton.tsx | 16 ++- .../frontend/quiz-master/lib/hooks/profile.ts | 79 ++++++++++++++ .../quiz-master/store/ProfileStore.tsx | 61 +++++++++++ 14 files changed, 456 insertions(+), 16 deletions(-) create mode 100644 WebApp/backend/QuizMaster.API.Account/Service/EmailSenderService.cs create mode 100644 WebApp/backend/QuizMaster.API.Account/Service/PasswordHandler.cs create mode 100644 WebApp/backend/QuizMaster.Library.Common/Models/Accounts/ChangePasswordDTO.cs create mode 100644 WebApp/frontend/quiz-master/lib/hooks/profile.ts create mode 100644 WebApp/frontend/quiz-master/store/ProfileStore.tsx diff --git a/WebApp/backend/QuizMaster.API.Account/Configuration/ApplicationSettings.cs b/WebApp/backend/QuizMaster.API.Account/Configuration/ApplicationSettings.cs index e3c89caa..b9268d7b 100644 --- a/WebApp/backend/QuizMaster.API.Account/Configuration/ApplicationSettings.cs +++ b/WebApp/backend/QuizMaster.API.Account/Configuration/ApplicationSettings.cs @@ -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; } } } diff --git a/WebApp/backend/QuizMaster.API.Account/Program.cs b/WebApp/backend/QuizMaster.API.Account/Program.cs index 30fa03e2..d2edaf6e 100644 --- a/WebApp/backend/QuizMaster.API.Account/Program.cs +++ b/WebApp/backend/QuizMaster.API.Account/Program.cs @@ -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(builder.Configuration.GetSection("ApplicationSettings")); - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle - builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddScoped(); + builder.Services.AddSingleton(); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContext( dbContextOptions => dbContextOptions.UseSqlServer( diff --git a/WebApp/backend/QuizMaster.API.Account/Proto/accounts.proto b/WebApp/backend/QuizMaster.API.Account/Proto/accounts.proto index 1d455d7c..520dd8bb 100644 --- a/WebApp/backend/QuizMaster.API.Account/Proto/accounts.proto +++ b/WebApp/backend/QuizMaster.API.Account/Proto/accounts.proto @@ -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 @@ -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; } \ No newline at end of file diff --git a/WebApp/backend/QuizMaster.API.Account/Service/EmailSenderService.cs b/WebApp/backend/QuizMaster.API.Account/Service/EmailSenderService.cs new file mode 100644 index 00000000..e17c543a --- /dev/null +++ b/WebApp/backend/QuizMaster.API.Account/Service/EmailSenderService.cs @@ -0,0 +1,39 @@ +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 appsettings) + { + _settings = appsettings.Value; + } + + public void SendEmail(string email, 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 = "Update Password like literal update"; + mailMessage.Body = $"Confirm Update password: Click to update"; + 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); + } + } +} diff --git a/WebApp/backend/QuizMaster.API.Account/Service/InformationService.cs b/WebApp/backend/QuizMaster.API.Account/Service/InformationService.cs index ac53b6f1..740c948e 100644 --- a/WebApp/backend/QuizMaster.API.Account/Service/InformationService.cs +++ b/WebApp/backend/QuizMaster.API.Account/Service/InformationService.cs @@ -15,12 +15,16 @@ public class InformationService : AccountService.AccountServiceBase private readonly UserManager _userManager; private readonly IMapper _mapper; private readonly AuditService.AuditServiceClient _auditServiceClient; + private readonly EmailSenderService _emailSenderService; + private readonly PasswordHandler _passwordHandler; - public InformationService(UserManager userManager, IMapper mapper, AuditService.AuditServiceClient auditServiceClient) + public InformationService(UserManager userManager, IMapper mapper, AuditService.AuditServiceClient auditServiceClient, EmailSenderService emailSenderService, PasswordHandler passwordHandler) { _userManager = userManager; _mapper = mapper; _auditServiceClient = auditServiceClient; + _emailSenderService = emailSenderService; + _passwordHandler = passwordHandler; } public override async Task GetAccountById(GetAccountByIdRequest request, ServerCallContext context) @@ -509,5 +513,72 @@ private void LogSetAdminEvent(bool setAdmin, Dictionary oldValue } } + public override async Task 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 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); + } + + string token = _passwordHandler.GenerateToken(request.Id.ToString(), request.CurrentPassword, request.NewPassword); + _emailSenderService.SendEmail(existingUser.Email, token); + + reply.Code = 200; + reply.Message = "A confirmation email was sent to account."; + + + return await Task.FromResult(reply); + } + } } diff --git a/WebApp/backend/QuizMaster.API.Account/Service/PasswordHandler.cs b/WebApp/backend/QuizMaster.API.Account/Service/PasswordHandler.cs new file mode 100644 index 00000000..1d38f93a --- /dev/null +++ b/WebApp/backend/QuizMaster.API.Account/Service/PasswordHandler.cs @@ -0,0 +1,31 @@ +namespace QuizMaster.API.Account.Service +{ + public class PasswordHandler + { + private readonly IDictionary> _passwordHolder; + public PasswordHandler() { + _passwordHolder = new Dictionary>(); + } + + public string GenerateToken(string userId,string currentPassword, string newPassword) + { + string token = Guid.NewGuid().ToString(); + IDictionary Passwords = new Dictionary(); + 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); + } + } +} diff --git a/WebApp/backend/QuizMaster.API.Account/appsettings.json b/WebApp/backend/QuizMaster.API.Account/appsettings.json index 1be1da48..a1563aa6 100644 --- a/WebApp/backend/QuizMaster.API.Account/appsettings.json +++ b/WebApp/backend/QuizMaster.API.Account/appsettings.json @@ -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 */ } } diff --git a/WebApp/backend/QuizMaster.API.Gatewway/Controllers/AccountGatewayController.cs b/WebApp/backend/QuizMaster.API.Gatewway/Controllers/AccountGatewayController.cs index bf53827e..66793008 100644 --- a/WebApp/backend/QuizMaster.API.Gatewway/Controllers/AccountGatewayController.cs +++ b/WebApp/backend/QuizMaster.API.Gatewway/Controllers/AccountGatewayController.cs @@ -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 { @@ -361,7 +362,39 @@ public async Task Update(int id, JsonPatchDocument 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 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 SetAdmin(string username, [FromQuery] bool setAdmin = false) diff --git a/WebApp/backend/QuizMaster.Library.Common/Models/Accounts/ChangePasswordDTO.cs b/WebApp/backend/QuizMaster.Library.Common/Models/Accounts/ChangePasswordDTO.cs new file mode 100644 index 00000000..478634ff --- /dev/null +++ b/WebApp/backend/QuizMaster.Library.Common/Models/Accounts/ChangePasswordDTO.cs @@ -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; + } +} diff --git a/WebApp/frontend/quiz-master/app/profile/page.tsx b/WebApp/frontend/quiz-master/app/profile/page.tsx index a7ff448d..af6ee02c 100644 --- a/WebApp/frontend/quiz-master/app/profile/page.tsx +++ b/WebApp/frontend/quiz-master/app/profile/page.tsx @@ -1,18 +1,93 @@ "use client"; +import PromptModal from "@/components/Commons/modals/PromptModal"; import EditField from "@/components/Commons/profile/EditField"; import EditFieldWithButton from "@/components/Commons/profile/EditFieldWithButton"; import SaveCancelButton from "@/components/Commons/profile/SaveCancelButton"; +import { + getAccountInfo, + getUserInfo, + saveUserDetails, + updateEmail, +} from "@/lib/hooks/profile"; +import { IAccount, useAccountStore } from "@/store/ProfileStore"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; export default function Page() { - const [firstName, setFirstName] = useState("Jay"); - const [lastName, setLastName] = useState("Abejar"); - const [userName, setUserName] = useState("jaymar921"); - const [email, setEmail] = useState("jay@gmail.com"); + const { push } = useRouter(); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [userName, setUserName] = useState(""); + const [email, setEmail] = useState(""); + const [userRoles, setUserRoles] = useState(""); const [editToggled, setEditToggled] = useState(false); + const { setAccount, getAccount, getRoles, setRoles } = useAccountStore(); + const [showErrorModal, setShowErrorModal] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + function updateChanges() { + const account = getAccount(); + if (!account) return; + setFirstName(account.firstName as string); + setLastName(account.lastName as string); + setEmail(account.email as string); + setUserName(account.userName as string); + setUserRoles(getRoles().toString()); + } + + function handleErrorOnUpdateUserDetails(message: string) { + setShowErrorModal(true); + setErrorMessage(message); + } + + function captureUserDetails() { + let newData = { + id: getAccount()?.id, + firstName, + lastName, + email, + userName, + } as IAccount; + + saveUserDetails( + newData, + setEditToggled, + handleErrorOnUpdateUserDetails + ); + } + + useEffect(() => { + (async () => { + const { userData, roles } = await getUserInfo(); + // set the account store + if (userData !== null) { + let _retrievedInfo = await getAccountInfo(userData.id); + setAccount(_retrievedInfo); + } + // redirect to login if userData is not found | unauthorized + else push("/login"); + // set the roles + setRoles(roles); + // saving changes + updateChanges(); + })(); + }, [getUserInfo]); + return ( <> + { + setShowErrorModal(false); + }} + onClose={() => { + setShowErrorModal(false); + }} + />

@@ -53,14 +128,17 @@ export default function Page() { {}} /> {editToggled && ( {}} + onSave={() => { + captureUserDetails(); + }} onCancel={() => { setEditToggled(false); + updateChanges(); }} /> )} @@ -75,6 +153,16 @@ export default function Page() { value={email} onInput={setEmail} changeBtnTitle="Change" + onSave={(values) => { + const account = getAccount(); + if (account) + updateEmail( + account.id, + values[0], + handleErrorOnUpdateUserDetails, + updateChanges + ); + }} /> onInput(e.target.value)} - value={`${value}`} + value={value ? value : ""} + placeholder={`Enter ${title}`} /> )}

diff --git a/WebApp/frontend/quiz-master/components/Commons/profile/EditFieldWithButton.tsx b/WebApp/frontend/quiz-master/components/Commons/profile/EditFieldWithButton.tsx index 1d584b32..f03ac19e 100644 --- a/WebApp/frontend/quiz-master/components/Commons/profile/EditFieldWithButton.tsx +++ b/WebApp/frontend/quiz-master/components/Commons/profile/EditFieldWithButton.tsx @@ -31,7 +31,11 @@ export default function EditFieldWithButton({ )} {editting && (
-
+ { + e.preventDefault(); + }} + >
-
+ { + e.preventDefault(); + }} + > {inputType === "password" && ( { diff --git a/WebApp/frontend/quiz-master/lib/hooks/profile.ts b/WebApp/frontend/quiz-master/lib/hooks/profile.ts new file mode 100644 index 00000000..3098943e --- /dev/null +++ b/WebApp/frontend/quiz-master/lib/hooks/profile.ts @@ -0,0 +1,79 @@ +import { QUIZMASTER_ACCOUNT_GET, QUIZMASTER_ACCOUNT_PATCH, QUIZMASTER_AUTH_GET_COOKIE_INFO } from "@/api/api-routes"; +import { IAccount, UserAccount } from "@/store/ProfileStore"; +import { Dispatch, SetStateAction } from "react"; +import { notification } from "../notifications"; + +export async function getUserInfo(){ + try{ + const response = await fetch(`${QUIZMASTER_AUTH_GET_COOKIE_INFO}`,{ + credentials: "include" + }); + + const data = await response.json(); + const isInvalidCredentials = response.status === 401; + + const userData = data.info.userData as IAccount; + return {userData:new UserAccount().parse(userData), roles:data.info.roles as Array}; + }catch(e){console.log("Error ",e)} + return {userData:new UserAccount(), roles:[""]};; +} + +export async function getAccountInfo(id: Number){ + try{ + const response = await fetch(`${QUIZMASTER_ACCOUNT_GET}/${id}`,{ + credentials: "include" + }); + + const data = await response.json(); + + const userData = data as IAccount; + return userData; + }catch(e){console.log("Error ",e)} + return new UserAccount(); +} + +export async function updateEmail(id: Number, newEmail: string, ErrorCallback: (message: string) => void, RefreshCallback?: () => void) { + if(!validateEmail(newEmail)){ + ErrorCallback("Invalid Email Address"); + RefreshCallback && RefreshCallback(); + return; + } + const response = await fetch(`${QUIZMASTER_ACCOUNT_PATCH}${id}`, { + method:"PATCH", + credentials: "include", + headers: {"content-type":"application/json"}, + body:JSON.stringify([{path: "email", op: "replace", value: newEmail}])}); + + const data = await response.json() + if(response.status !== 200){ + ErrorCallback(data.message); + RefreshCallback && RefreshCallback(); + }else {notification(data);} +} + +export async function saveUserDetails(account: IAccount, setEditToggle: Dispatch>, ErrorCallback: (message: string) => void){ + // apply patch + let payload = new Array; + for(let prop in account){ + if(prop === "id") continue; + payload.push({ + path: prop.toLowerCase(), + op: "replace", + value: account[prop as keyof IAccount] + }) + } + const response = await fetch(`${QUIZMASTER_ACCOUNT_PATCH}${account.id}`, { + method:"PATCH", + credentials: "include", + headers: {"content-type":"application/json"}, + body:JSON.stringify(payload)}); + + const data = await response.json() + if(response.status !== 200){ + ErrorCallback(data.message); + }else {setEditToggle(false);notification(data)}} + +function validateEmail(email: string) { + const res = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/; + return res.test(String(email).toLowerCase()); +} \ No newline at end of file diff --git a/WebApp/frontend/quiz-master/store/ProfileStore.tsx b/WebApp/frontend/quiz-master/store/ProfileStore.tsx new file mode 100644 index 00000000..e9e0457e --- /dev/null +++ b/WebApp/frontend/quiz-master/store/ProfileStore.tsx @@ -0,0 +1,61 @@ +import { create } from "zustand"; + +export interface IAccount { + id: Number; + lastName: string | null; + firstName: string | null; + email: string; + userName: string; + activeData: boolean; + dateCreated: Date; + dateUpdated: Date | null; + updatedByUser: Number | null; +} + +export class UserAccount implements IAccount { + id = 0; + lastName = null; + firstName = null; + email = ""; + userName = ""; + activeData = false; + dateCreated = new Date(); + dateUpdated = null; + updatedByUser = null; + constructor() {} + parse(d: IAccount) { + let copyOfThis = this as IAccount; + for (let key in copyOfThis) { + if (d[key as keyof IAccount] !== null) { + copyOfThis[key as keyof IAccount] = d[key as keyof IAccount]; + } + } + return copyOfThis; + } +} + +interface IAccountStore { + roles: Array; + account: IAccount | null | undefined; + setAccount: (accountData: IAccount) => void; + getAccount: () => IAccount | null | undefined; + setRoles: (roles: Array) => void; + getRoles: () => Array; +} + +export const useAccountStore = create((set, get) => ({ + roles: [""], + account: null, + setAccount: (accountData: IAccount) => { + set({ account: accountData }); + }, + getAccount: () => { + return get().account; + }, + setRoles: (roles: Array) => { + set({ roles }); + }, + getRoles: () => { + return get().roles; + }, +})); From b89107c776bb22a5d369efb8710b980bbb838a5c Mon Sep 17 00:00:00 2001 From: JayMar921 Date: Wed, 10 Jan 2024 17:22:51 +0800 Subject: [PATCH 3/5] Ref: Ignored QuizSession.db and updated the QuizSession Appsettings.json --- .gitignore | 1 + .../QuizMaster.API.QuizSession.csproj | 1 + .../QuizMaster.API.QuizSession/appsettings.Development.json | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4a23bb73..ff1ce1cd 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/WebApp/backend/QuizMaster.API.QuizSession/QuizMaster.API.QuizSession.csproj b/WebApp/backend/QuizMaster.API.QuizSession/QuizMaster.API.QuizSession.csproj index ae390770..471c15b7 100644 --- a/WebApp/backend/QuizMaster.API.QuizSession/QuizMaster.API.QuizSession.csproj +++ b/WebApp/backend/QuizMaster.API.QuizSession/QuizMaster.API.QuizSession.csproj @@ -49,6 +49,7 @@ + diff --git a/WebApp/backend/QuizMaster.API.QuizSession/appsettings.Development.json b/WebApp/backend/QuizMaster.API.QuizSession/appsettings.Development.json index cbb82343..d90f78ae 100644 --- a/WebApp/backend/QuizMaster.API.QuizSession/appsettings.Development.json +++ b/WebApp/backend/QuizMaster.API.QuizSession/appsettings.Development.json @@ -11,6 +11,6 @@ } }, "ConnectionStrings": { - "QuizMasterQuizSessionDBConnectionString": "Data Source=QuizSession.db" + "QuizMasterQuizSessionDBConnectionString": "Data Source=sessionDb\\QuizSession.db" } } From 191985ab323583378fe88993de8546bffc69ffc1 Mon Sep 17 00:00:00 2001 From: JayMar921 Date: Wed, 10 Jan 2024 20:48:06 +0800 Subject: [PATCH 4/5] Feature: Update Profile Functionality 2/2 - Details are now validated - Update password is now working with email sender (Email template was not yet designed) - Delete account is now working - FIX: Users that has ActiveData set to 'false' are no longer be able to login --- .../Service/InformationService.cs | 17 +++- .../Services/Auth/AuthenticationServices.cs | 2 +- WebApp/frontend/quiz-master/api/api-routes.ts | 2 + .../frontend/quiz-master/app/profile/page.tsx | 50 +++++++++- .../Commons/profile/EditFieldWithButton.tsx | 95 +++++++++++++++---- .../Commons/profile/SaveCancelButton.tsx | 3 + .../frontend/quiz-master/lib/hooks/profile.ts | 42 +++++++- 7 files changed, 189 insertions(+), 22 deletions(-) diff --git a/WebApp/backend/QuizMaster.API.Account/Service/InformationService.cs b/WebApp/backend/QuizMaster.API.Account/Service/InformationService.cs index 740c948e..af385fc1 100644 --- a/WebApp/backend/QuizMaster.API.Account/Service/InformationService.cs +++ b/WebApp/backend/QuizMaster.API.Account/Service/InformationService.cs @@ -570,11 +570,24 @@ public override async Task UpdateUserPassword(UpdatePas return await Task.FromResult(reply); } + PasswordHasher 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); - _emailSenderService.SendEmail(existingUser.Email, token); + Task.Run(() => { _emailSenderService.SendEmail(existingUser.Email, token); }); reply.Code = 200; - reply.Message = "A confirmation email was sent to account."; + reply.Message = "A confirmation email was sent to your account."; return await Task.FromResult(reply); diff --git a/WebApp/backend/QuizMaster.API.Authentication/Services/Auth/AuthenticationServices.cs b/WebApp/backend/QuizMaster.API.Authentication/Services/Auth/AuthenticationServices.cs index 179f1632..c84206ba 100644 --- a/WebApp/backend/QuizMaster.API.Authentication/Services/Auth/AuthenticationServices.cs +++ b/WebApp/backend/QuizMaster.API.Authentication/Services/Auth/AuthenticationServices.cs @@ -39,7 +39,7 @@ public async Task 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 keyValuePairs = new Dictionary(); diff --git a/WebApp/frontend/quiz-master/api/api-routes.ts b/WebApp/frontend/quiz-master/api/api-routes.ts index 21aab3b0..a68aadc7 100644 --- a/WebApp/frontend/quiz-master/api/api-routes.ts +++ b/WebApp/frontend/quiz-master/api/api-routes.ts @@ -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 @@ -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, diff --git a/WebApp/frontend/quiz-master/app/profile/page.tsx b/WebApp/frontend/quiz-master/app/profile/page.tsx index af6ee02c..e363e003 100644 --- a/WebApp/frontend/quiz-master/app/profile/page.tsx +++ b/WebApp/frontend/quiz-master/app/profile/page.tsx @@ -4,11 +4,14 @@ import EditField from "@/components/Commons/profile/EditField"; import EditFieldWithButton from "@/components/Commons/profile/EditFieldWithButton"; import SaveCancelButton from "@/components/Commons/profile/SaveCancelButton"; import { + DeleteAccount, + UpdatePassword, getAccountInfo, getUserInfo, saveUserDetails, updateEmail, } from "@/lib/hooks/profile"; +import { notification } from "@/lib/notifications"; import { IAccount, useAccountStore } from "@/store/ProfileStore"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { useRouter } from "next/navigation"; @@ -25,6 +28,7 @@ export default function Page() { const { setAccount, getAccount, getRoles, setRoles } = useAccountStore(); const [showErrorModal, setShowErrorModal] = useState(false); const [errorMessage, setErrorMessage] = useState(""); + const [showDeleteModal, setShowDeleteModal] = useState(false); function updateChanges() { const account = getAccount(); @@ -57,6 +61,30 @@ export default function Page() { ); } + async function handleUpdatePassword(password: Array) { + return await UpdatePassword( + getAccount()?.id as Number, + password, + setEditToggled, + handleErrorOnUpdateUserDetails + ); + } + + function onDeleteAccount() { + DeleteAccount(getAccount()?.id as Number).then( + ({ message, success }) => { + if (success) { + notification({ + type: "success", + title: message, + }); + } else { + handleErrorOnUpdateUserDetails(message); + } + } + ); + } + useEffect(() => { (async () => { const { userData, roles } = await getUserInfo(); @@ -88,6 +116,19 @@ export default function Page() { setShowErrorModal(false); }} /> + { + setShowDeleteModal(false); + onDeleteAccount(); + }} + onClose={() => { + setShowDeleteModal(false); + }} + />

@@ -169,6 +210,8 @@ export default function Page() { value="" inputType="password" changeBtnTitle="Change Password" + onError={handleErrorOnUpdateUserDetails} + onSave={handleUpdatePassword} />

@@ -182,7 +225,12 @@ export default function Page() { cannot be undone.

-
diff --git a/WebApp/frontend/quiz-master/components/Commons/profile/EditFieldWithButton.tsx b/WebApp/frontend/quiz-master/components/Commons/profile/EditFieldWithButton.tsx index f03ac19e..6855ed1d 100644 --- a/WebApp/frontend/quiz-master/components/Commons/profile/EditFieldWithButton.tsx +++ b/WebApp/frontend/quiz-master/components/Commons/profile/EditFieldWithButton.tsx @@ -1,6 +1,17 @@ "use client"; import { Dispatch, SetStateAction, useState } from "react"; import SaveCancelButton from "./SaveCancelButton"; +import { validatorFactory } from "@/lib/validation/creators"; +import { + isRequired, + mustBeEmail, + mustHaveDigit, + mustHaveLowerCase, + mustHaveSpecialCharacter, + mustHaveUpperCase, +} from "@/lib/validation/regex"; +import { validate } from "@/lib/validation/validate"; + export default function EditFieldWithButton({ title, value, @@ -9,19 +20,23 @@ export default function EditFieldWithButton({ inputType, onSave, onCancel, + onError, }: { title: string; value: string; onInput?: Dispatch>; changeBtnTitle?: string; inputType?: string; - onSave?: (s: Array) => void; + onSave?: (s: Array) => void | Promise; onCancel?: () => void; + onError?: (message: string) => void; }) { const [editting, setEditting] = useState(false); const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); + const [confirmEnabled, setConfirmDisabled] = useState(true); + return (

{title}

@@ -124,33 +139,79 @@ export default function EditFieldWithButton({
{ if (inputType === "password") { - if (currentPassword === "") { - alert("Current password is empty"); + const maxChar = validatorFactory( + 50, + "max" + ); + const passWordMinChar = + validatorFactory(8, "min"); + const validators = [ + isRequired, + passWordMinChar, + maxChar, + mustHaveLowerCase, + mustHaveUpperCase, + mustHaveDigit, + mustHaveSpecialCharacter, + ]; + // check empty + if (!currentPassword) { + onError && + onError( + "Current password is empty" + ); return; } + // check if old password is same as new if (currentPassword === newPassword) { - alert( - "Old password cannot be the same as new password" - ); + onError && + onError( + "Old password cannot be the same as new password" + ); return; } + const validatePassword = validate( + newPassword, + validators + ); + if (null != validatePassword) { + onError && + onError(validatePassword); + return; + } + // check if new and confirm is matched if (newPassword !== confirmPassword) { - alert( - "New Password and Confirm Password must match" - ); + onError && + onError( + "New Password and Confirm Password must match" + ); return; } - onSave && - onSave([ - currentPassword, - newPassword, - ]); - setEditting(false); + if (onSave != null) { + setConfirmDisabled(false); + Promise.resolve( + onSave([ + currentPassword, + newPassword, + ]) + ).then((res) => { + if (res) setEditting(false); + setConfirmDisabled(true); + }); + } } else { - onSave && onSave([value]); - setEditting(false); + if (onSave != null) { + setConfirmDisabled(false); + Promise.resolve( + onSave([value]) + ).then((res) => { + if (res) setEditting(false); + setConfirmDisabled(true); + }); + } } }} onCancel={() => { diff --git a/WebApp/frontend/quiz-master/components/Commons/profile/SaveCancelButton.tsx b/WebApp/frontend/quiz-master/components/Commons/profile/SaveCancelButton.tsx index 0a8d1969..8d733168 100644 --- a/WebApp/frontend/quiz-master/components/Commons/profile/SaveCancelButton.tsx +++ b/WebApp/frontend/quiz-master/components/Commons/profile/SaveCancelButton.tsx @@ -2,16 +2,19 @@ export default function SaveCancelButton({ onSave, onCancel, className, + enabled, }: { onSave: () => void; onCancel?: () => void; className?: string; + enabled?: boolean; }) { return (
diff --git a/WebApp/frontend/quiz-master/lib/hooks/profile.ts b/WebApp/frontend/quiz-master/lib/hooks/profile.ts index 3098943e..075e1e90 100644 --- a/WebApp/frontend/quiz-master/lib/hooks/profile.ts +++ b/WebApp/frontend/quiz-master/lib/hooks/profile.ts @@ -1,7 +1,8 @@ -import { QUIZMASTER_ACCOUNT_GET, QUIZMASTER_ACCOUNT_PATCH, QUIZMASTER_AUTH_GET_COOKIE_INFO } from "@/api/api-routes"; +import { QUIZMASTER_ACCOUNT_DELETE, QUIZMASTER_ACCOUNT_GET, QUIZMASTER_ACCOUNT_PASSWORD_RESET_POST, QUIZMASTER_ACCOUNT_PATCH, QUIZMASTER_AUTH_GET_COOKIE_INFO } from "@/api/api-routes"; import { IAccount, UserAccount } from "@/store/ProfileStore"; import { Dispatch, SetStateAction } from "react"; import { notification } from "../notifications"; +import { signOut } from "next-auth/react"; export async function getUserInfo(){ try{ @@ -53,6 +54,23 @@ export async function updateEmail(id: Number, newEmail: string, ErrorCallback: ( export async function saveUserDetails(account: IAccount, setEditToggle: Dispatch>, ErrorCallback: (message: string) => void){ // apply patch + console.log("called") + if(!account.firstName || !account.lastName || !account.userName){ + ErrorCallback("Make sure that the fields are not empty"); + return + } + if(account.firstName && account.firstName.length < 3){ + ErrorCallback("Firstname must have at least 3 characters"); + return + } + if(account.lastName && account.lastName.length < 3){ + ErrorCallback("Lastname must have at least 3 characters"); + return + } + if(account.userName && account.userName.length < 5){ + ErrorCallback("Username must have at least 5 characters"); + return + } let payload = new Array; for(let prop in account){ if(prop === "id") continue; @@ -76,4 +94,26 @@ export async function saveUserDetails(account: IAccount, setEditToggle: Dispatch function validateEmail(email: string) { const res = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/; return res.test(String(email).toLowerCase()); +} + +export async function UpdatePassword(id: Number,password: Array, setEditToggle: Dispatch>, ErrorCallback: (message: string) => void){ + const response = await fetch(QUIZMASTER_ACCOUNT_PASSWORD_RESET_POST(id), { + method:"POST", + credentials: "include", + headers: {"content-type":"application/json"}, + body:JSON.stringify({currentPassword: password[0], newPassword: password[1]})}); + const data = await response.json() + if(response.status !== 200){ + ErrorCallback(data.message); + return false; + }else {setEditToggle(false);notification(data); return true;} +} + + +export async function DeleteAccount(id: Number){ + await fetch(`${QUIZMASTER_ACCOUNT_DELETE}${id}`, { + method:"DELETE", + credentials: "include"}); + signOut(); + return {message:"Delete account success", success:true}; } \ No newline at end of file From 86a103f023284d0e3913d2e5120d172c1aaaf28b Mon Sep 17 00:00:00 2001 From: JayMar921 Date: Wed, 10 Jan 2024 21:29:50 +0800 Subject: [PATCH 5/5] Refactor: Updated Email Template for Reset Password --- .../Service/EmailSenderService.cs | 54 +++++++++++++++++-- .../Service/InformationService.cs | 2 +- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/WebApp/backend/QuizMaster.API.Account/Service/EmailSenderService.cs b/WebApp/backend/QuizMaster.API.Account/Service/EmailSenderService.cs index e17c543a..d51f916f 100644 --- a/WebApp/backend/QuizMaster.API.Account/Service/EmailSenderService.cs +++ b/WebApp/backend/QuizMaster.API.Account/Service/EmailSenderService.cs @@ -13,13 +13,61 @@ public EmailSenderService(IOptions appsettings) _settings = appsettings.Value; } - public void SendEmail(string email, string token) + 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 = "Update Password like literal update"; - mailMessage.Body = $"Confirm Update password: Click to update"; + mailMessage.Subject = "Quiz Master Password Reset"; + mailMessage.Body = @$" + +
+ +

+ Reset Password Confirmation +

+ +

Hello {firstname}

+

+ You are receiving this email because you requested a change on your + password. Click the button below to reset your password. +

+ +

+ If you did not request a password reset, you can ignore this email. +

+
+

+ Copyright 2023 Ⓒ QuizMaster +

+
+ +"; mailMessage.IsBodyHtml = true; // Create the credentials to login to the gmail account associated with my custom domain diff --git a/WebApp/backend/QuizMaster.API.Account/Service/InformationService.cs b/WebApp/backend/QuizMaster.API.Account/Service/InformationService.cs index af385fc1..ac52df89 100644 --- a/WebApp/backend/QuizMaster.API.Account/Service/InformationService.cs +++ b/WebApp/backend/QuizMaster.API.Account/Service/InformationService.cs @@ -584,7 +584,7 @@ public override async Task UpdateUserPassword(UpdatePas } string token = _passwordHandler.GenerateToken(request.Id.ToString(), request.CurrentPassword, request.NewPassword); - Task.Run(() => { _emailSenderService.SendEmail(existingUser.Email, token); }); + Task.Run(() => { _emailSenderService.SendEmail(existingUser.Email, existingUser.FirstName, token); }); reply.Code = 200; reply.Message = "A confirmation email was sent to your account.";