Feature: First version of the REST API done

This commit is contained in:
2015-08-14 08:04:31 +00:00
commit e6fc0be0ed
28 changed files with 3170 additions and 0 deletions

227
app/controllers/accounts.js Normal file
View File

@@ -0,0 +1,227 @@
var mongoose = require('mongoose'),
ObjectId = mongoose.Schema.Types.ObjectId,
Account = mongoose.model('Account'),
Entry = mongoose.model('Entry'),
Handler = require('../helpers/handler'),
categories = require('../models/categories');
var load_categories = function(language, callback) {
var catz = {};
categories.forEach(function(category) {
var selected_category = category[language],
key = category.en.categorygroup.toLowerCase().replace(/ /g, '_'),
cat = catz[key] || {key: key, label: selected_category.categorygroup, sub_categories: []};
if( !selected_category.category ) {
cat.sub_categories.push({label: selected_category.subcategory, key: selected_category.subcategory.toLowerCase().replace(/ /g, '_')});
}
catz[key] = cat;
});
callback(null, catz);
};
var check_account = function(request, response, callback) {
Account.findById(request.params.account_id, function(errors, account) {
if( errors ) {
return Handler.errorHandler(errors, 404, response);
}
if( !account ) {
return response.status(404).json({message: 'Unknown account'});
}
if( !account.user_id.equals(request.user.id) ) {
return response.status(401).end();
}
return callback(null, account);
});
};
var delete_account = function(account, callback) {
Entry.find({account_id: account.id}).remove(function(errors) {
if( errors ) {
if( callback ) {
return callback(errors);
}
return;
}
account.remove(function(errors) {
if( errors ) {
if( callback ) {
return callback(errors);
}
return;
}
if( callback ) {
return callback();
}
});
});
};
module.exports = {
create : function(request, response) {
var user = request.user,
account = new Account({
name: request.body.name,
reference: request.body.reference,
user_id: user.id
});
load_categories(user.language, function(error, result) {
for( var key in result ) {
account.categories.push( result[key] );
}
account.save(function(errors) {
if( errors ) {
return Handler.errorHandler(errors, 400, response);
}
return response.status(201).json(account);
});
});
},
modify : function(request, response) {
return check_account(request, response, function(error, account) {
account.name = request.body.name;
account.reference = request.body.reference;
account.save(function(errors) {
if( errors ) {
return Handler.errorHandler(errors, 400, response);
}
return response.json(account);
});
});
},
delete : function(request, response) {
return check_account(request, response, function(error, account) {
return delete_account(account, function(errors) {
if( errors ) {
return Handler.errorHandler(errors, 500, response);
}
return response.status(204).end();
});
});
},
delete_account : delete_account,
get : function(request, response) {
return check_account(request, response, function(error, account) {
return response.json(account);
});
},
add_entry : function(request, response) {
return check_account(request, response, function(error, account) {
var data = request.body,
entry = new Entry({
account_id: account.id,
category: data.category ? new ObjectId(data.category) : undefined,
sub_category: data.sub_category ? new ObjectId(data.sub_category) : undefined,
label: data.label,
amount: data.amount,
date: new Date(data.date),
type: data.amount >= 0 ? 'DEPOSIT' : 'BILL'
});
entry.save(function(errors) {
if( errors ) {
return Handler.errorHandler(errors, 400, response);
}
response.status(201).json(entry);
})
});
},
modify_entry : function(request, response) {
return check_account(request, response, function(error, account) {
Entry.findById(request.params.entry_id, function(errors, entry) {
if( errors ) {
return Handler.errorHandler(errors, 404, response);
}
if( !entry ) {
return response.status(404).end();
}
if( !entry.account_id.equals( account.id ) ) {
return response.status(401).end();
}
var data = request.body;
entry.category = data.category ? new ObjectId(data.category) : undefined;
entry.sub_category = data.sub_category ? new ObjectId(data.sub_category) : undefined;
entry.label = data.label;
entry.amount = data.amount;
entry.date = new Date(data.date);
entry.type = data.amount >= 0 ? 'DEPOSIT' : 'BILL';
entry.save(function(errors) {
if( errors ) {
return Handler.errorHandler(errors, 400, response );
}
return response.json(entry);
});
});
});
},
delete_entry : function(request, response) {
return check_account(request, response, function(errors, account) {
Entry.findById(request.params.entry_id, function(errors, entry) {
if( errors ) {
return Handler.errorHandler(errors, 404, response);
}
if( !entry ) {
return response.status(404).end();
}
if( !entry.account_id.equals( account.id ) ) {
return response.status(401).end();
}
entry.remove(function(errors) {
if( errors ) {
return Handler.errorHandler(errors, 500, response);
}
return response.status(204).end();
});
});
});
},
list_entries : function(request, response) {
return check_account(request, response, function(errors, account) {
Entry.find({
account_id: account.id
})
.sort('-date')
.exec(function(errors, entries) {
if( errors ) {
return Handler.errorHandler(errors, 500, response);
}
return response.json(entries);
});
});
}
}

