Large refactor of accounts-ui

- Split login_buttons.html and login_buttons.js into multiple files
  - Specifically, make it easier to reason about the display of loginButtons whether it is in dropdown mode or not
- Split templates into subtemplates to make it easier to read the login button .html files as "tables of contents"
- Introduce Meteor._loginButtonsSession, which makes it easier to access internal session fields for loginButtons
- Unify code that calls the various Meteor.loginWithFoo() functions

Breaking change: rename "configureLoginServicesDialogForFoo" to "configureLoginServiceDialogForFoo" (in packages such as accounts-facebook)
This commit is contained in:
Avital Oliver
2012-10-08 13:26:50 -07:00
parent 1816a1ba74
commit 147ef71195
22 changed files with 1147 additions and 1065 deletions

View File

@@ -1,4 +1,4 @@
<template name="configureLoginServicesDialogForFacebook"> <template name="configureLoginServiceDialogForFacebook">
<p> <p>
First, you'll need to register your app on Facebook. Follow these steps: First, you'll need to register your app on Facebook. Follow these steps:
</p> </p>

View File

@@ -1,8 +1,8 @@
Template.configureLoginServicesDialogForFacebook.siteUrl = function () { Template.configureLoginServiceDialogForFacebook.siteUrl = function () {
return Meteor.absoluteUrl(); return Meteor.absoluteUrl();
}; };
Template.configureLoginServicesDialogForFacebook.fields = function () { Template.configureLoginServiceDialogForFacebook.fields = function () {
return [ return [
{property: 'appId', label: 'App ID'}, {property: 'appId', label: 'App ID'},
{property: 'secret', label: 'App Secret'} {property: 'secret', label: 'App Secret'}

View File

@@ -1,4 +1,4 @@
<template name="configureLoginServicesDialogForGithub"> <template name="configureLoginServiceDialogForGithub">
<p> <p>
First, you'll need to get a Github Client ID. Follow these steps: First, you'll need to get a Github Client ID. Follow these steps:
</p> </p>

View File

@@ -1,8 +1,8 @@
Template.configureLoginServicesDialogForGithub.siteUrl = function () { Template.configureLoginServiceDialogForGithub.siteUrl = function () {
return Meteor.absoluteUrl(); return Meteor.absoluteUrl();
}; };
Template.configureLoginServicesDialogForGithub.fields = function () { Template.configureLoginServiceDialogForGithub.fields = function () {
return [ return [
{property: 'clientId', label: 'Client ID'}, {property: 'clientId', label: 'Client ID'},
{property: 'secret', label: 'Client Secret'} {property: 'secret', label: 'Client Secret'}

View File

@@ -1,4 +1,4 @@
<template name="configureLoginServicesDialogForGoogle"> <template name="configureLoginServiceDialogForGoogle">
<p> <p>
First, you'll need to get a Google Client ID. Follow these steps: First, you'll need to get a Google Client ID. Follow these steps:
</p> </p>

View File

@@ -1,8 +1,8 @@
Template.configureLoginServicesDialogForGoogle.siteUrl = function () { Template.configureLoginServiceDialogForGoogle.siteUrl = function () {
return Meteor.absoluteUrl(); return Meteor.absoluteUrl();
}; };
Template.configureLoginServicesDialogForGoogle.fields = function () { Template.configureLoginServiceDialogForGoogle.fields = function () {
return [ return [
{property: 'clientId', label: 'Client ID'}, {property: 'clientId', label: 'Client ID'},
{property: 'secret', label: 'Client secret'} {property: 'secret', label: 'Client secret'}

View File

@@ -156,6 +156,7 @@
if (error || !result) { if (error || !result) {
error = error || new Error("No result from call to resetPassword"); error = error || new Error("No result from call to resetPassword");
callback && callback(error); callback && callback(error);
return;
} }
Accounts._makeClientLoggedIn(result.id, result.token); Accounts._makeClientLoggedIn(result.id, result.token);
@@ -178,6 +179,7 @@
if (error || !result) { if (error || !result) {
error = error || new Error("No result from call to validateUser"); error = error || new Error("No result from call to validateUser");
callback && callback(error); callback && callback(error);
return;
} }
Accounts._makeClientLoggedIn(result.id, result.token); Accounts._makeClientLoggedIn(result.id, result.token);

View File

@@ -1,4 +1,4 @@
<template name="configureLoginServicesDialogForTwitter"> <template name="configureLoginServiceDialogForTwitter">
<p> <p>
First, you'll need to register your app on Twitter. Follow these steps: First, you'll need to register your app on Twitter. Follow these steps:
</p> </p>

View File

@@ -1,9 +1,9 @@
Template.configureLoginServicesDialogForTwitter.siteUrl = function () { Template.configureLoginServiceDialogForTwitter.siteUrl = function () {
// Twitter doesn't recognize localhost as a domain name // Twitter doesn't recognize localhost as a domain name
return Meteor.absoluteUrl({replaceLocalhost: true}); return Meteor.absoluteUrl({replaceLocalhost: true});
}; };
Template.configureLoginServicesDialogForTwitter.fields = function () { Template.configureLoginServiceDialogForTwitter.fields = function () {
return [ return [
{property: 'consumerKey', label: 'Consumer key'}, {property: 'consumerKey', label: 'Consumer key'},
{property: 'secret', label: 'Consumer secret'} {property: 'secret', label: 'Consumer secret'}

View File

@@ -1,129 +1,45 @@
<template name="loginButtons"> <template name="loginButtons">
<div id="login-buttons"> <div id="login-buttons">
{{#if currentUser}} {{#if currentUser}}
{{> loginButtonsLoggedIn}}
{{else}}
{{> loginButtonsLoggedOut}}
{{/if}}
</div>
</template>
<template name="loginButtonsLoggedIn">
{{#if dropdown}} {{#if dropdown}}
{{> loginButtonsLoggedInDropdown}} {{> loginButtonsLoggedInDropdown}}
{{else}} {{else}}
{{> loginButtonsLoggedInRow}}
{{/if}}
{{else}}
{{#if services}}
{{#if configurationLoaded}}
{{#if dropdown}}
{{> loginButtonsServicesDropdown}}
{{else}}
{{> loginButtonsServicesRow}}
{{/if}}
{{/if}}
{{else}}
<div class="no-services">No login services configured</div>
{{/if}}
{{/if}}
</div>
</template>
<template name="loginButtonsLoggedInDropdown">
<div class="login-link-and-dropdown-list">
{{#if currentUser.loading}}
<div class="loading"></div>
{{else}}
<a class="login-link-text" id="login-name-link">
{{displayName}} ▾
</a>
{{#if dropdownVisible}}
<div id="login-dropdown-list" class="accounts-dialog">
<a class="login-close-text">Close</a>
<div class="login-close-text-clear"></div>
{{#if inChangePasswordFlow}}
{{> loginButtonsChangePassword}}
{{else}}
{{#if allowChangingPassword}}
<div class="login-button" id="login-buttons-open-change-password">Change password</div>
{{/if}}
<div class="login-button" id="login-buttons-logout">Logout</div>
{{/if}}
</div>
{{/if}}
{{/if}}
</div>
</template>
<template name="loginButtonsLoggedInRow">
<div class="login-header"> <div class="login-header">
{{#if currentUser.loading}} {{#if currentUser.loading}} {{! XXX this will change }}
<div class="loading"></div> <div class="loading"></div>
{{else}} {{else}}
{{displayName}} {{displayName}}
{{/if}} {{/if}}
</div> </div>
<div class="login-button" id="login-buttons-logout">Logout</div> <div class="login-button" id="login-buttons-logout">Logout</div>
{{/if}}
</template> </template>
<template name="loginButtonsServicesRow"> <template name="loginButtonsLoggedOut">
{{#each services}} {{#if services}} {{! if at least one service is configured }}
{{#if isPasswordService}} {{#if configurationLoaded}}
{{#if hasOtherServices}} {{#if dropdown}} {{! if more than one service configured, or password is configured}}
<div class="or"> {{> loginButtonsLoggedOutDropdown}}
<span class="hline">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span class="or-text">or</span>
<span class="hline">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
</div>
{{/if}}
{{#if inForgotPasswordFlow}}
{{> forgotPasswordForm}}
{{else}} {{else}}
<div class="login-form login-password-form"> {{#with singleService}} {{! at this point there must be only one configured services }}
{{#each fields}} {{> loginButtonsLoggedOutSingleLoginButton}}
{{> loginButtonsFormField}} {{/with}}
{{/each}}
{{> loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-password">
{{#if inSignupFlow}}
Create account
{{else}}
Sign in
{{/if}} {{/if}}
</div>
{{#if inLoginFlow}}
<div class="additional-link-container">
<a id="signup-link" class="additional-link">Create account</a>
</div>
{{#if showForgotPasswordLink}}
<div class="additional-link-container">
<a id="forgot-password-link" class="additional-link">Forgot password</a>
</div>
{{/if}}
{{/if}}
</div>
{{/if}} {{/if}}
{{else}} {{else}}
{{#if configured}} <div class="no-services">No login services configured</div>
<div class="login-button" id="login-buttons-{{name}}">
<div class="login-image" id="login-buttons-image-{{name}}"></div>
Sign in with {{capitalizedName}}
</div>
{{else}}
<div class="login-button configure-button" id="login-buttons-{{name}}">
<div class="login-image" id="login-buttons-image-{{name}}"></div>
Configure {{capitalizedName}} Login
</div>
{{/if}} {{/if}}
{{/if}}
{{/each}}
</template>
<template name="loginButtonsFormField">
<label id="login-{{fieldName}}-label" for="login-{{fieldName}}">
{{fieldLabel}}
</label>
<input id="login-{{fieldName}}" type="{{inputType}}" />
</template> </template>
<!-- used in various places to display messages to user -->
<template name="loginButtonsMessages"> <template name="loginButtonsMessages">
{{#if errorMessage}} {{#if errorMessage}}
<div class="message error-message">{{errorMessage}}</div> <div class="message error-message">{{errorMessage}}</div>
@@ -132,163 +48,3 @@
<div class="message info-message">{{infoMessage}}</div> <div class="message info-message">{{infoMessage}}</div>
{{/if}} {{/if}}
</template> </template>
<template name="loginButtonsServicesDropdown">
<div class="login-link-and-dropdown-list">
<a class="login-link-text" id="login-sign-in-link">Sign in ▾</a>
{{#if dropdownVisible}}
<div id="login-dropdown-list" class="accounts-dialog">
<a class="login-close-text">Close</a>
<div class="login-close-text-clear"></div>
{{> loginButtonsServicesRow}}
{{#unless hasPasswordService}}
{{> loginButtonsMessages}}
{{/unless}}
</div>
{{/if}}
</div>
</template>
<template name="forgotPasswordForm">
<div class="login-form">
<label id="forgot-password-email-label" for="forgot-password-email">Email</label>
<input id="forgot-password-email"/>
{{> loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-forgot-password">
Reset password
</div>
</div>
</template>
<template name="loginButtonsChangePassword">
{{#each fields}}
{{> loginButtonsFormField}}
{{/each}}
{{> loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-do-change-password">
Change password
</div>
</template>
<template name="resetPasswordForm">
{{#if inResetPasswordFlow}}
<div class="hide-background"></div>
<div class="accounts-dialog accounts-centered-dialog">
<label id="reset-password-new-password-label" for="reset-password-new-password">
New password
</label>
<div>
<input id="reset-password-new-password" type="password" />
</div>
{{> loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-reset-password-button">
Reset password
</div>
<a class="additional-link" id="login-buttons-cancel-reset-password">
Cancel
</a>
</div>
{{/if}}
</template>
<template name="enrollAccountForm">
{{#if inEnrollAccountFlow}}
<div class="hide-background"></div>
<div class="accounts-dialog accounts-centered-dialog">
<label id="enroll-account-password-label" for="enroll-account-password">
Choose a password
</label>
<div>
<input id="enroll-account-password" type="password" />
</div>
{{> loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-enroll-account-button">
Create account
</div>
<a class="additional-link" id="login-buttons-cancel-enroll-account">
Cancel
</a>
</div>
{{/if}}
</template>
<template name="justValidatedUserForm">
{{#if visible}}
<div class="accounts-dialog accounts-centered-dialog">
Email validated
<div class="login-button" id="just-validated-dismiss-button">Dismiss</div>
</div>
{{/if}}
</template>
<!-- if we're not showing a dropdown, we need some other place to show messages -->
<template name="loginButtonsMessagesDialog">
{{#if visible}}
<div class="accounts-dialog accounts-centered-dialog">
{{> loginButtonsMessages}}
<div class="login-button" id="messages-dialog-dismiss-button">Dismiss</div>
</div>
{{/if}}
</template>
<template name="configureLoginServicesDialog">
{{#if visible}}
<div id="configure-login-services-dialog" class="accounts-dialog accounts-centered-dialog">
{{{configurationSteps}}}
<p>
Now, copy over some details.
</p>
<p>
<table>
<colgroup>
<col span="1" class="configuration_labels">
<col span="1" class="configuration_inputs">
</colgroup>
{{#each configurationFields}}
<tr>
<td>
<label for="configure-login-services-dialog-{{property}}">{{label}}</label>
</td>
<td>
<input id="configure-login-services-dialog-{{property}}" />
</td>
</tr>
{{/each}}
</table>
</p>
<div class="new-section">
<div class="login-button" id="configure-login-services-dismiss-button">I'll do this later</div>
{{#isolate}}
<div class="login-button login-button-configure {{#if saveDisabled}}login-button-disabled{{/if}}"
id="configure-login-services-dialog-save-configuration">
Save Configuration
</div>
{{/isolate}}
</div>
</div>
{{/if}}
</template>
<body>
{{> resetPasswordForm}}
{{> enrollAccountForm}}
{{> justValidatedUserForm}}
{{> loginButtonsMessagesDialog}}
{{> configureLoginServicesDialog}}
</body>

View File

@@ -1,324 +1,59 @@
(function () { (function () {
// if (!Accounts._loginButtons)
// Session Accounts._loginButtons = {};
//
var DROPDOWN_VISIBLE_KEY = 'Meteor.loginButtons.dropdownVisible'; // for convenience
var loginButtonsSession = Accounts._loginButtonsSession;
// XXX consider replacing these with one key that has an enum for values.
var IN_SIGNUP_FLOW_KEY = 'Meteor.loginButtons.inSignupFlow';
var IN_FORGOT_PASSWORD_FLOW_KEY = 'Meteor.loginButtons.inForgotPasswordFlow';
var IN_CHANGE_PASSWORD_FLOW_KEY = 'Meteor.loginButtons.inChangePasswordFlow';
var ERROR_MESSAGE_KEY = 'Meteor.loginButtons.errorMessage';
var INFO_MESSAGE_KEY = 'Meteor.loginButtons.infoMessage';
var SHOW_MESSAGES_DIALOG_KEY = 'Meteor.loginButtons.showMessagesDialog';
var RESET_PASSWORD_TOKEN_KEY = 'Meteor.loginButtons.resetPasswordToken';
var ENROLL_ACCOUNT_TOKEN_KEY = 'Meteor.loginButtons.enrollAccountToken';
var JUST_VALIDATED_USER_KEY = 'Meteor.loginButtons.justValidatedUser';
var CONFIGURE_LOGIN_SERVICES_DIALOG_VISIBLE = 'Meteor.loginButtons.configureLoginServicesDialogVisible';
var CONFIGURE_LOGIN_SERVICES_DIALOG_SERVICE_NAME = "Meteor.loginButtons.configureLoginServicesDialogServiceName";
var CONFIGURE_LOGIN_SERVICES_DIALOG_SAVE_ENABLED = "Meteor.loginButtons.saveEnabled";
var resetSession = function () {
Session.set(IN_SIGNUP_FLOW_KEY, false);
Session.set(IN_FORGOT_PASSWORD_FLOW_KEY, false);
Session.set(IN_CHANGE_PASSWORD_FLOW_KEY, false);
Session.set(DROPDOWN_VISIBLE_KEY, false);
resetMessages();
};
var resetMessages = function () {
Session.set(ERROR_MESSAGE_KEY, null);
Session.set(INFO_MESSAGE_KEY, null);
};
//
// loginButtons template
//
configureService = function(name) {
Session.set(CONFIGURE_LOGIN_SERVICES_DIALOG_VISIBLE, true);
Session.set(CONFIGURE_LOGIN_SERVICES_DIALOG_SERVICE_NAME, name);
Session.set(CONFIGURE_LOGIN_SERVICES_DIALOG_SAVE_ENABLED, false);
};
Template.loginButtons.events = {
'click #login-buttons-facebook': function () {
resetMessages();
Meteor.loginWithFacebook(makeLoginCallback('facebook'));
},
'click #login-buttons-google': function () {
resetMessages();
Meteor.loginWithGoogle(makeLoginCallback('google'));
},
'click #login-buttons-github': function () {
resetMessages();
Meteor.loginWithGithub(makeLoginCallback('github'))
},
'click #login-buttons-weibo': function () {
resetMessages();
Meteor.loginWithWeibo(makeLoginCallback('weibo'));
},
'click #login-buttons-twitter': function () {
resetMessages();
Meteor.loginWithTwitter(makeLoginCallback('twitter'));
},
'click #login-name-link': function () {
Session.set(DROPDOWN_VISIBLE_KEY, true);
Meteor.flush();
correctDropdownZIndexes();
},
'click .login-close-text': function () {
resetSession();
},
'click #login-buttons-open-change-password': function() {
resetMessages();
Session.set(IN_CHANGE_PASSWORD_FLOW_KEY, true);
},
// shared between dropdown and single mode
Template.loginButtons.events({
'click #login-buttons-logout': function() { 'click #login-buttons-logout': function() {
Meteor.logout(resetSession); Meteor.logout(function () {
loginButtonsSession.closeDropdown();
});
} }
});
//
// loginButtonLoggedOut template
//
Template.loginButtonsLoggedOut.dropdown = function () {
return Accounts._loginButtons.dropdown();
}; };
var makeLoginCallback = function(service) { Template.loginButtonsLoggedOut.services = function () {
return function (err) { return Accounts._loginButtons.getLoginServices();
if (!err) {
resetSession();
} else if (err instanceof Accounts.LoginCancelledError) {
// do nothing
} else if (err instanceof Accounts.ConfigError) {
configureService(service);
} else {
Session.set(ERROR_MESSAGE_KEY, err.reason || "Unknown error");
}
};
}; };
// decide whether we should show a dropdown rather than a row of Template.loginButtonsLoggedOut.singleService = function () {
// buttons var services = Accounts._loginButtons.getLoginServices();
Template.loginButtons.dropdown = function () { if (services.length !== 1)
return dropdown(); throw new Error(
"Shouldn't be rendering this template with more than one configured service");
return services[0];
}; };
Template.loginButtons.services = function () { Template.loginButtonsLoggedOut.configurationLoaded = function () {
return getLoginServices();
};
Template.loginButtons.configurationLoaded = function () {
return Accounts.loginServicesConfigured(); return Accounts.loginServicesConfigured();
}; };
// //
// loginButtonsLoggedInRow template // loginButtonsLoggedIn template
//
Template.loginButtonsLoggedInRow.displayName = function () {
return displayName();
};
//
// loginButtonsLoggedInDropdown template
// //
Template.loginButtonsLoggedInDropdown.displayName = function () { // decide whether we should show a dropdown rather than a row of
return displayName(); // buttons
Template.loginButtonsLoggedIn.dropdown = function () {
return Accounts._loginButtons.dropdown();
}; };
Template.loginButtonsLoggedInDropdown.inChangePasswordFlow = function () { Template.loginButtonsLoggedIn.displayName = function () {
return Session.get(IN_CHANGE_PASSWORD_FLOW_KEY); return Accounts._loginButtons.displayName();
}; };
Template.loginButtonsLoggedInDropdown.dropdownVisible = function () {
return Session.get(DROPDOWN_VISIBLE_KEY);
};
Template.loginButtonsLoggedInDropdown.allowChangingPassword = function () {
// it would be more correct to check whether the user has a password set,
// but in order to do that we'd have to send more data down to the client,
// and it'd be preferable not to send down the entire service.password document.
//
// instead we use the heuristic: if the user has a username or email set.
var user = Meteor.user();
return user.username || (user.emails && user.emails[0] && user.emails[0].address);
};
//
// loginButtonsServiceRow template
//
Template.loginButtonsServicesRow.events = {
'click #login-buttons-password': function () {
loginOrSignup();
},
'click #signup-link': function () {
resetMessages();
// store values of fields before swtiching to the signup form
var username = elementValueById('login-username');
var email = elementValueById('login-email');
var usernameOrEmail = elementValueById('login-username-or-email');
var password = elementValueById('login-password');
Session.set(IN_SIGNUP_FLOW_KEY, true);
Session.set(IN_FORGOT_PASSWORD_FLOW_KEY, false);
// force the ui to update so that we have the approprate fields to fill in
Meteor.flush();
// update new fields with appropriate defaults
if (username !== null)
document.getElementById('login-username').value = username;
else if (email !== null)
document.getElementById('login-email').value = email;
else if (usernameOrEmail !== null)
if (usernameOrEmail.indexOf('@') === -1)
document.getElementById('login-username').value = usernameOrEmail;
else
document.getElementById('login-email').value = usernameOrEmail;
document.getElementById('login-password').value = password;
// Forge redrawing the `login-dropdown-list` element because of
// a bizarre Chrome bug in which part of the DIV is not redrawn
// in case you had tried to unsuccessfully log in before
// switching to the signup form.
//
// Found tip on how to force a redraw on
// http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes/3485654#3485654
var redraw = document.getElementById('login-dropdown-list');
redraw.style.display = 'none';
redraw.offsetHeight; // it seems that this line does nothing but is necessary for the redraw to work
redraw.style.display = 'block';
},
'click #forgot-password-link': function () {
resetMessages();
// store values of fields before swtiching to the signup form
var email = elementValueById('login-email');
var usernameOrEmail = elementValueById('login-username-or-email');
Session.set(IN_SIGNUP_FLOW_KEY, false);
Session.set(IN_FORGOT_PASSWORD_FLOW_KEY, true);
// force the ui to update so that we have the approprate fields to fill in
Meteor.flush();
// update new fields with appropriate defaults
if (email !== null)
document.getElementById('forgot-password-email').value = email;
else if (usernameOrEmail !== null)
if (usernameOrEmail.indexOf('@') !== -1)
document.getElementById('forgot-password-email').value = usernameOrEmail;
},
'keypress #login-username, keypress #login-email, keypress #login-username-or-email, keypress #login-password, keypress #login-password-again': function (event) {
if (event.keyCode === 13)
loginOrSignup();
}
};
Template.loginButtonsServicesRow.fields = function () {
var loginFields = [
{fieldName: 'username-or-email', fieldLabel: 'Username or Email',
visible: function () {
return Accounts._options.requireUsername
&& Accounts._options.requireEmail;
}},
{fieldName: 'username', fieldLabel: 'Username',
visible: function () {
return Accounts._options.requireUsername
&& !Accounts._options.requireEmail;
}},
{fieldName: 'email', fieldLabel: 'Email',
visible: function () {
return !Accounts._options.requireUsername;
}},
{fieldName: 'password', fieldLabel: 'Password', inputType: 'password',
visible: function () {
return true;
}}
];
var signupFields = [
{fieldName: 'username', fieldLabel: 'Username',
visible: function () {
return Accounts._options.requireUsername;
}},
{fieldName: 'email', fieldLabel: 'Email',
visible: function () {
return !Accounts._options.requireUsername
|| Accounts._options.requireEmail;
}},
{fieldName: 'password', fieldLabel: 'Password', inputType: 'password',
visible: function () {
return true;
}},
{fieldName: 'password-again', fieldLabel: 'Password (again)',
inputType: 'password',
visible: function () {
return Accounts._options.requireUsername
&& !Accounts._options.requireEmail;
}}
];
var fields = Session.get(IN_SIGNUP_FLOW_KEY) ? signupFields : loginFields;
return _.filter(fields, function(info) {
return info.visible();
});
};
Template.loginButtonsServicesRow.services = function () {
return getLoginServices();
};
Template.loginButtonsServicesRow.isPasswordService = function () {
return this.name === 'password';
};
Template.loginButtonsServicesRow.hasOtherServices = function () {
return getLoginServices().length > 1;
};
Template.loginButtonsServicesRow.hasPasswordService = function () {
return hasPasswordService();
};
Template.loginButtonsServicesRow.inForgotPasswordFlow = function () {
return Session.get(IN_FORGOT_PASSWORD_FLOW_KEY);
};
Template.loginButtonsServicesRow.inLoginFlow = function () {
return !Session.get(IN_SIGNUP_FLOW_KEY) && !Session.get(IN_FORGOT_PASSWORD_FLOW_KEY);
};
Template.loginButtonsServicesRow.inSignupFlow = function () {
return Session.get(IN_SIGNUP_FLOW_KEY);
};
Template.loginButtonsServicesRow.showForgotPasswordLink = function () {
return Accounts._options.requireEmail
|| !Accounts._options.requireUsername;
};
Template.loginButtonsServicesRow.configured = function () {
return !!Accounts.configuration.findOne({service: this.name.toLowerCase()});
};
Template.loginButtonsServicesRow.capitalizedName = function () {
return capitalize(this.name);
};
// //
@@ -326,328 +61,19 @@
// //
Template.loginButtonsMessages.errorMessage = function () { Template.loginButtonsMessages.errorMessage = function () {
return Session.get(ERROR_MESSAGE_KEY); return loginButtonsSession.get('errorMessage');
}; };
Template.loginButtonsMessages.infoMessage = function () { Template.loginButtonsMessages.infoMessage = function () {
return Session.get(INFO_MESSAGE_KEY); return loginButtonsSession.get('infoMessage');
}; };
//
// forgotPasswordForm template
//
Template.forgotPasswordForm.events = {
'keypress #forgot-password-email': function (event) {
if (event.keyCode === 13)
forgotPassword();
},
'click #login-buttons-forgot-password': function () {
forgotPassword();
}
};
var forgotPassword = function () {
resetMessages();
var email = document.getElementById("forgot-password-email").value;
if (email.indexOf('@') !== -1) {
Accounts.forgotPassword({email: email}, function (error) {
if (error)
Session.set(ERROR_MESSAGE_KEY, error.reason || "Unknown error");
else
Session.set(INFO_MESSAGE_KEY, "Email sent");
});
} else {
Session.set(ERROR_MESSAGE_KEY, "Invalid email");
}
};
//
// loginButtonsServicesDropdown template
//
Template.loginButtonsServicesDropdown.events = {
'click #login-sign-in-link': function () {
Session.set(DROPDOWN_VISIBLE_KEY, true);
Meteor.flush();
correctDropdownZIndexes();
},
'click .login-close-text': function () {
resetSession();
}
};
Template.loginButtonsServicesDropdown.dropdownVisible = function () {
return Session.get(DROPDOWN_VISIBLE_KEY);
};
Template.loginButtonsServicesDropdown.hasPasswordService = function () {
return hasPasswordService();
};
//
// resetPasswordForm template
//
Template.resetPasswordForm.events = {
'click #login-buttons-reset-password-button': function () {
resetPassword();
},
'keypress #reset-password-new-password': function (event) {
if (event.keyCode === 13)
resetPassword();
},
'click #login-buttons-cancel-reset-password': function () {
Session.set(RESET_PASSWORD_TOKEN_KEY, null);
Accounts._enableAutoLogin();
}
};
var resetPassword = function () {
resetMessages();
var newPassword = document.getElementById('reset-password-new-password').value;
if (!validatePassword(newPassword))
return;
Accounts.resetPassword(
Session.get(RESET_PASSWORD_TOKEN_KEY), newPassword,
function (error) {
if (error) {
Session.set(ERROR_MESSAGE_KEY, error.reason || "Unknown error");
} else {
Session.set(RESET_PASSWORD_TOKEN_KEY, null);
Accounts._enableAutoLogin();
}
});
};
Template.resetPasswordForm.inResetPasswordFlow = function () {
return Session.get(RESET_PASSWORD_TOKEN_KEY);
};
if (Accounts._resetPasswordToken) {
Session.set(RESET_PASSWORD_TOKEN_KEY, Accounts._resetPasswordToken);
}
//
// enrollAccountForm template
//
Template.enrollAccountForm.events = {
'click #login-buttons-enroll-account-button': function () {
enrollAccount();
},
'keypress #enroll-account-password': function (event) {
if (event.keyCode === 13)
enrollAccount();
},
'click #login-buttons-cancel-enroll-account': function () {
Session.set(ENROLL_ACCOUNT_TOKEN_KEY, null);
Accounts._enableAutoLogin();
}
};
var enrollAccount = function () {
resetMessages();
var password = document.getElementById('enroll-account-password').value;
if (!validatePassword(password))
return;
Accounts.resetPassword(
Session.get(ENROLL_ACCOUNT_TOKEN_KEY), password,
function (error) {
if (error) {
Session.set(ERROR_MESSAGE_KEY, error.reason || "Unknown error");
} else {
Session.set(ENROLL_ACCOUNT_TOKEN_KEY, null);
Accounts._enableAutoLogin();
}
});
};
Template.enrollAccountForm.inEnrollAccountFlow = function () {
return Session.get(ENROLL_ACCOUNT_TOKEN_KEY);
};
if (Accounts._enrollAccountToken) {
Session.set(ENROLL_ACCOUNT_TOKEN_KEY, Accounts._enrollAccountToken);
}
//
// justValidatedUserForm template
//
Template.justValidatedUserForm.events = {
'click #just-validated-dismiss-button': function () {
Session.set(JUST_VALIDATED_USER_KEY, false);
}
};
Template.justValidatedUserForm.visible = function () {
return Session.get(JUST_VALIDATED_USER_KEY);
};
//
// loginButtonsMessagesDialog template
//
Template.loginButtonsMessagesDialog.events({
'click #messages-dialog-dismiss-button': function () {
resetMessages();
}
});
Template.loginButtonsMessagesDialog.visible = function () {
var hasMessage = Session.get(INFO_MESSAGE_KEY) || Session.get(ERROR_MESSAGE_KEY);
return !dropdown() && hasMessage;
};
// Needs to be in Meteor.startup because of a package loading order
// issue. We can't be sure that accounts-password is loaded earlier
// than accounts-ui so Accounts.validateEmail might not be defined.
Meteor.startup(function () {
if (Accounts._validateEmailToken) {
Accounts.validateEmail(Accounts._validateEmailToken, function(error) {
Accounts._enableAutoLogin();
if (!error)
Session.set(JUST_VALIDATED_USER_KEY, true);
// XXX show something if there was an error.
});
}
});
//
// loginButtonsChangePassword template
//
Template.loginButtonsChangePassword.events({
'keypress #login-old-password, keypress #login-password, keypress #login-password-again': function (event) {
if (event.keyCode === 13)
changePassword();
},
'click #login-buttons-do-change-password': function () {
changePassword();
}
});
Template.loginButtonsChangePassword.fields = function () {
return [
{fieldName: 'old-password', fieldLabel: 'Current Password', inputType: 'password',
visible: function () {
return true;
}},
{fieldName: 'password', fieldLabel: 'New Password', inputType: 'password',
visible: function () {
return true;
}},
{fieldName: 'password-again', fieldLabel: 'New Password (again)',
inputType: 'password',
visible: function () {
return Meteor.accounts._options.requireUsername
&& !Meteor.accounts._options.requireEmail;
}}
];
};
//
// configureLoginServicesDialog template
//
Template.configureLoginServicesDialog.events({
'click #configure-login-services-dismiss-button': function () {
Session.set(CONFIGURE_LOGIN_SERVICES_DIALOG_VISIBLE, false);
},
'click #configure-login-services-dialog-save-configuration': function () {
if (Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_SAVE_ENABLED)) {
// Prepare the configuration document for this login service
var serviceName = Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_SERVICE_NAME);
var configuration = {
service: serviceName
};
_.each(configurationFields(), function(field) {
configuration[field.property] = document.getElementById(
'configure-login-services-dialog-' + field.property).value;
});
// Configure this login service
Meteor.call("configureLoginService", configuration, function (error, result) {
if (error)
Meteor._debug("Error configurating login service " + serviceName, error);
else
Session.set(CONFIGURE_LOGIN_SERVICES_DIALOG_VISIBLE, false);
});
}
}
});
Template.configureLoginServicesDialog.events({
'input': function (event) {
// if the event fired on one of the configuration input fields,
// check whether we should enable the 'save configuration' button
if (event.target.id.indexOf('configure-login-services-dialog') === 0)
updateSaveDisabled();
}
});
// check whether the 'save configuration' button should be enabled.
// this is a really strange way to implement this and a Forms
// Abstraction would make all of this reactive, and simpler.
var updateSaveDisabled = function () {
var saveEnabled = true;
_.any(configurationFields(), function(field) {
if (document.getElementById(
'configure-login-services-dialog-' + field.property).value === '') {
saveEnabled = false;
return true;
} else {
return false;
}
});
Session.set(CONFIGURE_LOGIN_SERVICES_DIALOG_SAVE_ENABLED, saveEnabled);
};
// Returns the appropriate template for this login service. This
// template should be defined in the service's package
var configureLoginServicesDialogTemplateForService = function () {
var serviceName = Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_SERVICE_NAME);
return Template['configureLoginServicesDialogFor' + capitalize(serviceName)];
};
var configurationFields = function () {
var template = configureLoginServicesDialogTemplateForService();
return template.fields();
};
Template.configureLoginServicesDialog.configurationFields = function () {
return configurationFields();
};
Template.configureLoginServicesDialog.visible = function () {
return Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_VISIBLE);
};
Template.configureLoginServicesDialog.configurationSteps = function () {
// renders the appropriate template
return configureLoginServicesDialogTemplateForService()();
};
Template.configureLoginServicesDialog.saveDisabled = function () {
return !Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_SAVE_ENABLED);
};
// //
// helpers // helpers
// //
var displayName = function () { Accounts._loginButtons.displayName = function () {
var user = Meteor.user(); var user = Meteor.user();
if (!user) if (!user)
return ''; return '';
@@ -662,141 +88,7 @@
return ''; return '';
}; };
var elementValueById = function(id) { Accounts._loginButtons.getLoginServices = function () {
var element = document.getElementById(id);
if (!element)
return null;
else
return element.value;
};
var login = function () {
resetMessages();
var username = elementValueById('login-username');
var email = elementValueById('login-email');
var usernameOrEmail = elementValueById('login-username-or-email');
var password = elementValueById('login-password');
var loginSelector;
if (username !== null)
loginSelector = {username: username};
else if (email !== null)
loginSelector = {email: email};
else if (usernameOrEmail !== null)
loginSelector = usernameOrEmail;
else
throw new Error("Unexpected -- no element to use as a login user selector");
Meteor.loginWithPassword(loginSelector, password, function (error, result) {
if (error) {
Session.set(ERROR_MESSAGE_KEY, error.reason || "Unknown error");
} else {
resetSession();
}
});
};
var signup = function () {
resetMessages();
var options = {}; // to be passed to Meteor.createUser
var username = elementValueById('login-username');
if (username !== null) {
if (!validateUsername(username))
return;
else
options.username = username;
}
var email = elementValueById('login-email');
if (email !== null) {
if (!validateEmail(email))
return;
else
options.email = email;
}
var password = elementValueById('login-password');
if (!validatePassword(password))
return;
else
options.password = password;
if (!matchPasswordAgainIfPresent())
return;
if (Accounts._options.validateEmails)
options.validation = true;
Accounts.createUser(options, function (error) {
if (error) {
Session.set(ERROR_MESSAGE_KEY, error.reason || "Unknown error");
} else {
resetSession();
}
});
};
var loginOrSignup = function () {
if (Session.get(IN_SIGNUP_FLOW_KEY))
signup();
else
login();
};
var changePassword = function () {
resetMessages();
var oldPassword = elementValueById('login-old-password');
var password = elementValueById('login-password');
if (!validatePassword(password))
return;
if (!matchPasswordAgainIfPresent())
return;
Accounts.changePassword(oldPassword, password, function (error) {
if (error) {
Session.set(ERROR_MESSAGE_KEY, error.reason || "Unknown error");
} else {
Session.set(INFO_MESSAGE_KEY, "Password changed");
}
});
};
var matchPasswordAgainIfPresent = function () {
var passwordAgain = elementValueById('login-password-again');
if (passwordAgain !== null) {
var password = elementValueById('login-password');
if (password !== passwordAgain) {
Session.set(ERROR_MESSAGE_KEY, "Passwords don't match");
return false;
}
}
return true;
};
var correctDropdownZIndexes = function () {
// IE <= 7 has a z-index bug that means we can't just give the
// dropdown a z-index and expect it to stack above the rest of
// the page even if nothing else has a z-index. The nature of
// the bug is that all positioned elements are considered to
// have z-index:0 (not auto) and therefore start new stacking
// contexts, with ties broken by page order.
//
// The fix, then is to give z-index:1 to all ancestors
// of the dropdown having z-index:0.
for(var n = document.getElementById('login-dropdown-list').parentNode;
n.nodeName !== 'BODY';
n = n.parentNode)
if (n.style.zIndex === 0)
n.style.zIndex = 1;
};
var getLoginServices = function () {
var ret = []; var ret = [];
// make sure to put password last, since this is how it is styled // make sure to put password last, since this is how it is styled
// in the ui as well. // in the ui as well.
@@ -810,50 +102,42 @@
return ret; return ret;
}; };
var hasPasswordService = function () { Accounts._loginButtons.hasPasswordService = function () {
return _.any(getLoginServices(), function (service) { return Accounts.password;
return service.name === 'password';
});
}; };
var dropdown = function () { Accounts._loginButtons.dropdown = function () {
return hasPasswordService() || getLoginServices().length > 1; return Accounts._loginButtons.hasPasswordService() || Accounts._loginButtons.getLoginServices().length > 1;
}; };
// XXX improve these. should this be in accounts-password instead?
// XXX improve these? should this be in accounts-password instead?
// //
// XXX these will become configurable, and will be validated on // XXX these will become configurable, and will be validated on
// the server as well. // the server as well.
var validateUsername = function (username) { Accounts._loginButtons.validateUsername = function (username) {
if (username.length >= 3) { if (username.length >= 3) {
return true; return true;
} else { } else {
Session.set(ERROR_MESSAGE_KEY, "Username must be at least 3 characters long"); loginButtonsSession.set('errorMessage', "Username must be at least 3 characters long");
return false; return false;
} }
}; };
var validateEmail = function (email) { Accounts._loginButtons.validateEmail = function (email) {
if (email.indexOf('@') !== -1) { if (email.indexOf('@') !== -1) {
return true; return true;
} else { } else {
Session.set(ERROR_MESSAGE_KEY, "Invalid email"); loginButtonsSession.set('errorMessage', "Invalid email");
return false; return false;
} }
}; };
var validatePassword = function (password) { Accounts._loginButtons.validatePassword = function (password) {
if (password.length >= 6) { if (password.length >= 6) {
return true; return true;
} else { } else {
Session.set(ERROR_MESSAGE_KEY, "Password must be at least 6 characters long"); loginButtonsSession.set('errorMessage', "Password must be at least 6 characters long");
return false; return false;
} }
}; };
// XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js
var capitalize = function(str){
str = str == null ? '' : String(str);
return str.charAt(0).toUpperCase() + str.slice(1);
};
})(); })();

View File

@@ -0,0 +1,122 @@
<body>
{{> resetPasswordDialog}}
{{> enrollAccountDialog}}
{{> justValidatedUserDialog}}
{{> configureLoginServiceDialog}}
<!-- if we're not showing a dropdown, we need some other place to show messages -->
{{> loginButtonsMessagesDialog}}
</body>
<template name="resetPasswordDialog">
{{#if inResetPasswordFlow}}
<div class="hide-background"></div>
<div class="accounts-dialog accounts-centered-dialog">
<label id="reset-password-new-password-label" for="reset-password-new-password">
New password
</label>
<div>
<input id="reset-password-new-password" type="password" />
</div>
{{> loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-reset-password-button">
Set password
</div>
<a class="additional-link" id="login-buttons-cancel-reset-password">
Cancel
</a>
</div>
{{/if}}
</template>
<template name="enrollAccountDialog">
{{#if inEnrollAccountFlow}}
<div class="hide-background"></div>
<div class="accounts-dialog accounts-centered-dialog">
<label id="enroll-account-password-label" for="enroll-account-password">
Choose a password
</label>
<div>
<input id="enroll-account-password" type="password" />
</div>
{{> loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-enroll-account-button">
Create account
</div>
<a class="additional-link" id="login-buttons-cancel-enroll-account">
Cancel
</a>
</div>
{{/if}}
</template>
<template name="justValidatedUserDialog">
{{#if visible}}
<div class="accounts-dialog accounts-centered-dialog">
Email validated
<div class="login-button" id="just-validated-dismiss-button">Dismiss</div>
</div>
{{/if}}
</template>
<template name="configureLoginServiceDialog">
{{#if visible}}
<div id="configure-login-service-dialog" class="accounts-dialog accounts-centered-dialog">
{{{configurationSteps}}}
<p>
Now, copy over some details.
</p>
<p>
<table>
<colgroup>
<col span="1" class="configuration_labels">
<col span="1" class="configuration_inputs">
</colgroup>
{{#each configurationFields}}
<tr>
<td>
<label for="configure-login-service-dialog-{{property}}">{{label}}</label>
</td>
<td>
<input id="configure-login-service-dialog-{{property}}" />
</td>
</tr>
{{/each}}
</table>
</p>
<div class="new-section">
<div class="login-button" id="configure-login-service-dismiss-button">
I'll do this later
</div>
{{#isolate}}
<div class="login-button login-button-configure {{#if saveDisabled}}login-button-disabled{{/if}}"
id="configure-login-service-dialog-save-configuration">
Save Configuration
</div>
{{/isolate}}
</div>
</div>
{{/if}}
</template>
<template name="loginButtonsMessagesDialog">
{{#if visible}}
<div class="accounts-dialog accounts-centered-dialog" id="login-buttons-message-dialog">
{{> loginButtonsMessages}}
<div class="login-button" id="messages-dialog-dismiss-button">Dismiss</div>
</div>
{{/if}}
</template>

View File

@@ -0,0 +1,234 @@
(function () {
// for convenience
var loginButtonsSession = Accounts._loginButtonsSession;
//
// populate the session so that the appropriate dialogs are
// displayed by reading variables set by accounts-urls, which parses
// special URLs. since accounts-ui depends on accounts-urls, we are
// guaranteed to have these set at this point.
//
if (Accounts._resetPasswordToken) {
loginButtonsSession.set('resetPasswordToken', Accounts._resetPasswordToken);
}
if (Accounts._enrollAccountToken) {
loginButtonsSession.set('enrollAccountToken', Accounts._enrollAccountToken);
}
// Needs to be in Meteor.startup because of a package loading order
// issue. We can't be sure that accounts-password is loaded earlier
// than accounts-ui so Accounts.validateEmail might not be defined.
Meteor.startup(function () {
if (Accounts._validateEmailToken) {
Accounts.validateEmail(Accounts._validateEmailToken, function(error) {
Accounts._enableAutoLogin();
if (!error)
loginButtonsSession.set('justValidatedUser', true);
// XXX show something if there was an error.
});
}
});
//
// resetPasswordDialog template
//
Template.resetPasswordDialog.events({
'click #login-buttons-reset-password-button': function () {
resetPassword();
},
'keypress #reset-password-new-password': function (event) {
if (event.keyCode === 13)
resetPassword();
},
'click #login-buttons-cancel-reset-password': function () {
loginButtonsSession.set('resetPasswordToken', null);
Accounts._enableAutoLogin();
}
});
var resetPassword = function () {
loginButtonsSession.resetMessages();
var newPassword = document.getElementById('reset-password-new-password').value;
if (!Accounts._loginButtons.validatePassword(newPassword))
return;
Accounts.resetPassword(
loginButtonsSession.get('resetPasswordToken'), newPassword,
function (error) {
if (error) {
loginButtonsSession.set('errorMessage', error.reason || "Unknown error");
} else {
loginButtonsSession.set('resetPasswordToken', null);
Accounts._enableAutoLogin();
}
});
};
Template.resetPasswordDialog.inResetPasswordFlow = function () {
return loginButtonsSession.get('resetPasswordToken');
};
//
// enrollAccountDialog template
//
Template.enrollAccountDialog.events({
'click #login-buttons-enroll-account-button': function () {
enrollAccount();
},
'keypress #enroll-account-password': function (event) {
if (event.keyCode === 13)
enrollAccount();
},
'click #login-buttons-cancel-enroll-account': function () {
loginButtonsSession.set('enrollAccountToken', null);
Accounts._enableAutoLogin();
}
});
var enrollAccount = function () {
loginButtonsSession.resetMessages();
var password = document.getElementById('enroll-account-password').value;
if (!Accounts._loginButtons.validatePassword(password))
return;
Accounts.resetPassword(
loginButtonsSession.get('enrollAccountToken'), password,
function (error) {
if (error) {
loginButtonsSession.set('errorMessage', error.reason || "Unknown error");
} else {
loginButtonsSession.set('enrollAccountToken', null);
Accounts._enableAutoLogin();
}
});
};
Template.enrollAccountDialog.inEnrollAccountFlow = function () {
return loginButtonsSession.get('enrollAccountToken');
};
//
// justValidatedUserDialog template
//
Template.justValidatedUserDialog.events({
'click #just-validated-dismiss-button': function () {
loginButtonsSession.set('justValidatedUser', false);
}
});
Template.justValidatedUserDialog.visible = function () {
return loginButtonsSession.get('justValidatedUser');
};
//
// loginButtonsMessagesDialog template
//
Template.loginButtonsMessagesDialog.events({
'click #messages-dialog-dismiss-button': function () {
loginButtonsSession.resetMessages();
}
});
Template.loginButtonsMessagesDialog.visible = function () {
var hasMessage = loginButtonsSession.get('infoMessage') || loginButtonsSession.get('errorMessage');
return !Accounts._loginButtons.dropdown() && hasMessage;
};
//
// configureLoginServiceDialog template
//
Template.configureLoginServiceDialog.events({
'click #configure-login-service-dismiss-button': function () {
loginButtonsSession.set('configureLoginServiceDialogVisible', false);
},
'click #configure-login-service-dialog-save-configuration': function () {
if (loginButtonsSession.get('configureLoginServiceDialogVisible')) {
// Prepare the configuration document for this login service
var serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName');
var configuration = {
service: serviceName
};
_.each(configurationFields(), function(field) {
configuration[field.property] = document.getElementById(
'configure-login-service-dialog-' + field.property).value;
});
// Configure this login service
Meteor.call("configureLoginService", configuration, function (error, result) {
if (error)
Meteor._debug("Error configurating login service " + serviceName, error);
else
loginButtonsSession.set('configureLoginServiceDialogVisible', false);
});
}
},
'input': function (event) {
// if the event fired on one of the configuration input fields,
// check whether we should enable the 'save configuration' button
if (event.target.id.indexOf('configure-login-service-dialog') === 0)
updateSaveDisabled();
}
});
// check whether the 'save configuration' button should be enabled.
// this is a really strange way to implement this and a Forms
// Abstraction would make all of this reactive, and simpler.
var updateSaveDisabled = function () {
var anyFieldEmpty = _.any(configurationFields(), function(field) {
return document.getElementById(
'configure-login-service-dialog-' + field.property).value === '';
});
loginButtonsSession.set('configureLoginServiceDialogSaveDisabled', anyFieldEmpty);
};
// Returns the appropriate template for this login service. This
// template should be defined in the service's package
var configureLoginServiceDialogTemplateForService = function () {
var serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName');
return Template['configureLoginServiceDialogFor' + capitalize(serviceName)];
};
var configurationFields = function () {
var template = configureLoginServiceDialogTemplateForService();
return template.fields();
};
Template.configureLoginServiceDialog.configurationFields = function () {
return configurationFields();
};
Template.configureLoginServiceDialog.visible = function () {
return loginButtonsSession.get('configureLoginServiceDialogVisible');
};
Template.configureLoginServiceDialog.configurationSteps = function () {
// renders the appropriate template
return configureLoginServiceDialogTemplateForService()();
};
Template.configureLoginServiceDialog.saveDisabled = function () {
return loginButtonsSession.get('configureLoginServiceDialogSaveDisabled');
};
// XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js
var capitalize = function(str){
str = str == null ? '' : String(str);
return str.charAt(0).toUpperCase() + str.slice(1);
};
}) ();

View File

@@ -0,0 +1,145 @@
<!-- -->
<!-- LOGGED IN -->
<!-- -->
<template name="loginButtonsLoggedInDropdown">
<div class="login-link-and-dropdown-list">
{{#if currentUser.loading}}
<div class="loading"></div>
{{else}}
<a class="login-link-text" id="login-name-link">
{{displayName}} ▾
</a>
{{#if dropdownVisible}}
<div id="login-dropdown-list" class="accounts-dialog">
<a class="login-close-text">Close</a>
<div class="login-close-text-clear"></div>
{{#if inChangePasswordFlow}}
{{> loginButtonsChangePassword}}
{{else}}
{{> loginButtonsLoggedInDropdownActions}}
{{/if}}
</div>
{{/if}}
{{/if}}
</div>
</template>
<template name="loginButtonsLoggedInDropdownActions">
{{#if allowChangingPassword}}
<div class="login-button" id="login-buttons-open-change-password">
Change password
</div>
{{/if}}
<div class="login-button" id="login-buttons-logout">
Logout
</div>
</template>
<!-- -->
<!-- LOGGED OUT -->
<!-- -->
<template name="loginButtonsLoggedOutDropdown">
<div class="login-link-and-dropdown-list">
<a class="login-link-text" id="login-sign-in-link">Sign in ▾</a>
{{#if dropdownVisible}}
<div id="login-dropdown-list" class="accounts-dialog">
<a class="login-close-text">Close</a>
<div class="login-close-text-clear"></div>
{{> loginButtonsLoggedOutAllServices}}
</div>
{{/if}}
</div>
</template>
<template name="loginButtonsLoggedOutAllServices">
{{#each services}}
{{#if isPasswordService}}
{{#if hasOtherServices}} {{! the password service will always come last }}
<div class="or">
<span class="hline">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span class="or-text">or</span>
<span class="hline">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
</div>
{{/if}}
{{> loginButtonsLoggedOutPasswordService}}
{{else}}
{{> loginButtonsLoggedOutSingleLoginButton}}
{{/if}}
{{/each}}
{{#unless hasPasswordService}}
{{> loginButtonsMessages}}
{{/unless}}
</template>
<template name="loginButtonsLoggedOutPasswordService">
{{#if inForgotPasswordFlow}}
{{> forgotPasswordForm}}
{{else}}
<div class="login-form login-password-form">
{{#each fields}}
{{> loginButtonsFormField}}
{{/each}}
{{> loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-password">
{{#if inSignupFlow}}
Create account
{{else}}
Sign in
{{/if}}
</div>
{{#if inLoginFlow}}
<div class="additional-link-container">
<a id="signup-link" class="additional-link">Create account</a>
</div>
{{#if showForgotPasswordLink}}
<div class="additional-link-container">
<a id="forgot-password-link" class="additional-link">Forgot password</a>
</div>
{{/if}}
{{/if}}
</div>
{{/if}}
</template>
<template name="loginButtonsFormField">
{{#if visible}}
<label id="login-{{fieldName}}-label" for="login-{{fieldName}}">
{{fieldLabel}}
</label>
<input id="login-{{fieldName}}" type="{{inputType}}" />
{{/if}}
</template>
<template name="forgotPasswordForm">
<div class="login-form">
<label id="forgot-password-email-label" for="forgot-password-email">Email</label>
<input id="forgot-password-email"/>
{{> loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-forgot-password">
Reset password
</div>
</div>
</template>
<template name="loginButtonsChangePassword">
{{#each fields}}
{{> loginButtonsFormField}}
{{/each}}
{{> loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-do-change-password">
Change password
</div>
</template>

View File

@@ -0,0 +1,414 @@
(function () {
// for convenience
var loginButtonsSession = Accounts._loginButtonsSession;
// events shared between loginButtonsLoggedOutDropdown and
// loginButtonsLoggedInDropdown
Template.loginButtons.events({
'click #login-name-link, click #login-sign-in-link': function () {
loginButtonsSession.set('dropdownVisible', true);
Meteor.flush();
correctDropdownZIndexes();
},
'click .login-close-text': function () {
loginButtonsSession.closeDropdown();
}
});
//
// loginButtonsLoggedInDropdown template and related
//
Template.loginButtonsLoggedInDropdown.events({
'click #login-buttons-open-change-password': function() {
loginButtonsSession.resetMessages();
loginButtonsSession.set('inChangePasswordFlow', true);
}
});
Template.loginButtonsLoggedInDropdown.displayName = function () {
return Accounts._loginButtons.displayName();
};
Template.loginButtonsLoggedInDropdown.inChangePasswordFlow = function () {
return loginButtonsSession.get('inChangePasswordFlow');
};
Template.loginButtonsLoggedInDropdown.dropdownVisible = function () {
return loginButtonsSession.get('dropdownVisible');
};
Template.loginButtonsLoggedInDropdownActions.allowChangingPassword = function () {
// it would be more correct to check whether the user has a password set,
// but in order to do that we'd have to send more data down to the client,
// and it'd be preferable not to send down the entire service.password document.
//
// instead we use the heuristic: if the user has a username or email set.
var user = Meteor.user();
return user.username || (user.emails && user.emails[0] && user.emails[0].address);
};
//
// loginButtonsLoggedOutDropdown template and related
//
Template.loginButtonsLoggedOutDropdown.events({
'click #login-buttons-password': function () {
loginOrSignup();
},
'keypress #forgot-password-email': function (event) {
if (event.keyCode === 13)
forgotPassword();
},
'click #login-buttons-forgot-password': function () {
forgotPassword();
},
'click #signup-link': function () {
loginButtonsSession.resetMessages();
// store values of fields before swtiching to the signup form
var username = elementValueById('login-username');
var email = elementValueById('login-email');
var usernameOrEmail = elementValueById('login-username-or-email');
var password = elementValueById('login-password');
loginButtonsSession.set('inSignupFlow', true);
loginButtonsSession.set('inForgotPasswordFlow', false);
// force the ui to update so that we have the approprate fields to fill in
Meteor.flush();
// update new fields with appropriate defaults
if (username !== null)
document.getElementById('login-username').value = username;
else if (email !== null)
document.getElementById('login-email').value = email;
else if (usernameOrEmail !== null)
if (usernameOrEmail.indexOf('@') === -1)
document.getElementById('login-username').value = usernameOrEmail;
else
document.getElementById('login-email').value = usernameOrEmail;
document.getElementById('login-password').value = password;
// Forge redrawing the `login-dropdown-list` element because of
// a bizarre Chrome bug in which part of the DIV is not redrawn
// in case you had tried to unsuccessfully log in before
// switching to the signup form.
//
// Found tip on how to force a redraw on
// http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes/3485654#3485654
var redraw = document.getElementById('login-dropdown-list');
redraw.style.display = 'none';
redraw.offsetHeight; // it seems that this line does nothing but is necessary for the redraw to work
redraw.style.display = 'block';
},
'click #forgot-password-link': function () {
loginButtonsSession.resetMessages();
// store values of fields before swtiching to the signup form
var email = elementValueById('login-email');
var usernameOrEmail = elementValueById('login-username-or-email');
loginButtonsSession.set('inSignupFlow', false);
loginButtonsSession.set('inForgotPasswordFlow', true);
// force the ui to update so that we have the approprate fields to fill in
Meteor.flush();
// update new fields with appropriate defaults
if (email !== null)
document.getElementById('forgot-password-email').value = email;
else if (usernameOrEmail !== null)
if (usernameOrEmail.indexOf('@') !== -1)
document.getElementById('forgot-password-email').value = usernameOrEmail;
},
'keypress #login-username, keypress #login-email, keypress #login-username-or-email, keypress #login-password, keypress #login-password-again': function (event) {
if (event.keyCode === 13)
loginOrSignup();
}
});
Template.loginButtonsLoggedOutDropdown.dropdownVisible = function () {
return loginButtonsSession.get('dropdownVisible');
};
Template.loginButtonsLoggedOutDropdown.hasPasswordService = function () {
return Accounts._loginButtons.hasPasswordService();
};
Template.loginButtonsLoggedOutAllServices.services = function () {
return Accounts._loginButtons.getLoginServices();
};
Template.loginButtonsLoggedOutAllServices.isPasswordService = function () {
return this.name === 'password';
};
Template.loginButtonsLoggedOutAllServices.hasOtherServices = function () {
return Accounts._loginButtons.getLoginServices().length > 1;
};
Template.loginButtonsLoggedOutAllServices.hasPasswordService = function () {
return Accounts._loginButtons.hasPasswordService();
};
Template.loginButtonsLoggedOutPasswordService.fields = function () {
var loginFields = [
{fieldName: 'username-or-email', fieldLabel: 'Username or Email',
visible: function () {
return Accounts._options.requireUsername
&& Accounts._options.requireEmail;
}},
{fieldName: 'username', fieldLabel: 'Username',
visible: function () {
return Accounts._options.requireUsername
&& !Accounts._options.requireEmail;
}},
{fieldName: 'email', fieldLabel: 'Email',
visible: function () {
return !Accounts._options.requireUsername;
}},
{fieldName: 'password', fieldLabel: 'Password', inputType: 'password',
visible: function () {
return true;
}}
];
var signupFields = [
{fieldName: 'username', fieldLabel: 'Username',
visible: function () {
return Accounts._options.requireUsername;
}},
{fieldName: 'email', fieldLabel: 'Email',
visible: function () {
return !Accounts._options.requireUsername
|| Accounts._options.requireEmail;
}},
{fieldName: 'password', fieldLabel: 'Password', inputType: 'password',
visible: function () {
return true;
}},
{fieldName: 'password-again', fieldLabel: 'Password (again)',
inputType: 'password',
visible: function () {
return Accounts._options.requireUsername
&& !Accounts._options.requireEmail;
}}
];
return loginButtonsSession.get('inSignupFlow') ? signupFields : loginFields;
};
Template.loginButtonsLoggedOutPasswordService.inForgotPasswordFlow = function () {
return loginButtonsSession.get('inForgotPasswordFlow');
};
Template.loginButtonsLoggedOutPasswordService.inLoginFlow = function () {
return !loginButtonsSession.get('inSignupFlow') && !loginButtonsSession.get('inForgotPasswordFlow');
};
Template.loginButtonsLoggedOutPasswordService.inSignupFlow = function () {
return loginButtonsSession.get('inSignupFlow');
};
Template.loginButtonsLoggedOutPasswordService.showForgotPasswordLink = function () {
return Accounts._options.requireEmail
|| !Accounts._options.requireUsername;
};
//
// loginButtonsChangePassword template
//
Template.loginButtonsChangePassword.events({
'keypress #login-old-password, keypress #login-password, keypress #login-password-again': function (event) {
if (event.keyCode === 13)
changePassword();
},
'click #login-buttons-do-change-password': function () {
changePassword();
}
});
Template.loginButtonsChangePassword.fields = function () {
return [
{fieldName: 'old-password', fieldLabel: 'Current Password', inputType: 'password',
visible: function () {
return true;
}},
{fieldName: 'password', fieldLabel: 'New Password', inputType: 'password',
visible: function () {
return true;
}},
{fieldName: 'password-again', fieldLabel: 'New Password (again)',
inputType: 'password',
visible: function () {
return Meteor.accounts._options.requireUsername
&& !Meteor.accounts._options.requireEmail;
}}
];
};
//
// helpers
//
var elementValueById = function(id) {
var element = document.getElementById(id);
if (!element)
return null;
else
return element.value;
};
var loginOrSignup = function () {
if (loginButtonsSession.get('inSignupFlow'))
signup();
else
login();
};
var login = function () {
loginButtonsSession.resetMessages();
var username = elementValueById('login-username');
var email = elementValueById('login-email');
var usernameOrEmail = elementValueById('login-username-or-email');
var password = elementValueById('login-password');
var loginSelector;
if (username !== null)
loginSelector = {username: username};
else if (email !== null)
loginSelector = {email: email};
else if (usernameOrEmail !== null)
loginSelector = usernameOrEmail;
else
throw new Error("Unexpected -- no element to use as a login user selector");
Meteor.loginWithPassword(loginSelector, password, function (error, result) {
if (error) {
loginButtonsSession.set('errorMessage', error.reason || "Unknown error");
} else {
loginButtonsSession.closeDropdown();
}
});
};
var signup = function () {
loginButtonsSession.resetMessages();
var options = {}; // to be passed to Meteor.createUser
var username = elementValueById('login-username');
if (username !== null) {
if (!Accounts._loginButtons.validateUsername(username))
return;
else
options.username = username;
}
var email = elementValueById('login-email');
if (email !== null) {
if (!Accounts._loginButtons.validateEmail(email))
return;
else
options.email = email;
}
var password = elementValueById('login-password');
if (!Accounts._loginButtons.validatePassword(password))
return;
else
options.password = password;
if (!matchPasswordAgainIfPresent())
return;
if (Accounts._options.validateEmails)
options.validation = true;
Accounts.createUser(options, function (error) {
if (error) {
loginButtonsSession.set('errorMessage', error.reason || "Unknown error");
} else {
loginButtonsSession.closeDropdown();
}
});
};
var forgotPassword = function () {
loginButtonsSession.resetMessages();
var email = document.getElementById("forgot-password-email").value;
if (email.indexOf('@') !== -1) {
Accounts.forgotPassword({email: email}, function (error) {
if (error)
loginButtonsSession.set('errorMessage', error.reason || "Unknown error");
else
loginButtonsSession.set('infoMessage', "Email sent");
});
} else {
loginButtonsSession.set('errorMessage', "Invalid email");
}
};
var changePassword = function () {
loginButtonsSession.resetMessages();
var oldPassword = elementValueById('login-old-password');
var password = elementValueById('login-password');
if (!Accounts._loginButtons.validatePassword(password))
return;
if (!matchPasswordAgainIfPresent())
return;
Accounts.changePassword(oldPassword, password, function (error) {
if (error) {
loginButtonsSession.set('errorMessage', error.reason || "Unknown error");
} else {
loginButtonsSession.set('infoMessage', "Password changed");
}
});
};
var matchPasswordAgainIfPresent = function () {
var passwordAgain = elementValueById('login-password-again');
if (passwordAgain !== null) {
var password = elementValueById('login-password');
if (password !== passwordAgain) {
loginButtonsSession.set('errorMessage', "Passwords don't match");
return false;
}
}
return true;
};
var correctDropdownZIndexes = function () {
// IE <= 7 has a z-index bug that means we can't just give the
// dropdown a z-index and expect it to stack above the rest of
// the page even if nothing else has a z-index. The nature of
// the bug is that all positioned elements are considered to
// have z-index:0 (not auto) and therefore start new stacking
// contexts, with ties broken by page order.
//
// The fix, then is to give z-index:1 to all ancestors
// of the dropdown having z-index:0.
for(var n = document.getElementById('login-dropdown-list').parentNode;
n.nodeName !== 'BODY';
n = n.parentNode)
if (n.style.zIndex === 0)
n.style.zIndex = 1;
};
}) ();

View File

@@ -0,0 +1,60 @@
(function () {
var VALID_KEYS = [
'dropdownVisible',
// XXX consider replacing these with one key that has an enum for values.
'inSignupFlow',
'inForgotPasswordFlow',
'inChangePasswordFlow',
'errorMessage',
'infoMessage',
'resetPasswordToken',
'enrollAccountToken',
'justValidatedUser',
'configureLoginServiceDialogVisible',
'configureLoginServiceDialogServiceName',
'configureLoginServiceDialogSaveDisabled'
];
var validateKey = function (key) {
if (!_.contains(VALID_KEYS, key))
throw new Error("Invalid key in loginButtonsSession: " + key);
};
KEY_PREFIX = "Meteor.loginButtons.";
// XXX we should have a better pattern for code private to a package like this one
Accounts._loginButtonsSession = {
set: function(key, value) {
validateKey(key);
Session.set(KEY_PREFIX + key, value);
},
get: function(key) {
validateKey(key);
return Session.get(KEY_PREFIX + key);
},
closeDropdown: function () {
this.set('inSignupFlow', false);
this.set('inForgotPasswordFlow', false);
this.set('inChangePasswordFlow', false);
this.set('dropdownVisible', false);
this.resetMessages();
},
resetMessages: function () {
this.set("errorMessage", null);
this.set("infoMessage", null);
},
configureService: function (name) {
this.set('configureLoginServiceDialogVisible', true);
this.set('configureLoginServiceDialogServiceName', name);
this.set('configureLoginServiceDialogSaveDisabled', true);
}
};
}) ();

View File

@@ -0,0 +1,11 @@
<template name="loginButtonsLoggedOutSingleLoginButton">
<div class="login-button {{#unless configured}}configure-button{{/unless}}"
id="login-buttons-{{name}}">
<div class="login-image" id="login-buttons-image-{{name}}"></div>
{{#if configured}}
<span class="sign-in-text-{{name}}">Sign in with {{capitalizedName}}</span>
{{else}}
<span class="configure-text-{{name}}">Configure {{capitalizedName}} Login</span>
{{/if}}
</div>
</template>

View File

@@ -0,0 +1,40 @@
(function () {
// for convenience
var loginButtonsSession = Accounts._loginButtonsSession;
Template.loginButtonsLoggedOutSingleLoginButton.events({
'click .login-button': function () {
var serviceName = this.name;
loginButtonsSession.resetMessages();
Meteor["loginWith" + capitalize(serviceName)](function (err) {
if (!err) {
loginButtonsSession.closeDropdown();
} else if (err instanceof Accounts.LoginCancelledError) {
// do nothing
} else if (err instanceof Accounts.ConfigError) {
loginButtonsSession.configureService(serviceName);
} else {
loginButtonsSession.set('errorMessage', err.reason || "Unknown error");
}
});
}
});
Template.loginButtonsLoggedOutSingleLoginButton.configured = function () {
return !!Accounts.configuration.findOne({service: this.name});
};
Template.loginButtonsLoggedOutSingleLoginButton.capitalizedName = function () {
if (this.name === 'github')
// XXX we should allow service packages to set their capitalized name
return 'GitHub';
else
return capitalize(this.name);
};
// XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js
var capitalize = function(str){
str = str == null ? '' : String(str);
return str.charAt(0).toUpperCase() + str.slice(1);
};
}) ();

View File

@@ -8,5 +8,14 @@ Package.on_use(function (api) {
api.add_files([ api.add_files([
'login_buttons_images.css', 'login_buttons_images.css',
'login_buttons.html', 'login_buttons.html',
'login_buttons.js'], 'client'); 'login_buttons_single.html',
'login_buttons_dropdown.html',
'login_buttons_dialogs.html',
'login_buttons_session.js',
'login_buttons.js',
'login_buttons_single.js',
'login_buttons_dropdown.js',
'login_buttons_dialogs.js'], 'client');
}); });

View File

@@ -152,11 +152,16 @@
margin-top: 8px; margin-top: 8px;
} }
#login-buttons .message { .accounts-dialog .message {
font-size: 80%; font-size: 80%;
margin-top: 2px; margin-top: 2px;
} }
#login-buttons-message-dialog .message {
/* we intentionally want it bigger on this dialog since it's the only thing displayed */
font-size: 100%;
}
.accounts-dialog .error-message { .accounts-dialog .error-message {
color: red; color: red;
} }
@@ -197,15 +202,15 @@
margin-top: -40px; /* = approximately -height/2, though height can change */ margin-top: -40px; /* = approximately -height/2, though height can change */
} }
@configure-login-services-dialog-width: 530px; @configure-login-service-dialog-width: 530px;
#configure-login-services-dialog { #configure-login-service-dialog {
width: @configure-login-services-dialog-width; width: @configure-login-service-dialog-width;
margin-left: -(@configure-login-services-dialog-width margin-left: -(@configure-login-service-dialog-width
+ @login-buttons-accounts-dialog-padding-left) / 2; + @login-buttons-accounts-dialog-padding-left) / 2;
margin-top: -180px; /* = approximately -height/2, though height can change */ margin-top: -180px; /* = approximately -height/2, though height can change */
} }
#configure-login-services-dialog .login-button-configure { #configure-login-service-dialog .login-button-configure {
float: right; float: right;
} }
@@ -227,37 +232,37 @@
background-color: rgba(0, 0, 0, 0.7); background-color: rgba(0, 0, 0, 0.7);
} }
#configure-login-services-dialog table { #configure-login-service-dialog table {
width: 100%; width: 100%;
} }
#configure-login-services-dialog .configuration_labels { #configure-login-service-dialog .configuration_labels {
width: 30%; width: 30%;
} }
#configure-login-services-dialog .configuration_inputs { #configure-login-service-dialog .configuration_inputs {
width: 70%; width: 70%;
} }
#configure-login-services-dialog input { #configure-login-service-dialog input {
width: 100%; width: 100%;
font-family: "Courier New", Courier, monospace; font-family: "Courier New", Courier, monospace;
} }
#configure-login-services-dialog ol { #configure-login-service-dialog ol {
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
} }
#configure-login-services-dialog .new-section { #configure-login-service-dialog .new-section {
margin-top: 10px; margin-top: 10px;
} }
#configure-login-services-dialog ol li { #configure-login-service-dialog ol li {
margin-left: 30px; margin-left: 30px;
} }
#configure-login-services-dialog .url { #configure-login-service-dialog .url {
font-family: "Courier New", Courier, monospace; font-family: "Courier New", Courier, monospace;
} }

View File

@@ -1,4 +1,4 @@
<template name="configureLoginServicesDialogForWeibo"> <template name="configureLoginServiceDialogForWeibo">
<p> <p>
First, you'll need to register your app on Weibo. Follow these steps: First, you'll need to register your app on Weibo. Follow these steps:
</p> </p>

View File

@@ -1,9 +1,9 @@
Template.configureLoginServicesDialogForWeibo.siteUrl = function () { Template.configureLoginServiceDialogForWeibo.siteUrl = function () {
// Weibo doesn't recognize localhost as a domain // Weibo doesn't recognize localhost as a domain
return Meteor.absoluteUrl({replaceLocalhost: true}); return Meteor.absoluteUrl({replaceLocalhost: true});
}; };
Template.configureLoginServicesDialogForWeibo.fields = function () { Template.configureLoginServiceDialogForWeibo.fields = function () {
return [ return [
{property: 'clientId', label: 'App Key'}, {property: 'clientId', label: 'App Key'},
{property: 'secret', label: 'App Secret'} {property: 'secret', label: 'App Secret'}