Merge branch 'release-3.0' into release-3.0-mongo-tests

This commit is contained in:
Edimar Cardoso
2023-01-02 09:12:15 -03:00
9 changed files with 171 additions and 157 deletions

View File

@@ -7,6 +7,14 @@
* `email`:
- `Email.send` is no longer available. Use `Email.sendAsync` instead.
* `accounts-2fa`:
- Some methods are now async. See below:
- `Accounts._is2faEnabledForUser`
- `(Meteor Method) - generate2faActivationQrCode`
- `(Meteor Method) - enableUser2fa`
- `(Meteor Method) - disableUser2fa`
- `(Meteor Method) - has2faEnabled`
* `accounts-password`:
- `Accounts.sendResetPasswordEmail` is now async
- `Accounts.sendEnrollmentEmail` is now async
@@ -18,6 +26,14 @@
* `boilerplate-generator`:
- `toHTML` is no longer available (it was already deprecated). Use `toHTMLStream` instead.
* `oauth`:
- `_endOfPopupResponseTemplate` and `_endOfRedirectResponseTemplate` are no longer a property but now a function that returns a promise of the same value as before
- the following methods are now async:
- `OAuth._renderOauthResults`
- `OAuth._endOfLoginResponse`
- `OAuth.renderEndOfLoginResponse`
- `OAuth._storePendingCredential`
- `OAuth._retrievePendingCredential`
#### Internal API changes

View File

