Skip to content

Commit cfe22b3

Browse files
committed
feat: add event information on verifyUserEmails
1 parent e78e58d commit cfe22b3

File tree

8 files changed

+158
-14
lines changed

8 files changed

+158
-14
lines changed

spec/EmailVerificationToken.spec.js

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,15 @@ describe('Email Verification Token Expiration:', () => {
298298
};
299299
const verifyUserEmails = {
300300
method(req) {
301-
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
301+
expect(Object.keys(req)).toEqual([
302+
'original',
303+
'object',
304+
'master',
305+
'ip',
306+
'installationId',
307+
'createdWith',
308+
]);
309+
expect(req.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
302310
return false;
303311
},
304312
};
@@ -359,7 +367,15 @@ describe('Email Verification Token Expiration:', () => {
359367
};
360368
const verifyUserEmails = {
361369
method(req) {
362-
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
370+
expect(Object.keys(req)).toEqual([
371+
'original',
372+
'object',
373+
'master',
374+
'ip',
375+
'installationId',
376+
'createdWith',
377+
]);
378+
expect(req.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
363379
if (req.object.get('username') === 'no_email') {
364380
return false;
365381
}
@@ -394,6 +410,71 @@ describe('Email Verification Token Expiration:', () => {
394410
expect(verifySpy).toHaveBeenCalledTimes(5);
395411
});
396412

413+
it('provides createdWith on signup when verification blocks session creation', async () => {
414+
const verifyUserEmails = {
415+
method: params => {
416+
expect(params.object).toBeInstanceOf(Parse.User);
417+
expect(params.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
418+
return true;
419+
},
420+
};
421+
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
422+
await reconfigureServer({
423+
appName: 'emailVerifyToken',
424+
verifyUserEmails: verifyUserEmails.method,
425+
preventLoginWithUnverifiedEmail: true,
426+
preventSignupWithUnverifiedEmail: true,
427+
emailAdapter: MockEmailAdapterWithOptions({
428+
fromAddress: '[email protected]',
429+
apiKey: 'k',
430+
domain: 'd',
431+
}),
432+
publicServerURL: 'http://localhost:8378/1',
433+
});
434+
435+
const user = new Parse.User();
436+
user.setUsername('signup_created_with');
437+
user.setPassword('pass');
438+
user.setEmail('[email protected]');
439+
const res = await user.signUp().catch(e => e);
440+
expect(res.message).toBe('User email is not verified.');
441+
expect(user.getSessionToken()).toBeUndefined();
442+
expect(verifySpy).toHaveBeenCalledTimes(2); // before signup completion and on preventLoginWithUnverifiedEmail
443+
});
444+
445+
it('provides createdWith with auth provider on login verification', async () => {
446+
const user = new Parse.User();
447+
user.setUsername('user_created_with_login');
448+
user.setPassword('pass');
449+
user.set('email', '[email protected]');
450+
await user.signUp();
451+
452+
const verifyUserEmails = {
453+
method: async params => {
454+
expect(params.object).toBeInstanceOf(Parse.User);
455+
expect(params.createdWith).toEqual({ action: 'login', authProvider: 'password' });
456+
return true;
457+
},
458+
};
459+
const verifyUserEmailsSpy = spyOn(verifyUserEmails, 'method').and.callThrough();
460+
await reconfigureServer({
461+
appName: 'emailVerifyToken',
462+
publicServerURL: 'http://localhost:8378/1',
463+
verifyUserEmails: verifyUserEmails.method,
464+
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
465+
preventSignupWithUnverifiedEmail: true,
466+
emailAdapter: MockEmailAdapterWithOptions({
467+
fromAddress: '[email protected]',
468+
apiKey: 'k',
469+
domain: 'd',
470+
}),
471+
});
472+
473+
const res = await Parse.User.logIn('user_created_with_login', 'pass').catch(e => e);
474+
expect(res.code).toBe(205);
475+
expect(verifyUserEmailsSpy).toHaveBeenCalledTimes(2); // before login completion and on preventLoginWithUnverifiedEmail
476+
});
477+
397478
it_id('d812de87-33d1-495e-a6e8-3485f6dc3589')(it)('can conditionally send user email verification', async () => {
398479
const emailAdapter = {
399480
sendVerificationEmail: () => {},
@@ -797,6 +878,7 @@ describe('Email Verification Token Expiration:', () => {
797878
expect(params.master).toBeDefined();
798879
expect(params.installationId).toBeDefined();
799880
expect(params.resendRequest).toBeTrue();
881+
expect(params.createdWith).toBeUndefined();
800882
return true;
801883
},
802884
};

spec/ValidationAndPasswordsReset.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
284284
expect(params.ip).toBeDefined();
285285
expect(params.master).toBeDefined();
286286
expect(params.installationId).toBeDefined();
287+
expect(params.createdWith).toEqual({ action: 'login', authProvider: 'password' });
287288
return true;
288289
},
289290
};

src/Options/Definitions.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,6 @@ module.exports.ParseServerOptions = {
481481
env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL',
482482
help:
483483
'Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required.<br><br>Default is `false`.<br>Requires option `verifyUserEmails: true`.',
484-
action: parsers.booleanParser,
485484
default: false,
486485
},
487486
preventSignupWithUnverifiedEmail: {
@@ -637,7 +636,7 @@ module.exports.ParseServerOptions = {
637636
verifyUserEmails: {
638637
env: 'PARSE_SERVER_VERIFY_USER_EMAILS',
639638
help:
640-
'Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.<br><br>Default is `false`.',
639+
'Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.<br><br>Default is `false`.',
641640
default: false,
642641
},
643642
webhookKey: {

src/Options/docs.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Options/index.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ type RequestKeywordDenylist = {
4343
key: string | any,
4444
value: any,
4545
};
46+
type EmailVerificationRequest = {
47+
original?: any,
48+
object: any,
49+
master?: boolean,
50+
ip?: string,
51+
installationId?: string,
52+
createdWith?: {
53+
action: 'login' | 'signup',
54+
authProvider: string,
55+
},
56+
resendRequest?: boolean,
57+
};
4658

4759
export interface ParseServerOptions {
4860
/* Your Parse Application ID
@@ -174,18 +186,21 @@ export interface ParseServerOptions {
174186
/* Max file size for uploads, defaults to 20mb
175187
:DEFAULT: 20mb */
176188
maxUploadSize: ?string;
177-
/* Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.
189+
/* Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.
178190
<br><br>
179191
Default is `false`.
180192
:DEFAULT: false */
181-
verifyUserEmails: ?(boolean | void);
193+
verifyUserEmails: ?(boolean | (EmailVerificationRequest => boolean | Promise<boolean>));
182194
/* Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required.
183195
<br><br>
184196
Default is `false`.
185197
<br>
186198
Requires option `verifyUserEmails: true`.
187199
:DEFAULT: false */
188-
preventLoginWithUnverifiedEmail: ?boolean;
200+
preventLoginWithUnverifiedEmail: ?(
201+
| boolean
202+
| (EmailVerificationRequest => boolean | Promise<boolean>)
203+
);
189204
/* If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.
190205
<br><br>
191206
Default is `false`.

src/RestWrite.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,28 @@ RestWrite.prototype._validateUserName = function () {
771771
});
772772
};
773773

774+
RestWrite.prototype.getCreatedWith = function () {
775+
if (this.storage.createdWith) {
776+
return this.storage.createdWith;
777+
}
778+
const isCreateOperation = !this.query;
779+
// Determine authProvider: from stored authProvider or authData keys (e.g., anonymous, facebook).
780+
// Default to 'password' on signup with no authData so createdWith aligns with legacy expectations/tests.
781+
const authProvider =
782+
this.storage.authProvider ||
783+
(this.data &&
784+
this.data.authData &&
785+
Object.keys(this.data.authData).length &&
786+
Object.keys(this.data.authData).join(','));
787+
const action = authProvider ? 'login' : isCreateOperation ? 'signup' : undefined;
788+
if (!action) {
789+
return;
790+
}
791+
const resolvedAuthProvider = authProvider || (action === 'signup' ? 'password' : undefined);
792+
this.storage.createdWith = { action, authProvider: resolvedAuthProvider };
793+
return this.storage.createdWith;
794+
};
795+
774796
/*
775797
As with usernames, Parse should not allow case insensitive collisions of email.
776798
unlike with usernames (which can have case insensitive collisions in the case of
@@ -826,6 +848,7 @@ RestWrite.prototype._validateEmail = function () {
826848
master: this.auth.isMaster,
827849
ip: this.config.ip,
828850
installationId: this.auth.installationId,
851+
createdWith: this.getCreatedWith(),
829852
};
830853
return this.config.userController.setEmailVerifyToken(this.data, request, this.storage);
831854
}
@@ -961,6 +984,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () {
961984
master: this.auth.isMaster,
962985
ip: this.config.ip,
963986
installationId: this.auth.installationId,
987+
createdWith: this.getCreatedWith(),
964988
};
965989
// Get verification conditions which can be booleans or functions; the purpose of this async/await
966990
// structure is to avoid unnecessarily executing subsequent functions if previous ones fail in the
@@ -987,12 +1011,14 @@ RestWrite.prototype.createSessionToken = async function () {
9871011
this.storage.authProvider = Object.keys(this.data.authData).join(',');
9881012
}
9891013

990-
const { sessionData, createSession } = RestWrite.createSession(this.config, {
991-
userId: this.objectId(),
992-
createdWith: {
1014+
const createdWith =
1015+
this.getCreatedWith() || {
9931016
action: this.storage.authProvider ? 'login' : 'signup',
9941017
authProvider: this.storage.authProvider || 'password',
995-
},
1018+
};
1019+
const { sessionData, createSession } = RestWrite.createSession(this.config, {
1020+
userId: this.objectId(),
1021+
createdWith,
9961022
installationId: this.auth.installationId,
9971023
});
9981024

src/Routers/UsersRouter.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,20 @@ export class UsersRouter extends ClassesRouter {
140140
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
141141
}
142142
// Create request object for verification functions
143+
const authProvider =
144+
req.body &&
145+
req.body.authData &&
146+
Object.keys(req.body.authData).length &&
147+
Object.keys(req.body.authData).join(',');
143148
const request = {
144149
master: req.auth.isMaster,
145150
ip: req.config.ip,
146151
installationId: req.auth.installationId,
147152
object: Parse.User.fromJSON(Object.assign({ className: '_User' }, user)),
153+
createdWith: {
154+
action: 'login',
155+
authProvider: authProvider || 'password',
156+
},
148157
};
149158

150159
// If request doesn't use master or maintenance key with ignoring email verification

types/Options/index.d.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ type RequestKeywordDenylist = {
2626
key: string;
2727
value: any;
2828
};
29+
export interface VerifyUserEmailsRequest {
30+
original?: any;
31+
object: any;
32+
master?: boolean;
33+
ip?: string;
34+
installationId?: string;
35+
createdWith?: {
36+
action: 'login' | 'signup';
37+
authProvider: string;
38+
};
39+
resendRequest?: boolean;
40+
}
2941
export interface ParseServerOptions {
3042
appId: string;
3143
masterKey: (() => void) | string;
@@ -74,8 +86,8 @@ export interface ParseServerOptions {
7486
auth?: Record<string, AuthAdapter>;
7587
enableInsecureAuthAdapters?: boolean;
7688
maxUploadSize?: string;
77-
verifyUserEmails?: (boolean | void);
78-
preventLoginWithUnverifiedEmail?: boolean;
89+
verifyUserEmails?: boolean | ((params: VerifyUserEmailsRequest) => boolean | Promise<boolean>);
90+
preventLoginWithUnverifiedEmail?: boolean | ((params: VerifyUserEmailsRequest) => boolean | Promise<boolean>);
7991
preventSignupWithUnverifiedEmail?: boolean;
8092
emailVerifyTokenValidityDuration?: number;
8193
emailVerifyTokenReuseIfValid?: boolean;

0 commit comments

Comments
 (0)