Repository: 3drepo/3drepo.io
Pull request #5457: Issue 5438: IDP profile update
Author: login: Amantini1997
Created: 2025-03-12T14:48:48Z | Merged: 2025-03-27T14:14:08Z

This fixes #5438 

#### Description
- Resetting password now sends a link (notice how also the tab has been renamed as it only does one thing)
- User signed up via "invite user" now are created without a last name and country code. When they try to log in, they are forced to fill those values in.
**Note** first name exists and it's populated with the same value as the email. The field does not get cleared as they first reach the edit profile, and this is intended

#### Test cases
- Reset the password;
- Sign up with a new user and try to log in, you should see the edit profile opening on the user info. That modal can't be close until the info is correctly updated


Linked issue #5438: IDP: profile update
### Description
Product issue: https://github.com/3drepo/3D-Repo-Product-Team/issues/638
Parent issue: https://github.com/3drepo/3drepo.io/issues/5356
Requires : https://github.com/3drepo/3drepo.io/issues/5412

Resetting passords needs adjusting after #5412. 

Password reset will now be handled through email. The profile UI should have a button to trigger the email via this route: https://github.com/3drepo/3drepo.io/commit/68e6ca2e5dfafad58b50d6c9cea0b1ca70f0211e#diff-4d0831edcd220a5cbd7cbe3efad98da07335dd0992306b9600a2e2d56a2b7b97R105-R343

