+
Close
{{> loginButtonsServicesRow}}
@@ -91,3 +108,39 @@
{{/if}}
+
+
+
+
+
+
+ {{#if inResetPasswordFlow}}
+
+
+
+ {{/if}}
+
+
+
+ {{> resetPasswordForm}}
+
diff --git a/packages/accounts-ui/login_buttons.js b/packages/accounts-ui/login_buttons.js
index 82aa291439..ca38a51651 100644
--- a/packages/accounts-ui/login_buttons.js
+++ b/packages/accounts-ui/login_buttons.js
@@ -5,12 +5,21 @@
var DROPDOWN_VISIBLE_KEY = 'Meteor.loginButtons.dropdownVisible';
var IN_SIGNUP_FLOW_KEY = 'Meteor.loginButtons.inSignupFlow';
+ var IN_FORGOT_PASSWORD_FLOW_KEY = 'Meteor.loginButtons.inForgotPasswordFlow';
var ERROR_MESSAGE_KEY = 'Meteor.loginButtons.errorMessage';
+ var INFO_MESSAGE_KEY = 'Meteor.loginButtons.infoMessage';
+ var RESET_PASSWORD_TOKEN_KEY = 'Meteor.loginButtons.resetPasswordToken';
var resetSession = function () {
Session.set(IN_SIGNUP_FLOW_KEY, false);
+ Session.set(IN_FORGOT_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);
};
@@ -89,16 +98,18 @@
loginOrSignup();
},
'click #signup-link': function () {
- Session.set(ERROR_MESSAGE_KEY, null);
+ resetMessages();
Session.set(IN_SIGNUP_FLOW_KEY, true);
+ Session.set(IN_FORGOT_PASSWORD_FLOW_KEY, false);
+ },
+ 'click #forgot-password-link': function () {
+ resetMessages();
+ Session.set(IN_SIGNUP_FLOW_KEY, false);
+ Session.set(IN_FORGOT_PASSWORD_FLOW_KEY, true);
},
'keypress #login-username,#login-password,#login-password-again': function (event) {
if (event.keyCode === 13)
loginOrSignup();
- },
- 'keypress #login-password-again': function (event) {
- if (event.keyCode === 13)
- loginOrSignup();
}
};
@@ -114,13 +125,29 @@
return getLoginServices().length > 1;
};
+ Template.loginButtonsServicesRow.isForgotPasswordFlow = function () {
+ return Session.get(IN_FORGOT_PASSWORD_FLOW_KEY);
+ };
+
+ //
+ // loginButtonsMessage template
+ //
+
+ Template.loginButtonsMessages.errorMessage = function () {
+ return Session.get(ERROR_MESSAGE_KEY);
+ };
+
+ Template.loginButtonsMessages.infoMessage = function () {
+ return Session.get(INFO_MESSAGE_KEY);
+ };
+
//
// loginButtonsServicesRowDynamicPart template
//
- Template.loginButtonsServicesRowDynamicPart.errorMessage = function () {
- return Session.get(ERROR_MESSAGE_KEY);
+ Template.loginButtonsServicesRowDynamicPart.inLoginFlow = function () {
+ return !Session.get(IN_SIGNUP_FLOW_KEY) && !Session.get(IN_FORGOT_PASSWORD_FLOW_KEY);
};
Template.loginButtonsServicesRowDynamicPart.inSignupFlow = function () {
@@ -128,6 +155,36 @@
};
+ //
+ // 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) {
+ Meteor.forgotPassword({email: email}, function (error) {
+ if (error)
+ Session.set(ERROR_MESSAGE_KEY, error.reason);
+ else
+ Session.set(INFO_MESSAGE_KEY, "Email sent");
+ });
+ } else {
+ Session.set(ERROR_MESSAGE_KEY, "Invalid email");
+ }
+ };
+
+
//
// loginButtonsServicesDropdown template
//
@@ -161,11 +218,53 @@
};
+ //
+ // resetPasswordForm template
+ //
+
+ Template.resetPasswordForm.events = {
+ 'click #reset-password-button': function () {
+ resetPassword();
+ },
+ 'keypress #reset-password-new-password': function (event) {
+ if (event.keyCode === 13)
+ resetPassword();
+ }
+ };
+
+ var resetPassword = function () {
+ resetMessages();
+ var newPassword = document.getElementById('reset-password-new-password').value;
+ if (!validatePassword(newPassword))
+ return;
+
+ Meteor.resetPassword(
+ Session.get(RESET_PASSWORD_TOKEN_KEY), newPassword,
+ function (error) {
+ if (error) {
+ Session.set(ERROR_MESSAGE_KEY, error.reason);
+ } else {
+ Session.set(RESET_PASSWORD_TOKEN_KEY, null);
+ Meteor.accounts._preventAutoLogin = false;
+ }
+ });
+ };
+
+ Template.resetPasswordForm.inResetPasswordFlow = function () {
+ return Session.get(RESET_PASSWORD_TOKEN_KEY);
+ };
+
+ if (Meteor.accounts._resetPasswordToken) {
+ Session.set(RESET_PASSWORD_TOKEN_KEY, Meteor.accounts._resetPasswordToken);
+ }
+
+
//
// helpers
//
var login = function () {
+ resetMessages();
var username = document.getElementById('login-username').value;
var password = document.getElementById('login-password').value;
@@ -177,25 +276,26 @@
};
var signup = function () {
+ resetMessages();
var username = document.getElementById('login-username').value;
var password = document.getElementById('login-password').value;
var passwordAgain = document.getElementById('login-password-again').value;
// XXX these will become configurable, and will be validated on
// the server as well.
- if (username.length < 3) {
- Session.set(ERROR_MESSAGE_KEY, "Username must be at least 3 characters long");
- } else if (password.length < 6) {
- Session.set(ERROR_MESSAGE_KEY, "Password must be at least 6 characters long");
- } else if (password !== passwordAgain) {
+ if (!validateUsername(username) || !validatePassword(password))
+ return;
+
+ if (password !== passwordAgain) {
Session.set(ERROR_MESSAGE_KEY, "Passwords don't match");
- } else {
- Meteor.createUser({username: username, password: password}, function (error) {
- if (error) {
- Session.set(ERROR_MESSAGE_KEY, error.reason);
- }
- });
+ return;
}
+
+ Meteor.createUser({username: username, password: password}, function (error) {
+ if (error) {
+ Session.set(ERROR_MESSAGE_KEY, error.reason);
+ }
+ });
};
var loginOrSignup = function () {
@@ -223,4 +323,24 @@
return ret;
};
+
+
+ // XXX improve these? should this be in accounts-passwords instead?
+ var validateUsername = function (username) {
+ if (username.length >= 3) {
+ return true;
+ } else {
+ Session.set(ERROR_MESSAGE_KEY, "Username must be at least 3 characters long");
+ return false;
+ }
+ };
+ var validatePassword = function (password) {
+ if (password.length >= 6) {
+ return true;
+ } else {
+ Session.set(ERROR_MESSAGE_KEY, "Password must be at least 6 characters long");
+ return false;
+ }
+ };
})();
+
diff --git a/packages/accounts-ui/login_buttons.css b/packages/accounts-ui/login_buttons.less
similarity index 90%
rename from packages/accounts-ui/login_buttons.css
rename to packages/accounts-ui/login_buttons.less
index 9e8f35648a..c28601f8ce 100644
--- a/packages/accounts-ui/login_buttons.css
+++ b/packages/accounts-ui/login_buttons.less
@@ -15,7 +15,9 @@
margin-left: 4px;
}
-#login-buttons .login-button {
+@login-buttons-accounts-dialog-width: 158px;
+
+#login-buttons .login-button, .accounts-dialog .login-button {
float: left;
cursor: pointer;
padding: 1px 4px;
@@ -43,7 +45,7 @@
margin-left: 5px; /* so that other elements aren't too close */
}
-#login-dropdown-list .login-button {
+.accounts-dialog .login-button {
width: 158px;
margin-bottom: 4px;
}
@@ -80,24 +82,13 @@
clear: right;
}
-#login-dropdown-list {
- position: absolute;
+@login-buttons-accounts-dialog-padding-left: 8px;
+
+.accounts-dialog {
border: 1px solid black;
z-index: 1000;
- width: 167px;
- right: 0px;
- top: 0px;
background: white;
- margin-top: -5px;
- padding-top: 4px; /* = border-width - margin-top */
-
- margin-right: -8px;
- padding-right: 8px; /* = -margin-right */
-
- padding-left: 8px;
- padding-bottom: 8px;
-
-moz-box-shadow: -2px 3px 3px 1px rgba(0, 0, 0, 0.3);
-webkit-box-shadow: -2px 3px 3px 1px rgba(0, 0, 0, 0.3);
box-shadow: -2px 3px 3px 1px rgba(0, 0, 0, 0.3);
@@ -105,6 +96,23 @@
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
+
+ margin-top: -5px;
+ padding-top: 4px; /* = border-width - margin-top */
+
+ margin-right: -8px;
+ padding-right: 8px; /* = -margin-right */
+
+ padding-left: @login-buttons-accounts-dialog-padding-left;
+ padding-bottom: 8px;
+
+ width: 167px;
+}
+
+#login-dropdown-list {
+ position: absolute;
+ right: 0px;
+ top: 0px;
}
#login-buttons .hline {
@@ -123,26 +131,66 @@
font-weight: bold;
}
-#login-buttons label {
+.accounts-dialog label {
font-weight: bold;
font-size: 80%;
}
-#login-buttons .login-password-form input {
+.accounts-dialog input {
width: 162px;
}
-#login-buttons #login-buttons-password {
+.accounts-dialog .login-button-form-submit {
margin-top: 8px;
}
-#login-buttons .error-message {
- color: red;
+.accounts-dialog .message {
font-size: 80%;
margin-top: 2px;
}
+.accounts-dialog .error-message {
+ color: red;
+}
+
+.accounts-dialog .info-message {
+ color: green;
+}
+
+#login-buttons .additional-link {
+ font-size: 60%;
+}
+
#login-buttons #signup-link {
float: right;
- font-size: 70%;
-}
\ No newline at end of file
+}
+
+#login-buttons #forgot-password-link {
+ float: left;
+}
+
+#reset-password-form {
+ z-index: 1000;
+ position: fixed;
+
+ left: 50%;
+ margin-left: -(@login-buttons-accounts-dialog-width
+ + @login-buttons-accounts-dialog-padding-left) / 2;
+
+ top: 50%;
+ margin-top: -40px; /* = approximately -height/2, though height can change */
+}
+
+.hide-background {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 999;
+
+ /* XXX consider replacing with DXImageTransform */
+ background-color: rgb(0.2, 0.2, 0.2); /* fallback for IE7-8 */
+
+ background-color: rgba(0, 0, 0, 0.7);
+}
diff --git a/packages/accounts-ui/package.js b/packages/accounts-ui/package.js
index c0c17d4b36..6a08af2ba0 100644
--- a/packages/accounts-ui/package.js
+++ b/packages/accounts-ui/package.js
@@ -3,10 +3,11 @@ Package.describe({
});
Package.on_use(function (api) {
- api.use(['accounts', 'underscore', 'liveui', 'templating'], 'client');
+ api.use(['accounts-urls', 'accounts', 'underscore', 'liveui', 'templating'], 'client');
+ api.use('less', 'server');
api.add_files([
- 'login_buttons.css',
+ 'login_buttons.less',
'login_buttons_images.css',
'login_buttons.html',
'login_buttons.js'], 'client');
diff --git a/packages/accounts-urls/package.js b/packages/accounts-urls/package.js
new file mode 100644
index 0000000000..71e31e66dd
--- /dev/null
+++ b/packages/accounts-urls/package.js
@@ -0,0 +1,9 @@
+Package.describe({
+ summary: "Generate and consume reset password and validate account URLs",
+ internal: true
+});
+
+Package.on_use(function (api) {
+ api.add_files('url_client.js', 'client');
+ api.add_files('url_server.js', 'server');
+});
diff --git a/packages/accounts-urls/url_client.js b/packages/accounts-urls/url_client.js
new file mode 100644
index 0000000000..90d8d915a5
--- /dev/null
+++ b/packages/accounts-urls/url_client.js
@@ -0,0 +1,26 @@
+// reads a reset password token from the url's hash fragment, if it's there. if so
+// prevent automatically logging in since it could be confusing to be logged in as user
+// A while resetting password for user B
+//
+// reset password urls use hash fragments instead of url paths/query
+// strings so that the reset password token is not sent over the wire
+// on the http request
+(function () {
+ if (!Meteor.accounts)
+ Meteor.accounts = {};
+
+ var match;
+ match = window.location.hash.match(/^\#\?reset-password\/(.*)$/);
+ if (match) {
+ Meteor.accounts._preventAutoLogin = true;
+ Meteor.accounts._resetPasswordToken = match[1];
+ window.location.hash = '';
+ }
+
+ match = window.location.hash.match(/^\#\?validate-user\/(.*)$/);
+ if (match) {
+ Meteor.accounts._preventAutoLogin = true;
+ Meteor.accounts._validateUserToken = match[1];
+ window.location.hash = '';
+ }
+})();
diff --git a/packages/accounts-urls/url_server.js b/packages/accounts-urls/url_server.js
new file mode 100644
index 0000000000..238509ca3d
--- /dev/null
+++ b/packages/accounts-urls/url_server.js
@@ -0,0 +1,13 @@
+if (!Meteor.accounts)
+ Meteor.accounts = {};
+
+if (!Meteor.accounts.urls)
+ Meteor.accounts.urls = {};
+
+Meteor.accounts.urls.resetPassword = function (baseUrl, token) {
+ return baseUrl + '#?reset-password/' + token;
+};
+
+Meteor.accounts.urls.validateUser = function (baseUrl, token) {
+ return baseUrl + '#?validate-user/' + token;
+};
diff --git a/packages/accounts/localstorage_token.js b/packages/accounts/localstorage_token.js
index 1ac1452f8f..654b022740 100644
--- a/packages/accounts/localstorage_token.js
+++ b/packages/accounts/localstorage_token.js
@@ -66,23 +66,28 @@ Meteor.loginWithToken = function (token, errorCallback) {
});
};
-// Immediately try to log in via local storage, so that any DDP
-// messages are sent after we have established our user account
-var token = Meteor.accounts.storedLoginToken();
-if (token) {
- // On startup, optimistically present us as logged in while the
- // request is in flight. This reduces page flicker on startup.
- var userId = Meteor.accounts.storedUserId();
- userId && Meteor.default_connection.setUserId(userId);
- Meteor.loginWithToken(token, function () {
- Meteor.accounts.makeClientLoggedOut();
- });
+if (!Meteor.accounts._preventAutoLogin) {
+ // Immediately try to log in via local storage, so that any DDP
+ // messages are sent after we have established our user account
+ var token = Meteor.accounts.storedLoginToken();
+ if (token) {
+ // On startup, optimistically present us as logged in while the
+ // request is in flight. This reduces page flicker on startup.
+ var userId = Meteor.accounts.storedUserId();
+ userId && Meteor.default_connection.setUserId(userId);
+ Meteor.loginWithToken(token, function () {
+ Meteor.accounts.makeClientLoggedOut();
+ });
+ }
}
// Poll local storage every 3 seconds to login if someone logged in in
// another tab
Meteor.accounts._lastLoginTokenWhenPolled = token;
setInterval(function() {
+ if (Meteor.accounts._preventAutoLogin)
+ return;
+
var currentLoginToken = Meteor.accounts.storedLoginToken();
// != instead of !== just to make sure undefined and null are treated the same
@@ -94,4 +99,3 @@ setInterval(function() {
}
Meteor.accounts._lastLoginTokenWhenPolled = currentLoginToken;
}, 3000);
-
diff --git a/packages/accounts/package.js b/packages/accounts/package.js
index 16fdb6fa50..0f8590bb29 100644
--- a/packages/accounts/package.js
+++ b/packages/accounts/package.js
@@ -5,6 +5,7 @@ Package.describe({
Package.on_use(function (api) {
api.use('underscore', 'server');
api.use('localstorage-polyfill', 'client');
+ api.use('accounts-urls', 'client');
// need this because of the Meteor.users collection but in the future
// we'd probably want to abstract this away