mirror of
https://github.com/Febbweiss/CloudBudget.git
synced 2026-03-04 22:35:38 +00:00
Feature: First version of the REST API done
This commit is contained in:
227
app/controllers/accounts.js
Normal file
227
app/controllers/accounts.js
Normal 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
79
app/controllers/users.js
Normal 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
35
app/events/listeners.js
Normal 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
18
app/helpers/handler.js
Normal 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
25
app/models/account.js
Normal 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
1514
app/models/categories.js
Normal file
File diff suppressed because it is too large
Load Diff
20
app/models/entry.js
Normal file
20
app/models/entry.js
Normal 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
87
app/models/user.js
Normal 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
16
app/routes.js
Normal 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
22
app/routes/accounts.js
Normal 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
13
app/routes/users.js
Normal 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
40
app/security/passport.js
Normal 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})
|
||||
}
|
||||
Reference in New Issue
Block a user