Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions resources/buildConfigDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ function mapperFor(elt, t) {
return wrap(t.identifier('booleanParser'));
} else if (t.isObjectTypeAnnotation(elt)) {
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)) {
const type = elt.typeAnnotation.id.name;
if (type == 'Adapter') {
Expand Down
159 changes: 157 additions & 2 deletions spec/EmailVerificationToken.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,15 @@ describe('Email Verification Token Expiration:', () => {
};
const verifyUserEmails = {
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;
},
};
Expand Down Expand Up @@ -349,7 +357,15 @@ describe('Email Verification Token Expiration:', () => {
};
const verifyUserEmails = {
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') {
return false;
}
Expand Down Expand Up @@ -384,6 +400,144 @@ describe('Email Verification Token Expiration:', () => {
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 () => {
const emailAdapter = {
sendVerificationEmail: () => {},
Expand Down Expand Up @@ -779,6 +933,7 @@ describe('Email Verification Token Expiration:', () => {
expect(params.master).toBeDefined();
expect(params.installationId).toBeDefined();
expect(params.resendRequest).toBeTrue();
expect(params.createdWith).toBeUndefined();
return true;
},
};
Expand Down
1 change: 1 addition & 0 deletions spec/ValidationAndPasswordsReset.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
expect(params.ip).toBeDefined();
expect(params.master).toBeDefined();
expect(params.installationId).toBeDefined();
expect(params.createdWith).toEqual({ action: 'login', authProvider: 'password' });
return true;
},
};
Expand Down
66 changes: 66 additions & 0 deletions spec/buildConfigDefinitions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,72 @@ describe('buildConfigDefinitions', () => {
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', () => {
const mockElement = {
type: 'GenericTypeAnnotation',
Expand Down
18 changes: 18 additions & 0 deletions spec/parsers.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const {
numberOrBoolParser,
numberOrStringParser,
booleanParser,
booleanOrFunctionParser,
objectParser,
arrayParser,
moduleOrObjectParser,
Expand Down Expand Up @@ -48,6 +49,23 @@ describe('parsers', () => {
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', () => {
const parser = objectParser;
expect(parser({ hello: 'world' })).toEqual({ hello: 'world' });
Expand Down
8 changes: 5 additions & 3 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,8 @@ module.exports.ParseServerOptions = {
preventLoginWithUnverifiedEmail: {
env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL',
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`.',
action: parsers.booleanParser,
"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.booleanOrFunctionParser,
default: false,
},
preventSignupWithUnverifiedEmail: {
Expand Down Expand Up @@ -574,6 +574,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_SEND_USER_EMAIL_VERIFICATION',
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>',
action: parsers.booleanOrFunctionParser,
default: true,
},
serverCloseComplete: {
Expand Down Expand Up @@ -630,7 +631,8 @@ module.exports.ParseServerOptions = {
verifyUserEmails: {
env: 'PARSE_SERVER_VERIFY_USER_EMAILS',
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,
},
webhookKey: {
Expand Down
4 changes: 2 additions & 2 deletions src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading