feat: Add event information to verifyUserEmails, preventLoginWithUnverifiedEmail to identify invoking signup / login action and auth provider (#9963)

This commit is contained in:
Palixir
2026-02-06 04:48:35 +01:00
committed by GitHub
parent 617de9989b
commit ed98c15f90
12 changed files with 350 additions and 27 deletions

View File

@@ -158,6 +158,11 @@ function mapperFor(elt, t) {
return wrap(t.identifier('booleanParser')); return wrap(t.identifier('booleanParser'));
} else if (t.isObjectTypeAnnotation(elt)) { } else if (t.isObjectTypeAnnotation(elt)) {
return wrap(t.identifier('objectParser')); return wrap(t.identifier('objectParser'));
} else if (t.isUnionTypeAnnotation(elt)) {
const unionTypes = elt.typeAnnotation?.types || elt.types;
if (unionTypes?.some(type => t.isBooleanTypeAnnotation(type)) && unionTypes?.some(type => t.isFunctionTypeAnnotation(type))) {
return wrap(t.identifier('booleanOrFunctionParser'));
}
} else if (t.isGenericTypeAnnotation(elt)) { } else if (t.isGenericTypeAnnotation(elt)) {
const type = elt.typeAnnotation.id.name; const type = elt.typeAnnotation.id.name;
if (type == 'Adapter') { if (type == 'Adapter') {

View File

@@ -288,7 +288,15 @@ describe('Email Verification Token Expiration:', () => {
}; };
const verifyUserEmails = { const verifyUserEmails = {
method(req) { method(req) {
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']); expect(Object.keys(req)).toEqual([
'original',
'object',
'master',
'ip',
'installationId',
'createdWith',
]);
expect(req.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
return false; return false;
}, },
}; };
@@ -349,7 +357,15 @@ describe('Email Verification Token Expiration:', () => {
}; };
const verifyUserEmails = { const verifyUserEmails = {
method(req) { method(req) {
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']); expect(Object.keys(req)).toEqual([
'original',
'object',
'master',
'ip',
'installationId',
'createdWith',
]);
expect(req.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
if (req.object.get('username') === 'no_email') { if (req.object.get('username') === 'no_email') {
return false; return false;
} }
@@ -384,6 +400,144 @@ describe('Email Verification Token Expiration:', () => {
expect(verifySpy).toHaveBeenCalledTimes(5); expect(verifySpy).toHaveBeenCalledTimes(5);
}); });
it('provides createdWith on signup when verification blocks session creation', async () => {
const verifyUserEmails = {
method: params => {
expect(params.object).toBeInstanceOf(Parse.User);
expect(params.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
return true;
},
};
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
await reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: verifyUserEmails.method,
preventLoginWithUnverifiedEmail: true,
preventSignupWithUnverifiedEmail: true,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
publicServerURL: 'http://localhost:8378/1',
});
const user = new Parse.User();
user.setUsername('signup_created_with');
user.setPassword('pass');
user.setEmail('signup@example.com');
const res = await user.signUp().catch(e => e);
expect(res.message).toBe('User email is not verified.');
expect(user.getSessionToken()).toBeUndefined();
expect(verifySpy).toHaveBeenCalledTimes(2); // before signup completion and on preventLoginWithUnverifiedEmail
});
it('provides createdWith with auth provider on login verification', async () => {
const user = new Parse.User();
user.setUsername('user_created_with_login');
user.setPassword('pass');
user.set('email', 'login@example.com');
await user.signUp();
const verifyUserEmails = {
method: async params => {
expect(params.object).toBeInstanceOf(Parse.User);
expect(params.createdWith).toEqual({ action: 'login', authProvider: 'password' });
return true;
},
};
const verifyUserEmailsSpy = spyOn(verifyUserEmails, 'method').and.callThrough();
await reconfigureServer({
appName: 'emailVerifyToken',
publicServerURL: 'http://localhost:8378/1',
verifyUserEmails: verifyUserEmails.method,
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
preventSignupWithUnverifiedEmail: true,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
});
const res = await Parse.User.logIn('user_created_with_login', 'pass').catch(e => e);
expect(res.code).toBe(205);
expect(verifyUserEmailsSpy).toHaveBeenCalledTimes(2); // before login completion and on preventLoginWithUnverifiedEmail
});
it('provides createdWith with auth provider on signup verification', async () => {
const createdWithValues = [];
const verifyUserEmails = {
method: params => {
createdWithValues.push(params.createdWith);
return true;
},
};
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
await reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: verifyUserEmails.method,
preventLoginWithUnverifiedEmail: true,
preventSignupWithUnverifiedEmail: true,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
publicServerURL: 'http://localhost:8378/1',
});
const provider = {
authData: { id: '8675309', access_token: 'jenny' },
shouldError: false,
authenticate(options) {
options.success(this, this.authData);
},
restoreAuthentication() {
return true;
},
getAuthType() {
return 'facebook';
},
deauthenticate() {},
};
Parse.User._registerAuthenticationProvider(provider);
const res = await Parse.User._logInWith('facebook').catch(e => e);
expect(res.message).toBe('User email is not verified.');
// Called once in createSessionTokenIfNeeded (no email set, so _validateEmail skips)
expect(verifySpy).toHaveBeenCalledTimes(1);
expect(createdWithValues[0]).toEqual({ action: 'signup', authProvider: 'facebook' });
});
it('provides createdWith for preventLoginWithUnverifiedEmail function', async () => {
const user = new Parse.User();
user.setUsername('user_prevent_login_fn');
user.setPassword('pass');
user.set('email', 'preventlogin@example.com');
await user.signUp();
const preventLoginCreatedWith = [];
await reconfigureServer({
appName: 'emailVerifyToken',
publicServerURL: 'http://localhost:8378/1',
verifyUserEmails: true,
preventLoginWithUnverifiedEmail: params => {
preventLoginCreatedWith.push(params.createdWith);
return true;
},
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
});
const res = await Parse.User.logIn('user_prevent_login_fn', 'pass').catch(e => e);
expect(res.code).toBe(205);
expect(preventLoginCreatedWith.length).toBe(1);
expect(preventLoginCreatedWith[0]).toEqual({ action: 'login', authProvider: 'password' });
});
it_id('d812de87-33d1-495e-a6e8-3485f6dc3589')(it)('can conditionally send user email verification', async () => { it_id('d812de87-33d1-495e-a6e8-3485f6dc3589')(it)('can conditionally send user email verification', async () => {
const emailAdapter = { const emailAdapter = {
sendVerificationEmail: () => {}, sendVerificationEmail: () => {},
@@ -779,6 +933,7 @@ describe('Email Verification Token Expiration:', () => {
expect(params.master).toBeDefined(); expect(params.master).toBeDefined();
expect(params.installationId).toBeDefined(); expect(params.installationId).toBeDefined();
expect(params.resendRequest).toBeTrue(); expect(params.resendRequest).toBeTrue();
expect(params.createdWith).toBeUndefined();
return true; return true;
}, },
}; };

View File

@@ -284,6 +284,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
expect(params.ip).toBeDefined(); expect(params.ip).toBeDefined();
expect(params.master).toBeDefined(); expect(params.master).toBeDefined();
expect(params.installationId).toBeDefined(); expect(params.installationId).toBeDefined();
expect(params.createdWith).toEqual({ action: 'login', authProvider: 'password' });
return true; return true;
}, },
}; };

View File

@@ -133,6 +133,72 @@ describe('buildConfigDefinitions', () => {
expect(result.property.name).toBe('arrayParser'); expect(result.property.name).toBe('arrayParser');
}); });
it('should return booleanOrFunctionParser for UnionTypeAnnotation containing boolean (nullable)', () => {
const mockElement = {
type: 'UnionTypeAnnotation',
typeAnnotation: {
types: [
{ type: 'BooleanTypeAnnotation' },
{ type: 'FunctionTypeAnnotation' },
],
},
};
const result = mapperFor(mockElement, t);
expect(t.isMemberExpression(result)).toBe(true);
expect(result.object.name).toBe('parsers');
expect(result.property.name).toBe('booleanOrFunctionParser');
});
it('should return booleanOrFunctionParser for UnionTypeAnnotation containing boolean (non-nullable)', () => {
const mockElement = {
type: 'UnionTypeAnnotation',
types: [
{ type: 'BooleanTypeAnnotation' },
{ type: 'FunctionTypeAnnotation' },
],
};
const result = mapperFor(mockElement, t);
expect(t.isMemberExpression(result)).toBe(true);
expect(result.object.name).toBe('parsers');
expect(result.property.name).toBe('booleanOrFunctionParser');
});
it('should return undefined for UnionTypeAnnotation without boolean', () => {
const mockElement = {
type: 'UnionTypeAnnotation',
typeAnnotation: {
types: [
{ type: 'StringTypeAnnotation' },
{ type: 'NumberTypeAnnotation' },
],
},
};
const result = mapperFor(mockElement, t);
expect(result).toBeUndefined();
});
it('should return undefined for UnionTypeAnnotation with boolean but without function', () => {
const mockElement = {
type: 'UnionTypeAnnotation',
typeAnnotation: {
types: [
{ type: 'BooleanTypeAnnotation' },
{ type: 'VoidTypeAnnotation' },
],
},
};
const result = mapperFor(mockElement, t);
expect(result).toBeUndefined();
});
it('should return objectParser for unknown GenericTypeAnnotation', () => { it('should return objectParser for unknown GenericTypeAnnotation', () => {
const mockElement = { const mockElement = {
type: 'GenericTypeAnnotation', type: 'GenericTypeAnnotation',

View File

@@ -3,6 +3,7 @@ const {
numberOrBoolParser, numberOrBoolParser,
numberOrStringParser, numberOrStringParser,
booleanParser, booleanParser,
booleanOrFunctionParser,
objectParser, objectParser,
arrayParser, arrayParser,
moduleOrObjectParser, moduleOrObjectParser,
@@ -48,6 +49,23 @@ describe('parsers', () => {
expect(parser(2)).toEqual(false); expect(parser(2)).toEqual(false);
}); });
it('parses correctly with booleanOrFunctionParser', () => {
const parser = booleanOrFunctionParser;
// Preserves functions
const fn = () => true;
expect(parser(fn)).toBe(fn);
const asyncFn = async () => false;
expect(parser(asyncFn)).toBe(asyncFn);
// Parses booleans and string booleans like booleanParser
expect(parser(true)).toEqual(true);
expect(parser(false)).toEqual(false);
expect(parser('true')).toEqual(true);
expect(parser('false')).toEqual(false);
expect(parser('1')).toEqual(true);
expect(parser(1)).toEqual(true);
expect(parser(0)).toEqual(false);
});
it('parses correctly with objectParser', () => { it('parses correctly with objectParser', () => {
const parser = objectParser; const parser = objectParser;
expect(parser({ hello: 'world' })).toEqual({ hello: 'world' }); expect(parser({ hello: 'world' })).toEqual({ hello: 'world' });

View File

@@ -473,8 +473,8 @@ module.exports.ParseServerOptions = {
preventLoginWithUnverifiedEmail: { preventLoginWithUnverifiedEmail: {
env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL', env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL',
help: help:
'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`.', "Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. 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>The `createdWith` values per scenario:<ul><li>Password signup: `{ action: 'signup', authProvider: 'password' }`</li><li>Auth provider signup: `{ action: 'signup', authProvider: '<provider>' }`</li><li>Password login: `{ action: 'login', authProvider: 'password' }`</li><li>Auth provider login: function not invoked; auth provider login bypasses email verification</li></ul>Default is `false`.<br>Requires option `verifyUserEmails: true`.",
action: parsers.booleanParser, action: parsers.booleanOrFunctionParser,
default: false, default: false,
}, },
preventSignupWithUnverifiedEmail: { preventSignupWithUnverifiedEmail: {
@@ -574,6 +574,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_SEND_USER_EMAIL_VERIFICATION', env: 'PARSE_SERVER_SEND_USER_EMAIL_VERIFICATION',
help: help:
'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.<br><br>Default is `true`.<br>', 'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.<br><br>Default is `true`.<br>',
action: parsers.booleanOrFunctionParser,
default: true, default: true,
}, },
serverCloseComplete: { serverCloseComplete: {
@@ -630,7 +631,8 @@ module.exports.ParseServerOptions = {
verifyUserEmails: { verifyUserEmails: {
env: 'PARSE_SERVER_VERIFY_USER_EMAILS', env: 'PARSE_SERVER_VERIFY_USER_EMAILS',
help: help:
'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`.', "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>The `createdWith` values per scenario:<ul><li>Password signup: `{ action: 'signup', authProvider: 'password' }`</li><li>Auth provider signup: `{ action: 'signup', authProvider: '<provider>' }`</li><li>Password login: `{ action: 'login', authProvider: 'password' }`</li><li>Auth provider login: function not invoked; auth provider login bypasses email verification</li><li>Resend verification email: `createdWith` is `undefined`; use the `resendRequest` property to identify those</li></ul>Default is `false`.",
action: parsers.booleanOrFunctionParser,
default: false, default: false,
}, },
webhookKey: { webhookKey: {

View File

@@ -84,7 +84,7 @@
* @property {String} playgroundPath Mount path for the GraphQL Playground, defaults to /playground * @property {String} playgroundPath Mount path for the GraphQL Playground, defaults to /playground
* @property {Number} port The port to run the ParseServer, defaults to 1337. * @property {Number} port The port to run the ParseServer, defaults to 1337.
* @property {Boolean} preserveFileName Enable (or disable) the addition of a unique hash to the file names * @property {Boolean} preserveFileName Enable (or disable) the addition of a unique hash to the file names
* @property {Boolean} preventLoginWithUnverifiedEmail 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`. * @property {Boolean} preventLoginWithUnverifiedEmail Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. 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>The `createdWith` values per scenario:<ul><li>Password signup: `{ action: 'signup', authProvider: 'password' }`</li><li>Auth provider signup: `{ action: 'signup', authProvider: '<provider>' }`</li><li>Password login: `{ action: 'login', authProvider: 'password' }`</li><li>Auth provider login: function not invoked; auth provider login bypasses email verification</li></ul>Default is `false`.<br>Requires option `verifyUserEmails: true`.
* @property {Boolean} preventSignupWithUnverifiedEmail 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.<br><br>Default is `false`.<br>Requires option `verifyUserEmails: true`. * @property {Boolean} preventSignupWithUnverifiedEmail 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.<br><br>Default is `false`.<br>Requires option `verifyUserEmails: true`.
* @property {ProtectedFields} protectedFields Protected fields that should be treated with extra security when fetching details. * @property {ProtectedFields} protectedFields Protected fields that should be treated with extra security when fetching details.
* @property {Union} publicServerURL Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`. * @property {Union} publicServerURL Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`.
@@ -108,7 +108,7 @@
* @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields * @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields
* @property {Boolean} verbose Set the logging to verbose * @property {Boolean} verbose Set the logging to verbose
* @property {Boolean} verifyServerUrl Parse Server makes a HTTP request to the URL set in `serverURL` at the end of its launch routine to verify that the launch succeeded. If this option is set to `false`, the verification will be skipped. This can be useful in environments where the server URL is not accessible from the server itself, such as when running behind a firewall or in certain containerized environments.<br><br>⚠️ Server URL verification requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.<br><br>Default is `true`. * @property {Boolean} verifyServerUrl Parse Server makes a HTTP request to the URL set in `serverURL` at the end of its launch routine to verify that the launch succeeded. If this option is set to `false`, the verification will be skipped. This can be useful in environments where the server URL is not accessible from the server itself, such as when running behind a firewall or in certain containerized environments.<br><br>⚠️ Server URL verification requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.<br><br>Default is `true`.
* @property {Boolean} verifyUserEmails 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`. * @property {Boolean} verifyUserEmails 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>The `createdWith` values per scenario:<ul><li>Password signup: `{ action: 'signup', authProvider: 'password' }`</li><li>Auth provider signup: `{ action: 'signup', authProvider: '<provider>' }`</li><li>Password login: `{ action: 'login', authProvider: 'password' }`</li><li>Auth provider login: function not invoked; auth provider login bypasses email verification</li><li>Resend verification email: `createdWith` is `undefined`; use the `resendRequest` property to identify those</li></ul>Default is `false`.
* @property {String} webhookKey Key sent with outgoing webhook calls * @property {String} webhookKey Key sent with outgoing webhook calls
*/ */

View File

@@ -43,6 +43,22 @@ type RequestKeywordDenylist = {
key: string | any, key: string | any,
value: any, value: any,
}; };
type EmailVerificationRequest = {
original?: any,
object: any,
master?: boolean,
ip?: string,
installationId?: string,
createdWith?: {
action: 'login' | 'signup',
authProvider: string,
},
resendRequest?: boolean,
};
type SendEmailVerificationRequest = {
user: any,
master?: boolean,
};
export interface ParseServerOptions { export interface ParseServerOptions {
/* Your Parse Application ID /* Your Parse Application ID
@@ -174,18 +190,25 @@ export interface ParseServerOptions {
/* Max file size for uploads, defaults to 20mb /* Max file size for uploads, defaults to 20mb
:DEFAULT: 20mb */ :DEFAULT: 20mb */
maxUploadSize: ?string; maxUploadSize: ?string;
/* 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. /* 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> <br><br>
The `createdWith` values per scenario:
<ul><li>Password signup: `{ action: 'signup', authProvider: 'password' }`</li><li>Auth provider signup: `{ action: 'signup', authProvider: '<provider>' }`</li><li>Password login: `{ action: 'login', authProvider: 'password' }`</li><li>Auth provider login: function not invoked; auth provider login bypasses email verification</li><li>Resend verification email: `createdWith` is `undefined`; use the `resendRequest` property to identify those</li></ul>
Default is `false`. Default is `false`.
:DEFAULT: false */ :DEFAULT: false */
verifyUserEmails: ?(boolean | void); verifyUserEmails: ?(boolean | (EmailVerificationRequest => boolean | Promise<boolean>));
/* Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. /* Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. 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> <br><br>
The `createdWith` values per scenario:
<ul><li>Password signup: `{ action: 'signup', authProvider: 'password' }`</li><li>Auth provider signup: `{ action: 'signup', authProvider: '<provider>' }`</li><li>Password login: `{ action: 'login', authProvider: 'password' }`</li><li>Auth provider login: function not invoked; auth provider login bypasses email verification</li></ul>
Default is `false`. Default is `false`.
<br> <br>
Requires option `verifyUserEmails: true`. Requires option `verifyUserEmails: true`.
:DEFAULT: false */ :DEFAULT: false */
preventLoginWithUnverifiedEmail: ?boolean; preventLoginWithUnverifiedEmail: ?(
| boolean
| (EmailVerificationRequest => boolean | Promise<boolean>)
);
/* 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. /* 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.
<br><br> <br><br>
Default is `false`. Default is `false`.
@@ -214,7 +237,10 @@ export interface ParseServerOptions {
Default is `true`. Default is `true`.
<br> <br>
:DEFAULT: true */ :DEFAULT: true */
sendUserEmailVerification: ?(boolean | void); sendUserEmailVerification: ?(
| boolean
| (SendEmailVerificationRequest => boolean | Promise<boolean>)
);
/* The account lockout policy for failed login attempts. */ /* The account lockout policy for failed login attempts. */
accountLockout: ?AccountLockoutOptions; accountLockout: ?AccountLockoutOptions;
/* The password policy for enforcing password related rules. */ /* The password policy for enforcing password related rules. */

View File

@@ -68,6 +68,13 @@ function booleanParser(opt) {
return false; return false;
} }
function booleanOrFunctionParser(opt) {
if (typeof opt === 'function') {
return opt;
}
return booleanParser(opt);
}
function nullParser(opt) { function nullParser(opt) {
if (opt == 'null') { if (opt == 'null') {
return null; return null;
@@ -81,6 +88,7 @@ module.exports = {
numberOrStringParser, numberOrStringParser,
nullParser, nullParser,
booleanParser, booleanParser,
booleanOrFunctionParser,
moduleOrObjectParser, moduleOrObjectParser,
arrayParser, arrayParser,
objectParser, objectParser,

View File

@@ -771,6 +771,30 @@ RestWrite.prototype._validateUserName = function () {
}); });
}; };
RestWrite.buildCreatedWith = function (action, authProvider) {
return { action, authProvider: authProvider || 'password' };
};
RestWrite.prototype.getCreatedWith = function () {
if (this.storage.createdWith) {
return this.storage.createdWith;
}
const isCreateOperation = !this.query;
const authDataProvider =
this.data?.authData &&
Object.keys(this.data.authData).length &&
Object.keys(this.data.authData).join(',');
const authProvider = this.storage.authProvider || authDataProvider;
// storage.authProvider is only set for login (existing user found in handleAuthData)
const action = this.storage.authProvider ? 'login' : isCreateOperation ? 'signup' : undefined;
if (!action) {
return;
}
const resolvedAuthProvider = authProvider || (action === 'signup' ? 'password' : undefined);
this.storage.createdWith = RestWrite.buildCreatedWith(action, resolvedAuthProvider);
return this.storage.createdWith;
};
/* /*
As with usernames, Parse should not allow case insensitive collisions of email. As with usernames, Parse should not allow case insensitive collisions of email.
unlike with usernames (which can have case insensitive collisions in the case of unlike with usernames (which can have case insensitive collisions in the case of
@@ -826,6 +850,7 @@ RestWrite.prototype._validateEmail = function () {
master: this.auth.isMaster, master: this.auth.isMaster,
ip: this.config.ip, ip: this.config.ip,
installationId: this.auth.installationId, installationId: this.auth.installationId,
createdWith: this.getCreatedWith(),
}; };
return this.config.userController.setEmailVerifyToken(this.data, request, this.storage); return this.config.userController.setEmailVerifyToken(this.data, request, this.storage);
} }
@@ -961,6 +986,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () {
master: this.auth.isMaster, master: this.auth.isMaster,
ip: this.config.ip, ip: this.config.ip,
installationId: this.auth.installationId, installationId: this.auth.installationId,
createdWith: this.getCreatedWith(),
}; };
// Get verification conditions which can be booleans or functions; the purpose of this async/await // Get verification conditions which can be booleans or functions; the purpose of this async/await
// structure is to avoid unnecessarily executing subsequent functions if previous ones fail in the // structure is to avoid unnecessarily executing subsequent functions if previous ones fail in the
@@ -985,14 +1011,14 @@ RestWrite.prototype.createSessionToken = async function () {
if (this.storage.authProvider == null && this.data.authData) { if (this.storage.authProvider == null && this.data.authData) {
this.storage.authProvider = Object.keys(this.data.authData).join(','); this.storage.authProvider = Object.keys(this.data.authData).join(',');
// Invalidate cached createdWith since authProvider was just resolved
delete this.storage.createdWith;
} }
const createdWith = this.getCreatedWith();
const { sessionData, createSession } = RestWrite.createSession(this.config, { const { sessionData, createSession } = RestWrite.createSession(this.config, {
userId: this.objectId(), userId: this.objectId(),
createdWith: { createdWith,
action: this.storage.authProvider ? 'login' : 'signup',
authProvider: this.storage.authProvider || 'password',
},
installationId: this.auth.installationId, installationId: this.auth.installationId,
}); });

View File

@@ -140,11 +140,17 @@ export class UsersRouter extends ClassesRouter {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
} }
// Create request object for verification functions // Create request object for verification functions
const authProvider =
req.body &&
req.body.authData &&
Object.keys(req.body.authData).length &&
Object.keys(req.body.authData).join(',');
const request = { const request = {
master: req.auth.isMaster, master: req.auth.isMaster,
ip: req.config.ip, ip: req.config.ip,
installationId: req.auth.installationId, installationId: req.auth.installationId,
object: Parse.User.fromJSON(Object.assign({ className: '_User' }, user)), object: Parse.User.fromJSON(Object.assign({ className: '_User' }, user)),
createdWith: RestWrite.buildCreatedWith('login', authProvider),
}; };
// If request doesn't use master or maintenance key with ignoring email verification // If request doesn't use master or maintenance key with ignoring email verification
@@ -290,10 +296,7 @@ export class UsersRouter extends ClassesRouter {
const { sessionData, createSession } = RestWrite.createSession(req.config, { const { sessionData, createSession } = RestWrite.createSession(req.config, {
userId: user.objectId, userId: user.objectId,
createdWith: { createdWith: RestWrite.buildCreatedWith('login'),
action: 'login',
authProvider: 'password',
},
installationId: req.info.installationId, installationId: req.info.installationId,
}); });
@@ -360,10 +363,7 @@ export class UsersRouter extends ClassesRouter {
const { sessionData, createSession } = RestWrite.createSession(req.config, { const { sessionData, createSession } = RestWrite.createSession(req.config, {
userId, userId,
createdWith: { createdWith: RestWrite.buildCreatedWith('login', 'masterkey'),
action: 'login',
authProvider: 'masterkey',
},
installationId: req.info.installationId, installationId: req.info.installationId,
}); });

View File

@@ -26,6 +26,22 @@ type RequestKeywordDenylist = {
key: string; key: string;
value: any; value: any;
}; };
export interface EmailVerificationRequest {
original?: any;
object: any;
master?: boolean;
ip?: string;
installationId?: string;
createdWith?: {
action: 'login' | 'signup';
authProvider: string;
};
resendRequest?: boolean;
}
export interface SendEmailVerificationRequest {
user: any;
master?: boolean;
}
export interface ParseServerOptions { export interface ParseServerOptions {
appId: string; appId: string;
masterKey: (() => void) | string; masterKey: (() => void) | string;
@@ -74,12 +90,12 @@ export interface ParseServerOptions {
auth?: Record<string, AuthAdapter>; auth?: Record<string, AuthAdapter>;
enableInsecureAuthAdapters?: boolean; enableInsecureAuthAdapters?: boolean;
maxUploadSize?: string; maxUploadSize?: string;
verifyUserEmails?: (boolean | void); verifyUserEmails?: boolean | ((params: EmailVerificationRequest) => boolean | Promise<boolean>);
preventLoginWithUnverifiedEmail?: boolean; preventLoginWithUnverifiedEmail?: boolean | ((params: EmailVerificationRequest) => boolean | Promise<boolean>);
preventSignupWithUnverifiedEmail?: boolean; preventSignupWithUnverifiedEmail?: boolean;
emailVerifyTokenValidityDuration?: number; emailVerifyTokenValidityDuration?: number;
emailVerifyTokenReuseIfValid?: boolean; emailVerifyTokenReuseIfValid?: boolean;
sendUserEmailVerification?: (boolean | void); sendUserEmailVerification?: boolean | ((params: SendEmailVerificationRequest) => boolean | Promise<boolean>);
accountLockout?: AccountLockoutOptions; accountLockout?: AccountLockoutOptions;
passwordPolicy?: PasswordPolicyOptions; passwordPolicy?: PasswordPolicyOptions;
cacheAdapter?: Adapter<CacheAdapter>; cacheAdapter?: Adapter<CacheAdapter>;