├── .gitignore ├── smart.json ├── README.md ├── accounts_ui_tests.js ├── login_buttons_single.html ├── package.js ├── login_buttons_single.js ├── accounts_ui.js ├── login_buttons.html ├── login_buttons_session.js ├── login_buttons_dialogs.html ├── login_buttons_images.css ├── login_buttons.js ├── login_buttons_dropdown.html ├── login_buttons_dialogs.js └── login_buttons_dropdown.js /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "accounts-ui-bootstrap-dropdown", 3 | "description": "Meteor's accounts templates styled for bootstrap dropdown menu", 4 | "homepage": "https://github.com/erobit/meteor-accounts-ui-bootstrap-dropdown", 5 | "author": "Eric Robitaille ", 6 | "version": "0.8.3", 7 | "git": "https://github.com/erobit/meteor-accounts-ui-bootstrap-dropdown.git" 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | meteor-accounts-ui-bootstrap-dropdown 2 | ===================================== 3 | 4 | Meteor accounts-ui styled with twitter/bootstrap dropdown 5 | 6 | Prerequisites 7 | ------------- 8 | 9 | Use the meteorite package manager 10 | http://oortcloud.github.com/meteorite/ 11 | 12 | [sudo] npm install -g meteorite 13 | 14 | How to add to your meteor app 15 | ----------------------------- 16 | 17 | mrt add accounts-ui-bootstrap-dropdown 18 | 19 | meteor add bootstrap 20 | 21 | meteor add accounts-password 22 | 23 | How to use 24 | ------------- 25 | 26 | Add {{> loginButtons }} to your template 27 | 28 | Add Accounts configuration to your javascript file 29 | 30 | i.e. 31 | 32 | if (Meteor.isClient) { 33 | Accounts.ui.config({ 34 | passwordSignupFields: 'USERNAME_AND_OPTIONAL_EMAIL' 35 | }); 36 | 37 | ... -------------------------------------------------------------------------------- /accounts_ui_tests.js: -------------------------------------------------------------------------------- 1 | // XXX Most of the testing of accounts-ui is done manually, across 2 | // multiple browsers using examples/unfinished/accounts-ui-helper. We 3 | // should *definitely* automate this, but Tinytest is generally not 4 | // the right abstraction to use for this. 5 | 6 | 7 | // XXX it'd be cool to also test that the right thing happens if options 8 | // *are* validated, but Accouns.ui._options is global state which makes this hard 9 | // (impossible?) 10 | Tinytest.add('accounts-ui - config validates keys', function (test) { 11 | test.throws(function () { 12 | Accounts.ui.config({foo: "bar"}); 13 | }); 14 | 15 | test.throws(function () { 16 | Accounts.ui.config({passwordSignupFields: "not a valid option"}); 17 | }); 18 | 19 | test.throws(function () { 20 | Accounts.ui.config({requestPermissions: {facebook: "not an array"}}); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /login_buttons_single.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 29 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "Bootstrap styled version of login widgets" 3 | }); 4 | 5 | Package.on_use(function (api) { 6 | api.use(['deps', 'service-configuration', 'accounts-base', 7 | 'underscore', 'templating', 'session'], 'client'); 8 | 9 | // Export Accounts (etc) to packages using this one. 10 | api.imply('accounts-base', ['client', 'server']); 11 | 12 | // Allow us to call Accounts.oauth.serviceNames, if there are any OAuth 13 | // services. 14 | api.use('accounts-oauth', {weak: true}); 15 | // Allow us to directly test if accounts-password (which doesn't use 16 | // Accounts.oauth.registerService) exists. 17 | api.use('accounts-password', {weak: true}); 18 | 19 | api.add_files([ 20 | 'accounts_ui.js', 21 | 22 | 'login_buttons_images.css', 23 | 'login_buttons.html', 24 | 'login_buttons_single.html', 25 | 'login_buttons_dropdown.html', 26 | 'login_buttons_dialogs.html', 27 | 28 | 'login_buttons_session.js', 29 | 30 | 'login_buttons.js', 31 | 'login_buttons_single.js', 32 | 'login_buttons_dropdown.js', 33 | 'login_buttons_dialogs.js'], 'client'); 34 | }); 35 | 36 | Package.on_test(function (api) { 37 | //api.use('meteor-accounts-ui-bootstrap'); 38 | //api.use('tinytest'); 39 | //api.add_files('accounts_ui_tests.js', 'client'); 40 | }); 41 | -------------------------------------------------------------------------------- /login_buttons_single.js: -------------------------------------------------------------------------------- 1 | // for convenience 2 | var loginButtonsSession = Accounts._loginButtonsSession; 3 | 4 | Template._loginButtonsLoggedOutSingleLoginButton.events({ 5 | 'click .login-button': function () { 6 | var serviceName = this.name; 7 | loginButtonsSession.resetMessages(); 8 | var callback = function (err) { 9 | if (!err) { 10 | loginButtonsSession.closeDropdown(); 11 | } else if (err instanceof Accounts.LoginCancelledError) { 12 | // do nothing 13 | } else if (err instanceof ServiceConfiguration.ConfigError) { 14 | loginButtonsSession.configureService(serviceName); 15 | } else { 16 | loginButtonsSession.errorMessage(err.reason || "Unknown error"); 17 | } 18 | }; 19 | 20 | // XXX Service providers should be able to specify their 21 | // `Meteor.loginWithX` method name. 22 | var loginWithService = Meteor["loginWith" + 23 | (serviceName === 'meteor-developer' ? 24 | 'MeteorDeveloperAccount' : 25 | capitalize(serviceName))]; 26 | 27 | var options = {}; // use default scope unless specified 28 | if (Accounts.ui._options.requestPermissions[serviceName]) 29 | options.requestPermissions = Accounts.ui._options.requestPermissions[serviceName]; 30 | if (Accounts.ui._options.requestOfflineToken[serviceName]) 31 | options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName]; 32 | 33 | loginWithService(options, callback); 34 | } 35 | }); 36 | 37 | Template._loginButtonsLoggedOutSingleLoginButton.configured = function () { 38 | return !!ServiceConfiguration.configurations.findOne({service: this.name}); 39 | }; 40 | 41 | Template._loginButtonsLoggedOutSingleLoginButton.capitalizedName = function () { 42 | if (this.name === 'github') 43 | // XXX we should allow service packages to set their capitalized name 44 | return 'GitHub'; 45 | else if (this.name === 'meteor-developer') 46 | return 'Meteor'; 47 | else 48 | return capitalize(this.name); 49 | }; 50 | 51 | // XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js 52 | var capitalize = function(str){ 53 | str = str == null ? '' : String(str); 54 | return str.charAt(0).toUpperCase() + str.slice(1); 55 | }; 56 | -------------------------------------------------------------------------------- /accounts_ui.js: -------------------------------------------------------------------------------- 1 | Accounts.ui = {}; 2 | 3 | Accounts.ui._options = { 4 | requestPermissions: {}, 5 | requestOfflineToken: {} 6 | }; 7 | 8 | Accounts.ui.config = function(options) { 9 | // validate options keys 10 | var VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken']; 11 | _.each(_.keys(options), function (key) { 12 | if (!_.contains(VALID_KEYS, key)) 13 | throw new Error("Accounts.ui.config: Invalid key: " + key); 14 | }); 15 | 16 | // deal with `passwordSignupFields` 17 | if (options.passwordSignupFields) { 18 | if (_.contains([ 19 | "USERNAME_AND_EMAIL", 20 | "USERNAME_AND_OPTIONAL_EMAIL", 21 | "USERNAME_ONLY", 22 | "EMAIL_ONLY" 23 | ], options.passwordSignupFields)) { 24 | if (Accounts.ui._options.passwordSignupFields) 25 | throw new Error("Accounts.ui.config: Can't set `passwordSignupFields` more than once"); 26 | else 27 | Accounts.ui._options.passwordSignupFields = options.passwordSignupFields; 28 | } else { 29 | throw new Error("Accounts.ui.config: Invalid option for `passwordSignupFields`: " + options.passwordSignupFields); 30 | } 31 | } 32 | 33 | // deal with `requestPermissions` 34 | if (options.requestPermissions) { 35 | _.each(options.requestPermissions, function (scope, service) { 36 | if (Accounts.ui._options.requestPermissions[service]) { 37 | throw new Error("Accounts.ui.config: Can't set `requestPermissions` more than once for " + service); 38 | } else if (!(scope instanceof Array)) { 39 | throw new Error("Accounts.ui.config: Value for `requestPermissions` must be an array"); 40 | } else { 41 | Accounts.ui._options.requestPermissions[service] = scope; 42 | } 43 | }); 44 | } 45 | 46 | // deal with `requestOfflineToken` 47 | if (options.requestOfflineToken) { 48 | _.each(options.requestOfflineToken, function (value, service) { 49 | if (service !== 'google') 50 | throw new Error("Accounts.ui.config: `requestOfflineToken` only supported for Google login at the moment."); 51 | 52 | if (Accounts.ui._options.requestOfflineToken[service]) { 53 | throw new Error("Accounts.ui.config: Can't set `requestOfflineToken` more than once for " + service); 54 | } else { 55 | Accounts.ui._options.requestOfflineToken[service] = value; 56 | } 57 | }); 58 | } 59 | }; 60 | 61 | passwordSignupFields = function () { 62 | return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY"; 63 | }; 64 | -------------------------------------------------------------------------------- /login_buttons.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 34 | 35 | 44 | 45 | 66 | 67 | 68 | 76 | 77 | 82 | 83 | -------------------------------------------------------------------------------- /login_buttons_session.js: -------------------------------------------------------------------------------- 1 | var VALID_KEYS = [ 2 | 'dropdownVisible', 3 | 4 | // XXX consider replacing these with one key that has an enum for values. 5 | 'inSignupFlow', 6 | 'inForgotPasswordFlow', 7 | 'inChangePasswordFlow', 8 | 'inMessageOnlyFlow', 9 | 10 | 'errorMessage', 11 | 'infoMessage', 12 | 13 | // dialogs with messages (info and error) 14 | 'resetPasswordToken', 15 | 'enrollAccountToken', 16 | 'justVerifiedEmail', 17 | 18 | 'configureLoginServiceDialogVisible', 19 | 'configureLoginServiceDialogServiceName', 20 | 'configureLoginServiceDialogSaveDisabled' 21 | ]; 22 | 23 | var validateKey = function (key) { 24 | if (!_.contains(VALID_KEYS, key)) 25 | throw new Error("Invalid key in loginButtonsSession: " + key); 26 | }; 27 | 28 | var KEY_PREFIX = "Meteor.loginButtons."; 29 | 30 | // XXX This should probably be package scope rather than exported 31 | // (there was even a comment to that effect here from before we had 32 | // namespacing) but accounts-ui-viewer uses it, so leave it as is for 33 | // now 34 | Accounts._loginButtonsSession = { 35 | set: function(key, value) { 36 | validateKey(key); 37 | if (_.contains(['errorMessage', 'infoMessage'], key)) 38 | throw new Error("Don't set errorMessage or infoMessage directly. Instead, use errorMessage() or infoMessage()."); 39 | 40 | this._set(key, value); 41 | }, 42 | 43 | _set: function(key, value) { 44 | Session.set(KEY_PREFIX + key, value); 45 | }, 46 | 47 | get: function(key) { 48 | validateKey(key); 49 | return Session.get(KEY_PREFIX + key); 50 | }, 51 | 52 | closeDropdown: function () { 53 | this.set('inSignupFlow', false); 54 | this.set('inForgotPasswordFlow', false); 55 | this.set('inChangePasswordFlow', false); 56 | this.set('inMessageOnlyFlow', false); 57 | this.set('dropdownVisible', false); 58 | this.resetMessages(); 59 | }, 60 | 61 | infoMessage: function(message) { 62 | this._set("errorMessage", null); 63 | this._set("infoMessage", message); 64 | this.ensureMessageVisible(); 65 | }, 66 | 67 | errorMessage: function(message) { 68 | this._set("errorMessage", message); 69 | this._set("infoMessage", null); 70 | this.ensureMessageVisible(); 71 | }, 72 | 73 | // is there a visible dialog that shows messages (info and error) 74 | isMessageDialogVisible: function () { 75 | return this.get('resetPasswordToken') || 76 | this.get('enrollAccountToken') || 77 | this.get('justVerifiedEmail'); 78 | }, 79 | 80 | // ensure that somethings displaying a message (info or error) is 81 | // visible. if a dialog with messages is open, do nothing; 82 | // otherwise open the dropdown. 83 | // 84 | // notably this doesn't matter when only displaying a single login 85 | // button since then we have an explicit message dialog 86 | // (_loginButtonsMessageDialog), and dropdownVisible is ignored in 87 | // this case. 88 | ensureMessageVisible: function () { 89 | if (!this.isMessageDialogVisible()) 90 | this.set("dropdownVisible", true); 91 | }, 92 | 93 | resetMessages: function () { 94 | this._set("errorMessage", null); 95 | this._set("infoMessage", null); 96 | }, 97 | 98 | configureService: function (name) { 99 | this.set('configureLoginServiceDialogVisible', true); 100 | this.set('configureLoginServiceDialogServiceName', name); 101 | this.set('configureLoginServiceDialogSaveDisabled', true); 102 | } 103 | }; -------------------------------------------------------------------------------- /login_buttons_dialogs.html: -------------------------------------------------------------------------------- 1 | 2 | {{> _resetPasswordDialog}} 3 | {{> _enrollAccountDialog}} 4 | {{> _justVerifiedEmailDialog}} 5 | {{> _configureLoginServiceDialog}} 6 | 7 | 8 | {{> _loginButtonsMessagesDialog}} 9 | 10 | 11 | 28 | 29 | 45 | 46 | 54 | 55 | 94 | 95 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /login_buttons_images.css: -------------------------------------------------------------------------------- 1 | /* These should be in their respective packages. https://app.asana.com/0/988582960612/1477837179813 */ 2 | 3 | #login-buttons-image-google { 4 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAADCklEQVQ4jSXSy2ucVRjA4d97zvdNJpPJbTJJE9rYaCINShZtRCFIA1bbLryBUlyoLQjqVl12W7UbN4qb1gtuYhFRRBCDBITaesFbbI3RFBLSptEY05l0ZjLfnMvrov/Bs3gAcF71x6VVHTk+o8nDH+hrH89rUK9Z9Yaen57S3wVtGaMBNGC0IegWKIDxTtVaOHVugZVmH3HX3Zz+4l+W1xvkOjuZfPsspY4CNkZELEgEIJKwYlBjEwjec/mfCMVuorVs76R8+P0KYMmP30U2dT8eIZqAR2ipRcWjEYxGSCRhV08e04oYMoxYLi97EI9YCJ0FHBYbIVGDlUBLwRlLIuYW6chEmQt/rJO09RJjhjEJEYvJYGNhkbUhw43OXtIWDFRq9G87nAaSK6sVRm8r8fzRMWbOX2Xx7ypd7ZET03sQhDOz73DqSJOrd+7HSo4QIu0Nx/4rOzx+cRXZ9+z7+uqJ+3hiepxK3fHZT2tMjXYzOtzL6dmznPzhLexgN0QlxAAYxAlqUqRmkf5j59RlNQ6MFHhgcpCTTx8EUb5e+plD7x4jjg1ANCAgrRQAdR7xKXjBlGyLYi7PxaUmb8z8xcpGHVXLHaXdjI0egKyJiQYTEhSPREVIEUBNC+Mqm+xpz3j0njLPHB2nsh1QgeG+IS48dYbD5YNoo0ZUAbVEuTUoKuBSZOarX/WhyQn6eg2+usDWf0s0tq8zNPYk+WI/Lnge++hlvlyfQ3NdECzGRWKwEEA0qNY251n69kV6+Y0kbaCZoebG2X3oU7pKoyxuXOPe945zs9DCeosGIXoBDyaLdf6ce4Hbk+/Y299ksKtAuaeNsiyw8c1LKIZ95b0MdgxA5giixACpTxEPSau6QdFfI5/2cLPmEW+JAQrtJUJzDXF1dkwHzVodJMX4HFEcQQMaFdPeM0Jb/4PUtzzaLKAhRyJFwo6lbegRNFfk819muV5dR4JBQoQdQ2xFiDmSNDHiaptamR9Gq5cQ18AledrGDpOfeI5Lq8u88smbhMRisoSAgAYghdfn5H/JkHuRZ1owLAAAAABJRU5ErkJggg==); 5 | } 6 | 7 | #login-buttons-image-facebook { 8 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAq0lEQVQ4jWP8//8/AyWAhYGBgcEmauYZBgYGYxL1nj2yLN2ECcohVTNcDwsxKlXlhRm6yzwZRAS5GRgYGBhsombC5ZhwaUIGyJrRAVEuwGYzSS7AB/C64MiydKx8ZJfgNeDN+68MDAwIL8D4RLsgIHsJis0wPjKgOAyoE4hcnGwMGkpiBBUbacvA2TfuvaKiC759/3X23NUnOPMDtgTEwMBwloGBgYGR0uwMAGOPLJS9mkQHAAAAAElFTkSuQmCC); 9 | } 10 | 11 | #login-buttons-image-weibo { 12 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKySURBVDhPY2AgEpR5sjf/nS/6//UkoX+XJltuCvVxkcOp9cyZM1w/r13TuXvmDD9MkYIwg7qrNrubnzFb6J5intPHqrnvCnIwyKIYsmrVKuaFYWEFW2Sk79zX0f6/REHhKFABC0zRsky+rXMSeZdKCTLIHqgUvLAknW8L3IAQDw/RFlbWnQ801P+DNN8D4n0qyk94GRiEjTg5Lbz4+YOCdbhjVmTxbZwex7PUW58t8O1Ukf9gA2IDAoRPWFudfayt9f+mpsb/6yrK/28qKf4/ISf7YZu83K07QMNe6On9nyWusMtVm813azH/UWctZo/vc8TABjB3CApufAzSqKjw/7apyf+nMdH/XxUX/X+RnfX/qY/3/5tqqv/vq6v936KsfB2onltaiEHGx5AteFep4EmGUEHB1Adamv9v6er8fztp0v//79////nr1/+3X778B4N///5/O3jw/0N39//nlBQ/louLd4MMAWImcPhsU1G6DfLvt717wepnz537X0FB4T8fL+//AH///2/evgWL/7l///9dE+P/b4AWTZSWXg/UzAj2/w2gs59mZYEV7d+//z8rE9N/JUXF/w62tiD//a+urIS4BAgeA712Cxg2F40M36alpXGBDTgmI/3hdUU5WEFjff3/wvx8MNvcxARsQE1VFUQ30Et37Oz+P1RV+b/J0nIjUATigmgBvtzH5mb//9++/f/mkyf/A4KC/nv7+oI1W1hb/3/1+fP//9+//39ekP//CVDzTlnZxxtnz1ZBSUDeDAyZh7W13nybOeP/7W1b/09rbf2/FhgWHy9c+P912bL/D11d/l+WEP8/SUR4Ox8DA6pmmEkpHh4ya0JCim4lJGx7kZp8821CwrN7Hh4Pr7m6nDoSET61PjDQichsA3T7//+s/16/5gXSkIAa1AAAh8dhOVd5xHAAAAAASUVORK5CYII=); 13 | } 14 | 15 | #login-buttons-image-twitter { 16 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAByklEQVQ4jaVTz0sbQRh92V10l006GaKJCtEtmqMYU0Qpwqb4B6zgXdT0WEr7B0ih4MGLP05CUWMvHkQwglhvGhsvKmJOBhTUQjWU2slilKarrAfdZROTQ8m7fPMx33szb75vXKZpohpwVbEBCNaCMUYopXppAWOMxDNsOPf3H1WIeDoSURYYYwQAKKW6y7KgLe2vam11KyMRZcEpEP6SOkwbUgc4ATAKUF8YW2fXhZejvaHPsc7gvH2DnCfQGEtdxrd/5NRJteUDpVTf+5kLp2WlA6JsCyZv9ChplPKdTfJZkYWhEF3bvnV3fb36NZSY3dP6Q/5V4hFvIAaKPckE8W5pLBIQdwHAthBdPtpJuhpeAwDu74DrP4/R1/Ts4cwBWg/gN+DowoSqTBPezAMAeAHw+suSw4Q7schFApF6af19a+2yLVIB7xR+0Zk75yCveu82FMnMViKHCXcSa3PPVBJAX5BszL2SP2kNwvdy5M1e+S2AogME4HFYPibPpxKZC03nRAp/M+Dx2UWDzTXfpttrx72ikCoVtrrAAwgdXBk9iazxxtpskfhs1O86aHXXpAEcA7ivJGDBDcDnyAsA2FMsi1KB/0bVv/EBBBSY9mZ7PAsAAAAASUVORK5CYII=); 17 | } 18 | 19 | #login-buttons-image-github { 20 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=); 21 | } 22 | 23 | #login-dropdown-list .alert { 24 | margin:0 0 4px 0; 25 | } 26 | #forgot-password-link { 27 | display:block; 28 | padding-top:8px; 29 | } 30 | -------------------------------------------------------------------------------- /login_buttons.js: -------------------------------------------------------------------------------- 1 | // for convenience 2 | var loginButtonsSession = Accounts._loginButtonsSession; 3 | 4 | // shared between dropdown and single mode 5 | Template._loginButtons.events({ 6 | 'click #login-buttons-logout': function() { 7 | Meteor.logout(function () { 8 | loginButtonsSession.closeDropdown(); 9 | }); 10 | } 11 | }); 12 | 13 | UI.registerHelper('loginButtons', function () { 14 | throw new Error("Use {{> loginButtons}} instead of {{loginButtons}}"); 15 | }); 16 | 17 | // 18 | // helpers 19 | // 20 | 21 | displayName = function () { 22 | var user = Meteor.user(); 23 | if (!user) 24 | return ''; 25 | 26 | if (user.profile && user.profile.name) 27 | return user.profile.name; 28 | if (user.username) 29 | return user.username; 30 | if (user.emails && user.emails[0] && user.emails[0].address) 31 | return user.emails[0].address; 32 | 33 | return ''; 34 | }; 35 | 36 | // returns an array of the login services used by this app. each 37 | // element of the array is an object (eg {name: 'facebook'}), since 38 | // that makes it useful in combination with UI {{#each}}. 39 | // 40 | // don't cache the output of this function: if called during startup (before 41 | // oauth packages load) it might not include them all. 42 | // 43 | // NOTE: It is very important to have this return password last 44 | // because of the way we render the different providers in 45 | // login_buttons_dropdown.html 46 | getLoginServices = function () { 47 | var self = this; 48 | 49 | // First look for OAuth services. 50 | var services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : []; 51 | 52 | // Be equally kind to all login services. This also preserves 53 | // backwards-compatibility. (But maybe order should be 54 | // configurable?) 55 | services.sort(); 56 | 57 | // Add password, if it's there; it must come last. 58 | if (hasPasswordService()) 59 | services.push('password'); 60 | 61 | return _.map(services, function(name) { 62 | return {name: name}; 63 | }); 64 | }; 65 | 66 | hasPasswordService = function () { 67 | return !!Package['accounts-password']; 68 | }; 69 | 70 | dropdown = function () { 71 | return hasPasswordService() || getLoginServices().length > 1; 72 | }; 73 | 74 | // XXX improve these. should this be in accounts-password instead? 75 | // 76 | // XXX these will become configurable, and will be validated on 77 | // the server as well. 78 | validateUsername = function (username) { 79 | if (username.length >= 3) { 80 | return true; 81 | } else { 82 | loginButtonsSession.errorMessage("Username must be at least 3 characters long"); 83 | return false; 84 | } 85 | }; 86 | validateEmail = function (email) { 87 | if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '') 88 | return true; 89 | 90 | if (email.indexOf('@') !== -1) { 91 | return true; 92 | } else { 93 | loginButtonsSession.errorMessage("Invalid email"); 94 | return false; 95 | } 96 | }; 97 | validatePassword = function (password) { 98 | if (password.length >= 6) { 99 | return true; 100 | } else { 101 | loginButtonsSession.errorMessage("Password must be at least 6 characters long"); 102 | return false; 103 | } 104 | }; 105 | 106 | // 107 | // loginButtonLoggedOut template 108 | // 109 | 110 | Template._loginButtonsLoggedOut.helpers({ 111 | dropdown: function () { 112 | return dropdown(); 113 | } 114 | }); 115 | 116 | 117 | Template._loginButtonsLoggedOut.helpers({ 118 | services: function () { 119 | return getLoginServices(); 120 | } 121 | }); 122 | 123 | Template._loginButtonsLoggedOut.helpers({ 124 | singleService: function () { 125 | var services = getLoginServices(); 126 | if (services.length !== 1) 127 | throw new Error( 128 | "Shouldn't be rendering this template with more than one configured service"); 129 | return services[0]; 130 | } 131 | }); 132 | 133 | Template._loginButtonsLoggedOut.helpers({ 134 | configurationLoaded: function () { 135 | return Accounts.loginServicesConfigured(); 136 | } 137 | }); 138 | 139 | 140 | // 141 | // loginButtonsLoggedIn template 142 | // 143 | 144 | // decide whether we should show a dropdown rather than a row of 145 | // buttons 146 | Template._loginButtonsLoggedIn.helpers({ 147 | dropdown: function(){ 148 | return dropdown(); 149 | } 150 | }); 151 | 152 | 153 | 154 | // 155 | // loginButtonsLoggedInSingleLogoutButton template 156 | // 157 | 158 | Template._loginButtonsLoggedInSingleLogoutButton.helpers({ 159 | displayName: function(){ 160 | return displayName(); 161 | } 162 | }); 163 | 164 | 165 | 166 | 167 | // 168 | // loginButtonsMessage template 169 | // 170 | 171 | Template._loginButtonsMessages.helpers({ 172 | errorMessage: function () { 173 | return loginButtonsSession.get('errorMessage'); 174 | } 175 | }); 176 | 177 | Template._loginButtonsMessages.helpers({ 178 | infoMessage: function () { 179 | return loginButtonsSession.get('infoMessage'); 180 | } 181 | }); 182 | 183 | 184 | // 185 | // loginButtonsLoggingInPadding template 186 | // 187 | 188 | Template._loginButtonsLoggingInPadding.helpers({ 189 | dropdown: function () { 190 | return dropdown(); 191 | } 192 | }); 193 | -------------------------------------------------------------------------------- /login_buttons_dropdown.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 47 | 48 | 63 | 64 | 71 | 72 | 102 | 103 | 114 | 115 | 118 | 119 | 137 | 138 | 144 | 145 | 155 | -------------------------------------------------------------------------------- /login_buttons_dialogs.js: -------------------------------------------------------------------------------- 1 | // for convenience 2 | var loginButtonsSession = Accounts._loginButtonsSession; 3 | 4 | 5 | // 6 | // populate the session so that the appropriate dialogs are 7 | // displayed by reading variables set by accounts-urls, which parses 8 | // special URLs. since accounts-ui depends on accounts-urls, we are 9 | // guaranteed to have these set at this point. 10 | // 11 | 12 | if (Accounts._resetPasswordToken) { 13 | loginButtonsSession.set('resetPasswordToken', Accounts._resetPasswordToken); 14 | } 15 | 16 | if (Accounts._enrollAccountToken) { 17 | loginButtonsSession.set('enrollAccountToken', Accounts._enrollAccountToken); 18 | } 19 | 20 | // Needs to be in Meteor.startup because of a package loading order 21 | // issue. We can't be sure that accounts-password is loaded earlier 22 | // than accounts-ui so Accounts.verifyEmail might not be defined. 23 | Meteor.startup(function () { 24 | if (Accounts._verifyEmailToken) { 25 | Accounts.verifyEmail(Accounts._verifyEmailToken, function(error) { 26 | Accounts._enableAutoLogin(); 27 | if (!error) 28 | loginButtonsSession.set('justVerifiedEmail', true); 29 | // XXX show something if there was an error. 30 | }); 31 | } 32 | }); 33 | 34 | 35 | // 36 | // resetPasswordDialog template 37 | // 38 | 39 | Template._resetPasswordDialog.events({ 40 | 'click #login-buttons-reset-password-button': function () { 41 | resetPassword(); 42 | }, 43 | 'keypress #reset-password-new-password': function (event) { 44 | if (event.keyCode === 13) 45 | resetPassword(); 46 | }, 47 | 'click #login-buttons-cancel-reset-password': function () { 48 | loginButtonsSession.set('resetPasswordToken', null); 49 | Accounts._enableAutoLogin(); 50 | } 51 | }); 52 | 53 | var resetPassword = function () { 54 | loginButtonsSession.resetMessages(); 55 | var newPassword = document.getElementById('reset-password-new-password').value; 56 | if (!validatePassword(newPassword)) 57 | return; 58 | 59 | Accounts.resetPassword( 60 | loginButtonsSession.get('resetPasswordToken'), newPassword, 61 | function (error) { 62 | if (error) { 63 | loginButtonsSession.errorMessage(error.reason || "Unknown error"); 64 | } else { 65 | loginButtonsSession.set('resetPasswordToken', null); 66 | Accounts._enableAutoLogin(); 67 | } 68 | }); 69 | }; 70 | 71 | Template._resetPasswordDialog.helpers({ 72 | inResetPasswordFlow: function () { 73 | return loginButtonsSession.get('resetPasswordToken'); 74 | } 75 | }); 76 | 77 | 78 | // 79 | // enrollAccountDialog template 80 | // 81 | 82 | Template._enrollAccountDialog.events({ 83 | 'click #login-buttons-enroll-account-button': function () { 84 | enrollAccount(); 85 | }, 86 | 'keypress #enroll-account-password': function (event) { 87 | if (event.keyCode === 13) 88 | enrollAccount(); 89 | }, 90 | 'click #login-buttons-cancel-enroll-account': function () { 91 | loginButtonsSession.set('enrollAccountToken', null); 92 | Accounts._enableAutoLogin(); 93 | } 94 | }); 95 | 96 | var enrollAccount = function () { 97 | loginButtonsSession.resetMessages(); 98 | var password = document.getElementById('enroll-account-password').value; 99 | if (!validatePassword(password)) 100 | return; 101 | 102 | Accounts.resetPassword( 103 | loginButtonsSession.get('enrollAccountToken'), password, 104 | function (error) { 105 | if (error) { 106 | loginButtonsSession.errorMessage(error.reason || "Unknown error"); 107 | } else { 108 | loginButtonsSession.set('enrollAccountToken', null); 109 | Accounts._enableAutoLogin(); 110 | } 111 | }); 112 | }; 113 | 114 | Template._enrollAccountDialog.helpers({ 115 | inEnrollAccountFlow: function () { 116 | return loginButtonsSession.get('enrollAccountToken'); 117 | } 118 | }); 119 | 120 | 121 | // 122 | // justVerifiedEmailDialog template 123 | // 124 | 125 | Template._justVerifiedEmailDialog.events({ 126 | 'click #just-verified-dismiss-button': function () { 127 | loginButtonsSession.set('justVerifiedEmail', false); 128 | } 129 | }); 130 | 131 | Template._justVerifiedEmailDialog.helpers({ 132 | visible: function () { 133 | return loginButtonsSession.get('justVerifiedEmail'); 134 | } 135 | }); 136 | 137 | 138 | // 139 | // loginButtonsMessagesDialog template 140 | // 141 | 142 | Template._loginButtonsMessagesDialog.events({ 143 | 'click #messages-dialog-dismiss-button': function () { 144 | loginButtonsSession.resetMessages(); 145 | } 146 | }); 147 | 148 | Template._loginButtonsMessagesDialog.helpers({ 149 | visible: function () { 150 | var hasMessage = loginButtonsSession.get('infoMessage') || loginButtonsSession.get('errorMessage'); 151 | return !dropdown() && hasMessage; 152 | } 153 | }); 154 | 155 | 156 | // 157 | // configureLoginServiceDialog template 158 | // 159 | 160 | Template._configureLoginServiceDialog.events({ 161 | 'click .configure-login-service-dismiss-button': function () { 162 | loginButtonsSession.set('configureLoginServiceDialogVisible', false); 163 | }, 164 | 'click #configure-login-service-dialog-save-configuration': function () { 165 | if (loginButtonsSession.get('configureLoginServiceDialogVisible') && 166 | ! loginButtonsSession.get('configureLoginServiceDialogSaveDisabled')) { 167 | // Prepare the configuration document for this login service 168 | var serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName'); 169 | var configuration = { 170 | service: serviceName 171 | }; 172 | 173 | // Fetch the value of each input field 174 | _.each(configurationFields(), function(field) { 175 | configuration[field.property] = document.getElementById( 176 | 'configure-login-service-dialog-' + field.property).value 177 | .replace(/^\s*|\s*$/g, ""); // trim; 178 | }); 179 | 180 | // Configure this login service 181 | Accounts.connection.call( 182 | "configureLoginService", configuration, function (error, result) { 183 | if (error) 184 | Meteor._debug("Error configuring login service " + serviceName, 185 | error); 186 | else 187 | loginButtonsSession.set('configureLoginServiceDialogVisible', 188 | false); 189 | }); 190 | } 191 | }, 192 | // IE8 doesn't support the 'input' event, so we'll run this on the keyup as 193 | // well. (Keeping the 'input' event means that this also fires when you use 194 | // the mouse to change the contents of the field, eg 'Cut' menu item.) 195 | 'input, keyup input': function (event) { 196 | // if the event fired on one of the configuration input fields, 197 | // check whether we should enable the 'save configuration' button 198 | if (event.target.id.indexOf('configure-login-service-dialog') === 0) 199 | updateSaveDisabled(); 200 | } 201 | }); 202 | 203 | // check whether the 'save configuration' button should be enabled. 204 | // this is a really strange way to implement this and a Forms 205 | // Abstraction would make all of this reactive, and simpler. 206 | var updateSaveDisabled = function () { 207 | var anyFieldEmpty = _.any(configurationFields(), function(field) { 208 | return document.getElementById( 209 | 'configure-login-service-dialog-' + field.property).value === ''; 210 | }); 211 | 212 | loginButtonsSession.set('configureLoginServiceDialogSaveDisabled', anyFieldEmpty); 213 | }; 214 | 215 | // Returns the appropriate template for this login service. This 216 | // template should be defined in the service's package 217 | var configureLoginServiceDialogTemplateForService = function () { 218 | var serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName'); 219 | // XXX Service providers should be able to specify their configuration 220 | // template name. 221 | return Template['configureLoginServiceDialogFor' + 222 | (serviceName === 'meteor-developer' ? 223 | 'MeteorDeveloper' : 224 | capitalize(serviceName))]; 225 | }; 226 | 227 | var configurationFields = function () { 228 | var template = configureLoginServiceDialogTemplateForService(); 229 | return template.fields(); 230 | }; 231 | 232 | Template._configureLoginServiceDialog.helpers({ 233 | configurationFields: function () { 234 | return configurationFields(); 235 | } 236 | }); 237 | 238 | Template._configureLoginServiceDialog.helpers({ 239 | visible: function () { 240 | return loginButtonsSession.get('configureLoginServiceDialogVisible'); 241 | } 242 | }); 243 | 244 | Template._configureLoginServiceDialog.helpers({ 245 | configurationSteps: function () { 246 | // renders the appropriate template 247 | return configureLoginServiceDialogTemplateForService(); 248 | } 249 | }); 250 | 251 | Template._configureLoginServiceDialog.helpers({ 252 | saveDisabled: function () { 253 | return loginButtonsSession.get('configureLoginServiceDialogSaveDisabled'); 254 | } 255 | }); 256 | 257 | 258 | // XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js 259 | var capitalize = function(str){ 260 | str = str == null ? '' : String(str); 261 | return str.charAt(0).toUpperCase() + str.slice(1); 262 | }; 263 | -------------------------------------------------------------------------------- /login_buttons_dropdown.js: -------------------------------------------------------------------------------- 1 | // for convenience 2 | var loginButtonsSession = Accounts._loginButtonsSession; 3 | 4 | // events shared between loginButtonsLoggedOutDropdown and 5 | // loginButtonsLoggedInDropdown 6 | Template._loginButtons.events({ 7 | 'click input, click label, click button, click .dropdown-menu, click .alert': function(event) { 8 | event.stopPropagation(); 9 | }, 10 | 'click #login-name-link, click #login-sign-in-link': function () { 11 | event.stopPropagation(); 12 | loginButtonsSession.set('dropdownVisible', true); 13 | Deps.flush(); 14 | }, 15 | 'click .login-close': function () { 16 | loginButtonsSession.closeDropdown(); 17 | } 18 | }); 19 | 20 | 21 | // 22 | // loginButtonsLoggedInDropdown template and related 23 | // 24 | 25 | Template._loginButtonsLoggedInDropdown.events({ 26 | 'click #login-buttons-open-change-password': function(event) { 27 | event.stopPropagation(); 28 | loginButtonsSession.resetMessages(); 29 | loginButtonsSession.set('inChangePasswordFlow', true); 30 | Deps.flush(); 31 | toggleDropdown(); 32 | } 33 | }); 34 | 35 | Template._loginButtonsLoggedInDropdown.helpers({ 36 | displayName: function () { 37 | return displayName(); 38 | } 39 | }); 40 | 41 | Template._loginButtonsLoggedInDropdown.helpers({ 42 | inChangePasswordFlow: function () { 43 | return loginButtonsSession.get('inChangePasswordFlow'); 44 | } 45 | }); 46 | 47 | Template._loginButtonsLoggedInDropdown.helpers({ 48 | inMessageOnlyFlow: function () { 49 | return loginButtonsSession.get('inMessageOnlyFlow'); 50 | } 51 | }); 52 | 53 | Template._loginButtonsLoggedInDropdown.helpers({ 54 | dropdownVisible: function () { 55 | return loginButtonsSession.get('dropdownVisible'); 56 | } 57 | }); 58 | 59 | Template._loginButtonsLoggedInDropdownActions.helpers({ 60 | allowChangingPassword: function () { 61 | // it would be more correct to check whether the user has a password set, 62 | // but in order to do that we'd have to send more data down to the client, 63 | // and it'd be preferable not to send down the entire service.password document. 64 | // 65 | // instead we use the heuristic: if the user has a username or email set. 66 | var user = Meteor.user(); 67 | return user.username || (user.emails && user.emails[0] && user.emails[0].address); 68 | } 69 | }); 70 | 71 | 72 | // 73 | // loginButtonsLoggedOutDropdown template and related 74 | // 75 | 76 | Template._loginButtonsLoggedOutDropdown.events({ 77 | 'click #login-buttons-password': function () { 78 | loginOrSignup(); 79 | }, 80 | 81 | 'keypress #forgot-password-email': function (event) { 82 | if (event.keyCode === 13) 83 | forgotPassword(); 84 | }, 85 | 86 | 'click #login-buttons-forgot-password': function (event) { 87 | event.stopPropagation(); 88 | forgotPassword(); 89 | }, 90 | 91 | 'click #signup-link': function (event) { 92 | event.stopPropagation(); 93 | loginButtonsSession.resetMessages(); 94 | 95 | // store values of fields before swtiching to the signup form 96 | var username = trimmedElementValueById('login-username'); 97 | var email = trimmedElementValueById('login-email'); 98 | var usernameOrEmail = trimmedElementValueById('login-username-or-email'); 99 | // notably not trimmed. a password could (?) start or end with a space 100 | var password = elementValueById('login-password'); 101 | 102 | loginButtonsSession.set('inSignupFlow', true); 103 | loginButtonsSession.set('inForgotPasswordFlow', false); 104 | 105 | // force the ui to update so that we have the approprate fields to fill in 106 | Deps.flush(); 107 | 108 | // update new fields with appropriate defaults 109 | if (username !== null) 110 | document.getElementById('login-username').value = username; 111 | else if (email !== null) 112 | document.getElementById('login-email').value = email; 113 | else if (usernameOrEmail !== null) 114 | if (usernameOrEmail.indexOf('@') === -1) 115 | document.getElementById('login-username').value = usernameOrEmail; 116 | else 117 | document.getElementById('login-email').value = usernameOrEmail; 118 | }, 119 | 'click #forgot-password-link': function (event) { 120 | event.stopPropagation(); 121 | loginButtonsSession.resetMessages(); 122 | 123 | // store values of fields before swtiching to the signup form 124 | var email = trimmedElementValueById('login-email'); 125 | var usernameOrEmail = trimmedElementValueById('login-username-or-email'); 126 | 127 | loginButtonsSession.set('inSignupFlow', false); 128 | loginButtonsSession.set('inForgotPasswordFlow', true); 129 | 130 | // force the ui to update so that we have the approprate fields to fill in 131 | Deps.flush(); 132 | //toggleDropdown(); 133 | 134 | // update new fields with appropriate defaults 135 | if (email !== null) 136 | document.getElementById('forgot-password-email').value = email; 137 | else if (usernameOrEmail !== null) 138 | if (usernameOrEmail.indexOf('@') !== -1) 139 | document.getElementById('forgot-password-email').value = usernameOrEmail; 140 | }, 141 | 'click #back-to-login-link': function () { 142 | loginButtonsSession.resetMessages(); 143 | 144 | var username = trimmedElementValueById('login-username'); 145 | var email = trimmedElementValueById('login-email') 146 | || trimmedElementValueById('forgot-password-email'); // Ughh. Standardize on names? 147 | 148 | loginButtonsSession.set('inSignupFlow', false); 149 | loginButtonsSession.set('inForgotPasswordFlow', false); 150 | // force the ui to update so that we have the approprate fields to fill in 151 | Deps.flush(); 152 | 153 | if (document.getElementById('login-username')) 154 | document.getElementById('login-username').value = username; 155 | if (document.getElementById('login-email')) 156 | document.getElementById('login-email').value = email; 157 | // "login-password" is preserved thanks to the preserve-inputs package 158 | if (document.getElementById('login-username-or-email')) 159 | document.getElementById('login-username-or-email').value = email || username; 160 | }, 161 | 'keypress #login-username, keypress #login-email, keypress #login-username-or-email, keypress #login-password, keypress #login-password-again': function (event) { 162 | if (event.keyCode === 13) 163 | loginOrSignup(); 164 | } 165 | }); 166 | 167 | // additional classes that can be helpful in styling the dropdown 168 | Template._loginButtonsLoggedOutDropdown.helpers({ 169 | additionalClasses: function () { 170 | if (!hasPasswordService()) { 171 | return false; 172 | } else { 173 | if (loginButtonsSession.get('inSignupFlow')) { 174 | return 'login-form-create-account'; 175 | } else if (loginButtonsSession.get('inForgotPasswordFlow')) { 176 | return 'login-form-forgot-password'; 177 | } else { 178 | return 'login-form-sign-in'; 179 | } 180 | } 181 | } 182 | }); 183 | 184 | Template._loginButtonsLoggedOutDropdown.helpers({ 185 | dropdownVisible: function () { 186 | return loginButtonsSession.get('dropdownVisible'); 187 | } 188 | }); 189 | 190 | Template._loginButtonsLoggedOutDropdown.helpers({ 191 | hasPasswordService: function () { 192 | return hasPasswordService(); 193 | } 194 | }); 195 | 196 | // return all login services, with password last 197 | Template._loginButtonsLoggedOutAllServices.helpers({ 198 | services: function () { 199 | return getLoginServices(); 200 | } 201 | }); 202 | 203 | Template._loginButtonsLoggedOutAllServices.helpers({ 204 | isPasswordService: function () { 205 | return this.name === 'password'; 206 | } 207 | }); 208 | 209 | Template._loginButtonsLoggedOutAllServices.helpers({ 210 | hasOtherServices: function () { 211 | return getLoginServices().length > 1; 212 | } 213 | }); 214 | 215 | Template._loginButtonsLoggedOutAllServices.helpers({ 216 | hasPasswordService: function(){ 217 | return hasPasswordService(); 218 | } 219 | }); 220 | 221 | Template._loginButtonsLoggedOutPasswordService.helpers({ 222 | fields: function () { 223 | var loginFields = [ 224 | {fieldName: 'username-or-email', fieldLabel: 'Username or Email', 225 | visible: function () { 226 | return _.contains( 227 | ["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"], 228 | passwordSignupFields()); 229 | }}, 230 | {fieldName: 'username', fieldLabel: 'Username', 231 | visible: function () { 232 | return passwordSignupFields() === "USERNAME_ONLY"; 233 | }}, 234 | {fieldName: 'email', fieldLabel: 'Email', inputType: 'email', 235 | visible: function () { 236 | return passwordSignupFields() === "EMAIL_ONLY"; 237 | }}, 238 | {fieldName: 'password', fieldLabel: 'Password', inputType: 'password', 239 | visible: function () { 240 | return true; 241 | }} 242 | ]; 243 | 244 | var signupFields = [ 245 | {fieldName: 'username', fieldLabel: 'Username', 246 | visible: function () { 247 | return _.contains( 248 | ["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], 249 | passwordSignupFields()); 250 | }}, 251 | {fieldName: 'email', fieldLabel: 'Email', inputType: 'email', 252 | visible: function () { 253 | return _.contains( 254 | ["USERNAME_AND_EMAIL", "EMAIL_ONLY"], 255 | passwordSignupFields()); 256 | }}, 257 | {fieldName: 'email', fieldLabel: 'Email (optional)', inputType: 'email', 258 | visible: function () { 259 | return passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL"; 260 | }}, 261 | {fieldName: 'password', fieldLabel: 'Password', inputType: 'password', 262 | visible: function () { 263 | return true; 264 | }}, 265 | {fieldName: 'password-again', fieldLabel: 'Password (again)', 266 | inputType: 'password', 267 | visible: function () { 268 | // No need to make users double-enter their password if 269 | // they'll necessarily have an email set, since they can use 270 | // the "forgot password" flow. 271 | return _.contains( 272 | ["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], 273 | passwordSignupFields()); 274 | }} 275 | ]; 276 | 277 | return loginButtonsSession.get('inSignupFlow') ? signupFields : loginFields; 278 | } 279 | }); 280 | 281 | Template._loginButtonsLoggedOutPasswordService.helpers({ 282 | inForgotPasswordFlow: function () { 283 | return loginButtonsSession.get('inForgotPasswordFlow'); 284 | } 285 | }); 286 | 287 | Template._loginButtonsLoggedOutPasswordService.helpers({ 288 | inLoginFlow: function () { 289 | return !loginButtonsSession.get('inSignupFlow') && !loginButtonsSession.get('inForgotPasswordFlow'); 290 | } 291 | }); 292 | 293 | Template._loginButtonsLoggedOutPasswordService.helpers({ 294 | inSignupFlow: function () { 295 | return loginButtonsSession.get('inSignupFlow'); 296 | } 297 | }); 298 | 299 | Template._loginButtonsLoggedOutPasswordService.helpers({ 300 | showCreateAccountLink: function () { 301 | return !Accounts._options.forbidClientAccountCreation; 302 | } 303 | }); 304 | 305 | Template._loginButtonsLoggedOutPasswordService.helpers({ 306 | showForgotPasswordLink: function () { 307 | return _.contains( 308 | ["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "EMAIL_ONLY"], 309 | passwordSignupFields()); 310 | } 311 | }); 312 | 313 | Template._loginButtonsFormField.helpers({ 314 | inputType: function () { 315 | return this.inputType || "text"; 316 | } 317 | }); 318 | 319 | 320 | // 321 | // loginButtonsChangePassword template 322 | // 323 | 324 | Template._loginButtonsChangePassword.events({ 325 | 'keypress #login-old-password, keypress #login-password, keypress #login-password-again': function (event) { 326 | if (event.keyCode === 13) 327 | changePassword(); 328 | }, 329 | 'click #login-buttons-do-change-password': function (event) { 330 | event.stopPropagation(); 331 | changePassword(); 332 | } 333 | }); 334 | 335 | Template._loginButtonsChangePassword.helpers({ 336 | fields: function () { 337 | return [ 338 | {fieldName: 'old-password', fieldLabel: 'Current Password', inputType: 'password', 339 | visible: function () { 340 | return true; 341 | }}, 342 | {fieldName: 'password', fieldLabel: 'New Password', inputType: 'password', 343 | visible: function () { 344 | return true; 345 | }}, 346 | {fieldName: 'password-again', fieldLabel: 'New Password (again)', 347 | inputType: 'password', 348 | visible: function () { 349 | // No need to make users double-enter their password if 350 | // they'll necessarily have an email set, since they can use 351 | // the "forgot password" flow. 352 | return _.contains( 353 | ["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], 354 | passwordSignupFields()); 355 | }} 356 | ]; 357 | } 358 | }); 359 | 360 | 361 | // 362 | // helpers 363 | // 364 | 365 | var elementValueById = function(id) { 366 | var element = document.getElementById(id); 367 | if (!element) 368 | return null; 369 | else 370 | return element.value; 371 | }; 372 | 373 | var trimmedElementValueById = function(id) { 374 | var element = document.getElementById(id); 375 | if (!element) 376 | return null; 377 | else 378 | return element.value.replace(/^\s*|\s*$/g, ""); // trim() doesn't work on IE8; 379 | }; 380 | 381 | var loginOrSignup = function () { 382 | if (loginButtonsSession.get('inSignupFlow')) 383 | signup(); 384 | else 385 | login(); 386 | }; 387 | 388 | var login = function () { 389 | loginButtonsSession.resetMessages(); 390 | 391 | var username = trimmedElementValueById('login-username'); 392 | var email = trimmedElementValueById('login-email'); 393 | var usernameOrEmail = trimmedElementValueById('login-username-or-email'); 394 | // notably not trimmed. a password could (?) start or end with a space 395 | var password = elementValueById('login-password'); 396 | 397 | var loginSelector; 398 | if (username !== null) { 399 | if (!validateUsername(username)) 400 | return; 401 | else 402 | loginSelector = {username: username}; 403 | } else if (email !== null) { 404 | if (!validateEmail(email)) 405 | return; 406 | else 407 | loginSelector = {email: email}; 408 | } else if (usernameOrEmail !== null) { 409 | // XXX not sure how we should validate this. but this seems good enough (for now), 410 | // since an email must have at least 3 characters anyways 411 | if (!validateUsername(usernameOrEmail)) 412 | return; 413 | else 414 | loginSelector = usernameOrEmail; 415 | } else { 416 | throw new Error("Unexpected -- no element to use as a login user selector"); 417 | } 418 | 419 | Meteor.loginWithPassword(loginSelector, password, function (error, result) { 420 | if (error) { 421 | loginButtonsSession.errorMessage(error.reason || "Unknown error"); 422 | } else { 423 | loginButtonsSession.closeDropdown(); 424 | } 425 | }); 426 | }; 427 | 428 | var toggleDropdown = function() { 429 | $('#login-dropdown-list .dropdown-menu').dropdown('toggle'); 430 | }; 431 | 432 | var signup = function () { 433 | loginButtonsSession.resetMessages(); 434 | 435 | var options = {}; // to be passed to Accounts.createUser 436 | 437 | var username = trimmedElementValueById('login-username'); 438 | if (username !== null) { 439 | if (!validateUsername(username)) 440 | return; 441 | else 442 | options.username = username; 443 | } 444 | 445 | var email = trimmedElementValueById('login-email'); 446 | if (email !== null) { 447 | if (!validateEmail(email)) 448 | return; 449 | else 450 | options.email = email; 451 | } 452 | 453 | // notably not trimmed. a password could (?) start or end with a space 454 | var password = elementValueById('login-password'); 455 | if (!validatePassword(password)) 456 | return; 457 | else 458 | options.password = password; 459 | 460 | if (!matchPasswordAgainIfPresent()) 461 | return; 462 | 463 | Accounts.createUser(options, function (error) { 464 | if (error) { 465 | loginButtonsSession.errorMessage(error.reason || "Unknown error"); 466 | } else { 467 | loginButtonsSession.closeDropdown(); 468 | } 469 | }); 470 | }; 471 | 472 | var forgotPassword = function () { 473 | loginButtonsSession.resetMessages(); 474 | 475 | var email = trimmedElementValueById("forgot-password-email"); 476 | if (email.indexOf('@') !== -1) { 477 | Accounts.forgotPassword({email: email}, function (error) { 478 | if (error) 479 | loginButtonsSession.errorMessage(error.reason || "Unknown error"); 480 | else 481 | loginButtonsSession.infoMessage("Email sent"); 482 | }); 483 | } else { 484 | loginButtonsSession.infoMessage("Email sent"); 485 | } 486 | }; 487 | 488 | var changePassword = function () { 489 | loginButtonsSession.resetMessages(); 490 | 491 | // notably not trimmed. a password could (?) start or end with a space 492 | var oldPassword = elementValueById('login-old-password'); 493 | 494 | // notably not trimmed. a password could (?) start or end with a space 495 | var password = elementValueById('login-password'); 496 | if (!validatePassword(password)) 497 | return; 498 | 499 | if (!matchPasswordAgainIfPresent()) 500 | return; 501 | 502 | Accounts.changePassword(oldPassword, password, function (error) { 503 | if (error) { 504 | loginButtonsSession.errorMessage(error.reason || "Unknown error"); 505 | } else { 506 | loginButtonsSession.infoMessage("Password changed"); 507 | 508 | // wait 3 seconds, then expire the msg 509 | Meteor.setTimeout(function() { 510 | loginButtonsSession.resetMessages(); 511 | }, 3000); 512 | // loginButtonsSession.set('inChangePasswordFlow', false); 513 | // loginButtonsSession.set('inMessageOnlyFlow', true); 514 | // loginButtonsSession.infoMessage("Password changed"); 515 | } 516 | }); 517 | }; 518 | 519 | var matchPasswordAgainIfPresent = function () { 520 | // notably not trimmed. a password could (?) start or end with a space 521 | var passwordAgain = elementValueById('login-password-again'); 522 | if (passwordAgain !== null) { 523 | // notably not trimmed. a password could (?) start or end with a space 524 | var password = elementValueById('login-password'); 525 | if (password !== passwordAgain) { 526 | loginButtonsSession.errorMessage("Passwords don't match"); 527 | return false; 528 | } 529 | } 530 | return true; 531 | }; 532 | --------------------------------------------------------------------------------