79
app/controllers/users.js Normal file
View File

@@ -0,0 +1,79 @@
var mongoose = require('mongoose'),
User = mongoose.model('User'),
jwt = require('jsonwebtoken'),
security = require('../../config/security'),
Handler = require('../helpers/handler'),
EventEmitter = require('../events/listeners');
module.exports = {
login : function(request, response) {
var user = request.user;
if( !user ) {
return response.status(401).json({message: 'Authentication failed'});
}
return response.json(
{
username: user.username,
token: jwt.sign(
{
user_id: user.id
}, security.jwt.secretOrKey)
});
},
logout : function(request, response) {
return response.status(200).end();
},
subscribe : function(request, response) {
var registered = new User({username: request.body.username, password: request.body.password});
registered.validate(function(errors) {
if( errors ) {
return Handler.errorHandler(errors, 400, response);
}
User.findOne({username: request.body.username}, function(error, user) {
if( error ) {
return response.send(error);
}
if( !user ) {
registered.save(function(errors) {
if( errors ) {
return Handler.errorHandler(errors, 500, response);
}
return response.status(201).json({
username: registered.username,
token: jwt.sign(
{
user_id: registered.id
}, security.jwt.secretOrKey)
});
});
} else {
return response.status(409).json({message: 'Account already exists'});
}
});
});
},
unsubscribe : function(request, response) {
var user = request.user;
if( !user ) {
return response.status(401).json({message: 'Authentication failed'});
}
User.remove({username: user.username}, function(error) {
if( error ) {
return response.status(500).send(error);
}
EventEmitter.eventEmitter.emit(EventEmitter.events.ACCOUNTS_DELETE_BY_USER_ID_EVT, user.id);
return response.status(204).end();
});
}
}

35
app/events/listeners.js Normal file
View File

@@ -0,0 +1,35 @@
var mongoose = require('mongoose'),
Account = mongoose.model('Account'),
EventEmitter = require('events').EventEmitter,
AccountController = require('../controllers/accounts');
var eventEmitter = new EventEmitter(),
ACCOUNTS_DELETE_BY_USER_ID_EVT = 'accounts.delete.by.user.id',
ENTRIES_DELETE_BY_ACCOUNT_EVT = 'entries.delete.by.account';
eventEmitter.on(ACCOUNTS_DELETE_BY_USER_ID_EVT, function(user_id) {
Account.find({user_id: user_id}, function(errors, accounts) {
if( errors ) {
console.error('An error occurs during accounts deletion for user ' + user_id, errors);
return;
}
if( !accounts ) {
console.log('No accounts');
return;
}
for( var index in accounts ) {
eventEmitter.emit(ENTRIES_DELETE_BY_ACCOUNT_EVT, accounts[index]);
}
});
});
eventEmitter.on(ENTRIES_DELETE_BY_ACCOUNT_EVT, AccountController.delete_account);
module.exports = {
events : {
ACCOUNTS_DELETE_BY_USER_ID_EVT: ACCOUNTS_DELETE_BY_USER_ID_EVT,
ENTRIES_DELETE_BY_ACCOUNT_EVT: ENTRIES_DELETE_BY_ACCOUNT_EVT
},
eventEmitter: eventEmitter
}

18
app/helpers/handler.js Normal file
View File

@@ -0,0 +1,18 @@
module.exports = {
errorHandler : function(errors, status, response) {
var message = []
if( errors.errors) {
Object.keys(errors.errors).forEach(function (field) {
var error = errors.errors[field];
message.push({
field: error.path,
rule: error.kind,
message: error.message
});
});
return response.status(status).json(message);
} else {
return response.status(status).end();
}
}
}

25
app/models/account.js Normal file
View File

@@ -0,0 +1,25 @@
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
ObjectId = Schema.Types.ObjectId;
var CategorySchema = new Schema({
label: {type: String, required:true},
key: {type: String, required: true, index: {unique: false} },
sub_categories: [{
label: {type: String, required:true},
key: {type: String, required: true, index: {unique: false} },
}]
});
var AccountSchema = new Schema({
name: {type: String, required: true},
reference: {type: String, required: false},
categories: {type: [CategorySchema], required: true},
user_id: {type: ObjectId, ref: 'User', required: true},
created_at: {type: Date, default: Date.now}
});
var Account = mongoose.model('Account', AccountSchema);
var Category = mongoose.model('Category', CategorySchema);
module.exports = Account;

1514
app/models/categories.js Normal file

File diff suppressed because it is too large Load Diff

20
app/models/entry.js Normal file
View File

@@ -0,0 +1,20 @@
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
ObjectId = Schema.Types.ObjectId;
var DEBIT = 'D',
BILL = 'B';
var EntrySchema = new Schema({
account_id: {type: ObjectId, ref: 'Category', required: true},
category: {type: ObjectId, ref: 'Account', required: false},
sub_category: {type: ObjectId, required: false},
label: {type: String, required:false},
type: {type: String, required: true},
amount: {type: Number, required: true},
date: {type: Date, required: true},
created_at: {type: Date, default: Date.now}
});
var Entry = mongoose.model('Entry', EntrySchema);
module.exports = Entry;

