diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index 3356c3cef2..6061622a49 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -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); };