Permalink
Browse files

Improved API for registration section of password module. Handling of…

… registration and login failures are much better now.
  • Loading branch information...
bnoguchi committed May 2, 2011
1 parent 8a98435 commit 46a6e096a86309ed520410f6b20e76d65bcaa408
Showing with 173 additions and 83 deletions.
  1. +58 −24 README.md
  2. +13 −22 example/server.js
  3. +5 −1 example/views/login.jade
  4. +5 −3 example/views/register.jade
  5. +89 −32 lib/password.js
  6. +3 −1 lib/step.js
View
@@ -256,40 +256,57 @@ var everyauth = require('everyauth')
, connect = require('connect');
everyauth.password
- .getLoginPath('/login') // Page with the login form
- .postLoginPath('/login') // What you POST to
+ .getLoginPath('/login') // Uri path to the login page
+ .postLoginPath('/login') // Uri path that your login form POSTs to
.loginView('a string of html; OR the name of the jade/etc-view-engine view')
.authenticate( function (login, password) {
- // Returns a user if we can authenticate with the login + password.
- // If we cannot, returns null/undefined
+ // Either, we return an array [user, errors] if doing sync auth.
+ // Or, we return a Promise that can fulfill to promise.fulfill(user, errors).
+ // `errors` is an array of error message strings
+ //
+ // e.g.,
+ // Example 1 - Sync Example
+ // if (usersByLogin[login] && usersByLogin[login].password === password) {
+ // return [usersByLogin[login], []];
+ // } else {
+ // return [null, ['Login failed']]
+ // }
+ //
+ // Example 2 - Async Example
+ // var promise = this.Promise()
+ // YourUserModel.find({ login: login}, function (err, user) {
+ // if (err) return promise.fulfill(null, [err]);
+ // promise.fulfill(user, []);
+ // }
+ // return promise;
})
+ .loginSuccessRedirect('/') // Where to redirect to after a login
+
+ // If login fails, we render the errors via the login view template,
+ // so just make sure your loginView() template incorporates an `errors` local.
+ // See './example/views/login.jade'
- .getRegisterPath('/register') // Page with the registration form
- .postRegisterPath('/register') // What you POST to
- // TODO Complete documentation for validateRegistration
- .extractExtraRegistrationParams( function (req) {
- return {
- phone: req.body.phone
- , name: {
- first: req.body.first_name
- , last: req.body.last_name
- }
- };
- })
- .validateRegistration( function () {
- })
- .handleRegistrationError( function (req, res, errorMessages) {
- })
+ .getRegisterPath('/register') // Uri path to the registration page
+ .postRegisterPath('/register') // The Uri path that your registration form POSTs to
.registerView('a string of html; OR the name of the jade/etc-view-engine view')
- .registerUser( function (login, password) {
+ .validateRegistration( function (newUserAttributes) {
+ // Validate the registration input
+ // Return undefined, null, or [] if validation succeeds
+ // Return an array of error messages (or Promise promising this array)
+ // if validation fails
+ //
+ // e.g., assuming you define validate with the following signature
+ // var errors = validate(login, password, extraParams);
+ // return errors;
+ //
+ // The `errors` you return show up as an `errors` local in your jade template
+ })
+ .registerUser( function (newUserAttributes) {
// Returns a user (or a Promise that promises a user) after adding it to
// some user store. You can also do things here like registration validation
// and re-directing back to the registration page upon invalid registration
})
-
- .redirectPath('/'); // Where to redirect to after a login
-
var routes = function (app) {
// Define your routes here
};
@@ -327,6 +344,23 @@ object whose parameter name keys map to description values:
everyauth.password.configurable();
```
+### Password Recipe 1: Extra registration data besides login + password
+Sometimes your registration will ask for more information from the user besides the login and password.
+
+For this particular scenario, you can configure the optional step, `extractExtraRegistrationParams`.
+
+```javascript
+everyauth.password.extractExtraRegistrationParams( function (req) {
+ return {
+ phone: req.body.phone
+ , name: {
+ first: req.body.first_name
+ , last: req.body.last_name
+ }
+ };
+});
+```
+
## Setting up GitHub OAuth
```javascript
View
@@ -45,39 +45,30 @@ everyauth
.loginView('login.jade')
.authenticate( function (login, password) {
var user = usersByLogin[login];
- if (!user) return false;
- if (user.password !== password) return false;
- return user;
+ if (!user) return [null, ['Login failed']];
+ if (user.password !== password) return [null, ['Login failed']];
+ return [user, []];
})
.getRegisterPath('/register')
.postRegisterPath('/register')
.registerView('register.jade')
- .validateRegistration( function (login, password, extraParams, req, res) {
- if (!login)
- return this.breakTo('registrationError', req, res, 'Missing login');
- if (!password)
- return this.breakTo('registrationError', req, res, 'Missing password');
-
- var promise = this.Promise();
- // simulate an async user db
- setTimeout( function () {
- if (login in usersByLogin) {
- return promise.breakTo('registrationError', req, res, 'Someone already has the login ' + login);
- }
- promise.fulfill({
- login: login
- , password: password
- });
- }, 200);
- return promise;
+ .validateRegistration( function (newUserAttrs) {
+ var login = newUserAttrs.login
+ , password = newUserAttrs.password
+ , errors = [];
+ if (!login) errors.push('Missing login');
+ if (usersByLogin[login]) errors.push('Login already taken');
+ if (!password) errors.push('Missing password');
+ return errors;
})
.registerUser( function (newUserAttrs) {
var login = newUserAttrs.login;
return usersByLogin[login] = newUserAttrs;
})
- .redirectPath('/');
+ .loginSuccessRedirect('/')
+ .registerSuccessRedirect('/');
everyauth.github
.myHostname('http://local.host:3000')
View
@@ -1,7 +1,11 @@
+- if ('undefined' !== typeof errors && errors.length)
+ ul#errors
+ - each error in errors
+ li.error= error
form(action: '/login', method: 'POST')
#login
label(for: 'login') Login
- input(type: 'text', name: 'login')
+ input(type: 'text', name: 'login', value: login)
#password
label(for: 'password') Password
input(type: 'password', name: 'password')
@@ -1,10 +1,12 @@
h2 Register
-- if ('undefined' !== typeof errorMessage)
- #error= errorMessage
+- if ('undefined' !== typeof errors && errors.length)
+ ul#errors
+ - each error in errors
+ li.error= error
form(action: '/register', method: 'POST')
#login
label(for: 'login') Login
- input(type: 'text', name: 'login')
+ input(type: 'text', name: 'login', value: userParams.login)
#password
label(for: 'password') Password
input(type: 'password', name: 'password')
View
@@ -9,66 +9,91 @@ everyModule.submodule('password')
, passwordFormFieldName: 'the name of the login field. Same as what you put in your login form '
+ '- e.g., if <input type="password" name="pswd" />, then passwordFormFieldName '
+ 'should be set to "pswd".'
- , loginView: 'Either the name of the view (e.g., "login.jade") or the HTML string ' +
- 'that corresponds to the login page.'
+ , loginView: 'Either (A) the name of the view (e.g., "login.jade") or (B) the HTML string ' +
+ 'that corresponds to the login page OR (C) a function (errors, login) {...} that returns the HTML string incorporating the array of `errors` messages and the `login` used in the prior attempt'
+ , loginSuccessRedirect: 'The path we redirect to after a successful login.'
, registerView: 'Either the name of the view (e.g., "register.jade") or the HTML string ' +
'that corresponds to the register page.'
- , redirectPath: 'The path we redirect to after a login attempt.'
+ , registerSuccessRedirect: 'The path we redirect to after a successful registration.'
})
.loginFormFieldName('login')
.passwordFormFieldName('password')
+
.get('getLoginPath', "the login page's uri path.")
.step('displayLogin')
.accepts('req res')
.promises(null)
+ .displayLogin( function (req, res) {
+ if (res.render) {
+ res.render(this.loginView(), {login: null});
+ } else {
+ res.writeHead(200, {'Content-Type': 'text/html'});
+ res.end(this.loginView());
+ }
+ })
.post('postLoginPath', "the uri path that the login POSTs to. Same as the 'action' field of the login <form />.")
.step('extractLoginPassword')
.accepts('req res')
.promises('login password')
.step('authenticate')
- .accepts('login password')
- .promises('user')
+ .accepts('login password req res')
+ .promises('user errors')
.step('getSession')
.accepts('req')
.promises('session')
.step('addToSession')
- .accepts('session user')
+ .accepts('session user errors')
.promises(null)
- .step('sendResponse')
+ .step('respondToLoginSucceed') // TODO Rename to maybeRespondToLoginSucceed ?
.accepts('res user')
.promises(null)
- .displayLogin( function (req, res) {
- if (res.render) {
- res.render(this.loginView());
- } else {
- res.writeHead(200, {'Content-Type': 'text/html'});
- res.end(this.loginView());
- }
- })
+ .step('respondToLoginFail')
+ .accepts('res errors login')
+ .promises(null)
.extractLoginPassword( function (req, res) {
return [req.body[this.loginFormFieldName()], req.body[this.passwordFormFieldName()]];
})
.getSession( function (req) {
return req.session;
})
- .addToSession( function (sess, user) {
+ .addToSession( function (sess, user, errors) {
var _auth = sess.auth || (sess.auth = {});
if (user)
_auth.userId = user.id;
_auth.loggedIn = !!user;
})
- .sendResponse( function (res, user) {
- res.writeHead(303, {'Location': this.redirectPath()});
- res.end();
+ .respondToLoginSucceed( function (res, user) {
+ if (user) {
+ res.writeHead(303, {'Location': this.loginSuccessRedirect()});
+ res.end();
+ }
})
+ .respondToLoginFail( function (res, errors, login) {
+ if (!errors || !errors.length) return;
+ if (res.render) {
+ res.render(this.loginView(), {
+ errors: errors
+ , login: login
+ });
+ } else {
+ res.writeHead(200, {'Content-Type': 'text/html'});
+ if ('function' === typeof this.loginView()) {
+ res.end(this.loginView()(errors, login));
+ } else {
+ res.end(this.loginView());
+ }
+ }
+ })
+
.get('getRegisterPath', "the registration page's uri path.")
.step('displayRegister')
.accepts('req res')
.promises(null)
.displayRegister( function (req, res) {
+ var userParams = {};
if (res.render) {
- res.render(this.registerView());
+ res.render(this.registerView(), {userParams: userParams});
} else {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(this.registerView());
@@ -82,32 +107,64 @@ everyModule.submodule('password')
'incoming request and returns them in the `extraParams` object')
.accepts('req')
.promises('extraParams')
+ .step('aggregateParams')
+ .description('Combines login, password, and extraParams into a newUserAttributes Object containing everything in extraParams plus login and password key/value pairs')
+ .accepts('login password extraParams')
+ .promises('newUserAttributes')
.step('validateRegistration')
.description('Validates the registration parameters. Default includes check for existing user')
- .accepts('login password extraParams req res')
- .promises('newUserAttributes')
- .canBreakTo('registrationError') // canGoTo
+ .accepts('newUserAttributes')
+ .promises('errors')
+ .step('maybeBreakToRegistrationFailSteps')
+ .accepts('req res errors newUserAttributes')
+ .promises(null)
+ .canBreakTo('registrationFailSteps')
.step('registerUser')
.description('Creates and returns a new user with newUserAttributes')
.accepts('newUserAttributes')
.promises('user')
.step('getSession')
.step('addToSession')
- .step('sendResponse')
+ .step('respondToRegistrationSucceed')
+ .accepts('res user')
+ .promises(null)
.extractExtraRegistrationParams( function (req) {
return {};
})
- .validateRegistration( function (login, password, extraParams, req, res) {
- if (login && password) return { login: login, password: password };
- else return this.breakTo('registrationError', req, res, 'Missing login and/or password');
+ .aggregateParams( function (login, password, extraParams) {
+ var params = extraParams;
+ params.login = login;
+ params.password = password;
+ return params;
+ })
+ .validateRegistration( function (newUserAttributes) {
+ var login = newUserAttributes.login
+ , password = newUserAttributes.password
+ , errors = [];
+ if (!login) errors.push('Missing login');
+ if (!password) errors.push('Missing password');
+ return errors;
+ })
+ .maybeBreakToRegistrationFailSteps( function (req, res, errors, newUserAttributes) {
+ var user;
+ if (errors && errors.length) {
+ user = newUserAttributes;
+ delete user.password;
+ return this.breakTo('registrationFailSteps', req, res, errors, user);
+ }
+ })
+ .respondToRegistrationSucceed( function (res, user) {
+ res.writeHead(303, {'Location': this.registerSuccessRedirect()});
+ res.end();
})
- .stepseq('registrationError')
- .step('handleRegistrationError')
- .accepts('req res errorMessage')
+ .stepseq('registrationFailSteps')
+ .step('respondToRegistrationFail')
+ .accepts('req res errors newUserAttributes')
.promises(null)
- .handleRegistrationError( function (req, res, errorMessage) {
+ .respondToRegistrationFail( function (req, res, errors, newUserAttributes) {
res.render(this.registerView(), {
- errorMessage: errorMessage
+ errors: errors
+ , userParams: newUserAttributes
});
});
View
@@ -57,7 +57,9 @@ Step.prototype = {
ret = (ret instanceof Promise)
? ret
: Array.isArray(ret)
- ? this.module.Promise(ret)
+ ? promises.length === 1
+ ? this.module.Promise([ret])
+ : this.module.Promise(ret)
: this.module.Promise([ret]);
ret.callback( function () {

0 comments on commit 46a6e09

Please sign in to comment.