87
app/models/user.js Normal file
View File

@@ -0,0 +1,87 @@
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
bcrypt = require('bcrypt'),
SALT_WORK_FACTOR = 10;
var UserSchema = new Schema({
username: { type: String, required: true, index: { unique: true } },
password: { type: String, required: true },
language: {type: String, required: true, default: 'en'},
created_at: {type: Date, 'default': Date.now}
});
UserSchema.statics.getAuthenticated = function(username, password, callback) {
this.findOne({ username: username }, function(error, user) {
if (error) {
console.error(error);
return callback(error);
}
// make sure the user exists
if (!user) {
return callback(null, null, 404);
}
user.comparePassword(password, function(error, isMatch) {
if (isMatch) {
return callback(null, user);
}
return callback(null, null, 401);
});
});
};
UserSchema.pre('save', function(next) {
var user = this;
// only hash the password if it has been modified (or is new)
if (!user.isModified('password')) {
return next();
}
// generate a salt
bcrypt.genSalt(SALT_WORK_FACTOR, function(error, salt) {
if (error) {
console.log(error);
return next(error);
}
// hash the password using our new salt
bcrypt.hash(user.password, salt, function(error, hash) {
if (error) {
return next(error);
}
// override the cleartext password with the hashed one
user.password = hash;
next();
});
});
});
UserSchema.methods.comparePassword = function(candidatePassword, callback) {
bcrypt.compare(candidatePassword, this.password, function(error, isMatch) {
if (error) {
return callback(error);
}
callback(null, isMatch);
});
};
var User = mongoose.model('User', UserSchema);
User.schema.path('username').validate(function (username) {
return username.length;
}, 'Username cannot be blank');
User.schema.path('password').validate(function(password) {
return password.length;
}, 'Password cannot be blank');
User.schema.path('language').validate(function(language) {
return /en|fr/i.test(language);
}, 'Unknown language ("en" or "fr" only)')
module.exports = User;

16
app/routes.js Normal file
View File

@@ -0,0 +1,16 @@
var fs = require('fs');
module.exports = function(app) {
var routes_path = __dirname + '/routes'
fs.readdirSync(routes_path).forEach(function (file) {
if (~file.indexOf('.js')) {
var route = require(routes_path + '/' + file);
route(app);
}
})
app.get('*', function(req, res) {
res.sendfile('./public/views/index.html');
});
};

22
app/routes/accounts.js Normal file
View File

@@ -0,0 +1,22 @@
var passport = require('../security/passport'),
AccountController = require('../controllers/accounts');
module.exports = function(app) {
app.post('/api/accounts', passport.jwt, AccountController.create);
app.delete('/api/accounts/:account_id', passport.jwt, AccountController.delete);
app.get('/api/accounts/:account_id', passport.jwt, AccountController.get);
app.put('/api/accounts/:account_id', passport.jwt, AccountController.modify);
app.post('/api/accounts/:account_id/entries', passport.jwt, AccountController.add_entry);
app.put('/api/accounts/:account_id/entries/:entry_id', passport.jwt, AccountController.modify_entry);
app.delete('/api/accounts/:account_id/entries/:entry_id', passport.jwt, AccountController.delete_entry);
app.get('/api/accounts/:account_id/entries', passport.jwt, AccountController.list_entries);
};

13
app/routes/users.js Normal file
View File

@@ -0,0 +1,13 @@
var passport = require('../security/passport'),
UserController = require('../controllers/users');
module.exports = function(app) {
app.post('/api/users/login', passport.local, UserController.login);
app.delete('/api/users/login', UserController.logout);
app.post('/api/users', UserController.subscribe);
app.delete('/api/users', passport.jwt, UserController.unsubscribe);
};

40
app/security/passport.js Normal file
View File

@@ -0,0 +1,40 @@
var mongoose = require('mongoose'),
User = mongoose.model('User'),
passport = require('passport'),
LocalStrategy = require('passport-local'),
JwtStrategy = require('passport-jwt').Strategy,
security = require('../../config/security');
passport.use( new LocalStrategy(
function(username, password, done) {
User.getAuthenticated(username, password, function(error, user, errorStatus) {
if( error ) {
return done(error, null);
}
if( !user ) {
return done(null, false);
}
return done(null, user);
});
}
));
passport.use( new JwtStrategy(security.jwt, function(jwt_payload, done) {
User.findById(jwt_payload.user_id, function(error, user) {
if( error ) {
return done(error, null);
}
if( user ) {
return done(null, user);
} else {
return done(null, false);
}
});
}));
module.exports = {
jwt: passport.authenticate('jwt', {session: false}),
local: passport.authenticate('local', {session: false})
}