From 8433df4e3ae7d1e067d4c6a9b4814c8dcd27a02f Mon Sep 17 00:00:00 2001 From: arshiamasih Date: Tue, 25 Aug 2026 13:59:59 -0700 Subject: [PATCH 01/11] Fix update account to send email verification only when email is changed --- .../Authorization/UpdateAccount.jsx | 13 +++++++-- server/app/services/account.service.js | 29 ++++++++++++++++--- ...n_email_confirmed_only_on_email_update.sql | 28 ++++++++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 server/db/migration/V20260825.1208__update_login_email_confirmed_only_on_email_update.sql diff --git a/client/src/components/Authorization/UpdateAccount.jsx b/client/src/components/Authorization/UpdateAccount.jsx index 7bd5ca0d..f0b7403e 100644 --- a/client/src/components/Authorization/UpdateAccount.jsx +++ b/client/src/components/Authorization/UpdateAccount.jsx @@ -37,6 +37,7 @@ const UpdateAccount = props => { }; const [errorMsg, setErrorMsg] = useState(""); + const [successMsg, setSuccessMsg] = useState(""); const [submitted, setSubmitted] = useState(false); const updateAccountSchema = Yup.object().shape({ @@ -51,6 +52,9 @@ const UpdateAccount = props => { { firstName, lastName, email }, { setSubmitting } ) => { + setErrorMsg(""); + setSuccessMsg(""); + try { const response = await accountService.updateAccount( firstName, @@ -58,13 +62,18 @@ const UpdateAccount = props => { email ); - if (response.isSuccess) { + if (response.code === "ACCOUNT_EMAIL_UPDATE_SUCCESS") { setSubmitted(true); userContext.updateAccount({}); return; } switch (response.code) { + case "ACCOUNT_UPDATE_SUCCESS": + setSuccessMsg(response.message); + userContext.updateAccount(response.user); + break; + case "ERR_INVALID_ADMIN_DOMAIN": setErrorMsg(response.message); break; @@ -181,7 +190,7 @@ const UpdateAccount = props => {

- {errorMsg} + {successMsg || errorMsg}
)} diff --git a/server/app/services/account.service.js b/server/app/services/account.service.js index f994f4bc..6e1bdec3 100644 --- a/server/app/services/account.service.js +++ b/server/app/services/account.service.js @@ -7,7 +7,9 @@ const { sendRegistrationConfirmation, sendResetPasswordConfirmation } = require("./email.service"); -const allowedAdminDomains = process.env.ALLOWED_ADMIN_EMAIL_DOMAINS.split(","); +const allowedAdminDomains = process.env.ALLOWED_ADMIN_EMAIL_DOMAINS + ? process.env.ALLOWED_ADMIN_EMAIL_DOMAINS.split(",") + : ["dispostable.com"]; const SALT_ROUNDS = 10; @@ -148,15 +150,34 @@ const updateAccount = async model => { request.input("FirstName", mssql.NVarChar, model.firstName); request.input("LastName", mssql.NVarChar, model.lastName); request.input("Email", mssql.NVarChar, model.email); + await request.execute("Login_Update"); + const updatedUser = await selectByEmail(model.email); - const token = crypto.randomUUID(); - await handleVerifyUpdateConfirmation(model.email, token); + if (user.email !== model.email) { + const token = crypto.randomUUID(); + await handleVerifyUpdateConfirmation(model.email, token); + + return { + isSuccess: true, + code: "ACCOUNT_EMAIL_UPDATE_SUCCESS", + message: "Account updates succeeded." + }; + } return { isSuccess: true, code: "ACCOUNT_UPDATE_SUCCESS", - message: "Account updates succeeded." + message: "Account updates succeeded.", + user: { + id: updatedUser.id, + firstName: updatedUser.firstName, + lastName: updatedUser.lastName, + email: updatedUser.email, + isAdmin: updatedUser.isAdmin, + emailConfirmed: updatedUser.emailConfirmed, + isSecurityAdmin: updatedUser.isSecurityAdmin + } }; } catch (err) { return { diff --git a/server/db/migration/V20260825.1208__update_login_email_confirmed_only_on_email_update.sql b/server/db/migration/V20260825.1208__update_login_email_confirmed_only_on_email_update.sql new file mode 100644 index 00000000..3644556b --- /dev/null +++ b/server/db/migration/V20260825.1208__update_login_email_confirmed_only_on_email_update.sql @@ -0,0 +1,28 @@ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE OR ALTER PROC [dbo].[Login_Update] + @id int, + @firstName nvarchar(50), + @lastName nvarchar(50), + @email nvarchar(100) +AS +BEGIN + + UPDATE Login + SET + firstName = @firstName, + lastName = @lastName, + email = @email, + emailConfirmed = + CASE + WHEN email <> @email THEN 0 + ELSE emailConfirmed + END + + WHERE + id = @id; + +END +GO \ No newline at end of file From 83f44cbf4a9cd0852092b831625a34c18ce18f4c Mon Sep 17 00:00:00 2001 From: arshiamasih Date: Tue, 25 Aug 2026 14:36:47 -0700 Subject: [PATCH 02/11] Revert admin email import --- server/app/services/account.service.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/app/services/account.service.js b/server/app/services/account.service.js index 6e1bdec3..5a7fef22 100644 --- a/server/app/services/account.service.js +++ b/server/app/services/account.service.js @@ -7,9 +7,7 @@ const { sendRegistrationConfirmation, sendResetPasswordConfirmation } = require("./email.service"); -const allowedAdminDomains = process.env.ALLOWED_ADMIN_EMAIL_DOMAINS - ? process.env.ALLOWED_ADMIN_EMAIL_DOMAINS.split(",") - : ["dispostable.com"]; +const allowedAdminDomains = process.env.ALLOWED_ADMIN_EMAIL_DOMAINS.split(","); const SALT_ROUNDS = 10; From b0483b76a51f35c480d2397eac50bee1c9f8a81f Mon Sep 17 00:00:00 2001 From: arshiamasih Date: Thu, 27 Aug 2026 13:50:28 -0700 Subject: [PATCH 03/11] Add login update history table for login email updates and modify services that use confirm registration --- server/app/services/account.service.js | 71 ++++-- ...n_email_confirmed_only_on_email_update.sql | 28 --- ...260827.1208__create_login_email_update.sql | 222 ++++++++++++++++++ 3 files changed, 277 insertions(+), 44 deletions(-) delete mode 100644 server/db/migration/V20260825.1208__update_login_email_confirmed_only_on_email_update.sql create mode 100644 server/db/migration/V20260827.1208__create_login_email_update.sql diff --git a/server/app/services/account.service.js b/server/app/services/account.service.js index 5a7fef22..d8302e83 100644 --- a/server/app/services/account.service.js +++ b/server/app/services/account.service.js @@ -147,13 +147,24 @@ const updateAccount = async model => { request.input("id", mssql.Int, model.id); request.input("FirstName", mssql.NVarChar, model.firstName); request.input("LastName", mssql.NVarChar, model.lastName); - request.input("Email", mssql.NVarChar, model.email); - await request.execute("Login_Update"); - const updatedUser = await selectByEmail(model.email); + await request.execute("Login_Update"); // update user profile (name) + const updatedUser = await selectByEmail(model.email); // get updated user data + + // if requesting email change, send verification request if (user.email !== model.email) { const token = crypto.randomUUID(); + + const emailChangeRequest = pool.request(); + + emailChangeRequest.input("id", mssql.Int, model.id); + + emailChangeRequest.input("RequestedEmail", mssql.NVarChar, model.email); + + emailChangeRequest.input("ActiveEmail", mssql.NVarChar, user.email); + + await request.execute("LoginUpdateHistory_Insert"); await handleVerifyUpdateConfirmation(model.email, token); return { @@ -245,9 +256,9 @@ const confirmRegistration = async token => { try { await poolConnect; const request = pool.request(); + request.input("token", mssql.NVarChar(200), token); - request.input("token", mssql.NVarChar, token); - + // 1. Untouched token lookup const sqlResult = await request.execute("SecurityToken_SelectByToken"); const resultSet = sqlResult.recordset; const now = new Date(); @@ -260,7 +271,8 @@ const confirmRegistration = async token => { "Email confirmation failed. Invalid security token. Re-send confirmation email." }; } else if ( - (now.getTime() - resultSet[0].dateCreated.getTime()) / (60 * 60 * 1000) >= + (now.getTime() - new Date(resultSet[0].dateCreated).getTime()) / + (60 * 60 * 1000) >= 24 ) { return { @@ -271,20 +283,47 @@ const confirmRegistration = async token => { }; } - // If we get this far, we can update the login.email_confirmed flag const email = resultSet[0].email; - const updateRequest = await pool.request(); - updateRequest.input("email", mssql.NVarChar, email); - await updateRequest.execute("Login_ConfirmEmail"); + // Check for an active pending change request + const loginChangeHistoryRequest = pool.request(); + loginChangeHistoryRequest.input("email", mssql.NVarChar(100), email); + + const loginChangeHistoryResult = await loginChangeHistoryRequest.execute( + "LoginUpdateHistory_SelectByRecentRequestedEmail" + ); + + const pendingLoginEmailChange = loginChangeHistoryResult.recordset[0]; + + const confirmEmailRequest = pool.request(); + confirmEmailRequest.input("email", mssql.NVarChar(100), email); + + if (pendingLoginEmailChange) { + await confirmEmailRequest.execute("Login_ConfirmEmailUpdate"); + + return { + isSuccess: true, + code: "EMAIL_UPDATE_SUCCESS", + message: "Email change confirmed successfully.", + email + }; + } else { + // First-time registration + await confirmEmailRequest.execute("Login_ConfirmEmail"); + + return { + isSuccess: true, + code: "REG_CONFIRM_SUCCESS", + message: "Email confirmed successfully.", + email + }; + } + } catch (err) { return { - isSuccess: true, - code: "REG_CONFIRM_SUCCESS", - message: "Email confirmed.", - email + isSuccess: false, + code: "CONFIRM_FAILED", + message: err.message }; - } catch (err) { - return { message: err.message }; } }; diff --git a/server/db/migration/V20260825.1208__update_login_email_confirmed_only_on_email_update.sql b/server/db/migration/V20260825.1208__update_login_email_confirmed_only_on_email_update.sql deleted file mode 100644 index 3644556b..00000000 --- a/server/db/migration/V20260825.1208__update_login_email_confirmed_only_on_email_update.sql +++ /dev/null @@ -1,28 +0,0 @@ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE OR ALTER PROC [dbo].[Login_Update] - @id int, - @firstName nvarchar(50), - @lastName nvarchar(50), - @email nvarchar(100) -AS -BEGIN - - UPDATE Login - SET - firstName = @firstName, - lastName = @lastName, - email = @email, - emailConfirmed = - CASE - WHEN email <> @email THEN 0 - ELSE emailConfirmed - END - - WHERE - id = @id; - -END -GO \ No newline at end of file diff --git a/server/db/migration/V20260827.1208__create_login_email_update.sql b/server/db/migration/V20260827.1208__create_login_email_update.sql new file mode 100644 index 00000000..166c91ef --- /dev/null +++ b/server/db/migration/V20260827.1208__create_login_email_update.sql @@ -0,0 +1,222 @@ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE OR ALTER PROC [dbo].[Login_Update] + @id int, + @firstName nvarchar(50), + @lastName nvarchar(50) + +AS +BEGIN + + UPDATE Login + SET + firstName = @firstName, + lastName = @lastName + + WHERE + id = @id; + +END +GO + + + +CREATE TABLE [dbo].[LoginUpdateHistory] +( + [id] [int] IDENTITY(1,1) NOT NULL, + [userId] [int] NOT NULL INDEX IX_LoginUpdateHistory_UserId NONCLUSTERED, + [requestedEmail] [nvarchar](100) NOT NULL INDEX IX_LoginUpdateHistory_RequestedEmail NONCLUSTERED, + [activeEmail] [nvarchar](100) NULL, + [lastActiveEmail] [nvarchar](100) NULL, + [dateRequested] [datetime2](7) NOT NULL DEFAULT (SYSUTCDATETIME()), + [dateChanged] [datetime2](7) NULL, + PRIMARY KEY ([id]), + FOREIGN KEY ([userId]) REFERENCES [dbo].[Login] ([id]) +); +GO + + +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO + + + +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO + +CREATE OR ALTER PROCEDURE [dbo].[LoginUpdateHistory_Insert] + @userId INT, + @requestedEmail NVARCHAR(100), + @currentEmail NVARCHAR(100) +AS +BEGIN + SET NOCOUNT ON; + + BEGIN TRY + BEGIN TRANSACTION; + + -- Invalidate any prior pending change requests for this user + DELETE FROM [dbo].[LoginUpdateHistory] + WHERE [userId] = @userId + AND [dateChanged] IS NULL; + + + INSERT INTO [dbo].[LoginUpdateHistory] + ( + [userId], + [requestedEmail], + [activeEmail], + [lastActiveEmail], + [dateRequested], + [dateChanged] + ) + VALUES + ( + @userId, + @requestedEmail, + @currentEmail, -- Active at time of request + NULL, -- NULL until updated + SYSUTCDATETIME(), + NULL + ); + + COMMIT TRANSACTION; + END TRY + BEGIN CATCH + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + + THROW; + END CATCH; +END; +GO + +CREATE OR ALTER PROCEDURE [dbo].[Login_ConfirmUpdateEmail] + @email NVARCHAR(100) +AS +BEGIN + SET NOCOUNT ON; + + BEGIN TRY + BEGIN TRANSACTION; + + DECLARE @userId INT; + + -- Find the user ID for most recent change request + SELECT TOP (1) + @userId = [userId] + FROM [dbo].[LoginUpdateHistory] + WHERE [requestedEmail] = @email + AND [dateChanged] IS NULL + ORDER BY [dateRequested] DESC; + + + + IF @userId IS NULL + BEGIN + RAISERROR('No pending email update request found for this email.', 16, 1); + ROLLBACK TRANSACTION; + RETURN; + END; + + + BEGIN + -- Update the target user record in Login table + UPDATE [dbo].[Login] + SET [email] = @email, + [emailConfirmed] = 1 + WHERE [id] = @userId; + + -- Update history record in LoginUpdateHistory table + UPDATE [dbo].[LoginUpdateHistory] + SET + [lastActiveEmail] = [activeEmail], + [activeEmail] = @email, + [dateChanged] = SYSUTCDATETIME() + WHERE [requestedEmail] = @email + AND [userId] = @userId + AND [dateChanged] IS NULL; + END + + COMMIT TRANSACTION; + + END TRY + BEGIN CATCH + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + + THROW; + END CATCH; +END; +GO + + +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO + +CREATE OR ALTER PROCEDURE [dbo].[LoginUpdateHistory_SelectByRecentRequestedEmail] + @activeEmail NVARCHAR(100) +AS +BEGIN + SET NOCOUNT ON; + + SELECT TOP (1) + [id], + [userId], + [requestedEmail], + [activeEmail], + [lastActiveEmail], + [dateRequested], + [dateChanged] + FROM [dbo].[LoginUpdateHistory] + WHERE [activeEmail] = @activeEmail + ORDER BY [dateRequested] DESC; +END; +GO + + +CREATE OR ALTER PROCEDURE [dbo].[DeleteUserAndProjects] + @id INT +AS +BEGIN + SET NOCOUNT ON; + + IF EXISTS ( + SELECT 1 + FROM [dbo].[Project] + WHERE [loginId] = @id + AND [dateSubmitted] IS NOT NULL + AND [dateTrashed] IS NULL + ) + BEGIN + RAISERROR('Cannot delete account with submissions. Account has projects that have been submitted.', 16, 1); + RETURN; + END; + + BEGIN TRY + BEGIN TRANSACTION; + + DELETE FROM [dbo].[Project] WHERE [loginId] = @id; + DELETE FROM [dbo].[LoginHistory] WHERE [loginId] = @id; + DELETE FROM [dbo].[LoginUpdateHistory] WHERE [userId] = @id; + + -- Delete the parent user record + DELETE FROM [dbo].[Login] WHERE [id] = @id; + + COMMIT TRANSACTION; + END TRY + BEGIN CATCH + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + THROW; + END CATCH; +END; +GO \ No newline at end of file From c1f4c1161dbb4819a7e5a0c8ec92e8acf42ba576 Mon Sep 17 00:00:00 2001 From: arshiamasih Date: Thu, 27 Aug 2026 13:51:27 -0700 Subject: [PATCH 04/11] Clean up comment --- server/app/services/account.service.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/app/services/account.service.js b/server/app/services/account.service.js index d8302e83..9ee7ad98 100644 --- a/server/app/services/account.service.js +++ b/server/app/services/account.service.js @@ -258,7 +258,6 @@ const confirmRegistration = async token => { const request = pool.request(); request.input("token", mssql.NVarChar(200), token); - // 1. Untouched token lookup const sqlResult = await request.execute("SecurityToken_SelectByToken"); const resultSet = sqlResult.recordset; const now = new Date(); From 8395744528d63a7cb5eba67ae75eec4604a7b326 Mon Sep 17 00:00:00 2001 From: arshiamasih Date: Thu, 27 Aug 2026 15:43:21 -0700 Subject: [PATCH 05/11] Update resend confirmation to look for pending email and login email --- server/app/services/account.service.js | 46 ++++++++++++------- ...260827.1208__create_login_email_update.sql | 4 +- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/server/app/services/account.service.js b/server/app/services/account.service.js index 9ee7ad98..ff89d655 100644 --- a/server/app/services/account.service.js +++ b/server/app/services/account.service.js @@ -157,11 +157,8 @@ const updateAccount = async model => { const token = crypto.randomUUID(); const emailChangeRequest = pool.request(); - emailChangeRequest.input("id", mssql.Int, model.id); - emailChangeRequest.input("RequestedEmail", mssql.NVarChar, model.email); - emailChangeRequest.input("ActiveEmail", mssql.NVarChar, user.email); await request.execute("LoginUpdateHistory_Insert"); @@ -201,24 +198,44 @@ const updateAccount = async model => { const resendConfirmationEmail = async email => { try { await poolConnect; - const request = pool.request(); - request.input("email", mssql.NVarChar, email); - const selectByEmailResponse = await request.execute("Login_SelectByEmail"); - let result = { + const pendingEmailRequest = pool.request(); + pendingEmailRequest.input("email", mssql.NVarChar(100), email); + const pendingEmailResponse = await pendingEmailRequest.execute( + "LoginUpdateHistory_SelectByRecentRequestedEmail" + ); + + let userRecord = pendingEmailResponse.recordset[0]; + + // check login table if no pending requests for email change + if (!userRecord) { + const loginRequest = pool.request(); + loginRequest.input("email", mssql.NVarChar(100), email); + const loginResponse = await loginRequest.execute("Login_SelectByEmail"); + userRecord = loginResponse.recordset[0]; + } + + // if no record in either table, user not found + if (!userRecord) { + return { + isSuccess: false, + code: "REG_ACCOUNT_NOT_FOUND", + message: `Account not found for email: ${email}` + }; + } + + const result = { isSuccess: true, code: "REG_SUCCESS", - newId: selectByEmailResponse.recordset[0].id, + newId: userRecord.userId || userRecord.id, message: "Account found." }; - result = await requestRegistrationConfirmation(email, result); - return result; + + return await requestRegistrationConfirmation(email, result); } catch (err) { - // Assume any error is an email that does not correspond to - // an account. return { isSuccess: false, - code: "REG_ACCOUNT_NOT_FOUND", + code: "RESEND_FAILED", message: `Resending confirmation email to ${email} failed due to: ${err.message}` }; } @@ -287,13 +304,10 @@ const confirmRegistration = async token => { // Check for an active pending change request const loginChangeHistoryRequest = pool.request(); loginChangeHistoryRequest.input("email", mssql.NVarChar(100), email); - const loginChangeHistoryResult = await loginChangeHistoryRequest.execute( "LoginUpdateHistory_SelectByRecentRequestedEmail" ); - const pendingLoginEmailChange = loginChangeHistoryResult.recordset[0]; - const confirmEmailRequest = pool.request(); confirmEmailRequest.input("email", mssql.NVarChar(100), email); diff --git a/server/db/migration/V20260827.1208__create_login_email_update.sql b/server/db/migration/V20260827.1208__create_login_email_update.sql index 166c91ef..d60cd5a6 100644 --- a/server/db/migration/V20260827.1208__create_login_email_update.sql +++ b/server/db/migration/V20260827.1208__create_login_email_update.sql @@ -163,7 +163,7 @@ SET QUOTED_IDENTIFIER ON GO CREATE OR ALTER PROCEDURE [dbo].[LoginUpdateHistory_SelectByRecentRequestedEmail] - @activeEmail NVARCHAR(100) + @requestedEmail NVARCHAR(100) AS BEGIN SET NOCOUNT ON; @@ -177,7 +177,7 @@ BEGIN [dateRequested], [dateChanged] FROM [dbo].[LoginUpdateHistory] - WHERE [activeEmail] = @activeEmail + WHERE [requestedEmail] = @requestedEmail ORDER BY [dateRequested] DESC; END; GO From 3d910544904aa789e3ce4269f1449831e4af4462 Mon Sep 17 00:00:00 2001 From: arshiamasih Date: Thu, 27 Aug 2026 16:56:13 -0700 Subject: [PATCH 06/11] Update resend confirmation service to handle email in pending request --- server/app/services/account.service.js | 39 ++++---- ...260827.1208__create_login_email_update.sql | 88 ++++++++++++++++--- 2 files changed, 92 insertions(+), 35 deletions(-) diff --git a/server/app/services/account.service.js b/server/app/services/account.service.js index ff89d655..847c2c16 100644 --- a/server/app/services/account.service.js +++ b/server/app/services/account.service.js @@ -152,7 +152,7 @@ const updateAccount = async model => { const updatedUser = await selectByEmail(model.email); // get updated user data - // if requesting email change, send verification request + // if requesting email change, upddate change history log send email verification request if (user.email !== model.email) { const token = crypto.randomUUID(); @@ -161,7 +161,7 @@ const updateAccount = async model => { emailChangeRequest.input("RequestedEmail", mssql.NVarChar, model.email); emailChangeRequest.input("ActiveEmail", mssql.NVarChar, user.email); - await request.execute("LoginUpdateHistory_Insert"); + await request.execute("LoginEmailChangeHistory_Insert"); await handleVerifyUpdateConfirmation(model.email, token); return { @@ -198,24 +198,13 @@ const updateAccount = async model => { const resendConfirmationEmail = async email => { try { await poolConnect; - - const pendingEmailRequest = pool.request(); - pendingEmailRequest.input("email", mssql.NVarChar(100), email); - const pendingEmailResponse = await pendingEmailRequest.execute( - "LoginUpdateHistory_SelectByRecentRequestedEmail" + const emailRequest = pool.request(); + emailRequest.input("email", mssql.NVarChar(100), email); + const emailResponse = await emailRequest.execute( + "Login_SelectByEmailAndPendingEmail" ); + const userRecord = emailResponse.recordset[0]; - let userRecord = pendingEmailResponse.recordset[0]; - - // check login table if no pending requests for email change - if (!userRecord) { - const loginRequest = pool.request(); - loginRequest.input("email", mssql.NVarChar(100), email); - const loginResponse = await loginRequest.execute("Login_SelectByEmail"); - userRecord = loginResponse.recordset[0]; - } - - // if no record in either table, user not found if (!userRecord) { return { isSuccess: false, @@ -227,7 +216,7 @@ const resendConfirmationEmail = async email => { const result = { isSuccess: true, code: "REG_SUCCESS", - newId: userRecord.userId || userRecord.id, + newId: userRecord.id, message: "Account found." }; @@ -305,14 +294,18 @@ const confirmRegistration = async token => { const loginChangeHistoryRequest = pool.request(); loginChangeHistoryRequest.input("email", mssql.NVarChar(100), email); const loginChangeHistoryResult = await loginChangeHistoryRequest.execute( - "LoginUpdateHistory_SelectByRecentRequestedEmail" + "LoginEmailChangeHistory_SelectByRecentPendingEmail" ); - const pendingLoginEmailChange = loginChangeHistoryResult.recordset[0]; + const pendingEmailChangeResult = loginChangeHistoryResult.recordset[0]; + + const userId = pendingEmailChangeResult && pendingEmailChangeResult.userId; + const confirmEmailRequest = pool.request(); confirmEmailRequest.input("email", mssql.NVarChar(100), email); - if (pendingLoginEmailChange) { - await confirmEmailRequest.execute("Login_ConfirmEmailUpdate"); + if (pendingEmailChangeResult) { + await validateUniqueEmail(email, userId); + await confirmEmailRequest.execute("Login_ConfirmUpdateEmail"); return { isSuccess: true, diff --git a/server/db/migration/V20260827.1208__create_login_email_update.sql b/server/db/migration/V20260827.1208__create_login_email_update.sql index d60cd5a6..2d4a96ec 100644 --- a/server/db/migration/V20260827.1208__create_login_email_update.sql +++ b/server/db/migration/V20260827.1208__create_login_email_update.sql @@ -23,11 +23,11 @@ GO -CREATE TABLE [dbo].[LoginUpdateHistory] +CREATE TABLE [dbo].[LoginEmailChangeHistory] ( [id] [int] IDENTITY(1,1) NOT NULL, - [userId] [int] NOT NULL INDEX IX_LoginUpdateHistory_UserId NONCLUSTERED, - [requestedEmail] [nvarchar](100) NOT NULL INDEX IX_LoginUpdateHistory_RequestedEmail NONCLUSTERED, + [userId] [int] NOT NULL INDEX IX_LoginEmailChangeHistory_UserId NONCLUSTERED, + [requestedEmail] [nvarchar](100) NOT NULL INDEX IX_LoginEmailChangeHistory_RequestedEmail NONCLUSTERED, [activeEmail] [nvarchar](100) NULL, [lastActiveEmail] [nvarchar](100) NULL, [dateRequested] [datetime2](7) NOT NULL DEFAULT (SYSUTCDATETIME()), @@ -50,7 +50,7 @@ GO SET QUOTED_IDENTIFIER ON GO -CREATE OR ALTER PROCEDURE [dbo].[LoginUpdateHistory_Insert] +CREATE OR ALTER PROCEDURE [dbo].[LoginEmailChangeHistory_Insert] @userId INT, @requestedEmail NVARCHAR(100), @currentEmail NVARCHAR(100) @@ -62,12 +62,12 @@ BEGIN BEGIN TRANSACTION; -- Invalidate any prior pending change requests for this user - DELETE FROM [dbo].[LoginUpdateHistory] + DELETE FROM [dbo].[LoginEmailChangeHistory] WHERE [userId] = @userId AND [dateChanged] IS NULL; - INSERT INTO [dbo].[LoginUpdateHistory] + INSERT INTO [dbo].[LoginEmailChangeHistory] ( [userId], [requestedEmail], @@ -111,7 +111,7 @@ BEGIN -- Find the user ID for most recent change request SELECT TOP (1) @userId = [userId] - FROM [dbo].[LoginUpdateHistory] + FROM [dbo].[LoginEmailChangeHistory] WHERE [requestedEmail] = @email AND [dateChanged] IS NULL ORDER BY [dateRequested] DESC; @@ -133,8 +133,8 @@ BEGIN [emailConfirmed] = 1 WHERE [id] = @userId; - -- Update history record in LoginUpdateHistory table - UPDATE [dbo].[LoginUpdateHistory] + -- Update history record in LoginEmailChangeHistory table + UPDATE [dbo].[LoginEmailChangeHistory] SET [lastActiveEmail] = [activeEmail], [activeEmail] = @email, @@ -162,7 +162,7 @@ GO SET QUOTED_IDENTIFIER ON GO -CREATE OR ALTER PROCEDURE [dbo].[LoginUpdateHistory_SelectByRecentRequestedEmail] +CREATE OR ALTER PROCEDURE [dbo].[LoginEmailChangeHistory_SelectByRecentPendingEmail] @requestedEmail NVARCHAR(100) AS BEGIN @@ -176,13 +176,77 @@ BEGIN [lastActiveEmail], [dateRequested], [dateChanged] - FROM [dbo].[LoginUpdateHistory] + FROM [dbo].[LoginEmailChangeHistory] WHERE [requestedEmail] = @requestedEmail ORDER BY [dateRequested] DESC; END; GO +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO + +CREATE OR ALTER PROCEDURE [dbo].[Login_SelectByEmailAndPendingEmail] + @email NVARCHAR(100) +AS +BEGIN + SET NOCOUNT ON; + + -- Check active account in Login table + IF EXISTS (SELECT 1 FROM [dbo].[Login] WHERE [email] = @email) + BEGIN + SELECT + [id], + [firstName], + [lastName], + [email], + [dateCreated], + [emailConfirmed], + [isAdmin], + [passwordHash], + [isSecurityAdmin], + [archivedAt], + [isDro] + FROM [dbo].[Login] + WHERE [email] = @email; + + RETURN; + END; + + -- Check if pending change request in LoginEmailChangeHistory + DECLARE @userId INT; + + SELECT TOP (1) + @userId = [userId] + FROM [dbo].[LoginEmailChangeHistory] + WHERE [requestedEmail] = @email + AND [dateChanged] IS NULL + ORDER BY [dateRequested] DESC; + + -- If found as a pending request, return the user record using userId + IF @userId IS NOT NULL + BEGIN + SELECT + [id], + [firstName], + [lastName], + [email], + [dateCreated], + [emailConfirmed], + [isAdmin], + [passwordHash], + [isSecurityAdmin], + [archivedAt], + [isDro] + FROM [dbo].[Login] + WHERE [id] = @userId; + END; +END; +GO + + CREATE OR ALTER PROCEDURE [dbo].[DeleteUserAndProjects] @id INT AS @@ -206,7 +270,7 @@ BEGIN DELETE FROM [dbo].[Project] WHERE [loginId] = @id; DELETE FROM [dbo].[LoginHistory] WHERE [loginId] = @id; - DELETE FROM [dbo].[LoginUpdateHistory] WHERE [userId] = @id; + DELETE FROM [dbo].[LoginEmailChangeHistory] WHERE [userId] = @id; -- Delete the parent user record DELETE FROM [dbo].[Login] WHERE [id] = @id; From 6a218f52306bdbce13b1bd92a459011432788f2c Mon Sep 17 00:00:00 2001 From: arshiamasih Date: Fri, 28 Aug 2026 08:52:46 -0700 Subject: [PATCH 07/11] Fix pending update to also update login table --- server/app/services/account.service.js | 44 +++++++++---------- ...260827.1208__create_login_email_update.sql | 11 ++++- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/server/app/services/account.service.js b/server/app/services/account.service.js index 847c2c16..4936c045 100644 --- a/server/app/services/account.service.js +++ b/server/app/services/account.service.js @@ -149,14 +149,13 @@ const updateAccount = async model => { request.input("LastName", mssql.NVarChar, model.lastName); await request.execute("Login_Update"); // update user profile (name) - const updatedUser = await selectByEmail(model.email); // get updated user data // if requesting email change, upddate change history log send email verification request if (user.email !== model.email) { const token = crypto.randomUUID(); - const emailChangeRequest = pool.request(); + emailChangeRequest.input("id", mssql.Int, model.id); emailChangeRequest.input("RequestedEmail", mssql.NVarChar, model.email); emailChangeRequest.input("ActiveEmail", mssql.NVarChar, user.email); @@ -291,39 +290,38 @@ const confirmRegistration = async token => { const email = resultSet[0].email; // Check for an active pending change request - const loginChangeHistoryRequest = pool.request(); - loginChangeHistoryRequest.input("email", mssql.NVarChar(100), email); - const loginChangeHistoryResult = await loginChangeHistoryRequest.execute( + const historyRequest = pool.request(); + historyRequest.input("email", mssql.NVarChar(100), email); + + const historyResult = await historyRequest.execute( "LoginEmailChangeHistory_SelectByRecentPendingEmail" ); - const pendingEmailChangeResult = loginChangeHistoryResult.recordset[0]; - - const userId = pendingEmailChangeResult && pendingEmailChangeResult.userId; + const pendingEmailChange = historyResult.recordset[0]; - const confirmEmailRequest = pool.request(); - confirmEmailRequest.input("email", mssql.NVarChar(100), email); + const confirmRequest = pool.request(); + confirmRequest.input("email", mssql.NVarChar(100), email); - if (pendingEmailChangeResult) { + if (pendingEmailChange) { + const userId = pendingEmailChange.userId; await validateUniqueEmail(email, userId); - await confirmEmailRequest.execute("Login_ConfirmUpdateEmail"); - - return { - isSuccess: true, - code: "EMAIL_UPDATE_SUCCESS", - message: "Email change confirmed successfully.", - email - }; - } else { - // First-time registration - await confirmEmailRequest.execute("Login_ConfirmEmail"); + await confirmRequest.execute("Login_ConfirmUpdateEmail"); return { isSuccess: true, code: "REG_CONFIRM_SUCCESS", - message: "Email confirmed successfully.", + message: "Email change confirmed successfully.", email }; } + // First-time registration + await confirmRequest.execute("Login_ConfirmEmail"); + + return { + isSuccess: true, + code: "REG_CONFIRM_SUCCESS", + message: "Email confirmed successfully.", + email + }; } catch (err) { return { isSuccess: false, diff --git a/server/db/migration/V20260827.1208__create_login_email_update.sql b/server/db/migration/V20260827.1208__create_login_email_update.sql index 2d4a96ec..01588d59 100644 --- a/server/db/migration/V20260827.1208__create_login_email_update.sql +++ b/server/db/migration/V20260827.1208__create_login_email_update.sql @@ -53,7 +53,7 @@ GO CREATE OR ALTER PROCEDURE [dbo].[LoginEmailChangeHistory_Insert] @userId INT, @requestedEmail NVARCHAR(100), - @currentEmail NVARCHAR(100) + @activeEmail NVARCHAR(100) AS BEGIN SET NOCOUNT ON; @@ -80,12 +80,19 @@ BEGIN ( @userId, @requestedEmail, - @currentEmail, -- Active at time of request + @activeEmail, -- Active at time of request NULL, -- NULL until updated SYSUTCDATETIME(), NULL ); + + -- Reset confirmation status in Login table + UPDATE [dbo].[Login] + SET [emailConfirmed] = 0 + WHERE [email] = @activeEmail + AND [id] = @userId; + COMMIT TRANSACTION; END TRY BEGIN CATCH From 330119f518a34746ffea30eb98aa62caad7969cc Mon Sep 17 00:00:00 2001 From: arshiamasih Date: Wed, 9 Sep 2026 13:44:07 -0700 Subject: [PATCH 08/11] Fix email change request user id input and update migration file name --- .../src/components/Authorization/UpdateAccount.jsx | 7 ++++++- server/app/services/account.service.js | 12 ++++++------ ...=> V20260909.1208__create_login_email_update.sql} | 0 3 files changed, 12 insertions(+), 7 deletions(-) rename server/db/migration/{V20260827.1208__create_login_email_update.sql => V20260909.1208__create_login_email_update.sql} (100%) diff --git a/client/src/components/Authorization/UpdateAccount.jsx b/client/src/components/Authorization/UpdateAccount.jsx index f0b7403e..e2761312 100644 --- a/client/src/components/Authorization/UpdateAccount.jsx +++ b/client/src/components/Authorization/UpdateAccount.jsx @@ -64,11 +64,16 @@ const UpdateAccount = props => { if (response.code === "ACCOUNT_EMAIL_UPDATE_SUCCESS") { setSubmitted(true); - userContext.updateAccount({}); + userContext.updateAccount(response.user); return; } switch (response.code) { + case "ACCOUNT_EMAIL_UPDATE_SUCCESS": + setSuccessMsg(response.message); + userContext.updateAccount(response.user); + break; + case "ACCOUNT_UPDATE_SUCCESS": setSuccessMsg(response.message); userContext.updateAccount(response.user); diff --git a/server/app/services/account.service.js b/server/app/services/account.service.js index 4936c045..9438c859 100644 --- a/server/app/services/account.service.js +++ b/server/app/services/account.service.js @@ -151,29 +151,29 @@ const updateAccount = async model => { await request.execute("Login_Update"); // update user profile (name) const updatedUser = await selectByEmail(model.email); // get updated user data - // if requesting email change, upddate change history log send email verification request + // if requesting email change, update email change history if (user.email !== model.email) { const token = crypto.randomUUID(); const emailChangeRequest = pool.request(); - emailChangeRequest.input("id", mssql.Int, model.id); + emailChangeRequest.input("UserId", mssql.Int, model.id); emailChangeRequest.input("RequestedEmail", mssql.NVarChar, model.email); emailChangeRequest.input("ActiveEmail", mssql.NVarChar, user.email); - await request.execute("LoginEmailChangeHistory_Insert"); + await emailChangeRequest.execute("LoginEmailChangeHistory_Insert"); await handleVerifyUpdateConfirmation(model.email, token); return { isSuccess: true, code: "ACCOUNT_EMAIL_UPDATE_SUCCESS", - message: "Account updates succeeded." + message: "Account updates successful." }; } return { isSuccess: true, code: "ACCOUNT_UPDATE_SUCCESS", - message: "Account updates succeeded.", + message: "Account updates successful.", user: { id: updatedUser.id, firstName: updatedUser.firstName, @@ -291,7 +291,7 @@ const confirmRegistration = async token => { // Check for an active pending change request const historyRequest = pool.request(); - historyRequest.input("email", mssql.NVarChar(100), email); + historyRequest.input("RequestedEmail", mssql.NVarChar(100), email); const historyResult = await historyRequest.execute( "LoginEmailChangeHistory_SelectByRecentPendingEmail" diff --git a/server/db/migration/V20260827.1208__create_login_email_update.sql b/server/db/migration/V20260909.1208__create_login_email_update.sql similarity index 100% rename from server/db/migration/V20260827.1208__create_login_email_update.sql rename to server/db/migration/V20260909.1208__create_login_email_update.sql From 68669c8581978da9f622e4d141487a667ee51f25 Mon Sep 17 00:00:00 2001 From: arshiamasih Date: Wed, 9 Sep 2026 14:45:09 -0700 Subject: [PATCH 09/11] Fix account update service to get user id on any update and Update Account component --- .../Authorization/UpdateAccount.jsx | 7 +----- server/app/services/account.service.js | 24 +++++++++++++------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/client/src/components/Authorization/UpdateAccount.jsx b/client/src/components/Authorization/UpdateAccount.jsx index e2761312..f5462dd1 100644 --- a/client/src/components/Authorization/UpdateAccount.jsx +++ b/client/src/components/Authorization/UpdateAccount.jsx @@ -63,17 +63,12 @@ const UpdateAccount = props => { ); if (response.code === "ACCOUNT_EMAIL_UPDATE_SUCCESS") { - setSubmitted(true); userContext.updateAccount(response.user); + setSubmitted(true); return; } switch (response.code) { - case "ACCOUNT_EMAIL_UPDATE_SUCCESS": - setSuccessMsg(response.message); - userContext.updateAccount(response.user); - break; - case "ACCOUNT_UPDATE_SUCCESS": setSuccessMsg(response.message); userContext.updateAccount(response.user); diff --git a/server/app/services/account.service.js b/server/app/services/account.service.js index 9438c859..f584e9c6 100644 --- a/server/app/services/account.service.js +++ b/server/app/services/account.service.js @@ -148,17 +148,18 @@ const updateAccount = async model => { request.input("FirstName", mssql.NVarChar, model.firstName); request.input("LastName", mssql.NVarChar, model.lastName); - await request.execute("Login_Update"); // update user profile (name) - const updatedUser = await selectByEmail(model.email); // get updated user data + await request.execute("Login_Update"); - // if requesting email change, update email change history + const updatedUser = await selectById(model.id); + + // If requesting email change, record history if (user.email !== model.email) { const token = crypto.randomUUID(); const emailChangeRequest = pool.request(); - emailChangeRequest.input("UserId", mssql.Int, model.id); - emailChangeRequest.input("RequestedEmail", mssql.NVarChar, model.email); - emailChangeRequest.input("ActiveEmail", mssql.NVarChar, user.email); + emailChangeRequest.input("userId", mssql.Int, model.id); + emailChangeRequest.input("requestedEmail", mssql.NVarChar, model.email); + emailChangeRequest.input("activeEmail", mssql.NVarChar, user.email); await emailChangeRequest.execute("LoginEmailChangeHistory_Insert"); await handleVerifyUpdateConfirmation(model.email, token); @@ -166,7 +167,16 @@ const updateAccount = async model => { return { isSuccess: true, code: "ACCOUNT_EMAIL_UPDATE_SUCCESS", - message: "Account updates successful." + message: "Account updates successful.", + user: { + id: updatedUser.id, + firstName: updatedUser.firstName, + lastName: updatedUser.lastName, + email: updatedUser.email, // Note: this is still the active/old email until verified + isAdmin: updatedUser.isAdmin, + emailConfirmed: updatedUser.emailConfirmed, + isSecurityAdmin: updatedUser.isSecurityAdmin + } }; } From 51a2cc72aa0115a90b24f42792a5c968ff2b2c72 Mon Sep 17 00:00:00 2001 From: arshiamasih Date: Thu, 10 Sep 2026 12:56:55 -0700 Subject: [PATCH 10/11] Separate and create handle email change function and add token insert to fix confirmation registration email --- server/app/services/account.service.js | 79 ++++++++++++++------------ 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/server/app/services/account.service.js b/server/app/services/account.service.js index f584e9c6..0898b5b7 100644 --- a/server/app/services/account.service.js +++ b/server/app/services/account.service.js @@ -130,6 +130,38 @@ const handleVerifyUpdateConfirmation = async (email, token) => { } }; +const handleEmailAccountUpdate = async (model, user) => { + const token = crypto.randomUUID(); + + const tokenRequest = pool.request(); + tokenRequest.input("token", mssql.NVarChar, token); + tokenRequest.input("email", mssql.NVarChar, model.email); + await tokenRequest.execute("SecurityToken_Insert"); + + const emailChangeRequest = pool.request(); + emailChangeRequest.input("userId", mssql.Int, model.id); + emailChangeRequest.input("requestedEmail", mssql.NVarChar, model.email); + emailChangeRequest.input("activeEmail", mssql.NVarChar, user.email); + await emailChangeRequest.execute("LoginEmailChangeHistory_Insert"); + + await handleVerifyUpdateConfirmation(model.email, token); + + return { + isSuccess: true, + code: "ACCOUNT_EMAIL_UPDATE_SUCCESS", + message: "Account updates successful.", + user: { + id: user.id, + firstName: model.firstName, + lastName: model.lastName, + email: user.email, // remains current email until verified + isAdmin: user.isAdmin, + emailConfirmed: user.emailConfirmed, // reset to false by the stored procedure + isSecurityAdmin: user.isSecurityAdmin + } + }; +}; + const updateAccount = async model => { try { const user = await selectById(model.id); @@ -143,55 +175,32 @@ const updateAccount = async model => { await validateUniqueEmail(model.email, model.id); await poolConnect; + + // Update names const request = pool.request(); request.input("id", mssql.Int, model.id); request.input("FirstName", mssql.NVarChar, model.firstName); request.input("LastName", mssql.NVarChar, model.lastName); - await request.execute("Login_Update"); - const updatedUser = await selectById(model.id); - - // If requesting email change, record history + // Email change flow if (user.email !== model.email) { - const token = crypto.randomUUID(); - const emailChangeRequest = pool.request(); - - emailChangeRequest.input("userId", mssql.Int, model.id); - emailChangeRequest.input("requestedEmail", mssql.NVarChar, model.email); - emailChangeRequest.input("activeEmail", mssql.NVarChar, user.email); - - await emailChangeRequest.execute("LoginEmailChangeHistory_Insert"); - await handleVerifyUpdateConfirmation(model.email, token); - - return { - isSuccess: true, - code: "ACCOUNT_EMAIL_UPDATE_SUCCESS", - message: "Account updates successful.", - user: { - id: updatedUser.id, - firstName: updatedUser.firstName, - lastName: updatedUser.lastName, - email: updatedUser.email, // Note: this is still the active/old email until verified - isAdmin: updatedUser.isAdmin, - emailConfirmed: updatedUser.emailConfirmed, - isSecurityAdmin: updatedUser.isSecurityAdmin - } - }; + return await handleEmailAccountUpdate(model, user); } + // Name-only update flow return { isSuccess: true, code: "ACCOUNT_UPDATE_SUCCESS", message: "Account updates successful.", user: { - id: updatedUser.id, - firstName: updatedUser.firstName, - lastName: updatedUser.lastName, - email: updatedUser.email, - isAdmin: updatedUser.isAdmin, - emailConfirmed: updatedUser.emailConfirmed, - isSecurityAdmin: updatedUser.isSecurityAdmin + id: user.id, + firstName: model.firstName, + lastName: model.lastName, + email: user.email, + isAdmin: user.isAdmin, + emailConfirmed: user.emailConfirmed, + isSecurityAdmin: user.isSecurityAdmin } }; } catch (err) { From 05110365ef41c832a1d8862d6affe78252bfde1e Mon Sep 17 00:00:00 2001 From: arshiamasih Date: Thu, 10 Sep 2026 16:00:00 -0700 Subject: [PATCH 11/11] Update email confirmed logic to preserve authentication integrity on logins while pending new email request --- server/app/services/account.service.js | 2 +- ...260909.1208__create_login_email_update.sql | 62 ++++++++----------- 2 files changed, 26 insertions(+), 38 deletions(-) diff --git a/server/app/services/account.service.js b/server/app/services/account.service.js index 0898b5b7..82495fb1 100644 --- a/server/app/services/account.service.js +++ b/server/app/services/account.service.js @@ -156,7 +156,7 @@ const handleEmailAccountUpdate = async (model, user) => { lastName: model.lastName, email: user.email, // remains current email until verified isAdmin: user.isAdmin, - emailConfirmed: user.emailConfirmed, // reset to false by the stored procedure + emailConfirmed: user.emailConfirmed, // remains confirmed to allow authorized login on current email isSecurityAdmin: user.isSecurityAdmin } }; diff --git a/server/db/migration/V20260909.1208__create_login_email_update.sql b/server/db/migration/V20260909.1208__create_login_email_update.sql index 01588d59..315cbe10 100644 --- a/server/db/migration/V20260909.1208__create_login_email_update.sql +++ b/server/db/migration/V20260909.1208__create_login_email_update.sql @@ -22,7 +22,10 @@ END GO - +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO CREATE TABLE [dbo].[LoginEmailChangeHistory] ( [id] [int] IDENTITY(1,1) NOT NULL, @@ -38,13 +41,6 @@ CREATE TABLE [dbo].[LoginEmailChangeHistory] GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO - - - SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON @@ -87,11 +83,6 @@ BEGIN ); - -- Reset confirmation status in Login table - UPDATE [dbo].[Login] - SET [emailConfirmed] = 0 - WHERE [email] = @activeEmail - AND [id] = @userId; COMMIT TRANSACTION; END TRY @@ -104,6 +95,11 @@ BEGIN END; GO + +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO CREATE OR ALTER PROCEDURE [dbo].[Login_ConfirmUpdateEmail] @email NVARCHAR(100) AS @@ -115,7 +111,7 @@ BEGIN DECLARE @userId INT; - -- Find the user ID for most recent change request + -- Find the user ID for the most recent pending change request SELECT TOP (1) @userId = [userId] FROM [dbo].[LoginEmailChangeHistory] @@ -123,33 +119,25 @@ BEGIN AND [dateChanged] IS NULL ORDER BY [dateRequested] DESC; - - IF @userId IS NULL BEGIN - RAISERROR('No pending email update request found for this email.', 16, 1); - ROLLBACK TRANSACTION; - RETURN; + ;THROW 50001, 'No pending email update request found for this email.', 1; END; - - BEGIN - -- Update the target user record in Login table - UPDATE [dbo].[Login] - SET [email] = @email, - [emailConfirmed] = 1 - WHERE [id] = @userId; - - -- Update history record in LoginEmailChangeHistory table - UPDATE [dbo].[LoginEmailChangeHistory] - SET - [lastActiveEmail] = [activeEmail], - [activeEmail] = @email, - [dateChanged] = SYSUTCDATETIME() - WHERE [requestedEmail] = @email - AND [userId] = @userId - AND [dateChanged] IS NULL; - END + -- Update Login table with verified email + UPDATE [dbo].[Login] + SET [email] = @email + WHERE [id] = @userId; + + -- Mark history record as completed + UPDATE [dbo].[LoginEmailChangeHistory] + SET + [lastActiveEmail] = [activeEmail], + [activeEmail] = @email, + [dateChanged] = SYSUTCDATETIME() + WHERE [requestedEmail] = @email + AND [userId] = @userId + AND [dateChanged] IS NULL; COMMIT TRANSACTION;