From f996abf028423d03d72e31e2a2394499a3c13c6c Mon Sep 17 00:00:00 2001 From: John Furrow Date: Tue, 21 Jun 2016 23:16:24 -0700 Subject: [PATCH] Implement JWT authentication --- client/source/scripts/actions/AuthActions.js | 129 ++++++++++++++++ .../scripts/components/auth/AuthForm.js | 105 +++++++++++++ .../scripts/components/auth/LoginForm.js | 10 ++ .../components/auth/RegistrationForm.js | 10 ++ .../source/scripts/constants/ActionTypes.js | 12 ++ client/source/scripts/constants/EventTypes.js | 12 ++ client/source/scripts/stores/AuthStore.js | 145 ++++++++++++++++++ client/source/scripts/stores/BaseStore.js | 11 +- client/source/scripts/stores/TorrentStore.js | 2 +- .../scripts/stores/TransferDataStore.js | 4 +- config.js | 1 + package.json | 7 +- server/app.js | 75 ++++----- server/config/passport.js | 29 ++++ server/models/Users.js | 99 ++++++++++++ server/routes/{index.js => api.js} | 30 ++-- server/routes/auth.js | 61 ++++++++ server/routes/client.js | 2 +- 18 files changed, 674 insertions(+), 70 deletions(-) create mode 100644 client/source/scripts/actions/AuthActions.js create mode 100644 client/source/scripts/components/auth/AuthForm.js create mode 100644 client/source/scripts/components/auth/LoginForm.js create mode 100644 client/source/scripts/components/auth/RegistrationForm.js create mode 100644 client/source/scripts/stores/AuthStore.js create mode 100644 server/config/passport.js create mode 100644 server/models/Users.js rename server/routes/{index.js => api.js} (51%) create mode 100644 server/routes/auth.js diff --git a/client/source/scripts/actions/AuthActions.js b/client/source/scripts/actions/AuthActions.js new file mode 100644 index 00000000..e932f6ac --- /dev/null +++ b/client/source/scripts/actions/AuthActions.js @@ -0,0 +1,129 @@ +import axios from 'axios'; + +import ActionTypes from '../constants/ActionTypes'; +import AppDispatcher from '../dispatcher/AppDispatcher'; + +const AuthActions = { + authenticate: (credentials) => { + return axios.post('/auth/authenticate', credentials) + .then((json = {}) => { + return json.data; + }) + .then((data) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.AUTH_LOGIN_SUCCESS, + data + }); + }) + .catch((error) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.AUTH_LOGIN_ERROR, + error: error.data.message + }); + }); + }, + + createUser: (credentials) => { + return axios.put('/auth/users', credentials) + .then((json = {}) => { + return json.data; + }) + .then((data) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.AUTH_CREATE_USER_SUCCESS, + data + }); + }) + .catch((error) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.AUTH_CREATE_USER_ERROR, + error + }); + }); + }, + + deleteUser: (username) => { + return axios.delete(`/auth/users/${username}`) + .then((json = {}) => { + return json.data; + }) + .then((data) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.AUTH_DELETE_USER_SUCCESS, + data: { + username, + ...data + } + }); + }) + .catch((error) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.AUTH_DELETE_USER_ERROR, + error: { + username, + ...error + } + }); + }); + }, + + fetchUsers: () => { + return axios.get('/auth/users') + .then((json = {}) => { + return json.data; + }) + .then((data) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.AUTH_LIST_USERS_SUCCESS, + data + }); + }) + .catch((error) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.AUTH_LIST_USERS_ERROR, + error + }); + }); + }, + + register: (credentials) => { + return axios.post('/auth/register', credentials) + .then((json = {}) => { + return json.data; + }) + .then((data) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.AUTH_REGISTER_SUCCESS, + data + }); + }) + .catch((error) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.AUTH_REGISTER_ERROR, + error + }); + }); + }, + + verify: () => { + // We need to prevent caching this endpoint. + return axios.get(`/auth/verify?${Date.now()}`) + .then((json = {}) => { + return json.data; + }) + .then((data) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.AUTH_VERIFY_SUCCESS, + data + }); + }) + .catch((error) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.AUTH_VERIFY_ERROR, + error + }); + }); + } +}; + +export default AuthActions; diff --git a/client/source/scripts/components/auth/AuthForm.js b/client/source/scripts/components/auth/AuthForm.js new file mode 100644 index 00000000..76a8ea03 --- /dev/null +++ b/client/source/scripts/components/auth/AuthForm.js @@ -0,0 +1,105 @@ +import classnames from'classnames'; +import React from'react'; + +import EventTypes from '../../constants/EventTypes'; +import FloodActions from '../../actions/FloodActions'; +import AuthStore from '../../stores/AuthStore'; + +const METHODS_TO_BIND = ['handleAuthError', 'handleSubmitClick']; + +export default class AuthForm extends React.Component { + constructor() { + super(); + + this.state = {error: null} + + METHODS_TO_BIND.forEach((method) => { + this[method] = this[method].bind(this); + }); + } + + componentDidMount() { + AuthStore.listen(EventTypes.AUTH_LOGIN_ERROR, this.handleAuthError); + AuthStore.listen(EventTypes.AUTH_REGISTER_ERROR, this.handleAuthError); + } + + componentWillUnmount() { + AuthStore.unlisten(EventTypes.AUTH_LOGIN_ERROR, this.handleAuthError); + AuthStore.unlisten(EventTypes.AUTH_REGISTER_ERROR, this.handleAuthError); + } + + getValue(fieldName) { + return this.state[fieldName]; + } + + handleSubmitClick() { + if (this.props.mode === 'login') { + AuthStore.authenticate({ + username: this.refs.username.value, + password: this.refs.password.value + }); + } else { + AuthStore.register({ + username: this.refs.username.value, + password: this.refs.password.value + }); + } + } + + handleAuthError(error) { + this.setState({error}); + } + + render() { + let actionText = null; + let error = null; + let headerText = null; + + if (this.props.mode === 'login') { + actionText = 'Log In'; + headerText = 'Login'; + } else { + actionText = 'Create Account'; + headerText = 'Create an Account'; + } + + if (!!this.state.error) { + error = ( +
+
+ {this.state.error} +
+
+ ); + } + + return ( +
+
+
+

{headerText}

+
+
+
+ +
+
+
+
+ +
+
+ {error} +
+
+ +
+
+ ); + } +} diff --git a/client/source/scripts/components/auth/LoginForm.js b/client/source/scripts/components/auth/LoginForm.js new file mode 100644 index 00000000..3aba6c5d --- /dev/null +++ b/client/source/scripts/components/auth/LoginForm.js @@ -0,0 +1,10 @@ +import classnames from'classnames'; +import React from'react'; + +import AuthForm from './AuthForm'; + +export default class LoginForm extends React.Component { + render() { + return ; + } +} diff --git a/client/source/scripts/components/auth/RegistrationForm.js b/client/source/scripts/components/auth/RegistrationForm.js new file mode 100644 index 00000000..84be2ab7 --- /dev/null +++ b/client/source/scripts/components/auth/RegistrationForm.js @@ -0,0 +1,10 @@ +import classnames from'classnames'; +import React from'react'; + +import AuthForm from './AuthForm'; + +export default class RegistrationForm extends React.Component { + render() { + return ; + } +} diff --git a/client/source/scripts/constants/ActionTypes.js b/client/source/scripts/constants/ActionTypes.js index c3ae0438..c571cfdb 100644 --- a/client/source/scripts/constants/ActionTypes.js +++ b/client/source/scripts/constants/ActionTypes.js @@ -1,4 +1,16 @@ const ActionTypes = { + AUTH_CREATE_USER_ERROR: 'AUTH_CREATE_USER_ERROR', + AUTH_CREATE_USER_SUCCESS: 'AUTH_CREATE_USER_SUCCESS', + AUTH_DELETE_USER_ERROR: 'AUTH_DELETE_USER_ERROR', + AUTH_DELETE_USER_SUCCESS: 'AUTH_DELETE_USER_SUCCESS', + AUTH_LIST_USERS_ERROR: 'AUTH_LIST_USERS_ERROR', + AUTH_LIST_USERS_SUCCESS: 'AUTH_LIST_USERS_SUCCESS', + AUTH_LOGIN_ERROR: 'AUTH_LOGIN_ERROR', + AUTH_LOGIN_SUCCESS: 'AUTH_LOGIN_SUCCESS', + AUTH_REGISTER_ERROR: 'AUTH_REGISTER_ERROR', + AUTH_REGISTER_SUCCESS: 'AUTH_REGISTER_SUCCESS', + AUTH_VERIFY_ERROR: 'AUTH_VERIFY_ERROR', + AUTH_VERIFY_SUCCESS: 'AUTH_VERIFY_SUCCESS', CLIENT_ADD_TORRENT_ERROR: 'CLIENT_ADD_TORRENT_ERROR', CLIENT_ADD_TORRENT_SUCCESS: 'CLIENT_ADD_TORRENT_SUCCESS', CLIENT_CHECK_HASH_ERROR: 'CLIENT_CHECK_HASH_ERROR', diff --git a/client/source/scripts/constants/EventTypes.js b/client/source/scripts/constants/EventTypes.js index 647bcc5d..13085447 100644 --- a/client/source/scripts/constants/EventTypes.js +++ b/client/source/scripts/constants/EventTypes.js @@ -1,4 +1,16 @@ const EventTypes = { + AUTH_CREATE_USER_ERROR: 'AUTH_CREATE_USER_ERROR', + AUTH_CREATE_USER_SUCCESS: 'AUTH_CREATE_USER_SUCCESS', + AUTH_DELETE_USER_ERROR: 'AUTH_DELETE_USER_ERROR', + AUTH_DELETE_USER_SUCCESS: 'AUTH_DELETE_USER_SUCCESS', + AUTH_LIST_USERS_ERROR: 'AUTH_LIST_USERS_ERROR', + AUTH_LIST_USERS_SUCCESS: 'AUTH_LIST_USERS_SUCCESS', + AUTH_LOGIN_ERROR: 'AUTH_LOGIN_ERROR', + AUTH_LOGIN_SUCCESS: 'AUTH_LOGIN_SUCCESS', + AUTH_REGISTER_ERROR: 'AUTH_REGISTER_ERROR', + AUTH_REGISTER_SUCCESS: 'AUTH_REGISTER_SUCCESS', + AUTH_VERIFY_ERROR: 'AUTH_VERIFY_ERROR', + AUTH_VERIFY_SUCCESS: 'AUTH_VERIFY_SUCCESS', CLIENT_ADD_TORRENT_ERROR: 'CLIENT_ADD_TORRENT_ERROR', CLIENT_ADD_TORRENT_SUCCESS: 'CLIENT_ADD_TORRENT_SUCCESS', CLIENT_SET_THROTTLE_ERROR: 'CLIENT_SET_THROTTLE_ERROR', diff --git a/client/source/scripts/stores/AuthStore.js b/client/source/scripts/stores/AuthStore.js new file mode 100644 index 00000000..356cf05a --- /dev/null +++ b/client/source/scripts/stores/AuthStore.js @@ -0,0 +1,145 @@ +import AuthActions from '../actions/AuthActions'; +import ActionTypes from '../constants/ActionTypes'; +import AppDispatcher from '../dispatcher/AppDispatcher'; +import BaseStore from './BaseStore'; +import EventTypes from '../constants/EventTypes'; + +class AuthStoreClass extends BaseStore { + constructor() { + super(); + this.token = null; + this.users = []; + } + + authenticate(credentials) { + AuthActions.authenticate({ + username: credentials.username, + password: credentials.password + }); + } + + createUser(credentials) { + AuthActions.createUser(credentials); + } + + deleteUser(username) { + AuthActions.deleteUser(username); + } + + fetchUserList() { + AuthActions.fetchUsers(); + } + + getToken() { + return this.token; + } + + getUsers() { + return this.users; + } + + handleCreateUserError(error) { + this.emit(EventTypes.AUTH_CREATE_USER_ERROR, error); + } + + handleCreateUserSuccess(data) { + this.emit(EventTypes.AUTH_CREATE_USER_SUCCESS); + } + + handleDeleteUserError(error) { + this.emit(EventTypes.AUTH_DELETE_USER_ERROR, error.username); + } + + handleDeleteUserSuccess(data) { + this.emit(EventTypes.AUTH_DELETE_USER_SUCCESS, data.username); + } + + handleListUsersError(error) { + this.emit(EventTypes.AUTH_LIST_USERS_ERROR); + } + + handleListUsersSuccess(data) { + this.users = data; + this.emit(EventTypes.AUTH_LIST_USERS_SUCCESS); + } + + handleLoginSuccess(data) { + this.emit(EventTypes.AUTH_LOGIN_SUCCESS); + this.token = data.token; + } + + handleLoginError(error) { + this.token = null; + this.emit(EventTypes.AUTH_LOGIN_ERROR, error); + } + + handleRegisterSuccess(data) { + this.emit(EventTypes.AUTH_REGISTER_SUCCESS, data); + } + + handleRegisterError(error) { + this.emit(EventTypes.AUTH_REGISTER_ERROR, error); + } + + register(credentials) { + AuthActions.register({ + username: credentials.username, + password: credentials.password + }); + } + + verify() { + AuthActions.verify(); + } +} + +let AuthStore = new AuthStoreClass(); + +AuthStore.dispatcherID = AppDispatcher.register((payload) => { + const {action, source} = payload; + + switch (action.type) { + case ActionTypes.AUTH_LOGIN_SUCCESS: + AuthStore.handleLoginSuccess(action.data); + break; + case ActionTypes.AUTH_LOGIN_ERROR: + AuthStore.handleLoginError(action.error); + break; + case ActionTypes.AUTH_LIST_USERS_SUCCESS: + AuthStore.handleListUsersSuccess(action.data); + break; + case ActionTypes.AUTH_LIST_USERS_ERROR: + AuthStore.handleListUsersError(action.error); + break; + case ActionTypes.AUTH_CREATE_USER_SUCCESS: + AuthStore.handleCreateUserSuccess(action.data); + break; + case ActionTypes.AUTH_CREATE_USER_ERROR: + AuthStore.handleCreateUserError(action.error.data); + break; + case ActionTypes.AUTH_DELETE_USER_SUCCESS: + AuthStore.handleDeleteUserSuccess(action.data); + break; + case ActionTypes.AUTH_DELETE_USER_ERROR: + AuthStore.handleDeleteUserError(action.error); + break; + case ActionTypes.AUTH_REGISTER_SUCCESS: + AuthStore.handleRegisterSuccess(action.data); + AuthStore.emit(EventTypes.AUTH_REGISTER_SUCCESS, + action.data); + break; + case ActionTypes.AUTH_REGISTER_ERROR: + AuthStore.handleRegisterError(action.error.data); + break; + case ActionTypes.AUTH_VERIFY_SUCCESS: + AuthStore.emit(EventTypes.AUTH_VERIFY_SUCCESS, + action.data); + break; + case ActionTypes.AUTH_VERIFY_ERROR: + AuthStore.emit(EventTypes.AUTH_VERIFY_ERROR, + action.error); + break; + } +}); + +export default AuthStore; diff --git a/client/source/scripts/stores/BaseStore.js b/client/source/scripts/stores/BaseStore.js index 6bdde7e8..195f871e 100644 --- a/client/source/scripts/stores/BaseStore.js +++ b/client/source/scripts/stores/BaseStore.js @@ -10,6 +10,13 @@ export default class BaseStore extends EventEmitter { this.setMaxListeners(20); } + emit(eventName) { + super.emit(...arguments); + if (eventName == null) { + console.warn('Event is undefined!'); + } + } + beginRequest(id) { this.requests[id] = true; } @@ -19,7 +26,7 @@ export default class BaseStore extends EventEmitter { } isRequestPending(id) { - if (this.requests[id] == null || this.requests[id] === false) { + if (this.requests[id] == null) { return false; } @@ -31,7 +38,7 @@ export default class BaseStore extends EventEmitter { } resolveRequest(id) { - this.requests[id] = false; + delete this.requests[id]; } unlisten(event, callback) { diff --git a/client/source/scripts/stores/TorrentStore.js b/client/source/scripts/stores/TorrentStore.js index 374dd200..dceef172 100644 --- a/client/source/scripts/stores/TorrentStore.js +++ b/client/source/scripts/stores/TorrentStore.js @@ -178,7 +178,7 @@ class TorrentStoreClass extends BaseStore { } handleFetchTorrentsError(error) { - console.trace(error); + this.resolveRequest('fetch-torrents'); } handleFetchTorrentsSuccess(torrents) { diff --git a/client/source/scripts/stores/TransferDataStore.js b/client/source/scripts/stores/TransferDataStore.js index 108bcfc7..88c8f60d 100644 --- a/client/source/scripts/stores/TransferDataStore.js +++ b/client/source/scripts/stores/TransferDataStore.js @@ -59,11 +59,11 @@ class TransferDataStoreClass extends BaseStore { handleSetThrottleSuccess(data) { this.fetchTransferData(); - this.emit(EventTypes.CLIENT_SET_THROTTLE_SUCCESS); + // this.emit(EventTypes.CLIENT_SET_THROTTLE_SUCCESS); } handleSetThrottleError(error) { - this.emit(EventTypes.CLIENT_SET_THROTTLE_ERROR); + // this.emit(EventTypes.CLIENT_SET_THROTTLE_ERROR); } handleTransferDataSuccess(transferData) { diff --git a/config.js b/config.js index 79030e81..dd6b7611 100644 --- a/config.js +++ b/config.js @@ -3,6 +3,7 @@ const config = { dbPath: './server/db/', maxHistoryStates: 30, pollInterval: 1000 * 5, + secret: 'flood', scgi: { host: 'localhost', port: 5000, diff --git a/package.json b/package.json index cfba3447..1b4ead96 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "dependencies": { "axios": "^0.7.0", "babel-polyfill": "^6.9.1", - "body-parser": "~1.12.0", + "bcrypt-nodejs": "0.0.3", + "body-parser": "^1.12.4", "classnames": "^2.1.5", "compression": "^1.6.1", "cookie-parser": "~1.3.4", @@ -28,9 +29,10 @@ "inuit-page": "^0.2.1", "inuit-reset": "^0.1.1", "isomorphic-fetch": "^2.1.1", + "jsonwebtoken": "^7.0.1", "keymirror": "^0.1.1", "lodash": "^4.3.0", - "morgan": "~1.5.1", + "morgan": "^1.5.3", "multer": "^1.1.0", "mv": "^2.1.1", "nedb": "^1.7.2", @@ -38,6 +40,7 @@ "object-assign": "^2.0.0", "passport": "^0.3.2", "passport-http": "^0.3.0", + "passport-jwt": "^2.1.0", "pug": "^2.0.0-beta3", "q": "^1.2.0", "react": "^15.0.2", diff --git a/server/app.js b/server/app.js index 25da4885..fe4fe06a 100644 --- a/server/app.js +++ b/server/app.js @@ -1,54 +1,47 @@ +'use strict'; + require('events').EventEmitter.defaultMaxListeners = Infinity; -var bodyParser = require('body-parser'); -var compression = require('compression'); -var cookieParser = require('cookie-parser'); -var express = require('express'); -var favicon = require('serve-favicon'); -var logger = require('morgan'); -var path = require('path'); +let bodyParser = require('body-parser'); +let compression = require('compression'); +let cookieParser = require('cookie-parser'); +let express = require('express'); +let favicon = require('serve-favicon'); +let morgan = require('morgan'); +let passport = require('passport'); +let path = require('path'); -var clientRoutes = require('./routes/client'); -var mainRoutes = require('./routes/index'); - -var app = express(); +let app = express(); +let apiRoutes = require('./routes/api'); +let authRoutes = require('./routes/auth'); +let Users = require('./models/Users'); app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug'); -// uncomment after placing your favicon in /assets -//app.use(favicon(__dirname + '/assets/favicon.ico')); -app.use(logger('dev')); +// TODO: Add favicon... +// app.use(favicon(__dirname + '/assets/favicon.ico')); +app.use(morgan('dev')); +app.use(passport.initialize()); app.use(compression()); app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: false })); +app.use(bodyParser.urlencoded({extended: false})); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'assets'))); -app.use((req, res, next) => { - req.socket.on("error", (err) => { - console.trace(err); - }); - res.socket.on("error", (err) => { - console.trace(err); - }); - next(); -}); +require('./config/passport')(passport); -app.use('/', mainRoutes); -app.use('/client', clientRoutes); +app.use('/auth', authRoutes); +app.use('/api', apiRoutes); -// catch 404 and forward to error handler +// Catch 404 and forward to error handler. app.use((req, res, next) => { var err = new Error('Not Found'); err.status = 404; next(err); }); -// error handlers - -// development error handler -// will print stacktrace +// Development error handler, will print stacktrace. if (app.get('env') === 'development') { app.use((err, req, res, next) => { res.status(err.status || 500); @@ -57,17 +50,15 @@ if (app.get('env') === 'development') { error: err }); }); +} else { + // Production error handler, no stacktraces leaked to user. + app.use((err, req, res, next) => { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: {} + }); + }); } -// production error handler -// no stacktraces leaked to user -app.use((err, req, res, next) => { - res.status(err.status || 500); - res.render('error', { - message: err.message, - error: {} - }); -}); - - module.exports = app; diff --git a/server/config/passport.js b/server/config/passport.js new file mode 100644 index 00000000..5e9e3ca1 --- /dev/null +++ b/server/config/passport.js @@ -0,0 +1,29 @@ +'use strict'; + +let extractJWT = require('passport-jwt').ExtractJwt; +let jwtStrategy = require('passport-jwt').Strategy; + +let config = require('../../config'); +let Users = require('../models/Users'); + +// Setup work and export for the JWT passport strategy +module.exports = (passport) => { + let options = { + jwtFromRequest: extractJWT.fromAuthHeader(), + secretOrKey: config.secret + }; + + passport.use(new jwtStrategy(options, (jwtPayload, callback) => { + Users.lookupUser({username: jwtPayload.username}, (err, user) => { + if (err) { + return callback(err, false); + } + + if (user) { + return callback(null, user); + } + + return callback(null, false); + }); + })); +}; diff --git a/server/models/Users.js b/server/models/Users.js new file mode 100644 index 00000000..5de88908 --- /dev/null +++ b/server/models/Users.js @@ -0,0 +1,99 @@ +'use strict'; + +let Datastore = require('nedb'); + +let bcrypt = require('bcrypt-nodejs'); +let config = require('../../config'); + +class Users { + constructor() { + this.ready = false; + this.db = this.loadDatabase(); + } + + comparePassword(credentials, callback) { + this.db.findOne({username: credentials.username}).exec((err, user) => { + if (err) { + return callback(err); + } + + // Username not found. + if (user == null) { + return callback(null, user); + } + + bcrypt.compare(credentials.password, user.password, (err, isMatch) => { + if (err) { + return callback(err); + } + + return callback(null, isMatch); + }); + }); + } + + createUser(credentials, callback) { + if (!this.ready) { + callback({message: 'Users database is not ready.'}); + } + + bcrypt.genSalt(10, (err, salt) => { + if (err) { + callback(err); + return; + } + + let username = credentials.username; + + bcrypt.hash(credentials.password, salt, null, (err, hash) => { + if (err) { + callback(err); + return; + } + + this.db.insert({username: username, password: hash}, (err, user) => { + if (err) { + callback(err); + return; + } + + callback(null, {username: credentials.username}); + }); + }); + }); + } + + initialUserGate(handlers) { + this.db.find({}, (err, users) => { + if (users && users.length > 0) { + return handlers.handleSubsequentUser(); + } + + return handlers.handleInitialUser(); + }); + } + + loadDatabase() { + let db = new Datastore({ + autoload: true, + filename: `${config.dbPath}authUsers.db` + }); + + db.ensureIndex({fieldName: 'username', unique: true}); + + this.ready = true; + return db; + } + + lookupUser(credentials, callback) { + this.db.findOne({username: credentials.username}, (err, user) => { + if (err) { + return callback(err); + } + + return callback(null, user); + }); + } +} + +module.exports = new Users(); diff --git a/server/routes/index.js b/server/routes/api.js similarity index 51% rename from server/routes/index.js rename to server/routes/api.js index b0df7ea4..e5648013 100644 --- a/server/routes/index.js +++ b/server/routes/api.js @@ -1,34 +1,24 @@ 'use strict'; -var express = require('express'); -var passport = require('passport'); -var router = express.Router(); -var Strategy = require('passport-http').BasicStrategy; +let express = require('express'); let ajaxUtil = require('../util/ajaxUtil'); let client = require('../models/client'); +let clientRoutes = require('./client'); let history = require('../models/history'); +let passport = require('passport'); +let router = express.Router(); let settings = require('../models/settings'); -var users = require('../db/users'); history.startPolling(); -passport.use(new Strategy( - (username, password, callback) => { - users.findByUsername(username, (err, user) => { - if (err) { return callback(err); } - if (!user) { return callback(null, false); } - if (user.password != password) { return callback(null, false); } - return callback(null, user); - }); - } -)); +router.use(passport.authenticate('jwt', {session: false})); -router.get('/', passport.authenticate('basic', { session: false }), - (req, res) => { - res.render('index', { title: 'Flood' }); - } -); +router.use('/client', clientRoutes); + +router.get('/', (req, res) => { + res.render('index', {title: 'Flood'}); +}); router.get('/history', function(req, res, next) { history.get(req.query, ajaxUtil.getResponseFn(res)); diff --git a/server/routes/auth.js b/server/routes/auth.js new file mode 100644 index 00000000..ac2a5bf6 --- /dev/null +++ b/server/routes/auth.js @@ -0,0 +1,61 @@ +'use strict'; + +let express = require('express'); +let jwt = require('jsonwebtoken'); +let multer = require('multer'); +let passport = require('passport'); + +let config = require('../../config'); +let router = express.Router(); +let Users = require('../models/Users'); + +// Allow unauthenticated registration if no users are currently registered. +router.use('/register', (req, res, next) => { + Users.initialUserGate({ + handleInitialUser: next.bind(this), + handleSubsequentUser: passport.authenticate('jwt', {session: false}).bind(this, req, res, next) + }); +}); + +router.post('/register', (req, res) => { + if(!req.body.username || !req.body.password) { + return res.json({success: false, message: 'Please enter username and password.'}); + } else { + // Attempt to save the user + Users.createUser({ + username: req.body.username, + password: req.body.password + }, (err, user) => { + if (err) { + return res.json({success: false, message: 'That username already exists.'}); + } + + return res.json({success: true, message: `Successfully created new user, ${user.username}.`}); + }); + } +}); + +router.post('/authenticate', (req, res) => { + let credentials = { + password: req.body.password, + username: req.body.username + }; + + Users.comparePassword(credentials, function(err, isMatch) { + if (isMatch == null) { + return res.send({success: false, message: 'Username not found.'}); + } + + if (isMatch && !err) { + // Create token if the password matched and no error was thrown. + let token = jwt.sign(credentials, config.secret, { + expiresIn: 60 * 60 * 24 * 7 // one week + }); + return res.json({success: true, token: 'JWT ' + token}); + } else { + return res.send({success: false, message: 'Authentication failed. Passwords did not match.'}); + } + }); +}); + +module.exports = router; diff --git a/server/routes/client.js b/server/routes/client.js index 0035ec4a..4f256982 100644 --- a/server/routes/client.js +++ b/server/routes/client.js @@ -2,12 +2,12 @@ let express = require('express'); let multer = require('multer'); -let router = express.Router(); let ajaxUtil = require('../util/ajaxUtil'); let client = require('../models/client'); let clientUtil = require('../util/clientUtil'); let history = require('../models/history'); +let router = express.Router(); let settings = require('../models/settings'); let upload = multer({