Implement a reusable AccountsCommon constructor.

This involves moving Accounts.* methods defined in accounts_common.js onto
AccountsCommon.prototype.*.
This commit is contained in:
Ben Newman
2015-04-16 18:01:21 -04:00
parent b6348d4939
commit ccab6ca621

View File

@@ -1,12 +1,64 @@
/**
* @namespace Accounts
* @summary The namespace for all accounts-related methods.
*/
Accounts = {};
// @summary Super-constructor for AccountsClient an AccountsServer.
// @locus Anywhere
// @param options {Object} an object with fields:
// - connection {Object} Optional DDP connection to reuse.
// - ddpUrl {String} Optional URL for creating a new DDP connection.
AccountsCommon = function AccountsCommon(options) {
// Currently this is read directly by packages like accounts-password
// and accounts-ui-unstyled.
this._options = {};
// Currently this is read directly by packages like accounts-password
// and accounts-ui-unstyled.
Accounts._options = {};
// Note that setting this.connection = null causes this.users to be a
// LocalCollection, which is not what we want.
this.connection = undefined;
this._initConnection(options || {});
// There is an allow call in accounts_server.js that restricts writes to
// this collection.
this.users = new Mongo.Collection("users", {
_preventAutopublish: true,
connection: this.connection
});
// Callback exceptions are printed with Meteor._debug and ignored.
this._onLoginHook = new Hook({
debugPrintExceptions: "onLogin callback"
});
this._onLoginFailureHook = new Hook({
debugPrintExceptions: "onLoginFailure callback"
});
};
var Ap = AccountsCommon.prototype;
/**
* @summary Get the current user id, or `null` if no user is logged in. A reactive data source.
* @locus Anywhere but publish functions
*/
Ap.userId = function () {
throw new Error("userId method not implemented");
};
Ap.user = function () {
var userId = this.userId();
return userId ? this.users.findOne(userId) : null;
};
// Note that Accounts is defined separately in accounts_client.js and
// accounts_server.js.
Meteor.userId = function () {
return Accounts.userId();
};
/**
* @summary Get the current user record, or `null` if no user is logged in. A reactive data source.
* @locus Anywhere but publish functions
*/
Meteor.user = function () {
return Accounts.user();
};
// how long (in days) until a login token expires
var DEFAULT_LOGIN_EXPIRATION_DAYS = 90;
@@ -23,6 +75,9 @@ CONNECTION_CLOSE_DELAY_MS = 10 * 1000;
// Set up config for the accounts system. Call this on both the client
// and the server.
//
// Note that this method gets overridden on AccountsServer.prototype, but
// the overriding method calls the overridden method.
//
// XXX we should add some enforcement that this is called on both the
// client and the server. Otherwise, a user can
// 'forbidClientAccountCreation' only on the client and while it looks
@@ -52,7 +107,9 @@ CONNECTION_CLOSE_DELAY_MS = 10 * 1000;
* @param {Number} options.loginExpirationInDays The number of days from when a user logs in until their token expires and they are logged out. Defaults to 90. Set to `null` to disable login expiration.
* @param {String} options.oauthSecretKey When using the `oauth-encryption` package, the 16 byte key using to encrypt sensitive account credentials in the database, encoded in base64. This option may only be specifed on the server. See packages/oauth-encryption/README.md for details.
*/
Accounts.config = function(options) {
Ap.config = function (options) {
var self = this;
// We don't want users to accidentally only call Accounts.config on the
// client, where some of the options will have partial effects (eg removing
// the "create account" button from accounts-ui if forbidClientAccountCreation
@@ -91,21 +148,19 @@ Accounts.config = function(options) {
// set values in Accounts._options
_.each(VALID_KEYS, function (key) {
if (key in options) {
if (key in Accounts._options) {
if (key in self._options) {
throw new Error("Can't set `" + key + "` more than once");
} else {
Accounts._options[key] = options[key];
}
self._options[key] = options[key];
}
});
// If the user set loginExpirationInDays to null, then we need to clear the
// timer that periodically expires tokens.
if (Meteor.isServer)
maybeStopExpireTokensInterval();
};
if (Meteor.isClient) {
Ap._initConnection = function (options) {
if (! Meteor.isClient) {
return;
}
// The connection used by the Accounts system. This is the connection
// that will get logged in by Meteor.login(), and this is the
// connection whose login state will be reflected by Meteor.userId().
@@ -113,10 +168,13 @@ if (Meteor.isClient) {
// It would be much preferable for this to be in accounts_client.js,
// but it has to be here because it's needed to create the
// Meteor.users collection.
Accounts.connection = Meteor.connection;
if (typeof __meteor_runtime_config__ !== "undefined" &&
__meteor_runtime_config__.ACCOUNTS_CONNECTION_URL) {
if (options.connection) {
this.connection = options.connection;
} else if (options.ddpUrl) {
this.connection = DDP.connect(options.ddpUrl);
} else if (typeof __meteor_runtime_config__ !== "undefined" &&
__meteor_runtime_config__.ACCOUNTS_CONNECTION_URL) {
// Temporary, internal hook to allow the server to point the client
// to a different authentication server. This is for a very
// particular use case that comes up when implementing a oauth
@@ -124,83 +182,62 @@ if (Meteor.isClient) {
//
// We will eventually provide a general way to use account-base
// against any DDP connection, not just one special one.
Accounts.connection = DDP.connect(
__meteor_runtime_config__.ACCOUNTS_CONNECTION_URL)
this.connection =
DDP.connect(__meteor_runtime_config__.ACCOUNTS_CONNECTION_URL);
} else {
this.connection = Meteor.connection;
}
}
// Users table. Don't use the normal autopublish, since we want to hide
// some fields. Code to autopublish this is in accounts_server.js.
// XXX Allow users to configure this collection name.
/**
* @summary A [Mongo.Collection](#collections) containing user documents.
* @locus Anywhere
* @type {Mongo.Collection}
*/
Meteor.users = new Mongo.Collection("users", {
_preventAutopublish: true,
connection: Meteor.isClient ? Accounts.connection : Meteor.connection
});
// There is an allow call in accounts_server that restricts this
// collection.
};
// loginServiceConfiguration and ConfigError are maintained for backwards compatibility
Meteor.startup(function () {
var ServiceConfiguration =
Package['service-configuration'].ServiceConfiguration;
Accounts.loginServiceConfiguration = ServiceConfiguration.configurations;
Accounts.ConfigError = ServiceConfiguration.ConfigError;
Ap.loginServiceConfiguration = ServiceConfiguration.configurations;
Ap.ConfigError = ServiceConfiguration.ConfigError;
});
// Thrown when the user cancels the login process (eg, closes an oauth
// popup, declines retina scan, etc)
Accounts.LoginCancelledError = function(description) {
Ap.LoginCancelledError = function (description) {
Error.apply(this, arguments);
this.message = description;
};
// This is used to transmit specific subclass errors over the wire. We should
// come up with a more generic way to do this (eg, with some sort of symbolic
// error code rather than a number).
Accounts.LoginCancelledError.numericError = 0x8acdc2f;
Accounts.LoginCancelledError.prototype = new Error();
Accounts.LoginCancelledError.prototype.name = 'Accounts.LoginCancelledError';
Ap.LoginCancelledError.numericError = 0x8acdc2f;
var LCEp = Ap.LoginCancelledError.prototype = Object.create(Error.prototype);
LCEp.constructor = Ap.LoginCancelledError;
LCEp.name = 'Accounts.LoginCancelledError';
getTokenLifetimeMs = function () {
return (Accounts._options.loginExpirationInDays ||
Ap._getTokenLifetimeMs = function () {
return (this._options.loginExpirationInDays ||
DEFAULT_LOGIN_EXPIRATION_DAYS) * 24 * 60 * 60 * 1000;
};
Accounts._tokenExpiration = function (when) {
Ap._tokenExpiration = function (when) {
// We pass when through the Date constructor for backwards compatibility;
// `when` used to be a number.
return new Date((new Date(when)).getTime() + getTokenLifetimeMs());
return new Date((new Date(when)).getTime() + this._getTokenLifetimeMs());
};
Accounts._tokenExpiresSoon = function (when) {
var minLifetimeMs = .1 * getTokenLifetimeMs();
Ap._tokenExpiresSoon = function (when) {
var minLifetimeMs = .1 * this._getTokenLifetimeMs();
var minLifetimeCapMs = MIN_TOKEN_LIFETIME_CAP_SECS * 1000;
if (minLifetimeMs > minLifetimeCapMs)
minLifetimeMs = minLifetimeCapMs;
return new Date() > (new Date(when) - minLifetimeMs);
};
// Callback exceptions are printed with Meteor._debug and ignored.
onLoginHook = new Hook({
debugPrintExceptions: "onLogin callback"
});
onLoginFailureHook = new Hook({
debugPrintExceptions: "onLoginFailure callback"
});
/**
* @summary Register a callback to be called after a login attempt succeeds.
* @locus Anywhere
* @param {Function} func The callback to be called when login is successful.
*/
Accounts.onLogin = function (func) {
return onLoginHook.register(func);
Ap.onLogin = function (func) {
return this._onLoginHook.register(func);
};
/**
@@ -208,6 +245,6 @@ Accounts.onLogin = function (func) {
* @locus Anywhere
* @param {Function} func The callback to be called after the login has failed.
*/
Accounts.onLoginFailure = function (func) {
return onLoginFailureHook.register(func);
Ap.onLoginFailure = function (func) {
return this._onLoginFailureHook.register(func);
};