diff --git a/README.md b/README.md index 0fb2a57..ff27889 100644 --- a/README.md +++ b/README.md @@ -200,15 +200,21 @@ If you have `accounts-password` in your app, and you want to be able to use it o Make sure to serve your app over HTTPS if you are using this for login, otherwise people can hijack your passwords. Try the [`force-ssl` package](https://atmospherejs.com/meteor/force-ssl). -### POST /users/login, POST /users/register +### POST /users/login, POST /users/register, POST /users/token-login The login and registration endpoints take the same inputs. Pass an object with the following properties: - `username` - `email` - `password` +- `dbId` -`password` is required, and you must have at least one of `username` or `email`. +`password` is required, and you must have at least one of `username` or `email`. dbId is optional and for multi-database scenarios. + +The token-login endpoint only requires a token and optionally a Database ID. + +- `dbId` +- `loginToken` #### Responses @@ -321,6 +327,12 @@ JsonRoutes.add('get', 'handle-error', function () { # Change Log +#### 2.0.0 + +- Added ability to log in with token at ```/users/token-login```. +- Pass in an optional Database ID for multi database scenarios to ```/users/login```. +- The log in option ```/users/login``` now has the option to pass in a Database ID for multi database scenarios. +- Use the setting.json file in your root project to specify your database ID. #### 1.0.0 - 1.0.12 diff --git a/json-routes.js b/json-routes.js index 7782761..41b4f4d 100644 --- a/json-routes.js +++ b/json-routes.js @@ -1,8 +1,8 @@ -/* global JsonRoutes:true */ +import Fiber from 'fibers'; +import connect from 'connect'; +import connectRoute from 'connect-route'; -var Fiber = require('fibers'); -var connect = require('connect'); -var connectRoute = require('connect-route'); +import { Mongo, MongoInternals } from 'meteor/mongo'; JsonRoutes = {}; @@ -131,16 +131,13 @@ JsonRoutes.Middleware.authenticateMeteorUserByToken = * is invalid */ function getUserIdFromAuthToken(token) { - if (!token) { - return null; - } + if (!token) return null; - const user = Meteor.users.findOne({ - 'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(token) - }, {fields: {_id: 1}}); - if (user) { - return user._id; - } + const driver = new MongoInternals.RemoteCollectionDriver(Meteor.settings[dbId]); // must have the database URL in your settings.json file + const users = new Mongo.Collection("users", { _driver: driver, _suppressSameNameError: true }); + + const user = users.findOne({ 'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(token) }, { fields: { _id: 1 } }); + if (user) return user._id; return null; } @@ -210,6 +207,7 @@ JsonRoutes.Middleware.use(JsonRoutes.Middleware.authenticateMeteorUserByToken); // Handle errors specifically for the login routes correctly JsonRoutes.ErrorMiddleware.use('/users/login', RestMiddleware.handleErrorAsJson); +JsonRoutes.ErrorMiddleware.use('/users/token-login', RestMiddleware.handleErrorAsJson); JsonRoutes.ErrorMiddleware.use('/users/register', RestMiddleware.handleErrorAsJson); @@ -284,19 +282,39 @@ JsonRoutes.add('options', '/users/login', (req, res) => { JsonRoutes.add('post', '/users/login', (req, res) => { const options = req.body; - var user; - if (options.hasOwnProperty('email')) { + let user; + if (options.hasOwnProperty('email') && options.hasOwnProperty('dbId')) { check(options, { email: String, password: String, + dbId: String }); - user = Meteor.users.findOne({ 'emails.address': options.email }); - } else { + const driver = new MongoInternals.RemoteCollectionDriver(Meteor.settings[dbId]); // must have the database URL in your settings.json file + const users = new Mongo.Collection("users", { _driver: driver, _suppressSameNameError: true }); + user = users.findOne({ 'emails.address': options.email }); + } else if (options.hasOwnProperty('username') && options.hasOwnProperty('dbId')) { check(options, { username: String, password: String, + dbId: String + }); + const driver = new MongoInternals.RemoteCollectionDriver(Meteor.settings[dbId]); // must have the database URL in your settings.json file + const users = new Mongo.Collection("users", { _driver: driver, _suppressSameNameError: true }); + user = users.findOne({ username: options.username }); + } else if (options.hasOwnProperty('email')) { + check(options, { + email: String, + password: String }); - user = Meteor.users.findOne({ username: options.username }); + const users = new Mongo.Collection("users"); + user = users.findOne({ 'emails.address': options.email }); + } else if (options.hasOwnProperty('username')) { + check(options, { + username: String, + password: String + }); + const users = new Mongo.Collection("users"); + user = users.findOne({ username: options.username }); } if (!user) { @@ -320,7 +338,8 @@ JsonRoutes.add('post', '/users/login', (req, res) => { when: Date, }); - Accounts._insertLoginToken(result.userId, stampedLoginToken); + const hashedToken = Accounts._hashStampedToken(stampedLoginToken); + users.update({ _id: result.userId }, { $addToSet: { "services.resume.loginTokens": hashedToken } }); const tokenExpiration = Accounts._tokenExpiration(stampedLoginToken.when); check(tokenExpiration, Date); @@ -335,6 +354,76 @@ JsonRoutes.add('post', '/users/login', (req, res) => { }); + +JsonRoutes.add('options', '/users/token-login', (req, res) => { + JsonRoutes.sendResult(res); +}); + +JsonRoutes.add('post', '/users/token-login', (req, res) => { + const options = req.body; + + let multiMode = true; + if (options.hasOwnProperty('dbId')) { + check(options, { + dbId: String, + loginToken: String + }); + } else { + multiMode = false + check(options, { + loginToken: String + }); + } + + const dbId = options.dbId; + const loginToken = options.loginToken; + + let users = null; + if (multiMode) { + const driver = new MongoInternals.RemoteCollectionDriver(Meteor.settings[dbId]); // must have the database URL in your settings.json file + users = new Mongo.Collection("users", { _driver: driver, _suppressSameNameError: true }); + } + else { + users = new Mongo.Collection("users"); + } + let user = users.findOne({ 'services.login.token': loginToken }); + + // No user, in the wrong, could be invalid userId or database, or event token. + if (!user) { + throw new Meteor.Error('not-found', + 'User with that login token not found.'); + } + + // We have a valid user, now assign the id to a variable. + const userId = user._id; + + // You're done with this one-time login token, now throw it away so it can't be used again. + users.update({ _id: userId }, { $unset: { 'services.login.token': '' } }); + + // Generate the stamped token and add it to the user collection + const stampedLoginToken = Accounts._generateStampedLoginToken(); + check(stampedLoginToken, { + token: String, + when: Date, + }); + + const hashedToken = Accounts._hashStampedToken(stampedLoginToken); + users.update({ _id: userId }, { $addToSet: { "services.resume.loginTokens": hashedToken } }); + + const tokenExpiration = Accounts._tokenExpiration(stampedLoginToken.when); + check(tokenExpiration, Date); + + JsonRoutes.sendResult(res, { + data: { + id: userId, + token: stampedLoginToken.token, + tokenExpires: tokenExpiration, + }, + }); + +}); + + JsonRoutes.add('options', '/users/register', (req, res) => { JsonRoutes.sendResult(res); }); diff --git a/package.js b/package.js index e5629d3..7977ad0 100644 --- a/package.js +++ b/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'fine-rest', - version: '1.0.12', + version: '2.0.0', // Brief, one-line summary of the package. summary: 'A fine way to define server-side routes that return JSON', diff --git a/package.json b/package.json index 4b7e2f4..0be790a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fine-rest", - "version": "1.0.12", + "version": "2.0.0", "license": "MIT", "description": "Json Routing for Web API", "main": "json-routes.js",