Users created through invite(#5437) - we will not know what is their full name either. When a user logs in and their profile data does not containing the necessary information (i.e. FirstName, lastName, Country), we want to prompt the user to fill that in before proceeding.

### Goals
- [ ] As a user I want to be able to reset my password
- [ ] As a 3DR admin I want to ensure that we know of the user's name and basic information before we allow them to use the platform.

### Tasks
- Frontend
  - TBC by frontend staff
- Backend
  - [x] Do not allow the user to change their email
  - [x] Create endpoint to trigger reset password
    - [x] Clean up any associated endpoints if they're still around


Linked issue comments: user: login: carmenfan
created_at: 2025-03-27T17:18:23Z
body: Merged into #5356 
author_association: MEMBER

Diff:
diff --git a/backend/src/v5/middleware/dataConverter/inputs/users.js b/backend/src/v5/middleware/dataConverter/inputs/users.js
index 3bbb4265fc4..952fd18c8a4 100644
--- a/backend/src/v5/middleware/dataConverter/inputs/users.js
+++ b/backend/src/v5/middleware/dataConverter/inputs/users.js
@@ -17,53 +17,25 @@
 
 const { createResponseCode, templates } = require('../../../utils/responseCodes');
 const Yup = require('yup');
-const { getUserFromSession } = require('../../../utils/sessions');
+const { deleteIfUndefined } = require('../../../utils/helper/objects');
 const { respond } = require('../../../utils/responder');
 const { types } = require('../../../utils/helper/yup');
 
 const Users = {};
 
-const generateUpdateSchema = () => {
-	const schema = Yup.object().shape({
-		oldPassword: Yup.string().optional().when('newPassword', {
-			is: (newPass) => newPass?.length > 0,
-			then: Yup.string().required(),
-		}),
-		newPassword: types.strings.password.optional().when('oldPassword', {
-			is: (oldPass) => oldPass?.length > 0,
-			then: types.strings.password.required(),
-		}).test({
-			name: 'notTheSameAsOldPassword',
-			exclusive: false,
-			params: {},
-			message: 'New password must be different than the old one',
-			test(value) {
-				const { oldPassword } = this.parent;
-				return !oldPassword || (value !== oldPassword);
-			},
-		}),
-		firstName: types.strings.name,
-		lastName: types.strings.name,
+const schema = Yup.object().shape({
+	firstName: types.strings.name,
+	lastName: types.strings.name,
 
-		company: types.strings.title,
-		countryCode: types.strings.countryCode.optional(),
-	}, [['oldPassword', 'newPassword']]).strict(true).noUnknown()
-		.test(
-			'at-least-one-property',
-			'You must provide at least one setting value',
-			(value) => Object.keys(value).length,
-		)
-		.required();
-
-	return schema;
-};
+	company: types.strings.title,
+	countryCode: types.strings.countryCode.optional(),
+}).required();
 
 Users.validateUpdateData = async (req, res, next) => {
 	try {
-		const username = getUserFromSession(req.session);
-		const schema = generateUpdateSchema(username);
-		await schema.validate(req.body);
+		req.body = deleteIfUndefined(await schema.validate(req.body, { stripUnknown: true }));
 
+		if (!Object.keys(req.body).length) throw new Error('Nothing to update');
 		await next();
 	} catch (err) {
 		respond(req, res, createResponseCode(templates.invalidArguments, err?.message));
diff --git a/backend/src/v5/models/users.js b/backend/src/v5/models/users.js
index e7f5121577a..b2cc3ef2ce2 100644
--- a/backend/src/v5/models/users.js
+++ b/backend/src/v5/models/users.js
@@ -106,11 +106,6 @@ User.deleteFavourites = async (username, teamspace, favouritesToRemove) => {
 	}
 };
 
-User.updatePassword = async (username, newPassword) => {
-	await db.setPassword(username, newPassword);
-	await updateUser(username, { $unset: { 'customData.resetPasswordToken': 1 } });
-};
-
 User.updateProfile = async (username, updatedProfile) => {
 	const updateData = {};
 	const billingInfoFields = ['countryCode', 'company'];
diff --git a/backend/src/v5/processors/users.js b/backend/src/v5/processors/users.js
index 01d9b8f43c3..6ecb6836208 100644
--- a/backend/src/v5/processors/users.js
+++ b/backend/src/v5/processors/users.js
@@ -18,16 +18,18 @@
 const Users = {};
 
 const { AVATARS_COL_NAME, USERS_DB_NAME } = require('../models/users.constants');
-const { addUser, deleteApiKey, generateApiKey,
-	getUserByUsername, removeUser, updatePassword, updateProfile } = require('../models/users');
+const { addUser, deleteApiKey, generateApiKey, getUserByUsername,
+	removeUser, updatePassword, updateProfile } = require('../models/users');
 const { fileExists, getFile, removeFile, storeFile } = require('../services/filesManager');
-const { isEmpty, removeFields } = require('../utils/helper/objects');
 const { events } = require('../services/eventsManager/eventsManager.constants');
 const { generateHashString } = require('../utils/helper/strings');
 const { generateUserHash } = require('../services/intercom');
+const { logger } = require('../utils/logger');
 const { publish } = require('../services/eventsManager/eventsManager');
 const { removeAllUserNotifications } = require('../models/notifications');
 const { removeAllUserRecords } = require('../models/loginRecords');
+const { templates } = require('../utils/responseCodes');
+const { triggerPasswordReset } = require('../services/sso/frontegg');
 
 // This is used for the situation where a user has a record from
 // the IDP but we don't have a matching record in the db. We need
@@ -93,14 +95,20 @@ Users.getProfileByUsername = async (username) => {
 	};
 };
 
-Users.updateProfile = async (username, updatedProfile) => {
-	if (updatedProfile.oldPassword) {
-		await updatePassword(username, updatedProfile.newPassword);
-	}
+Users.updateProfile = async (username, fieldsToUpdate) => {
+	await updateProfile(username, fieldsToUpdate);
+};
+
+Users.resetPassword = async (user) => {
+	try {
+		const { customData: { email } } = await getUserByUsername(user, {
+			'customData.email': 1,
+		});
+		await triggerPasswordReset(email);
+	} catch (err) {
+		logger.logError(`Failed to reset password: ${err.message}`);
 
-	const fieldsToUpdate = removeFields(updatedProfile, 'oldPassword', 'newPassword');
-	if (!isEmpty(fieldsToUpdate)) {
-		await updateProfile(username, fieldsToUpdate);
+		throw templates.unknown;
 	}
 };
 
diff --git a/backend/src/v5/routes/users.js b/backend/src/v5/routes/users.js
index fcf39fe8e23..9538acec68a 100644
--- a/backend/src/v5/routes/users.js
+++ b/backend/src/v5/routes/users.js
@@ -103,11 +103,20 @@ const uploadAvatar = async (req, res) => {
 	}
 };
 
+const resetPassword = async (req, res) => {
+	try {
+		const user = getUserFromSession(req.session);
+		await Users.resetPassword(user);
+		respond(req, res, templates.ok);
+	} catch (err) {
+		// istanbul ignore next
+		respond(req, res, err);
+	}
+};
+
 const establishRoutes = () => {
 	const router = Router({ mergeParams: true });
 
-	router.post('/login', routeDecommissioned());
-
 	/**
 	 * @openapi
 	 * /logout:
@@ -226,11 +235,6 @@ const establishRoutes = () => {
 	*                 type: string
 	*                 description: The last name of the user (applies only to non SSO users)
 	*                 example: Voorhees
-	*               email:
-	*                 type: string
-	*                 description: The email of the user (applies only to non SSO users)
-	*                 example: jason@vorhees.com
-	*                 format: email
 	*               company:
 	*                 type: string
 	*                 description: Name of the company
@@ -239,16 +243,7 @@ const establishRoutes = () => {
 	*                 type: string
 	*                 description: Country Code
 	*                 example: GB
-	*               oldPassword:
-	*                 type: string
-	*                 description: The old password of the user (applies only to non SSO users)
-	*                 example: password12345
-    *                 format: password
-	*               newPassword:
-	*                 type: string
-	*                 description: The new password of the user (applies only to non SSO users)
-	*                 example: password12345
-    *                 format: password
+
 	*     responses:
 	*       401:
 	*         $ref: "#/components/responses/notLoggedIn"
@@ -341,13 +336,27 @@ const establishRoutes = () => {
 	*/
 	router.put('/user/avatar', isLoggedIn, singleImageUpload('file'), uploadAvatar);
 
-	router.post('/user/password', routeDecommissioned());
+	/**
+	* @openapi
+	* /user/password/reset:
+	*   post:
+	*     description: Triggers a password reset email to the user
+	*     tags: [User]
+	*     operationId: resetPassword
+	*     responses:
+	*       401:
+	*         $ref: "#/components/responses/notLoggedIn"
+	*       200:
+	*         description: A password reset email will be sent to the logged in user
+	*/
+	router.post('/user/password/reset', isLoggedIn, resetPassword);
 
+	router.post('/login', routeDecommissioned());
+	router.post('/user/password', routeDecommissioned());
 	router.put('/user/password', routeDecommissioned());
-
 	router.post('/user', routeDecommissioned());
-
 	router.post('/user/verify', routeDecommissioned());
+
 	return router;
 };
 
diff --git a/backend/src/v5/services/sso/frontegg/components/users.js b/backend/src/v5/services/sso/frontegg/components/users.js
index 4be73ca34b5..73f35bb9a7d 100644
--- a/backend/src/v5/services/sso/frontegg/components/users.js
+++ b/backend/src/v5/services/sso/frontegg/components/users.js
@@ -67,4 +67,10 @@ Users.createUser = async (accountId, email, name, userData, privateUserData, byp
 	}
 };
 
+Users.triggerPasswordReset = async (email) => {
+	const config = await getConfig();
+	const url = `${config.vendorDomain}/identity/resources/users/v1/passwords/reset`;
+	await post(url, { email }, { headers: await getBearerHeader() });
+};
+
 module.exports = Users;
diff --git a/backend/src/v5/utils/helper/yup.js b/backend/src/v5/utils/helper/yup.js
index 7791e15203a..813db2053bb 100644
--- a/backend/src/v5/utils/helper/yup.js
+++ b/backend/src/v5/utils/helper/yup.js
@@ -20,7 +20,6 @@ const { fileExtensionFromBuffer, isString, isUUID, isUUIDString } = require('./t
 const Yup = require('yup');
 const { fileUploads } = require('../config');
 const tz = require('countries-and-timezones');
-const zxcvbn = require('zxcvbn');
 
 const YupHelper = { validators: {}, types: { strings: {} }, utils: {} };
 
@@ -97,17 +96,6 @@ YupHelper.types.surveyPoints = Yup.array()
 YupHelper.types.strings.unit = Yup.string()
 	.oneOf(['mm', 'cm', 'dm', 'm', 'ft']);
 
-YupHelper.types.strings.password = Yup.string().max(65)
-	.test('checkPasswordStrength', 'Password is too weak',
-		(value) => {
-			if (value) {
-				if (value.length < 8) return false;
-				const passwordScore = zxcvbn(value).score;
-				return passwordScore >= 2;
-			}
-			return true;
-		});
-
 YupHelper.types.strings.email = Yup.string().email();
 
 YupHelper.types.strings.name = Yup.string().min(1).max(35);
diff --git a/backend/tests/v5/e2e/routes/users.test.js b/backend/tests/v5/e2e/routes/users.test.js
index dcab447c05d..8672c19d440 100644
--- a/backend/tests/v5/e2e/routes/users.test.js
+++ b/backend/tests/v5/e2e/routes/users.test.js
@@ -26,6 +26,8 @@ const { templates } = require(`${src}/utils/responseCodes`);
 let server;
 let agent;
 
+const FronteggService = require(`${src}/services/sso/frontegg`);
+
 // This is the user being used for tests
 const testUser = ServiceHelper.generateUserCredentials();
 const userWithFsAvatar = ServiceHelper.generateUserCredentials();
@@ -201,12 +203,6 @@ const testUpdateProfile = () => {
 				expect(res.body.code).toEqual(templates.invalidArguments.code);
 			});
 
-			test('should fail if the update data have extra properties', async () => {
-				const data = { firstName: 'newName', extra: 'extraProp' };
-				const res = await testSession.put('/v5/user/').send(data).expect(templates.invalidArguments.status);
-				expect(res.body.code).toEqual(templates.invalidArguments.code);
-			});
-
 			test('should update the profile if the user is logged in', async () => {
 				const data = { firstName: 'newName', company: 'newCompany', countryCode: 'GR' };
 				await testSession.put('/v5/user/').send(data).expect(200);
@@ -358,6 +354,32 @@ const testDeleteApiKey = () => {
 	});
 };
 
+const testTriggerResetPassword = () => {
+	describe('Trigger reset password (Known user)', () => {
+		const route = '/v5/user/password/reset';
+		test('Should fail the user is not logged in', async () => {
+			const res = await agent.post(route).send({})
+				.expect(templates.notLoggedIn.status);
+			expect(res.body.code).toEqual(templates.notLoggedIn.code);
+		});
+
+		test('Should fail the user is not logged in (api key)', async () => {
+			const res = await agent.post(`${route}?key=${testUser.apiKey}`).send({})
+				.expect(templates.notLoggedIn.status);
+			expect(res.body.code).toEqual(templates.notLoggedIn.code);
+		});
+
+		test('Should trigger email if the user is logged in', async () => {
+			const testSession = SessionTracker(agent);
+			await testSession.login(testUser);
+			await testSession.post(route).send({})
+				.expect(templates.ok.status);
+			expect(FronteggService.triggerPasswordReset).toHaveBeenCalledTimes(1);
+			expect(FronteggService.triggerPasswordReset).toHaveBeenCalledWith(testUser.basicData.email);
+		});
+	});
+};
+
 const testForgotPassword = () => {
 	describe('Send forgot password email', () => {
 		test(`should fail if ${templates.endpointDecommissioned.code}`, async () => {
@@ -412,6 +434,7 @@ describe(ServiceHelper.determineTestGroup(__filename), () => {
 	testLogout();
 	testGetUsername();
 	testGetProfile();
+	testTriggerResetPassword();
 	testUpdateProfile();
 	testGetAvatar();
 	testUploadAvatar();
diff --git a/backend/tests/v5/helper/fronteggMock/components/users.js b/backend/tests/v5/helper/fronteggMock/components/users.js
index f8650bd1628..72bffdeccdd 100644
--- a/backend/tests/v5/helper/fronteggMock/components/users.js
+++ b/backend/tests/v5/helper/fronteggMock/components/users.js
@@ -42,4 +42,6 @@ Users.createUser = (accountId, email, name) => {
 	usersById[data.email] = data;
 	return Promise.resolve(data.id);
 };
+
+Users.triggerPasswordReset = process.env.NODE_ENV === 'testV5' ? jest.fn() : (() => {});
 module.exports = Users;
diff --git a/backend/tests/v5/unit/middleware/dataConverter/inputs/users.test.js b/backend/tests/v5/unit/middleware/dataConverter/inputs/users.test.js
index 3b62a9bed4f..3b8b3ae6e9d 100644
--- a/backend/tests/v5/unit/middleware/dataConverter/inputs/users.test.js
+++ b/backend/tests/v5/unit/middleware/dataConverter/inputs/users.test.js
@@ -40,7 +40,6 @@ WebRequests.post.mockImplementation(() => Promise.resolve({
 const availableUsername = 'nonExistingUser';
 const existingUsername = 'existingUsername';
 const availableEmail = 'availableEmail@email.com';
-const validPassword = 'Abcdef12345!';
 
 UsersModel.getUserByQuery.mockImplementation((query) => {
 	if ((query.$or && query.$or[0]?.user === availableUsername) || query['customData.email'] === availableEmail) {
@@ -69,23 +68,16 @@ UsersModel.getUserByEmail.mockImplementation((email) => {
 
 const testValidateUpdateData = () => {
 	describe.each([
-		[{ body: { firstName: generateRandomString(100) } }, false, 'with too large firstName', templates.invalidArguments],
-		[{ body: { lastName: generateRandomString(100) } }, false, 'with too large lastName', templates.invalidArguments],
-		[{ body: { email: availableEmail, extraProp: 'extra' } }, false, 'with extra properties', templates.invalidArguments],
-		[{ body: { company: '' } }, false, 'with empty company', templates.invalidArguments],
-		[{ body: { company: generateRandomString() } }, true, 'with company'],
-		[{ body: { countryCode: 'invalid country' } }, false, 'with invalid country', templates.invalidArguments],
-		[{ body: { countryCode: 'GB' } }, true, 'with valid country'],
-		[{ body: { oldPassword: validPassword } }, false, 'with oldPassword but not newPassword', templates.invalidArguments],
-		[{ body: { newPassword: 'Abcdef123456!' } }, false, 'with newPassword but not oldPassword', templates.invalidArguments],
-		[{ body: { oldPassword: validPassword, newPassword: 'abc' } }, false, 'with short newPassword', templates.invalidArguments],
-		[{ body: { oldPassword: validPassword, newPassword: 'abcdefghi' } }, false, 'with weak newPassword', templates.invalidArguments],
-		[{ body: { oldPassword: validPassword, newPassword: 'Abcdef123!Abcdef123!Abcdef123!Abcdef123!Abcdef123!Abcdef123!Abcdef' } }, false, 'with too long newPassword', templates.invalidArguments],
-		[{ body: { oldPassword: validPassword, newPassword: validPassword } }, false, 'with newPassword same as old', templates.invalidArguments],
-		[{ body: { oldPassword: validPassword, newPassword: 'Abcdef12345!!' } }, true, 'with strong newPassword'],
-		[{ body: {} }, false, 'with empty body', templates.invalidArguments],
-		[{ body: undefined }, false, 'with undefined body', templates.invalidArguments],
-	])('Check if req arguments for updating profile are valid', (data, shouldPass, desc, expectedError) => {
+		[{ body: { firstName: generateRandomString(100) } }, false, false, 'with too large firstName', templates.invalidArguments],
+		[{ body: { lastName: generateRandomString(100) } }, false, false, 'with too large lastName', templates.invalidArguments],
+		[{ body: { email: availableEmail } }, false, false, 'with email', templates.invalidArguments],
+		[{ body: { company: '' } }, false, false, 'with empty company', templates.invalidArguments],
+		[{ body: { company: generateRandomString() } }, false, true, 'with company'],
+		[{ body: { countryCode: 'invalid country' } }, false, false, 'with invalid country', templates.invalidArguments],
+		[{ body: { countryCode: 'GB' } }, false, true, 'with valid country'],
+		[{ body: {} }, false, false, 'with empty body', templates.invalidArguments],
+		[{ body: undefined }, false, false, 'with undefined body', templates.invalidArguments],
+	])('Check if req arguments for updating profile are valid', (data, isSso, shouldPass, desc, expectedError) => {
 		test(`${desc} ${shouldPass ? ' should call next()' : `should respond with ${expectedError.code}`}`, async () => {
 			const mockCB = jest.fn();
 			const req = { ...cloneDeep(data), session: { user: { username: existingUsername } } };
diff --git a/backend/tests/v5/unit/models/users.test.js b/backend/tests/v5/unit/models/users.test.js
index cdb12a7728f..dcb26a61f57 100644
--- a/backend/tests/v5/unit/models/users.test.js
+++ b/backend/tests/v5/unit/models/users.test.js
@@ -247,22 +247,6 @@ const testDeleteFromFavourites = () => {
 	});
 };
 
-const testUpdatePassword = () => {
-	describe('Update user password', () => {
-		test('should update a user password', async () => {
-			const fn1 = jest.spyOn(db, 'updateOne').mockImplementationOnce(() => { });
-			const fn2 = jest.spyOn(db, 'setPassword').mockImplementationOnce(() => { });
-			const user = generateRandomString();
-			const newPassword = generateRandomString();
-			await expect(User.updatePassword(user, newPassword)).resolves.toBeUndefined();
-			expect(fn1).toHaveBeenCalledTimes(1);
-			expect(fn1).toHaveBeenCalledWith(USERS_DB_NAME, USERS_COL, { user }, { $unset: { 'customData.resetPasswordToken': 1 } });
-			expect(fn2).toHaveBeenCalledTimes(1);
-			expect(fn2).toHaveBeenCalledWith(user, newPassword);
-		});
-	});
-};
-
 const testUpdateProfile = () => {
 	describe('Update user profile', () => {
 		test('should update a user profile', async () => {
@@ -526,7 +510,6 @@ describe(determineTestGroup(__filename), () => {
 	testUpdateProfile();
 	testGenerateApiKey();
 	testDeleteApiKey();
-	testUpdatePassword();
 	testGetUserByUsernameOrEmail();
 	testGetUserByEmail();
 	testGetUsersByQuery();
diff --git a/backend/tests/v5/unit/processors/users.test.js b/backend/tests/v5/unit/processors/users.test.js
index 038571849d7..1bf8dcc35bc 100644
--- a/backend/tests/v5/unit/processors/users.test.js
+++ b/backend/tests/v5/unit/processors/users.test.js
@@ -40,6 +40,9 @@ const Notifications = require(`${src}/models/notifications`);
 jest.mock('../../../../src/v5/services/intercom');
 const Intercom = require(`${src}/services/intercom`);
 
+jest.mock('../../../../src/v5/services/sso/frontegg');
+const FronteggService = require(`${src}/services/sso/frontegg`);
+
 const { generateRandomString, determineTestGroup } = require('../../helper/services');
 
 const user = {
@@ -149,23 +152,6 @@ const tesUpdateProfile = () => {
 			expect(UsersModel.updateProfile.mock.calls.length).toBe(1);
 			expect(UsersModel.updateProfile.mock.calls[0][1]).toEqual(updatedProfile);
 		});
-
-		test('should update user profile and password', async () => {
-			const updatedProfile = { firstName: 'Nick', oldPassword: 'oldPass', newPassword: 'newPass' };
-			await Users.updateProfile(user.user, updatedProfile);
-			expect(UsersModel.updateProfile.mock.calls.length).toBe(1);
-			expect(UsersModel.updateProfile.mock.calls[0][1]).toEqual({ firstName: 'Nick' });
-			expect(UsersModel.updatePassword.mock.calls.length).toBe(1);
-			expect(UsersModel.updatePassword.mock.calls[0][1]).toEqual('newPass');
-		});
-
-		test('should update password', async () => {
-			const updatedProfile = { oldPassword: 'oldPass', newPassword: 'newPass' };
-			await Users.updateProfile(user.user, updatedProfile);
-			expect(UsersModel.updateProfile.mock.calls.length).toBe(0);
-			expect(UsersModel.updatePassword.mock.calls.length).toBe(1);
-			expect(UsersModel.updatePassword.mock.calls[0][1]).toEqual('newPass');
-		});
 	});
 };
 
@@ -320,6 +306,36 @@ const testCreateNewUserRecord = () => {
 	});
 };
 
+const testResetPassword = () => {
+	describe('Reset password', () => {
+		test('Should get the email from the database then trigger a password reset', async () => {
+			const username = generateRandomString();
+			const email = generateRandomString();
+			UsersModel.getUserByUsername.mockResolvedValueOnce({ customData: { email } });
+
+			await Users.resetPassword(username);
+
+			expect(UsersModel.getUserByUsername).toHaveBeenCalledTimes(1);
+			expect(UsersModel.getUserByUsername).toHaveBeenCalledWith(username, { 'customData.email': 1 });
+
+			expect(FronteggService.triggerPasswordReset).toHaveBeenCalledTimes(1);
+			expect(FronteggService.triggerPasswordReset).toHaveBeenCalledWith(email);
+		});
+
+		test(`Should throw with ${templates.unknown.code} if it failed`, async () => {
+			const username = generateRandomString();
+			UsersModel.getUserByUsername.mockRejectedValueOnce({ message: generateRandomString() });
+
+			await expect(Users.resetPassword(username)).rejects.toEqual(templates.unknown);
+
+			expect(UsersModel.getUserByUsername).toHaveBeenCalledTimes(1);
+			expect(UsersModel.getUserByUsername).toHaveBeenCalledWith(username, { 'customData.email': 1 });
+
+			expect(FronteggService.triggerPasswordReset).not.toHaveBeenCalled();
+		});
+	});
+};
+
 describe(determineTestGroup(__filename), () => {
 	tesGetProfileByUsername();
 	tesUpdateProfile();
@@ -327,4 +343,5 @@ describe(determineTestGroup(__filename), () => {
 	testUploadAvatar();
 	testRemoveUser();
 	testCreateNewUserRecord();
+	testResetPassword();
 });
diff --git a/backend/tests/v5/unit/services/sso/frontegg/components/users.test.js b/backend/tests/v5/unit/services/sso/frontegg/components/users.test.js
index 6c1fa11768e..08a55e87e95 100644
--- a/backend/tests/v5/unit/services/sso/frontegg/components/users.test.js
+++ b/backend/tests/v5/unit/services/sso/frontegg/components/users.test.js
@@ -160,8 +160,33 @@ const testCreateUser = () => {
 	});
 };
 
+const testTriggerPasswordReset = () => {
+	describe('Trigger password reset', () => {
+		test('Should trigger password reset with the email provided', async () => {
+			const email = generateRandomString();
+
+			await Users.triggerPasswordReset(email);
+
+			expect(WebRequests.post).toHaveBeenCalledTimes(1);
+			expect(WebRequests.post).toHaveBeenCalledWith(expect.any(String), { email }, postOptions);
+		});
+
+		test('Should throw error it failed to trigger password reset', async () => {
+			const email = generateRandomString();
+
+			WebRequests.post.mockRejectedValueOnce({ message: generateRandomString() });
+
+			await expect(Users.triggerPasswordReset(email)).rejects.not.toBeUndefined();
+
+			expect(WebRequests.post).toHaveBeenCalledTimes(1);
+			expect(WebRequests.post).toHaveBeenCalledWith(expect.any(String), { email }, postOptions);
+		});
+	});
+};
+
 describe(determineTestGroup(__filename), () => {
 	testGetUserById();
 	testDoesUserExist();
 	testCreateUser();
+	testTriggerPasswordReset();
 });
diff --git a/frontend/src/v5/services/api/auth.ts b/frontend/src/v5/services/api/auth.ts
index bf3098084c9..cc9f45e356b 100644
--- a/frontend/src/v5/services/api/auth.ts
+++ b/frontend/src/v5/services/api/auth.ts
@@ -28,6 +28,6 @@ export const login = (user, password): Promise<AxiosResponse<void>> => api.post(
 
 export const logout = (): Promise<AxiosResponse<void>> => api.post('logout');
 
-export const resetPassword = (user) => api.post('user/password', { user });
+export const resetPassword = () => api.post('user/password/reset');
 
 export const changePassword = (user, newPassword, token) => api.put('user/password', { user, newPassword, token });
diff --git a/frontend/src/v5/store/users/users.helpers.ts b/frontend/src/v5/store/users/users.helpers.ts
index 255d1a6fb54..ea41ec1e029 100644
--- a/frontend/src/v5/store/users/users.helpers.ts
+++ b/frontend/src/v5/store/users/users.helpers.ts
@@ -37,3 +37,5 @@ export const getDefaultUserNotFound = (name: string): IUser => ({
 	avatarUrl: '',
 	user: name,
 });
+
+export const userHasMissingRequiredData = ({ lastName }: ICurrentUser) => !lastName;
diff --git a/frontend/src/v5/ui/components/shared/sso/microsoftButton.component.tsx b/frontend/src/v5/ui/components/shared/sso/microsoftButton.component.tsx
index 666a5a2ff92..49961e0febc 100644
--- a/frontend/src/v5/ui/components/shared/sso/microsoftButton.component.tsx
+++ b/frontend/src/v5/ui/components/shared/sso/microsoftButton.component.tsx
@@ -17,13 +17,10 @@
 
 import styled from 'styled-components';
 import { Button } from '@controls/button';
-import MicrosoftIcon from '@assets/icons/thirdParty/microsoft.svg';
-import { createElement } from 'react';
 
 export const MicrosoftButton = styled(Button).attrs({
 	variant: 'contained',
 	color: 'primary',
-	startIcon: createElement(MicrosoftIcon),
 })`
 	display: flex;
 	width: fit-content;
diff --git a/frontend/src/v5/ui/components/shared/sso/newUserHandler/newUserHandler.component.tsx b/frontend/src/v5/ui/components/shared/sso/newUserHandler/newUserHandler.component.tsx
new file mode 100644
index 00000000000..849fa1c22ff
--- /dev/null
+++ b/frontend/src/v5/ui/components/shared/sso/newUserHandler/newUserHandler.component.tsx
@@ -0,0 +1,35 @@
+/**
+ *  Copyright (C) 2023 3D Repo Ltd
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as
+ *  published by the Free Software Foundation, either version 3 of the
+ *  License, or (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { DialogsActionsDispatchers } from '@/v5/services/actionsDispatchers';
+import { CurrentUserHooksSelectors } from '@/v5/services/selectorsHooks';
+import { userHasMissingRequiredData } from '@/v5/store/users/users.helpers';
+import { EditProfileModal } from '@components/shared/userMenu/editProfileModal/editProfileModal.component';
+import { useEffect } from 'react';
+
+export const NewUserHandler = () => {
+	const user = CurrentUserHooksSelectors.selectCurrentUser();
+
+	useEffect(() => {
+		if (!user.username) return;
+		if (userHasMissingRequiredData(user)) {
+			DialogsActionsDispatchers.open(EditProfileModal);
+		}
+	}, [user.username]);
+
+	return (<></>);
+};
diff --git a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileAuthenticationTab/editProfileAuthenticationNonSSOTab.component.tsx b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileAuthenticationTab/editProfileAuthenticationNonSSOTab.component.tsx
deleted file mode 100644
index b91e0e0c00e..00000000000
--- a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileAuthenticationTab/editProfileAuthenticationNonSSOTab.component.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-/**
- *  Copyright (C) 2023 3D Repo Ltd
- *
- *  This program is free software: you can redistribute it and/or modify
- *  it under the terms of the GNU Affero General Public License as
- *  published by the Free Software Foundation, either version 3 of the
- *  License, or (at your option) any later version.
- *
- *  This program is distributed in the hope that it will be useful,
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *  GNU Affero General Public License for more details.
- *
- *  You should have received a copy of the GNU Affero General Public License
- *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-import { useFormContext } from 'react-hook-form';
-import { useEffect, useState } from 'react';
-import { formatMessage } from '@/v5/services/intl';
-import { FormattedMessage } from 'react-intl';
-import { SuccessMessage } from '@controls/successMessage/successMessage.component';
-
-import * as API from '@/v5/services/api';
-import { UnhandledError } from '@controls/errorMessage/unhandledError/unhandledError.component';
-import { isPasswordIncorrect } from '@/v5/validation/errors.helpers';
-import { FormPasswordField } from '@controls/inputs/formInputs.component';
-import { FormModalActions } from '@controls/formModal/modalButtons/modalButtons.styles';
-import { ModalCancelButton, ModalSubmitButton } from '@controls/formModal/modalButtons/modalButtons.component';
-import { MicrosoftButton } from '@components/shared/sso/microsoftButton.component';
-import { SSOErrorResponseMessage } from '@components/shared/sso/ssoLinkingResponseHandler/ssoLinkingErrorResponseMessage.component';
-import { Gap } from '@controls/gap';
-import { linkAccount, postActions } from '@/v5/services/api/sso';
-import { useSSOParams } from '@/v5/services/sso.hooks';
-import { TabContent } from '../editProfileModal.styles';
-import { MicrosoftText } from './editProfileAuthenticationTab.styles';
-
-export interface IUpdatePasswordInputs {
-	oldPassword: string;
-	newPassword: string;
-}
-
-type EditProfileAuthenticationNonSSOTabProps = {
-	incorrectPassword: boolean;
-	setIncorrectPassword: (isIncorrect: boolean) => void;
-	unexpectedError: any,
-	onClickClose: () => void,
-};
-
-export const EditProfileAuthenticationNonSSOTab = ({
-	incorrectPassword,
-	setIncorrectPassword,
-	unexpectedError,
-	onClickClose,
-}: EditProfileAuthenticationNonSSOTabProps) => {
-	const [submitWasSuccessful, setSubmitWasSuccessful] = useState(false);
-	const [{ error, action }, resetSSOParams] = useSSOParams();
-	const unlinkPost = action === postActions.UNLINK_POST;
-
-	const {
-		formState: { errors, isValid: formIsValid, isSubmitting, touchedFields },
-		control,
-		trigger,
-		reset,
-		watch,
-		handleSubmit,
-	} = useFormContext();
-
-	const oldPassword = watch('oldPassword');
-	const newPassword = watch('newPassword');
-
-	const onSubmit = async () => {
-		setIncorrectPassword(false);
-		await API.CurrentUser.updateUser({ oldPassword, newPassword });
-		setSubmitWasSuccessful(true);
-		reset();
-		resetSSOParams();
-	};
-
-	const onSubmitError = (apiError) => {
-		setSubmitWasSuccessful(false);
-		if (isPasswordIncorrect(apiError)) {
-			setIncorrectPassword(true);
-		}
-	};
-
-	const onSubmitClick = (event) => handleSubmit(onSubmit)(event).catch(onSubmitError);
-
-	const handleLinkAccount = async () => {
-		const res = await linkAccount();
-		window.location.href = res.data.link;
-	};
-
-	useEffect(() => {
-		if (incorrectPassword && touchedFields.oldPassword) {
-			setIncorrectPassword(false);
-		}
-	}, [oldPassword]);
-
-	// re-trigger validation on oldPassword when incorrect
-	useEffect(() => {
-		if (oldPassword) {
-			trigger('oldPassword');
-		}
-	}, [incorrectPassword]);
-
-	useEffect(() => {
-		trigger(Object.keys(touchedFields) as Array<keyof IUpdatePasswordInputs>);
-	}, [oldPassword, newPassword]);
-
-	return (
-		<>
-			<TabContent>
-				{unlinkPost && !error && (
-					<SuccessMessage>
-						<FormattedMessage
-							id="editProfile.authentication.unlinkWithMicrosoft.success"
-							defaultMessage="You have successfully unlinked your Microsoft account with 3D Repo."
-						/>
-					</SuccessMessage>
-				)}
-				<SSOErrorResponseMessage />
-				{unlinkPost && (<Gap $height="10px" />)}
-				<FormPasswordField
-					control={control}
-					name="oldPassword"
-					label={formatMessage({
-						id: 'editProfile.form.oldPassword',
-						defaultMessage: 'Current Password',
-					})}
-					formError={errors.oldPassword}
-					required
-				/>
-				<FormPasswordField
-					control={control}
-					name="newPassword"
-					label={formatMessage({
-						id: 'editProfile.form.newPassword',
-						defaultMessage: 'New Password',
-					})}
-					formError={errors.newPassword}
-					required
-					autoComplete="new-password"
-				/>
-				<UnhandledError
-					error={unexpectedError}
-					expectedErrorValidators={[isPasswordIncorrect]}
-				/>
-				{submitWasSuccessful && (
-					<SuccessMessage>
-						<FormattedMessage
-							id="editProfile.form.updatePasswordSuccess"
-							defaultMessage="Your password has been changed successfully."
-						/>
-					</SuccessMessage>
-				)}
-				<Gap />
-				<MicrosoftText
-					title={formatMessage({
-						id: 'editProfile.authentication.linkAccountWithMicrosoft.title',
-						defaultMessage: 'Link account with Microsoft',
-					})}
-				/>
-				<MicrosoftButton onClick={handleLinkAccount}>
-					<FormattedMessage id="editProfile.authentication.signInWithMicrosoft.button" defaultMessage="Link account with Microsoft" />
-				</MicrosoftButton>
-			</TabContent>
-			<FormModalActions>
-				<ModalCancelButton onClick={onClickClose} />
-				<ModalSubmitButton disabled={!formIsValid} onClick={onSubmitClick} isPending={isSubmitting}>
-					<FormattedMessage
-						defaultMessage="Update password"
-						id="editProfile.authentication.submitButton.updatePassword"
-					/>
-				</ModalSubmitButton>
-			</FormModalActions>
-		</>
-	);
-};
diff --git a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileAuthenticationTab/editProfileAuthenticationSSOTab.component.tsx b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileAuthenticationTab/editProfileAuthenticationSSOTab.component.tsx
deleted file mode 100644
index 02e0997b844..00000000000
--- a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileAuthenticationTab/editProfileAuthenticationSSOTab.component.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-/**
- *  Copyright (C) 2023 3D Repo Ltd
- *
- *  This program is free software: you can redistribute it and/or modify
- *  it under the terms of the GNU Affero General Public License as
- *  published by the Free Software Foundation, either version 3 of the
- *  License, or (at your option) any later version.
- *
- *  This program is distributed in the hope that it will be useful,
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *  GNU Affero General Public License for more details.
- *
- *  You should have received a copy of the GNU Affero General Public License
- *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-import { useFormContext } from 'react-hook-form';
-import { useHistory, useLocation } from 'react-router-dom';
-import { useEffect } from 'react';
-import { formatMessage } from '@/v5/services/intl';
-import { FormattedMessage } from 'react-intl';
-import { SuccessMessage } from '@controls/successMessage/successMessage.component';
-
-import { UnhandledError } from '@controls/errorMessage/unhandledError/unhandledError.component';
-import { isPasswordIncorrect } from '@/v5/validation/errors.helpers';
-import { FormPasswordField } from '@controls/inputs/formInputs.component';
-import { FormModalActions } from '@controls/formModal/modalButtons/modalButtons.styles';
-import { ModalCancelButton, ModalSubmitButton } from '@controls/formModal/modalButtons/modalButtons.component';
-import { postActions, unlinkAccount } from '@/v5/services/api/sso';
-import { MicrosoftInstructionsText } from '@components/shared/sso/microsoftText.styles';
-import { CurrentUserActionsDispatchers } from '@/v5/services/actionsDispatchers';
-import { SSOErrorResponseMessage } from '@components/shared/sso/ssoLinkingResponseHandler/ssoLinkingErrorResponseMessage.component';
-import { Gap } from '@controls/gap';
-import { useSSOParams } from '@/v5/services/sso.hooks';
-import { TabContent, MicrosoftTitleText } from '../editProfileModal.styles';
-
-type IUpdateSSOPasswordInputs = {
-	newPassword: string;
-};
-
-type EditProfileAuthenticationSSOTabProps = {
-	unexpectedError: any,
-	onClickClose: () => void,
-};
-
-export const EditProfileAuthenticationSSOTab = ({
-	unexpectedError,
-	onClickClose,
-}: EditProfileAuthenticationSSOTabProps) => {
-	const [{ error, action }, resetSSOParams] = useSSOParams();
-	const linkPost = action === postActions.LINK_POST;
-
-	const history = useHistory();
-	const { search } = useLocation();
-	const searchParams = new URLSearchParams(search);
-	const {
-		formState: { errors, isValid: formIsValid, isSubmitting, isSubmitSuccessful, touchedFields },
-		control,
-		trigger,
-		watch,
-		handleSubmit,
-		reset,
-	} = useFormContext();
-
-	const newPassword = watch('newPassword');
-
-	const onSubmit = async () => {
-		await unlinkAccount({ password: newPassword });
-		searchParams.append(postActions.UNLINK_POST, '1');
-		resetSSOParams();
-		reset();
-		CurrentUserActionsDispatchers.updateUserSuccess({ sso: null });
-		history.push({ search: searchParams.toString() });
-	};
-
-	useEffect(() => {
-		trigger(Object.keys(touchedFields) as Array<keyof IUpdateSSOPasswordInputs>);
-	}, [newPassword]);
-
-	return (
-		<>
-			<TabContent>
-				{linkPost && !error && (
-					<SuccessMessage>
-						<FormattedMessage
-							id="editProfile.authentication.linkWithMicrosoft.success"
-							defaultMessage={`You have successfully linked your Microsoft account with 3D Repo.
-											Please use the Microsoft button to sign in at log in on your next visit.`}
-						/>
-					</SuccessMessage>
-				)}
-				<SSOErrorResponseMessage />
-				{linkPost && (<Gap $height="28px" />)}
-				<MicrosoftTitleText>
-					<FormattedMessage
-						defaultMessage="Unlink Microsoft Account"
-						id="editProfile.authentication.unlinkMicrosoftAccount.title"
-					/>
-				</MicrosoftTitleText>
-				<MicrosoftInstructionsText>
-					<FormattedMessage
-						defaultMessage={`
-							Your account is currently linked with Microsoft. If you would like to
-							unlink your account, please create a new password and click the unlink button.
-						`}
-						id="editProfile.authentication.unlinkMicrosoftAccount.description"
-					/>
-				</MicrosoftInstructionsText>
-				<FormPasswordField
-					control={control}
-					name="newPassword"
-					label={formatMessage({
-						id: 'editProfile.form.newPassword',
-						defaultMessage: 'New Password',
-					})}
-					formError={errors.newPassword}
-					required
-				/>
-				<UnhandledError
-					error={unexpectedError}
-					expectedErrorValidators={[isPasswordIncorrect]}
-				/>
-				{isSubmitSuccessful && (
-					<SuccessMessage>
-						<FormattedMessage
-							id="editProfile.authentication.unlinkAccount"
-							defaultMessage="Your account has been unlinked successfully."
-						/>
-					</SuccessMessage>
-				)}
-			</TabContent>
-			<FormModalActions>
-				<ModalCancelButton onClick={onClickClose} />
-				<ModalSubmitButton disabled={!formIsValid} onClick={handleSubmit(onSubmit)} isPending={isSubmitting}>
-					<FormattedMessage
-						defaultMessage="Unlink with Microsoft"
-						id="editProfile.authentication.submitButton.unlinkWithMicrosoft"
-					/>
-				</ModalSubmitButton>
-			</FormModalActions>
-		</>
-	);
-};
diff --git a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileAuthenticationTab/editProfileAuthenticationTab.component.tsx b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileAuthenticationTab/editProfileAuthenticationTab.component.tsx
deleted file mode 100644
index bd089e7827e..00000000000
--- a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileAuthenticationTab/editProfileAuthenticationTab.component.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- *  Copyright (C) 2022 3D Repo Ltd
- *
- *  This program is free software: you can redistribute it and/or modify
- *  it under the terms of the GNU Affero General Public License as
- *  published by the Free Software Foundation, either version 3 of the
- *  License, or (at your option) any later version.
- *
- *  This program is distributed in the hope that it will be useful,
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *  GNU Affero General Public License for more details.
- *
- *  You should have received a copy of the GNU Affero General Public License
- *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-import { CurrentUserHooksSelectors } from '@/v5/services/selectorsHooks';
-import { useEffect } from 'react';
-import { useSSOParams } from '@/v5/services/sso.hooks';
-import { EditProfileAuthenticationSSOTab } from './editProfileAuthenticationSSOTab.component';
-import { EditProfileAuthenticationNonSSOTab } from './editProfileAuthenticationNonSSOTab.component';
-
-export interface IUpdatePasswordInputs {
-	oldPassword: string;
-	newPassword: string;
-}
-
-export const EMPTY_PASSWORDS = {
-	oldPassword: '',
-	newPassword: '',
-};
-
-type EditProfileAuthenticationTabProps = {
-	incorrectPassword: boolean;
-	setIncorrectPassword: (isIncorrect: boolean) => void;
-	unexpectedError: any,
-	onClickClose: () => void,
-};
-
-export const EditProfileAuthenticationTab = ({
-	incorrectPassword,
-	setIncorrectPassword,
-	...props
-}: EditProfileAuthenticationTabProps) => {
-	const [, resetSSOParams] = useSSOParams();
-	const { sso } = CurrentUserHooksSelectors.selectCurrentUser();
-
-	useEffect(() => resetSSOParams, []);
-
-	if (sso) return (<EditProfileAuthenticationSSOTab {...props} />);
-
-	return (
-		<EditProfileAuthenticationNonSSOTab
-			incorrectPassword={incorrectPassword}
-			setIncorrectPassword={setIncorrectPassword}
-			{...props}
-		/>
-	);
-};
diff --git a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileIntegrationsTab/editProfileIntegrationsTab.component.tsx b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileIntegrationsTab/editProfileIntegrationsTab.component.tsx
index a294c61e8f3..079d4432260 100644
--- a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileIntegrationsTab/editProfileIntegrationsTab.component.tsx
+++ b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileIntegrationsTab/editProfileIntegrationsTab.component.tsx
@@ -28,7 +28,7 @@ import { TabContent } from '../editProfileModal.styles';
 
 type EditProfileIntegrationsTabProps = {
 	unexpectedError: any,
-	onClickClose: () => void,
+	onClickClose?: () => void,
 };
 
 export const EditProfileIntegrationsTab = ({
@@ -72,7 +72,7 @@ export const EditProfileIntegrationsTab = ({
 				<UnhandledError error={unexpectedError} />
 			</TabContent>
 			<FormModalActions>
-				<ModalCancelButton onClick={onClickClose} />
+				<ModalCancelButton disabled={!onClickClose} onClick={onClickClose} />
 			</FormModalActions>
 		</>
 	);
diff --git a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileModal.component.tsx b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileModal.component.tsx
index f72ec8f4d82..b50ee5a40f7 100644
--- a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileModal.component.tsx
+++ b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileModal.component.tsx
@@ -21,45 +21,45 @@ import { TabContext } from '@mui/lab';
 import { FormProvider, useForm } from 'react-hook-form';
 import { useErrorInterceptor } from '@controls/errorMessage/useErrorInterceptor';
 import { yupResolver } from '@hookform/resolvers/yup';
-import { EditProfileUpdatePasswordSchema, EditProfileUpdatePersonalSchema, EditProfileUpdateSSOPasswordSchema } from '@/v5/validation/userSchemes/editProfileSchemes';
+import { EditProfileUpdatePersonalSchema } from '@/v5/validation/userSchemes/editProfileSchemes';
 import { CurrentUserHooksSelectors } from '@/v5/services/selectorsHooks';
 import { FormModal, TabList, Tab, TabPanel, TruncatableName } from './editProfileModal.styles';
 import { EditProfilePersonalTab, IUpdatePersonalInputs } from './editProfilePersonalTab/editProfilePersonalTab.component';
-import { EditProfileAuthenticationTab, EMPTY_PASSWORDS, IUpdatePasswordInputs } from './editProfileAuthenticationTab/editProfileAuthenticationTab.component';
+import { EditProfilePasswordTab } from './editProfilePasswordTab/editProfilePasswordTab.component';
 import { EditProfileIntegrationsTab } from './editProfileIntegrationsTab/editProfileIntegrationsTab.component';
+import { userHasMissingRequiredData } from '@/v5/store/users/users.helpers';
 
 const PERSONAL_TAB = 'personal';
-const AUTHENTICATION_TAB = 'authentication';
+const PASSWORD_TAB = 'password';
 const INTEGRATIONS_TAB = 'integrations';
 
 const TAB_LABELS = {
 	personal: formatMessage({ defaultMessage: 'Personal', id: 'editProfile.tab.title.personal' }),
-	authentication: formatMessage({ defaultMessage: 'Authentication', id: 'editProfile.tab.title.authentication' }),
+	password: formatMessage({ defaultMessage: 'Password', id: 'editProfile.tab.title.password' }),
 	integrations: formatMessage({ defaultMessage: 'Integrations', id: 'editProfile.tab.title.integrations' }),
 };
 
 type EditProfileUnexpectedErrors = {
 	[PERSONAL_TAB]?: any;
-	[AUTHENTICATION_TAB]?: any;
 	[INTEGRATIONS_TAB]?: any;
 };
 
 type EditProfileModalProps = {
 	open: boolean;
 	onClickClose: () => void;
-	initialTab?: 'authentication' | 'integrations';
+	initialTab?: 'password' | 'integrations';
 };
-export const EditProfileModal = ({ open, onClickClose, initialTab }: EditProfileModalProps) => {
+export const EditProfileModal = ({ open, onClickClose: defaultOnClickClose, initialTab }: EditProfileModalProps) => {
 	const [activeTab, setActiveTab] = useState(initialTab || PERSONAL_TAB);
-	const [incorrectPassword, setIncorrectPassword] = useState(false);
 	const [alreadyExistingEmails, setAlreadyExistingEmails] = useState([]);
 	const [unexpectedErrors, setUnexpectedErrors] = useState<EditProfileUnexpectedErrors>({});
 	const interceptedError = useErrorInterceptor();
 	const user = CurrentUserHooksSelectors.selectCurrentUser();
+	const onClickClose = userHasMissingRequiredData(user) ? null : defaultOnClickClose;
 
 	const defaultPersonalValues = defaults(
 		pick(omitBy(user, isNull), ['firstName', 'lastName', 'email', 'company', 'countryCode']),
-		{ company: '', countryCode: 'GB', avatarFile: '' },
+		{ company: '', avatarFile: '' },
 	);
 
 	const onTabChange = (_, selectedTab) => setActiveTab(selectedTab);
@@ -71,17 +71,7 @@ export const EditProfileModal = ({ open, onClickClose, initialTab }: EditProfile
 		defaultValues: defaultPersonalValues,
 	});
 
-	const authenticationFormData = useForm<IUpdatePasswordInputs>({
-		mode: 'all',
-		resolver: yupResolver(
-			user.sso
-				? EditProfileUpdateSSOPasswordSchema
-				: EditProfileUpdatePasswordSchema(incorrectPassword),
-		),
-		defaultValues: EMPTY_PASSWORDS,
-	});
-
-	const isSubmitting = personalFormData.formState.isSubmitting || authenticationFormData.formState.isSubmitting;
+	const isSubmitting = personalFormData.formState.isSubmitting;
 
 	useEffect(() => {
 		setUnexpectedErrors({ ...unexpectedErrors, [activeTab]: interceptedError });
@@ -101,7 +91,7 @@ export const EditProfileModal = ({ open, onClickClose, initialTab }: EditProfile
 			<TabContext value={activeTab}>
 				<TabList onChange={onTabChange} textColor="primary" indicatorColor="primary">
 					<Tab value={PERSONAL_TAB} label={TAB_LABELS.personal} disabled={isSubmitting} />
-					<Tab value={AUTHENTICATION_TAB} label={TAB_LABELS.authentication} disabled={isSubmitting} />
+					<Tab value={PASSWORD_TAB} label={TAB_LABELS.password} disabled={isSubmitting} />
 					<Tab value={INTEGRATIONS_TAB} label={TAB_LABELS.integrations} disabled={isSubmitting} />
 				</TabList>
 				<FormProvider {...personalFormData}>
@@ -114,16 +104,9 @@ export const EditProfileModal = ({ open, onClickClose, initialTab }: EditProfile
 						/>
 					</TabPanel>
 				</FormProvider>
-				<FormProvider {...authenticationFormData}>
-					<TabPanel value={AUTHENTICATION_TAB}>
-						<EditProfileAuthenticationTab
-							incorrectPassword={incorrectPassword}
-							setIncorrectPassword={setIncorrectPassword}
-							unexpectedError={unexpectedErrors[AUTHENTICATION_TAB]}
-							onClickClose={onClickClose}
-						/>
-					</TabPanel>
-				</FormProvider>
+				<TabPanel value={PASSWORD_TAB}>
+					<EditProfilePasswordTab onClickClose={onClickClose} />
+				</TabPanel>
 				<TabPanel value={INTEGRATIONS_TAB}>
 					<EditProfileIntegrationsTab
 						unexpectedError={unexpectedErrors[INTEGRATIONS_TAB]}
diff --git a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePasswordTab/editProfilePasswordTab.component.tsx b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePasswordTab/editProfilePasswordTab.component.tsx
new file mode 100644
index 00000000000..88d4ec255c5
--- /dev/null
+++ b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePasswordTab/editProfilePasswordTab.component.tsx
@@ -0,0 +1,82 @@
+/**
+ *  Copyright (C) 2025 3D Repo Ltd
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as
+ *  published by the Free Software Foundation, either version 3 of the
+ *  License, or (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { FormattedMessage } from 'react-intl';
+import { FormModalActions } from '@controls/formModal/modalButtons/modalButtons.styles';
+import { ModalCancelButton } from '@controls/formModal/modalButtons/modalButtons.component';
+import { TabContent } from '../editProfileModal.styles';
+import { UnhandledError } from '@controls/errorMessage/unhandledError/unhandledError.component';
+import { resetPassword } from '@/v5/services/api/auth';
+import { useState } from 'react';
+import { SuccessMessage } from '@controls/successMessage/successMessage.component';
+import { Button, Title } from './editProfilePasswordTab.styles';
+import { Gap } from '@controls/gap';
+
+type EditProfilePasswordTabProps = {
+	onClickClose?: () => void,
+};
+	
+export const EditProfilePasswordTab = ({ onClickClose }: EditProfilePasswordTabProps) => {
+	const [status, setStatus] = useState<{ success: boolean, error? }>(null);
+	const [isSubmitting, setIsSubmitting] = useState(false);
+
+	const sendEmail = async () => {
+		setStatus(null);
+		setIsSubmitting(true);
+		try {
+			await resetPassword();
+			setStatus({ success: true });
+		} catch (error) {
+			setStatus({ success: false, error });
+		}
+		setIsSubmitting(false);
+	};
+
+	return (
+		<>
+			<TabContent>
+				<Gap $height='10px' />
+				<Title>
+					<FormattedMessage
+						id="editProfile.passwordTab.title"
+						defaultMessage="Clicking the button below will send an email to your inbox with a link and the instructions to update your password"
+					/>
+				</Title>
+				<Button onClick={sendEmail} isPending={isSubmitting}>
+					<FormattedMessage
+						id="editProfile.passwordTab.changePassword"
+						defaultMessage="Change password"
+					/>
+				</Button>
+				{status?.success && (
+					<SuccessMessage>
+						<FormattedMessage
+							id="editProfile.passwordTab.successMessage"
+							defaultMessage="Link sent"
+						/>
+					</SuccessMessage>
+				)}
+				{status?.error && (
+					<UnhandledError error={status?.error} />
+				)}
+			</TabContent>
+			<FormModalActions>
+				<ModalCancelButton disabled={!onClickClose} onClick={onClickClose} />
+			</FormModalActions>
+		</>
+	);
+};
\ No newline at end of file
diff --git a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileAuthenticationTab/editProfileAuthenticationTab.styles.ts b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePasswordTab/editProfilePasswordTab.styles.ts
similarity index 66%
rename from frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileAuthenticationTab/editProfileAuthenticationTab.styles.ts
rename to frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePasswordTab/editProfilePasswordTab.styles.ts
index 422a47df150..34b3b5976c8 100644
--- a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileAuthenticationTab/editProfileAuthenticationTab.styles.ts
+++ b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePasswordTab/editProfilePasswordTab.styles.ts
@@ -1,5 +1,5 @@
 /**
- *  Copyright (C) 2022 3D Repo Ltd
+ *  Copyright (C) 2025 3D Repo Ltd
  *
  *  This program is free software: you can redistribute it and/or modify
  *  it under the terms of the GNU Affero General Public License as
@@ -14,13 +14,17 @@
  *  You should have received a copy of the GNU Affero General Public License
  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
+
+import { SubmitButton } from '@controls/submitButton';
 import styled from 'styled-components';
-import { MicrosoftText as MicrosoftTextBase } from '@components/shared/sso/microsoftText.component';
-import { MicrosoftTitleText } from '@components/shared/sso/microsoftText.styles';
 
-export const MicrosoftText = styled(MicrosoftTextBase)`
-	${MicrosoftTitleText} {
-		margin-top: 10px;
-		${({ theme }) => theme.typography.h3}
-	}
+export const Button = styled(SubmitButton)`
+	margin-bottom: 0;
+	width: fit-content;
+`;
+
+export const Title = styled.div`
+	${({ theme }) => theme.typography.caption};
+	color: ${({ theme }) => theme.palette.base.main};
+	font-size: 11px;
 `;
diff --git a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx
index 4907e850983..116bad74c38 100644
--- a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx
+++ b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx
@@ -31,6 +31,8 @@ import { FormModalActions } from '@controls/formModal/modalButtons/modalButtons.
 import { ModalCancelButton, ModalSubmitButton } from '@controls/formModal/modalButtons/modalButtons.component';
 import { EditProfileAvatar } from './editProfileAvatar/editProfileAvatar.component';
 import { TabContent } from '../editProfileModal.styles';
+import { userHasMissingRequiredData } from '@/v5/store/users/users.helpers';
+import { WelcomeMessage } from './editProfilePersonalTab.styles';
 
 export type IUpdatePersonalInputs = Partial<{
 	firstName: string;
@@ -45,7 +47,7 @@ type EditProfilePersonalTabProps = {
 	alreadyExistingEmails: string[];
 	setAlreadyExistingEmails: (emails: string[]) => void;
 	unexpectedError: any,
-	onClickClose: () => void,
+	onClickClose?: () => void,
 };
 
 export const EditProfilePersonalTab = ({
@@ -65,6 +67,7 @@ export const EditProfilePersonalTab = ({
 		control,
 		formState: { errors: formErrors, isDirty, touchedFields, isSubmitting },
 	} = useFormContext();
+	const userIsMissingRequiredData = userHasMissingRequiredData(user);
 
 	const getSubmittableValues = (): IUpdatePersonalInputs => {
 		const values = getValues();
@@ -120,6 +123,12 @@ export const EditProfilePersonalTab = ({
 		}
 	}, [JSON.stringify(isDirty), touchedFields]);
 
+	useEffect(() => {
+		if (userIsMissingRequiredData) {
+			trigger();
+		}
+	}, []);
+
 	return (
 		<>
 			<TabContent>
@@ -132,10 +141,19 @@ export const EditProfilePersonalTab = ({
 						/>
 					</SuccessMessage>
 				)}
-				<UnhandledError
-					error={unexpectedError}
-					expectedErrorValidators={[emailAlreadyExists, isFileFormatUnsupported]}
-				/>
+				{userHasMissingRequiredData(user) ? (
+					<WelcomeMessage>
+						<FormattedMessage
+							id="editProfile.form.newUserMessage"
+							defaultMessage="Welcome to 3D Repo, please tell us more about yourself..."
+						/>
+					</WelcomeMessage>
+				) : (
+					<UnhandledError
+						error={unexpectedError}
+						expectedErrorValidators={[emailAlreadyExists, isFileFormatUnsupported]}
+					/>
+				)}
 				<FormTextField
 					name="firstName"
 					control={control}
@@ -195,7 +213,7 @@ export const EditProfilePersonalTab = ({
 				</FormSelect>
 			</TabContent>
 			<FormModalActions>
-				<ModalCancelButton onClick={onClickClose} />
+				<ModalCancelButton disabled={userIsMissingRequiredData} onClick={onClickClose} />
 				<ModalSubmitButton disabled={!canSubmit} onClick={handleSubmit(onSubmit)} isPending={isSubmitting}>
 					<FormattedMessage
 						defaultMessage="Update profile"
diff --git a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.styles.ts b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.styles.ts
new file mode 100644
index 00000000000..d63bf01d11f
--- /dev/null
+++ b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.styles.ts
@@ -0,0 +1,24 @@
+/**
+ *  Copyright (C) 2025 3D Repo Ltd
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as
+ *  published by the Free Software Foundation, either version 3 of the
+ *  License, or (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { PostSubmitSuccessfulMessage } from '@controls/successMessage/successMessage.styles';
+import styled from 'styled-components';
+
+export const WelcomeMessage = styled(PostSubmitSuccessfulMessage)`
+	display: flex;
+	align-items: center;
+`;
\ No newline at end of file
diff --git a/frontend/src/v5/ui/routes/index.tsx b/frontend/src/v5/ui/routes/index.tsx
index 505885c4f9e..37f9fe99c26 100644
--- a/frontend/src/v5/ui/routes/index.tsx
+++ b/frontend/src/v5/ui/routes/index.tsx
@@ -25,6 +25,7 @@ import { IntercomProvider } from 'react-use-intercom';
 import { clientConfigService } from '@/v4/services/clientConfig';
 import { MainRoute } from './dashboard';
 import { V4Adapter } from '../v4Adapter/v4Adapter';
+import { NewUserHandler } from '@components/shared/sso/newUserHandler/newUserHandler.component';
 
 export const Root = () => {
 	const { intercomLicense } = clientConfigService;
@@ -35,6 +36,7 @@ export const Root = () => {
 				<V4Adapter>
 					<IntercomProvider appId={intercomLicense}>
 						<MainRoute />
+						<NewUserHandler />
 						<SSOResponseHandler />
 						<ModalsDispatcher />
 					</IntercomProvider>
diff --git a/frontend/src/v5/ui/routes/login/passwordForgot/passwordForgot.component.tsx b/frontend/src/v5/ui/routes/login/passwordForgot/passwordForgot.component.tsx
index 8387d5e8fbb..57450b9f608 100644
--- a/frontend/src/v5/ui/routes/login/passwordForgot/passwordForgot.component.tsx
+++ b/frontend/src/v5/ui/routes/login/passwordForgot/passwordForgot.component.tsx
@@ -34,7 +34,7 @@ export const PasswordForgot = (): JSX.Element => {
 		defaultValues: { username: '' },
 		resolver: yupResolver(PasswordForgotSchema),
 	});
-	const onSubmit = ({ username }) => API.Auth.resetPassword(username);
+	const onSubmit = () => API.Auth.resetPassword();
 
 	return (
 		<AuthTemplate>
diff --git a/frontend/src/v5/validation/userSchemes/editProfileSchemes.ts b/frontend/src/v5/validation/userSchemes/editProfileSchemes.ts
index 78b479a81c0..4cad4d0008b 100644
--- a/frontend/src/v5/validation/userSchemes/editProfileSchemes.ts
+++ b/frontend/src/v5/validation/userSchemes/editProfileSchemes.ts
@@ -16,8 +16,7 @@
  */
 
 import * as Yup from 'yup';
-import { formatMessage } from '@/v5/services/intl';
-import { firstName, lastName, email, company, countryCode, password } from './validators';
+import { firstName, lastName, email, company, countryCode } from './validators';
 
 export const EditProfileUpdatePersonalSchema = Yup.object().shape({
 	firstName,
@@ -26,33 +25,3 @@ export const EditProfileUpdatePersonalSchema = Yup.object().shape({
 	company,
 	countryCode,
 });
-
-export const EditProfileUpdatePasswordSchema = (incorrectPassword) => Yup.object().shape({
-	oldPassword: Yup.string()
-		.min(1,
-			formatMessage({
-				id: 'editProfile.password.error.empty',
-				defaultMessage: 'Current password is a required field',
-			}))
-		.test(
-			'incorrectPassword',
-			formatMessage({
-				id: 'editProfile.password.error.incorrectPassword',
-				defaultMessage: 'Your existing password was incorrect. Please try again',
-			}),
-			() => !incorrectPassword,
-		),
-	newPassword: password()
-		.test(
-			'newPasswordIsDifferent',
-			formatMessage({
-				id: 'editProfile.password.error.samePassword',
-				defaultMessage: 'New password must be different from the old one',
-			}),
-			(value, testContext) => value !== testContext.parent.oldPassword,
-		),
-});
-
-export const EditProfileUpdateSSOPasswordSchema = Yup.object().shape({
-	newPassword: password(),
-});


Reviews:
user: login: The-Daniel
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T10:53:45Z
user: login: carmenfan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T13:40:27Z
user: login: carmenfan
body: Can we lose the unexpected error message? It's very confusing for someone who logs in for the first time.

Ideally we should replace it with a helpful message that says somethign along the lines of "Welcome to 3DR, please tell us more about yourself..." But if it's too much trouble just keep it empty
![image](https://github.com/user-attachments/assets/c58acd52-388c-4696-962e-ba5f3162fd1d)

state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-17T13:49:28Z
user: login: The-Daniel
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T13:54:04Z
user: login: carmenfan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T14:09:06Z
user: login: Amantini1997
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T17:02:37Z
user: login: carmenfan
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-21T18:41:52Z
user: login: ChristosTsiotsias
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T15:40:52Z
user: login: The-Daniel
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T17:44:58Z
user: login: carmenfan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T09:55:45Z
user: login: Amantini1997
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T13:50:49Z
user: login: The-Daniel
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:41:38Z
user: login: sanmont3drepo
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T21:34:43Z
user: login: carmenfan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T09:44:45Z
user: login: carmenfan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T09:45:02Z
user: login: Amantini1997
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T12:55:46Z
user: login: Amantini1997
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T12:56:12Z
user: login: sanmont3drepo
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T14:10:52Z

Review comments:
path: frontend/src/v5/ui/components/shared/sso/newUserHandler/newUserHandler.component.tsx
user: login: The-Daniel
body: I can't seem to create a new account.
![image](https://github.com/user-attachments/assets/1e32dd46-b711-4292-81f9-30d59a71a323)
![image](https://github.com/user-attachments/assets/481d3eb4-68bb-4a52-b6d9-814685f750f3)
I'm trying to do the password creation in a Chrome incognito page. I'm not sure if this PR needs any further setup that the initial 5.16 migration?
created_at: 2025-03-17T10:49:03Z
author_association: CONTRIBUTOR
path: frontend/src/v5/services/api/auth.ts
user: login: The-Daniel
body: When you get the 'unauthorised for this teamspace' error you get redirected to the teamspace that just errored. Although this isn't terrible as it doesn't cause an endless loop of errormessages, it just seems odd
![image](https://github.com/user-attachments/assets/e7905f0e-ddee-44cc-aab1-d2de4b4cf082)

created_at: 2025-03-17T10:49:39Z
author_association: CONTRIBUTOR
path: frontend/src/v5/ui/components/shared/sso/newUserHandler/newUserHandler.component.tsx
user: login: carmenfan
body: @The-Daniel should work after the migration. what's the email address? you shoudln't even need to activate any account that's migrated
created_at: 2025-03-17T13:40:27Z
author_association: MEMBER
path: frontend/src/v5/ui/components/shared/sso/newUserHandler/newUserHandler.component.tsx
user: login: The-Daniel
body: `daniel.collins+frontegg@3drepo.org` could it be the `+` messing things up?
created_at: 2025-03-17T13:54:04Z
author_association: CONTRIBUTOR
path: frontend/src/v5/ui/components/shared/sso/newUserHandler/newUserHandler.component.tsx
user: login: carmenfan
body: ![image](https://github.com/user-attachments/assets/8d248adc-7809-4ac1-adb9-c0213ac25be7)
dashboard thinks it's activated, have you tried logging in?
created_at: 2025-03-17T14:09:06Z
author_association: MEMBER
path: frontend/src/v5/services/api/auth.ts
user: login: Amantini1997
body: This PR is only about the profile update, so this would be out of scope here, but it has been fixed here https://github.com/3drepo/3drepo.io/pull/5456
created_at: 2025-03-21T17:02:37Z
author_association: CONTRIBUTOR
path: frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx
user: login: The-Daniel
body: ![image](https://github.com/user-attachments/assets/db9361c0-60ae-48a1-a490-326a7d78d43f)
I don't think the "First name" should be auto-filled with the email
created_at: 2025-03-24T15:55:33Z
author_association: CONTRIBUTOR
path: frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx
user: login: The-Daniel
body: Also are you sure it should say 3DR and not 3D Repo?
created_at: 2025-03-24T17:43:03Z
author_association: CONTRIBUTOR
path: frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx
user: login: carmenfan
body: yea it should say 3D Repo 😆 probably my fault for not typing out the message properly on teams

re Firstname, that's frontegg doing it by default so we may as well display it as such for consistency
created_at: 2025-03-25T09:55:45Z
author_association: MEMBER
path: frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx
user: login: Amantini1997
body: I should have probably thought about that actually, my bad!
created_at: 2025-03-25T13:50:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,7 +28,7 @@ import { TabContent } from '../editProfileModal.styles';
 
 type EditProfileIntegrationsTabProps = {
 	unexpectedError: any,
-	onClickClose: () => void,
+	onClickClose?: () => void,
path: frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileIntegrationsTab/editProfileIntegrationsTab.component.tsx
user: login: sanmont3drepo
body: - Would get rid of this userid part  (@carmenfan  I dont know if we care enough but its a bit jarring to me)
- Also wouldnt fill in the first name with the email It can lead to incorrect data filled  in

![image](https://github.com/user-attachments/assets/077c0ead-291e-4cb3-9219-7794e8430032)


created_at: 2025-03-26T20:34:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,7 +34,7 @@ export const PasswordForgot = (): JSX.Element => {
 		defaultValues: { username: '' },
 		resolver: yupResolver(PasswordForgotSchema),
 	});
-	const onSubmit = ({ username }) => API.Auth.resetPassword(username);
+	const onSubmit = () => API.Auth.resetPassword();
path: frontend/src/v5/ui/routes/login/passwordForgot/passwordForgot.component.tsx
user: login: sanmont3drepo
body: I guess this whole component is not being used anymore?
created_at: 2025-03-26T21:12:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,7 +212,7 @@ export const EditProfilePersonalTab = ({
 				</FormSelect>
 			</TabContent>
 			<FormModalActions>
-				<ModalCancelButton onClick={onClickClose} />
+				<ModalCancelButton disabled={!onClickClose} onClick={onClickClose} />
path: frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx
user: login: sanmont3drepo
body: This disable is kind of weird. There is some semantics lost here, I  know its because the user has missing data but is not what the code is describing here.
created_at: 2025-03-26T21:15:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,7 +47,7 @@ type EditProfilePersonalTabProps = {
 	alreadyExistingEmails: string[];
 	setAlreadyExistingEmails: (emails: string[]) => void;
 	unexpectedError: any,
-	onClickClose: () => void,
+	onClickClose?: () => void,
 };
 
 export const EditProfilePersonalTab = ({
path: frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx
user: login: sanmont3drepo
body: My guess is that the email field should be read only here because you cant change it?
created_at: 2025-03-26T21:34:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,7 +28,7 @@ import { TabContent } from '../editProfileModal.styles';
 
 type EditProfileIntegrationsTabProps = {
 	unexpectedError: any,
-	onClickClose: () => void,
+	onClickClose?: () => void,
path: frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileIntegrationsTab/editProfileIntegrationsTab.component.tsx
user: login: carmenfan
body: 1. yes, that's https://github.com/3drepo/3drepo.io/issues/5423 which you guys have merged into staging now
2. not our choice, frontegg auto fill it with email. I got @apeitup to change the mockup on the RBAC invites to take the invitees name so it should be a temporary thing.
created_at: 2025-03-27T09:44:45Z
author_association: MEMBER
diff_hunk: @@ -28,7 +28,7 @@ import { TabContent } from '../editProfileModal.styles';
 
 type EditProfileIntegrationsTabProps = {
 	unexpectedError: any,
-	onClickClose: () => void,
+	onClickClose?: () => void,
path: frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileIntegrationsTab/editProfileIntegrationsTab.component.tsx
user: login: carmenfan
body: resolving this!
created_at: 2025-03-27T09:45:02Z
author_association: MEMBER
diff_hunk: @@ -45,7 +47,7 @@ type EditProfilePersonalTabProps = {
 	alreadyExistingEmails: string[];
 	setAlreadyExistingEmails: (emails: string[]) => void;
 	unexpectedError: any,
-	onClickClose: () => void,
+	onClickClose?: () => void,
 };
 
 export const EditProfilePersonalTab = ({
path: frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx
user: login: Amantini1997
body: This was addressed here https://github.com/3drepo/3drepo.io/pull/5456
created_at: 2025-03-27T12:55:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,7 +34,7 @@ export const PasswordForgot = (): JSX.Element => {
 		defaultValues: { username: '' },
 		resolver: yupResolver(PasswordForgotSchema),
 	});
-	const onSubmit = ({ username }) => API.Auth.resetPassword(username);
+	const onSubmit = () => API.Auth.resetPassword();
path: frontend/src/v5/ui/routes/login/passwordForgot/passwordForgot.component.tsx
user: login: Amantini1997
body: I removed the unused files, including this one, in https://github.com/3drepo/3drepo.io/pull/5456
created_at: 2025-03-27T12:56:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: Amantini1997
created_at: 2025-03-21T17:02:59Z
body: /azp run
author_association: CONTRIBUTOR
user: login: azure-pipelines[bot]
created_at: 2025-03-21T17:03:11Z
body: <samp>
Azure Pipelines successfully started running 2 pipeline(s).<br>

</samp>
author_association: NONE
user: login: ChristosTsiotsias
created_at: 2025-03-24T12:23:21Z
body: ![image](https://github.com/user-attachments/assets/1b764c6c-e558-4710-ace4-e00706211ff8)

I see my user Id on the details popup, is that expected ?
author_association: CONTRIBUTOR
user: login: ChristosTsiotsias
created_at: 2025-03-24T12:30:28Z
body: Also, even though the company can be null when I have a company populated and delete it I cannot save it
author_association: CONTRIBUTOR
user: login: carmenfan
created_at: 2025-03-24T12:31:01Z
body: > ![image](https://private-user-images.githubusercontent.com/71635617/426074914-1b764c6c-e558-4710-ace4-e00706211ff8.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDI4MTk2OTksIm5iZiI6MTc0MjgxOTM5OSwicGF0aCI6Ii83MTYzNTYxNy80MjYwNzQ5MTQtMWI3NjRjNmMtZTU1OC00NzEwLWFjZTQtZTAwNzA2MjExZmY4LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMjQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzI0VDEyMjk1OVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWE3ZmQyN2ZmYTI0Njg0MDU2NjkyYWIwMWNlZTU4MTQzYjM1ODVmOTQ4M2IzY2FiZjRlZDliOTBmNjA2M2UyODYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.NF-NDPZPIXGaGUYg-wGeWg79vjWG4SCtcHyPz4B-1iw)
> 
> I see my user Id on the details popup, is that expected ?

yep. that's because your user ID is used as your username. this will be gone with another branch
author_association: MEMBER
user: login: carmenfan
created_at: 2025-03-24T12:57:27Z
body: > Also, even though the company can be null when I have a company populated and delete it I cannot save it

@Amantini1997 I think that's you
author_association: MEMBER