Skip to content

Commit 06fcf79

Browse files
authored
🛂 feat: Social Login by Provider ID First then Email (danny-avila#10358)
1 parent c9e1127 commit 06fcf79

5 files changed

Lines changed: 381 additions & 6 deletions

File tree

api/strategies/appleStrategy.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ describe('Apple Login Strategy', () => {
304304
fileStrategy: 'local',
305305
balance: { enabled: false },
306306
}),
307+
'jane.doe@example.com',
307308
);
308309
});
309310

api/strategies/process.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,25 @@ const { resizeAvatar } = require('~/server/services/Files/images/avatar');
55
const { updateUser, createUser, getUserById } = require('~/models');
66

77
/**
8-
* Updates the avatar URL of an existing user. If the user's avatar URL does not include the query parameter
8+
* Updates the avatar URL and email of an existing user. If the user's avatar URL does not include the query parameter
99
* '?manual=true', it updates the user's avatar with the provided URL. For local file storage, it directly updates
1010
* the avatar URL, while for other storage types, it processes the avatar URL using the specified file strategy.
11+
* Also updates the email if it has changed (e.g., when a Google Workspace email is updated).
1112
*
1213
* @param {IUser} oldUser - The existing user object that needs to be updated.
1314
* @param {string} avatarUrl - The new avatar URL to be set for the user.
1415
* @param {AppConfig} appConfig - The application configuration object.
16+
* @param {string} [email] - Optional. The new email address to update if it has changed.
1517
*
1618
* @returns {Promise<void>}
17-
* The function updates the user's avatar and saves the user object. It does not return any value.
19+
* The function updates the user's avatar and/or email and saves the user object. It does not return any value.
1820
*
1921
* @throws {Error} Throws an error if there's an issue saving the updated user object.
2022
*/
21-
const handleExistingUser = async (oldUser, avatarUrl, appConfig) => {
23+
const handleExistingUser = async (oldUser, avatarUrl, appConfig, email) => {
2224
const fileStrategy = appConfig?.fileStrategy ?? process.env.CDN_PROVIDER;
2325
const isLocal = fileStrategy === FileSources.local;
26+
const updates = {};
2427

2528
let updatedAvatar = false;
2629
const hasManualFlag =
@@ -39,7 +42,16 @@ const handleExistingUser = async (oldUser, avatarUrl, appConfig) => {
3942
}
4043

4144
if (updatedAvatar) {
42-
await updateUser(oldUser._id, { avatar: updatedAvatar });
45+
updates.avatar = updatedAvatar;
46+
}
47+
48+
/** Update email if it has changed */
49+
if (email && email.trim() !== oldUser.email) {
50+
updates.email = email.trim();
51+
}
52+
53+
if (Object.keys(updates).length > 0) {
54+
await updateUser(oldUser._id, updates);
4355
}
4456
};
4557

api/strategies/process.test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,76 @@ describe('handleExistingUser', () => {
167167
// This should throw an error when trying to access oldUser._id
168168
await expect(handleExistingUser(null, avatarUrl)).rejects.toThrow();
169169
});
170+
171+
it('should update email when it has changed', async () => {
172+
const oldUser = {
173+
_id: 'user123',
174+
email: 'old@example.com',
175+
avatar: 'https://example.com/avatar.png?manual=true',
176+
};
177+
const avatarUrl = 'https://example.com/avatar.png';
178+
const newEmail = 'new@example.com';
179+
180+
await handleExistingUser(oldUser, avatarUrl, {}, newEmail);
181+
182+
expect(updateUser).toHaveBeenCalledWith('user123', { email: 'new@example.com' });
183+
});
184+
185+
it('should update both avatar and email when both have changed', async () => {
186+
const oldUser = {
187+
_id: 'user123',
188+
email: 'old@example.com',
189+
avatar: null,
190+
};
191+
const avatarUrl = 'https://example.com/new-avatar.png';
192+
const newEmail = 'new@example.com';
193+
194+
await handleExistingUser(oldUser, avatarUrl, {}, newEmail);
195+
196+
expect(updateUser).toHaveBeenCalledWith('user123', {
197+
avatar: avatarUrl,
198+
email: 'new@example.com',
199+
});
200+
});
201+
202+
it('should not update email when it has not changed', async () => {
203+
const oldUser = {
204+
_id: 'user123',
205+
email: 'same@example.com',
206+
avatar: 'https://example.com/avatar.png?manual=true',
207+
};
208+
const avatarUrl = 'https://example.com/avatar.png';
209+
const sameEmail = 'same@example.com';
210+
211+
await handleExistingUser(oldUser, avatarUrl, {}, sameEmail);
212+
213+
expect(updateUser).not.toHaveBeenCalled();
214+
});
215+
216+
it('should trim email before comparison and update', async () => {
217+
const oldUser = {
218+
_id: 'user123',
219+
email: 'test@example.com',
220+
avatar: 'https://example.com/avatar.png?manual=true',
221+
};
222+
const avatarUrl = 'https://example.com/avatar.png';
223+
const newEmailWithSpaces = ' newemail@example.com ';
224+
225+
await handleExistingUser(oldUser, avatarUrl, {}, newEmailWithSpaces);
226+
227+
expect(updateUser).toHaveBeenCalledWith('user123', { email: 'newemail@example.com' });
228+
});
229+
230+
it('should not update when email parameter is not provided', async () => {
231+
const oldUser = {
232+
_id: 'user123',
233+
email: 'test@example.com',
234+
avatar: 'https://example.com/avatar.png?manual=true',
235+
};
236+
const avatarUrl = 'https://example.com/avatar.png';
237+
238+
await handleExistingUser(oldUser, avatarUrl, {});
239+
240+
expect(updateUser).not.toHaveBeenCalled();
241+
});
170242
});

api/strategies/socialLogin.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,24 @@ const socialLogin =
2525
return cb(error);
2626
}
2727

28-
const existingUser = await findUser({ email: email.trim() });
28+
const providerKey = `${provider}Id`;
29+
let existingUser = null;
30+
31+
/** First try to find user by provider ID (e.g., googleId, facebookId) */
32+
if (id && typeof id === 'string') {
33+
existingUser = await findUser({ [providerKey]: id });
34+
}
35+
36+
/** If not found by provider ID, try finding by email */
37+
if (!existingUser) {
38+
existingUser = await findUser({ email: email?.trim() });
39+
if (existingUser) {
40+
logger.warn(`[${provider}Login] User found by email: ${email} but not by ${providerKey}`);
41+
}
42+
}
2943

3044
if (existingUser?.provider === provider) {
31-
await handleExistingUser(existingUser, avatarUrl, appConfig);
45+
await handleExistingUser(existingUser, avatarUrl, appConfig, email);
3246
return cb(null, existingUser);
3347
} else if (existingUser) {
3448
logger.info(

0 commit comments

Comments
 (0)