@@ -20,6 +20,10 @@ export class AuthService {
2020 private _notificationService : NotificationService ,
2121 private _emailService : EmailService
2222 ) { }
23+
24+ private isUnifiedEmailEnabled ( ) : boolean {
25+ return process . env . UNIFIED_EMAIL_ACCOUNTS === 'true' ;
26+ }
2327 async canRegister ( provider : string ) {
2428 if ( process . env . DISABLE_REGISTRATION !== 'true' || provider === Provider . GENERIC ) {
2529 return true ;
@@ -45,6 +49,30 @@ export class AuthService {
4549 throw new Error ( 'Email already exists' ) ;
4650 }
4751
52+ // Check if email exists with any other provider (e.g., Google, GitHub)
53+ // If UNIFIED_EMAIL_ACCOUNTS is enabled, require email verification before adding password
54+ if ( this . isUnifiedEmailEnabled ( ) ) {
55+ const existingUserAnyProvider = await this . _userService . getUserByEmailAnyProvider ( body . email ) ;
56+ if ( existingUserAnyProvider ) {
57+ // Don't set password immediately - require email verification first
58+ // This prevents account hijacking by someone who knows an email
59+ const addPasswordToken = AuthChecker . signJWT ( {
60+ id : existingUserAnyProvider . id ,
61+ email : existingUserAnyProvider . email ,
62+ passwordHash : AuthChecker . hashPassword ( body . password ) ,
63+ type : 'add-password' ,
64+ } ) ;
65+
66+ await this . _emailService . sendEmail (
67+ existingUserAnyProvider . email ,
68+ 'Verify to add password to your account' ,
69+ `Someone is trying to add a password to your account. If this was you, click <a href="${ process . env . FRONTEND_URL } /auth/activate/${ addPasswordToken } ">here</a> to verify and set your password. If you did not request this, please ignore this email.`
70+ ) ;
71+
72+ return { addedOrg : false , jwt : '' , activationRequired : true } ;
73+ }
74+ }
75+
4876 if ( ! ( await this . canRegister ( provider ) ) ) {
4977 throw new Error ( 'Registration is disabled' ) ;
5078 }
@@ -65,24 +93,36 @@ export class AuthService {
6593 )
6694 : false ;
6795
68- const obj = { addedOrg , jwt : await this . jwt ( create . users [ 0 ] . user ) } ;
96+ const jwt = await this . jwt ( create . users [ 0 ] . user ) ;
6997 await this . _emailService . sendEmail (
7098 body . email ,
7199 'Activate your account' ,
72- `Click <a href="${ process . env . FRONTEND_URL } /auth/activate/${ obj . jwt } ">here</a> to activate your account`
100+ `Click <a href="${ process . env . FRONTEND_URL } /auth/activate/${ jwt } ">here</a> to activate your account`
73101 ) ;
74- return obj ;
102+ // Activation required if email provider is configured
103+ const activationRequired = this . _emailService . hasProvider ( ) ;
104+ return { addedOrg, jwt, activationRequired } ;
105+ }
106+
107+ // For login: check LOCAL account first, then any provider with password (if unified emails enabled)
108+ let loginUser = user ;
109+ if ( ! loginUser && this . isUnifiedEmailEnabled ( ) ) {
110+ // Check if there's an OAuth account with this email that has a password set
111+ const oauthUser = await this . _userService . getUserByEmailAnyProvider ( body . email ) ;
112+ if ( oauthUser && oauthUser . password ) {
113+ loginUser = oauthUser ;
114+ }
75115 }
76116
77- if ( ! user || ! AuthChecker . comparePassword ( body . password , user . password ) ) {
117+ if ( ! loginUser || ! loginUser . password || ! AuthChecker . comparePassword ( body . password , loginUser . password ) ) {
78118 throw new Error ( 'Invalid user name or password' ) ;
79119 }
80120
81- if ( ! user . activated ) {
121+ if ( ! loginUser . activated ) {
82122 throw new Error ( 'User is not activated' ) ;
83123 }
84124
85- return { addedOrg : false , jwt : await this . jwt ( user ) } ;
125+ return { addedOrg : false , jwt : await this . jwt ( loginUser ) , activationRequired : false } ;
86126 }
87127
88128 const user = await this . loginOrRegisterProvider (
@@ -101,7 +141,7 @@ export class AuthService {
101141 addToOrg . role
102142 )
103143 : false ;
104- return { addedOrg, jwt : await this . jwt ( user ) } ;
144+ return { addedOrg, jwt : await this . jwt ( user ) , activationRequired : false } ;
105145 }
106146
107147 public getOrgFromCookie ( cookie ?: string ) {
@@ -147,6 +187,21 @@ export class AuthService {
147187 return user ;
148188 }
149189
190+ // Check if there's an existing account with the same email (any provider)
191+ // This allows users with email/password accounts to also sign in via OAuth (if unified emails enabled)
192+ if ( this . isUnifiedEmailEnabled ( ) ) {
193+ const existingUserByEmail = await this . _userService . getUserByEmailAnyProvider (
194+ providerUser . email
195+ ) ;
196+ if ( existingUserByEmail ) {
197+ // If user is not activated, activate them now since OAuth provider verified the email
198+ if ( ! existingUserByEmail . activated ) {
199+ await this . _userService . activateUser ( existingUserByEmail . id ) ;
200+ }
201+ return existingUserByEmail ;
202+ }
203+ }
204+
150205 if ( ! ( await this . canRegister ( provider ) ) ) {
151206 throw new Error ( 'Registration is disabled' ) ;
152207 }
@@ -168,10 +223,24 @@ export class AuthService {
168223 return create . users [ 0 ] . user ;
169224 }
170225
171- async forgot ( email : string ) {
172- const user = await this . _userService . getUserByEmail ( email ) ;
173- if ( ! user || user . providerName !== Provider . LOCAL ) {
174- return false ;
226+ async forgot ( email : string ) : Promise < { success : boolean ; message ?: string } > {
227+ // Check for user with this email
228+ const user = this . isUnifiedEmailEnabled ( )
229+ ? await this . _userService . getUserByEmailAnyProvider ( email )
230+ : await this . _userService . getUserByEmail ( email ) ;
231+
232+ if ( ! user ) {
233+ // Don't reveal if email exists for security
234+ return { success : true } ;
235+ }
236+
237+ // Check if user has a password set (only relevant when unified emails enabled)
238+ if ( this . isUnifiedEmailEnabled ( ) && ! user . password ) {
239+ // User registered with OAuth and hasn't set a password
240+ return {
241+ success : false ,
242+ message : 'This account was registered with OAuth (Google, GitHub, etc.). Please sign in using your OAuth provider, or register with email/password to set a password.'
243+ } ;
175244 }
176245
177246 const resetValues = AuthChecker . signJWT ( {
@@ -182,8 +251,10 @@ export class AuthService {
182251 await this . _notificationService . sendEmail (
183252 user . email ,
184253 'Reset your password' ,
185- `You have requested to reset your passsord . <br />Click <a href="${ process . env . FRONTEND_URL } /auth/forgot/${ resetValues } ">here</a> to reset your password<br />The link will expire in 20 minutes`
254+ `You have requested to reset your password . <br />Click <a href="${ process . env . FRONTEND_URL } /auth/forgot/${ resetValues } ">here</a> to reset your password<br />The link will expire in 20 minutes`
186255 ) ;
256+
257+ return { success : true } ;
187258 }
188259
189260 forgotReturn ( body : ForgotReturnPasswordDto ) {
@@ -195,24 +266,42 @@ export class AuthService {
195266 return false ;
196267 }
197268
198- return this . _userService . updatePassword ( user . id , body . password ) ;
269+ // Use setPassword (works with any provider) if unified emails enabled, otherwise updatePassword (LOCAL only)
270+ return this . isUnifiedEmailEnabled ( )
271+ ? this . _userService . setPassword ( user . id , body . password )
272+ : this . _userService . updatePassword ( user . id , body . password ) ;
199273 }
200274
201275 async activate ( code : string ) {
202- const user = AuthChecker . verifyJWT ( code ) as {
276+ const tokenData = AuthChecker . verifyJWT ( code ) as {
203277 id : string ;
204278 activated : boolean ;
205279 email : string ;
280+ passwordHash ?: string ;
281+ type ?: string ;
206282 } ;
207- if ( user . id && ! user . activated ) {
208- const getUserAgain = await this . _userService . getUserByEmail ( user . email ) ;
209- if ( getUserAgain . activated ) {
283+
284+ // Handle add-password flow (OAuth user adding password) - only when unified emails enabled
285+ if ( this . isUnifiedEmailEnabled ( ) && tokenData . type === 'add-password' && tokenData . passwordHash ) {
286+ const user = await this . _userService . getUserById ( tokenData . id ) ;
287+ if ( ! user ) {
210288 return false ;
211289 }
212- await this . _userService . activateUser ( user . id ) ;
213- user . activated = true ;
214- await NewsletterService . register ( user . email ) ;
215- return this . jwt ( user as any ) ;
290+ // Set the password (passwordHash is already hashed)
291+ await this . _userService . setPasswordHash ( tokenData . id , tokenData . passwordHash ) ;
292+ return this . jwt ( user ) ;
293+ }
294+
295+ // Handle normal activation flow (new LOCAL user)
296+ if ( tokenData . id && ! tokenData . activated && tokenData . email ) {
297+ const getUserAgain = await this . _userService . getUserByEmail ( tokenData . email ) ;
298+ if ( ! getUserAgain || getUserAgain . activated ) {
299+ return false ;
300+ }
301+ await this . _userService . activateUser ( tokenData . id ) ;
302+ getUserAgain . activated = true ; // Reflect DB change in local object
303+ await NewsletterService . register ( tokenData . email ) ;
304+ return this . jwt ( getUserAgain ) ;
216305 }
217306
218307 return false ;
@@ -242,6 +331,21 @@ export class AuthService {
242331 return { jwt : await this . jwt ( checkExists ) } ;
243332 }
244333
334+ // Check if there's an existing account with the same email (any provider) - only when unified emails enabled
335+ // This allows users with email/password accounts to also sign in via OAuth
336+ if ( this . isUnifiedEmailEnabled ( ) ) {
337+ const existingUserByEmail = await this . _userService . getUserByEmailAnyProvider (
338+ user . email
339+ ) ;
340+ if ( existingUserByEmail ) {
341+ // If user is not activated, activate them now since OAuth provider verified the email
342+ if ( ! existingUserByEmail . activated ) {
343+ await this . _userService . activateUser ( existingUserByEmail . id ) ;
344+ }
345+ return { jwt : await this . jwt ( existingUserByEmail ) } ;
346+ }
347+ }
348+
245349 return { token } ;
246350 }
247351
0 commit comments