@@ -13,8 +13,8 @@ Accounts._check2faEnabled = user => {
);
};
Accounts._is2faEnabledForUser = () => {
const user = Meteor.user();
Accounts._is2faEnabledForUser = async () => {
const user = await Meteor.user();
if (!user) {
throw new Meteor.Error('no-logged-user', 'No user logged in.');
}
@@ -34,9 +34,9 @@ Accounts._isTokenValid = (secret, code) => {
};
Meteor.methods({
generate2faActivationQrCode(appName) {
async generate2faActivationQrCode(appName) {
check(appName, String);
const user = Meteor.user();
const user = await Meteor.user();
if (!user) {
throw new Meteor.Error(
@@ -59,7 +59,7 @@ Meteor.methods({
});
const svg = new QRCode(uri).svg();
Meteor.users.update(
await Meteor.users.update(
{ _id: user._id },
{
$set: {
@@ -72,9 +72,9 @@ Meteor.methods({
return { svg, secret, uri };
},
enableUser2fa(code) {
async enableUser2fa(code) {
check(code, String);
const user = Meteor.user();
const user = await Meteor.user();
if (!user) {
throw new Meteor.Error(400, 'No user logged in.');
@@ -94,7 +94,7 @@ Meteor.methods({
Accounts._handleError('Invalid 2FA code', true, 'invalid-2fa-code');
}
Meteor.users.update(
await Meteor.users.update(
{ _id: user._id },
{
$set: {
@@ -106,14 +106,14 @@ Meteor.methods({
}
);
},
disableUser2fa() {
async disableUser2fa() {
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error(400, 'No user logged in.');
}
Meteor.users.update(
await Meteor.users.update(
{ _id: userId },
{
$unset: {
@@ -122,8 +122,8 @@ Meteor.methods({
}
);
},
has2faEnabled() {
return Accounts._is2faEnabledForUser();
async has2faEnabled() {
return await Accounts._is2faEnabledForUser();
},
});

View File

@@ -1,15 +1,16 @@
import { Accounts } from 'meteor/accounts-base';
import { Random } from 'meteor/random';
const findUserById = id => Meteor.users.findOne(id);
const findUserById =
async id => await Meteor.users.findOne(id);
Tinytest.add('account - 2fa - has2faEnabled - server', test => {
Tinytest.addAsync('account - 2fa - has2faEnabled - server', async test => {
// Create users
const userWithout2FA = Accounts.insertUserDoc(
const userWithout2FA = await Accounts.insertUserDoc(
{},
{ emails: [{ address: `${Random.id()}@meteorapp.com`, verified: true }] }
);
const userWith2FA = Accounts.insertUserDoc(
const userWith2FA = await Accounts.insertUserDoc(
{},
{
emails: [{ address: `${Random.id()}@meteorapp.com`, verified: true }],
@@ -19,10 +20,10 @@ Tinytest.add('account - 2fa - has2faEnabled - server', test => {
}
);
test.equal(Accounts._check2faEnabled(findUserById(userWithout2FA)), false);
test.equal(Accounts._check2faEnabled(findUserById(userWith2FA)), true);
test.equal(Accounts._check2faEnabled(await findUserById(userWithout2FA)), false);
test.equal(Accounts._check2faEnabled(await findUserById(userWith2FA)), true);
// cleanup
Accounts.users.remove(userWithout2FA);
Accounts.users.remove(userWith2FA);
await Accounts.users.remove(userWithout2FA);
await Accounts.users.remove(userWith2FA);
});

View File

@@ -1,23 +1,22 @@
Tinytest.add('minifier-js - verify how terser handles an empty string', (test) => {
let result = meteorJsMinify('');
Tinytest.addAsync('minifier-js - verify how terser handles an empty string', async (test) => {
let result = await meteorJsMinify('');
test.equal(result.code, '');
test.equal(result.minifier, 'terser');
});
Tinytest.add('minifier-js - verify terser is able to minify valid javascript', (test) => {
let result = meteorJsMinify('function add(first,second){return first + second; }\n');
Tinytest.addAsync('minifier-js - verify terser is able to minify valid javascript', async (test) => {
let result = await meteorJsMinify('function add(first,second){return first + second; }\n');
test.equal(result.code, 'function add(n,d){return n+d}');
test.equal(result.minifier, 'terser');
});
Tinytest.add('minifier-js - verify error handling is done as expected', (test) => {
test.throws( () => meteorJsMinify('let name = {;\n'), undefined );
Tinytest.addAsync('minifier-js - verify error handling is done as expected', async (test) => {
await test.throwsAsync( async () => await meteorJsMinify('let name = {;\n'), undefined );
});
Tinytest.add('minifier-js - verify tersers error object has the fields we use for reporting errors to users', (test) => {
let result;
Tinytest.addAsync('minifier-js - verify tersers error object has the fields we use for reporting errors to users', async (test) => {
try {
result = meteorJsMinify('let name = {;\n');
await meteorJsMinify('let name = {;\n');
}
catch (err) {
test.isNotUndefined(err.name);

View File

@@ -1,18 +1,11 @@
let terser;
const terserMinify = async (source, options, callback) => {
const terserMinify = async (source, options) => {
terser = terser || Npm.require("terser");
try {
const result = await terser.minify(source, options);
callback(null, result);
return result;
} catch (e) {
callback(e);
return e;
}
return await terser.minify(source, options);
};
export const meteorJsMinify = function (source) {
export const meteorJsMinify = async function (source) {
const result = {};
const NODE_ENV = process.env.NODE_ENV || "development";
@@ -33,13 +26,7 @@ export const meteorJsMinify = function (source) {
safari10: true, // set this option to true to work around the Safari 10/11 await bug
};
const terserJsMinify = Meteor.wrapAsync(terserMinify);
let terserResult;
try {
terserResult = terserJsMinify(source, options);
} catch (e) {
throw e;
}
const terserResult = await terserMinify(source, options);
// this is kept to maintain backwards compatability
result.code = terserResult.code;

View File

@@ -251,7 +251,7 @@ const isSafe = value => {
};
// Internal: used by the oauth1 and oauth2 packages
OAuth._renderOauthResults = (res, query, credentialSecret) => {
OAuth._renderOauthResults = async (res, query, credentialSecret) => {
// For tests, we support the `only_credential_secret_for_test`
// parameter, which just returns the credential secret without any
// surrounding HTML. (The test needs to be able to easily grab the
@@ -282,18 +282,23 @@ OAuth._renderOauthResults = (res, query, credentialSecret) => {
}
}
OAuth._endOfLoginResponse(res, details);
await OAuth._endOfLoginResponse(res, details);
}
};
const getAsset = (name) => {
return new Promise((resolve, reject) => Assets.getText(
`${name}.html`,
(err, data) => err ? reject(err) : resolve(data)))
}
// This "template" (not a real Spacebars template, just an HTML file
// with some ##PLACEHOLDER##s) communicates the credential secret back
// to the main window and then closes the popup.
OAuth._endOfPopupResponseTemplate = Assets.getText(
"end_of_popup_response.html");
OAuth._endOfPopupResponseTemplate =
async () => await getAsset('end_of_popup_response')
OAuth._endOfRedirectResponseTemplate = Assets.getText(
"end_of_redirect_response.html");
OAuth._endOfRedirectResponseTemplate =
async () => await getAsset('end_of_redirect_response')
// Renders the end of login response template into some HTML and JavaScript
// that closes the popup or redirects at the end of the OAuth flow.
@@ -306,7 +311,7 @@ OAuth._endOfRedirectResponseTemplate = Assets.getText(
// - redirectUrl
// - isCordova (boolean)
//
const renderEndOfLoginResponse = options => {
const renderEndOfLoginResponse = async options => {
// It would be nice to use Blaze here, but it's a little tricky
// because our mustaches would be inside a <script> tag, and Blaze
// would treat the <script> tag contents as text (e.g. encode '&' as
@@ -338,13 +343,12 @@ const renderEndOfLoginResponse = options => {
let template;
if (options.loginStyle === 'popup') {
template = OAuth._endOfPopupResponseTemplate;
template = await OAuth._endOfPopupResponseTemplate();
} else if (options.loginStyle === 'redirect') {
template = OAuth._endOfRedirectResponseTemplate;
template = await OAuth._endOfRedirectResponseTemplate();
} else {
throw new Error(`invalid loginStyle: ${options.loginStyle}`);
}
const result = template.replace(/##CONFIG##/, JSON.stringify(config))
.replace(
/##ROOT_URL_PATH_PREFIX##/, __meteor_runtime_config__.ROOT_URL_PATH_PREFIX
@@ -384,7 +388,7 @@ const renderEndOfLoginResponse = options => {
// so shouldn't be trusted for security decisions or included in
// the response without sanitizing it first. Only one of `error`
// or `credentials` should be set.
OAuth._endOfLoginResponse = (res, details) => {
OAuth._endOfLoginResponse = async (res, details) => {
res.writeHead(200, {'Content-Type': 'text/html'});
let redirectUrl;
@@ -406,7 +410,7 @@ OAuth._endOfLoginResponse = (res, details) => {
Log.warn("Error in OAuth Server: " +
(details.error instanceof Error ?
details.error.message : details.error));
res.end(renderEndOfLoginResponse({
res.end(await renderEndOfLoginResponse({
loginStyle: details.loginStyle,
setCredentialToken: false,
redirectUrl,
@@ -418,7 +422,7 @@ OAuth._endOfLoginResponse = (res, details) => {
// If we have a credentialSecret, report it back to the parent
// window, with the corresponding credentialToken. The parent window
// uses the credentialToken and credentialSecret to log in over DDP.
res.end(renderEndOfLoginResponse({
res.end(await renderEndOfLoginResponse({
loginStyle: details.loginStyle,
setCredentialToken: true,
credentialToken: details.credentials.token,

View File

@@ -1,60 +1,65 @@
Tinytest.add("oauth - pendingCredential handles Errors", test => {
const credentialToken = Random.id();
Tinytest.addAsync("oauth - pendingCredential handles Errors",
async test => {
const credentialToken = Random.id();
const testError = new Error("This is a test error");
testError.stack = 'test stack';
OAuth._storePendingCredential(credentialToken, testError);
const testError = new Error("This is a test error");
testError.stack = 'test stack';
await OAuth._storePendingCredential(credentialToken, testError);
// Test that the result for the token is the expected error
const result = OAuth._retrievePendingCredential(credentialToken);
test.instanceOf(result, Error);
test.equal(result.message, testError.message);
test.equal(result.stack, testError.stack);
});
// Test that the result for the token is the expected error
const result = await OAuth._retrievePendingCredential(credentialToken);
test.instanceOf(result, Error);
test.equal(result.message, testError.message);
test.equal(result.stack, testError.stack);
});
Tinytest.add("oauth - pendingCredential handles Meteor.Errors", test => {
const credentialToken = Random.id();
Tinytest.addAsync("oauth - pendingCredential handles Meteor.Errors",
async test => {
const credentialToken = Random.id();
const testError = new Meteor.Error(401, "This is a test error");
testError.stack = 'test stack';
OAuth._storePendingCredential(credentialToken, testError);
const testError = new Meteor.Error(401, "This is a test error");
testError.stack = 'test stack';
await OAuth._storePendingCredential(credentialToken, testError);
// Test that the result for the token is the expected error
const result = OAuth._retrievePendingCredential(credentialToken);
test.instanceOf(result, Meteor.Error);
test.equal(result.error, testError.error);
test.equal(result.message, testError.message);
test.equal(result.reason, testError.reason);
test.equal(result.stack, testError.stack);
test.isUndefined(result.meteorError);
});
// Test that the result for the token is the expected error
const result = await OAuth._retrievePendingCredential(credentialToken);
test.instanceOf(result, Meteor.Error);
test.equal(result.error, testError.error);
test.equal(result.message, testError.message);
test.equal(result.reason, testError.reason);
test.equal(result.stack, testError.stack);
test.isUndefined(result.meteorError);
});
Tinytest.add("oauth - null, undefined key for pendingCredential", test => {
const cred = Random.id();
test.throws(() => OAuth._storePendingCredential(null, cred));
test.throws(() => OAuth._storePendingCredential(undefined, cred));
});
Tinytest.addAsync("oauth - null, undefined key for pendingCredential",
async test => {
const cred = Random.id();
await test.throwsAsync(() => OAuth._storePendingCredential(null, cred));
await test.throwsAsync(() => OAuth._storePendingCredential(undefined, cred));
});
Tinytest.add("oauth - pendingCredential handles duplicate key", test => {
const key = Random.id();
const cred = Random.id();
OAuth._storePendingCredential(key, cred);
const newCred = Random.id();
OAuth._storePendingCredential(key, newCred);
test.equal(OAuth._retrievePendingCredential(key), newCred);
});
Tinytest.addAsync("oauth - pendingCredential handles duplicate key",
async test => {
const key = Random.id();
const cred = Random.id();
await OAuth._storePendingCredential(key, cred);
const newCred = Random.id();
await OAuth._storePendingCredential(key, newCred);
test.equal(await OAuth._retrievePendingCredential(key), newCred);
});
Tinytest.add("oauth - pendingCredential requires credential secret", test => {
const key = Random.id();
const cred = Random.id();
const secret = Random.id();
OAuth._storePendingCredential(key, cred, secret);
test.equal(OAuth._retrievePendingCredential(key), undefined);
test.equal(OAuth._retrievePendingCredential(key, secret), cred);
});
Tinytest.addAsync("oauth - pendingCredential requires credential secret",
async test => {
const key = Random.id();
const cred = Random.id();
const secret = Random.id();
await OAuth._storePendingCredential(key, cred, secret);
test.equal(await OAuth._retrievePendingCredential(key), undefined);
test.equal(await OAuth._retrievePendingCredential(key, secret), cred);
});
Tinytest.add("oauth - _endOfLoginResponse with popup loginStyle supports unspecified ROOT_URL_PATH_PREFIX",
test => {
Tinytest.addAsync("oauth - _endOfLoginResponse with popup loginStyle supports unspecified ROOT_URL_PATH_PREFIX",
async test => {
const res = {
writeHead: () => {},
end: content => {
@@ -68,12 +73,12 @@ Tinytest.add("oauth - _endOfLoginResponse with popup loginStyle supports unspeci
credentials: {},
loginStyle: 'popup'
};
OAuth._endOfLoginResponse(res, details);
await OAuth._endOfLoginResponse(res, details);
}
);
Tinytest.add("oauth - _endOfLoginResponse with popup loginStyle supports ROOT_URL_PATH_PREFIX",
test => {
Tinytest.addAsync("oauth - _endOfLoginResponse with popup loginStyle supports ROOT_URL_PATH_PREFIX",
async test => {
const rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX;
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX = '/test-root-url-prefix';
const res = {
@@ -90,12 +95,12 @@ Tinytest.add("oauth - _endOfLoginResponse with popup loginStyle supports ROOT_UR
credentials: {},
loginStyle: 'popup'
};
OAuth._endOfLoginResponse(res, details);
await OAuth._endOfLoginResponse(res, details);
}
);
Tinytest.add("oauth - _endOfLoginResponse with redirect loginStyle supports unspecified ROOT_URL_PATH_PREFIX",
test => {
Tinytest.addAsync("oauth - _endOfLoginResponse with redirect loginStyle supports unspecified ROOT_URL_PATH_PREFIX",
async test => {
const res = {
writeHead: () => {},
end: content => {
@@ -114,13 +119,13 @@ Tinytest.add("oauth - _endOfLoginResponse with redirect loginStyle supports unsp
}), 'binary').toString('base64')
}
};
OAuth._endOfLoginResponse(res, details);
await OAuth._endOfLoginResponse(res, details);
}
);
Tinytest.add("oauth - _endOfLoginResponse with redirect loginStyle supports ROOT_URL_PATH_PREFIX",
test => {
Tinytest.addAsync("oauth - _endOfLoginResponse with redirect loginStyle supports ROOT_URL_PATH_PREFIX",
async test => {
const rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX;
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX = '/test-root-url-prefix';
const res = {
@@ -142,6 +147,6 @@ Tinytest.add("oauth - _endOfLoginResponse with redirect loginStyle supports ROOT
}), 'binary').toString('base64')
}
};
OAuth._endOfLoginResponse(res, details);
await OAuth._endOfLoginResponse(res, details);
}
);

View File

@@ -40,28 +40,29 @@ const _cleanupHandle = Meteor.setInterval(_cleanStaleResults, 60 * 1000);
// @param credentialSecret {string} A secret that must be presented in
// addition to the `key` to retrieve the credential
//
OAuth._storePendingCredential = (key, credential, credentialSecret = null) => {
check(key, String);
check(credentialSecret, Match.Maybe(String));
OAuth._storePendingCredential =
async (key, credential, credentialSecret = null) => {
check(key, String);
check(credentialSecret, Match.Maybe(String));
if (credential instanceof Error) {
credential = storableError(credential);
} else {
credential = OAuth.sealSecret(credential);
}
if (credential instanceof Error) {
credential = storableError(credential);
} else {
credential = OAuth.sealSecret(credential);
}
// We do an upsert here instead of an insert in case the user happens
// to somehow send the same `state` parameter twice during an OAuth
// login; we don't want a duplicate key error.
OAuth._pendingCredentials.upsert({
key,
}, {
key,
credential,
credentialSecret,
createdAt: new Date()
});
};
// We do an upsert here instead of an insert in case the user happens
// to somehow send the same `state` parameter twice during an OAuth
// login; we don't want a duplicate key error.
await OAuth._pendingCredentials.upsert({
key,
}, {
key,
credential,
credentialSecret,
createdAt: new Date()
});
};
// Retrieves and removes a credential from the _pendingCredentials collection
@@ -69,24 +70,25 @@ OAuth._storePendingCredential = (key, credential, credentialSecret = null) => {
// @param key {string}
// @param credentialSecret {string}
//
OAuth._retrievePendingCredential = (key, credentialSecret = null) => {
check(key, String);
OAuth._retrievePendingCredential =
async (key, credentialSecret = null) => {
check(key, String);
const pendingCredential = OAuth._pendingCredentials.findOne({
key,
credentialSecret,
});
const pendingCredential = await OAuth._pendingCredentials.findOne({
key,
credentialSecret,
});
if (pendingCredential) {
OAuth._pendingCredentials.remove({ _id: pendingCredential._id });
if (pendingCredential.credential.error)
return recreateError(pendingCredential.credential.error);
else
return OAuth.openSecret(pendingCredential.credential);
} else {
return undefined;
}
};
if (pendingCredential) {
await OAuth._pendingCredentials.remove({ _id: pendingCredential._id });
if (pendingCredential.credential.error)
return recreateError(pendingCredential.credential.error);
else
return OAuth.openSecret(pendingCredential.credential);
} else {
return undefined;
}
};
// Convert an Error into an object that can be stored in mongo

View File

@@ -9,7 +9,7 @@ Plugin.registerMinifier({
class MeteorMinifier {
processFilesForBundle (files, options) {
async processFilesForBundle (files, options) {
const mode = options.minifyMode;
// don't minify anything for development
@@ -63,7 +63,7 @@ class MeteorMinifier {
stats: Object.create(null)
};
files.forEach(file => {
for await (file of files) {
// Don't reminify *.min.js.
if (/\.min\.js$/.test(file.getPathInBundle())) {
toBeAdded.data += file.getContentsAsString();
@@ -71,7 +71,7 @@ class MeteorMinifier {
else {
let minified;
try {
minified = meteorJsMinify(file.getContentsAsString());
minified = await meteorJsMinify(file.getContentsAsString());
}
catch (err) {
maybeThrowMinifyErrorBySourceFile(err, file);
@@ -94,7 +94,7 @@ class MeteorMinifier {
toBeAdded.data += '\n\n';
Plugin.nudge();
});
}
// this is where the minified code gets added to one
// JS file that is delivered to the client