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({