Coverage

72%
994
723
271

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/api/controller.js

82%
35
29
6
LineHitsSource
11var _ = require('lodash');
21var r = require('rethinkdb');
31var op = require('objectpath');
41var qs = require('qs');
51var li = require('li');
6
71var filters = ['gt', 'lt', 'gte', 'le', 'ge', 'eq', 'not', 'contains'];
8
91module.exports = {
10 sort: function(req, query) {
1110 if(typeof req.query.sort !== 'string')
1210 return query
13
140 var pointer = r.row;
150 op.parse(req.query.sort).forEach(function(key){
160 pointer = pointer(key);
17 });
180 return req.query.direction === 'desc' ? query.orderBy(r.desc(pointer)) : query.orderBy(pointer);
19 },
20 filter: function(req, query) {
2110 if(!req.query.filter)
229 return query;
23
241 _.each(req.query.filter, function(expr, path) {
25
26 // make sure a method is specified
271 if(typeof expr !== 'object')
280 return;
29
301 _.each(expr, function(value, method){
31 // ignore non-whitelisted methods
321 if(filters.indexOf(method) === -1)
330 return;
34
35 // try to parse the value as json
362 try { value = JSON.parse(value); } catch(e){}
37
38 // apply the filter methods
391 var pointer = r.row;
401 op.parse(path).forEach(function(key){
411 pointer = pointer(key);
42 });
43
44 // apply the filter
451 query = query.filter(pointer[method](value));
46 });
47 });
48
491 return query;
50 },
51 paginate: function(req, res, page, per_page, total) {
5210 var pages = {
53 first: 1,
54 last: Math.ceil(total / per_page)
55 };
56
5710 if(page > 1)
581 pages.prev = page - 1;
59
6010 if(page < pages.last)
611 pages.next = page + 1;
62
6310 res.set('Pages', JSON.stringify(pages));
6410 res.set('Link', li.stringify(_.mapValues(pages, function(value){
6522 return req.path + '?' + qs.stringify(_.extend({}, req.query, {page: value, per_page: per_page}));
66 })));
67 }
68}

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/api/controllers/cycles.js

73%
146
108
38
LineHitsSource
11'use strict';
2
31var _ = require('lodash');
41var r = require('rethinkdb');
5
61var controller = require('../controller.js');
7
81function prepare(o){
97 if(_.isArray(o))
101 return _.map(o, prepare);
11
126 return _.assign(o, {href: '/api/cycles/' + o.id});
13}
14
151function sanitize(o){
164 delete o.href;
174 delete o.id;
18}
19
201module.exports = function(config, resources) {
211 return {
22 create: function(req, res, next) {
233 if(!req.user)
240 return next(401);
25
26 // only admin can create a cycle
273 if(!req.user.admin)
281 return next(403);
29
30 // sanitize the input
312 sanitize(req.body);
32
33 // validate request against schema
342 var err = resources.validator.validate('cycle', req.body, {useDefault: true});
352 if(err)
361 return next({code: 400, message: err});
37
38 // set timestamps
391 req.body.created = req.body.updated = r.now();
40
41 // transform dates
421 if(typeof req.body.open === 'string')
430 req.body.open = new Date(req.body.open);
44
451 if(typeof req.body.close === 'string')
460 req.body.close = new Date(req.body.close);
47
481 resources.db.acquire(function(err, conn) {
491 if(err)
500 return next(err);
51
52 // insert the cycle
531 r.table('cycles').insert(req.body, {returnChanges: true}).run(conn, function(err, result){
541 resources.db.release(conn);
55
561 if(err)
570 return next(err);
58
591 var cycle = result.changes[0].new_val;
60
611 return res.status(201).send(cycle);
62 });
63 });
64 },
65 list: function(req, res, next) {
661 if(!req.user)
670 return next(401);
68
69 // get the cycles from the DB
701 var query = r.table('cycles');
71
72 // hide drafts fron non-admin users
731 if(!req.user.admin)
740 query = query.filter(r.row('status').eq('draft').not());
75
76 // only include open cycles
771 if(req.query.open && req.query.open !== 'false')
780 query = query.filter(r.row('open').eq(true).or(r.row('open').ne(false).and(r.row('open').lt(r.now()))).and(r.row('close').eq(false).or(r.row('open').gt(r.now()))));
79
80 // filter
811 query = controller.filter(req, query);
82
83 // search
841 if(req.query.search && req.query.search !== '')
850 query = query.filter(r.row('title').downcase().match(req.query.search.toLowerCase()));
86
87 // sort
881 query = controller.sort(req, query);
89
901 resources.db.acquire(function(err, conn) {
911 if(err)
920 return next(err);
93
941 var per_page = parseInt(req.query.per_page, 10) || 50;
951 var page = parseInt(req.query.page, 10) || 1;
96
971 r.expr({
98 // get the total results count
99 total: query.count(),
100 // get the results for this page
101 cycles: query.skip(per_page * (page - 1)).limit(per_page).coerceTo('array')
102 }).run(conn, function(err, results){
1031 resources.db.release(conn);
104
1051 if(err)
1060 return next(err);
107
1081 var cycles = results.cycles;
109
110 // set pagination headers
1111 controller.paginate(req, res, page, per_page, results.total);
112
1131 res.send(prepare(cycles));
114 });
115 });
116 },
117 show: function(req, res, next) {
1181 if(!req.user)
1190 return next(401);
120
1211 var id;
122
1231 if(req.params.cycle) // get by ID
1241 id = req.params.cycle;
1250 else if(req.params.project) // get by project
1260 id = r.table('projects').get(req.params.project)('cycle_id');
127 else
1280 return next(400);
129
1301 return resources.db.acquire(function(err, conn) {
1311 if(err)
1320 return next(err);
133
134 // get cycles from the DB
1351 r.table('cycles').get(id).run(conn, function(err, cycle){
1361 resources.db.release(conn);
137
1381 if(err)
1390 return next(err);
140
141 // hide draft cycles from non-admin users
1421 if(!cycle || (!req.user.admin && cycle.status === 'draft'))
1430 return next(404);
144
1451 return res.send(prepare(cycle));
146 });
147 });
148 },
149 update: function(req, res, next) {
1502 if(!req.user)
1510 return next(401);
152
1532 if(!req.user.admin)
1541 return next(403, 'Only administrators may update a cycle.');
155
156 // sanitize the input
1571 sanitize(req.body);
158
159 // validate request against schema
1601 var err = resources.validator.validate('cycle', req.body, {checkRequired: false});
1611 if(err)
1620 return next({code: 400, message: err});
163
164 // set timestamps
1651 delete req.body.created;
1661 req.body.updated = r.now();
167
168 // transform dates
1691 if(typeof req.body.open === 'string')
1700 req.body.open = new Date(req.body.open);
171
1721 if(typeof req.body.close === 'string')
1730 req.body.close = new Date(req.body.close);
174
175 // remove indexed objects with null values
1761 var without = {};
1771 _.each(['events', 'flow', 'roles', 'statuses', 'users'], function(index){
1785 _.each(req.body[index], function(value, id){
1790 if(value !== null)
1800 return;
181
1820 without[index] = without[index] || {};
1830 without[index][id] = true;
1840 delete req.body[index][id];
185 });
186 });
187
1881 resources.db.acquire(function(err, conn) {
1891 if(err)
1900 return next(err);
191
192 // get cycles from the DB
1931 r.table('cycles').get(req.params.cycle).replace(r.row.without(without).merge(req.body), {returnChanges: true}).run(conn, function(err, result){
1941 resources.db.release(conn);
195
1961 if(err)
1970 return next(err);
198
1991 var cycle = result.changes[0].new_val;
200
2011 return res.send(prepare(cycle));
202 });
203 });
204 },
205 replace: function(req, res, next) {
2062 if(!req.user)
2070 return next(401);
208
2092 if(!req.user.admin)
2101 return next(403, 'Only administrators may replace a cycle.');
211
212 // sanitize the input
2131 sanitize(req.body);
214
215 // validate request against schema
2161 var err = resources.validator.validate('cycle', req.body, {useDefault: true});
2171 if(err)
2180 return next({code: 400, message: err});
219
220 // inject ID
2211 req.body.id = req.params.cycle;
222
223 // set timestamps
2241 req.body.created = r.row('created');
2251 req.body.updated = r.now();
226
227 // transform dates
2281 if(typeof req.body.open === 'string')
2290 req.body.open = new Date(req.body.open);
230
2311 if(typeof req.body.close === 'string')
2320 req.body.close = new Date(req.body.close);
233
2341 resources.db.acquire(function(err, conn) {
2351 if(err)
2360 return next(err);
237
238 // get cycles from the DB
2391 r.table('cycles').get(req.params.cycle).replace(req.body, {returnChanges: true}).run(conn, function(err, result){
2401 resources.db.release(conn);
241
2421 if(err)
2430 return next(err);
244
2451 var cycle = result.changes[0].new_val;
246
2471 return res.send(prepare(cycle));
248 });
249 });
250 },
251 destroy: function(req, res, next) {
2523 if(!req.user)
2530 return next(401);
254
2553 if(!req.user.admin)
2561 return next(403, 'Only administrators may delete a cycle.');
257
2582 resources.db.acquire(function(err, conn) {
2592 if(err)
2600 return next(err);
261
2622 return r.branch(r.table('projects').filter({cycle_id: req.params.cycle}).limit(1).count().eq(0),
263 r.table('cycles').get(req.params.cycle).delete({returnChanges: true}),
264 null
265 ).run(conn, function(err, result){
2662 resources.db.release(conn);
267
2682 if(err)
2690 return next(err);
270
2712 if(result === null)
2721 return next(400, 'The cycle cannot be destroyed because projects depend on it.');
273
2741 var cycle = result.changes[0].old_val;
275
2761 return res.send(prepare(cycle));
277 });
278 });
279 }
280 };
281}
282

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/api/controllers/files.js

75%
136
103
33
LineHitsSource
11'use strict';
2
31var url = require('url');
41var _ = require('lodash');
51var r = require('rethinkdb');
61var async = require('async');
71var fs = require('fs');
8
91var controller = require('../controller.js');
10
111function sanitize(o){
123 delete o.href;
133 delete o.id;
14}
15
161module.exports = function(config, resources) {
171 return {
18 create: function(req, res, next) {
191 if(!req.user)
200 return next(401);
21
22 // sanitize the input
231 sanitize(req.body);
24
25 // make sure we have files to upload
261 if(!req.files || Object.keys(req.files).length === 0)
270 return next(400);
28
291 resources.db.acquire(function(err, conn) {
301 if(err)
310 return next(err);
32
33 // insert the file records into the db
341 async.map(_.map(req.files),
35 function(file, callback){
36 // read the file from the uploads directory
371 fs.readFile(file.path, function(err, data) {
381 if(err)
390 return callback(err);
40
411 return callback(null, {
42 user_id: req.user.id,
43 name: file.originalname,
44 encoding: file.encoding,
45 mimetype: file.mimetype,
46 data: data,
47 extension: file.extension,
48 size: file.size,
49 lock: false,
50 created: r.now(),
51 updated: r.now()
52 });
53 });
54 },
55 function(err, inserts){
561 if(err)
570 return next(err);
58
591 r.table('files').insert(inserts, {returnChanges: true})('changes').map(function(row){
601 return row('new_val').without('data');
61 }).run(conn, function(err, results){
621 resources.db.release(conn);
63
641 if(err)
650 return next(err);
66
67 // TODO: unlink the uploaded files
68
691 res.status(201).send(results);
70 });
71 }
72 );
73 });
74 },
75 list: function(req, res, next) {
763 if(!req.user)
770 return next(401);
78
79 // get the file records from the DB
803 var query = r.table('files').without('data');
81
82 // hide others' files from non-admin users
833 if(!req.user.admin)
842 query = query.filter({user_id: req.user.id});
85
86 // filter by user
873 if(req.params.user)
880 query = query.filter({user_id: req.params.user});
89
90 // filter
913 query = controller.filter(req, query);
92
93 // search
943 if(req.query.search && req.query.search !== '')
950 query = query.filter(r.row('title').downcase().match(req.query.search.toLowerCase()));
96
97 // sort
983 query = controller.sort(req, query);
99
1003 resources.db.acquire(function(err, conn) {
1013 if(err)
1020 return next(err);
103
1043 var per_page = parseInt(req.query.per_page, 10) || 50;
1053 var page = parseInt(req.query.page, 10) || 1;
106
1073 r.expr({
108 // get the total results count
109 total: query.count(),
110 // get the results for this page
111 files: query.skip(per_page * (page - 1)).limit(per_page).coerceTo('array')
112 }).run(conn, function(err, results){
1133 resources.db.release(conn);
114
1153 if(err)
1160 return next(err);
117
1183 var files = results.files;
119
120 // set pagination headers
1213 controller.paginate(req, res, page, per_page, results.total);
122
1233 res.send(files);
124 });
125 });
126 },
127 show: function(req, res, next) {
128 // if(!req.user)
129 // return next(401);
130
1311 resources.db.acquire(function(err, conn) {
1321 if(err)
1330 return next(err);
134
135 // get files from the DB
1361 r.table('files').get(req.params.file).run(conn, function(err, file){
1371 resources.db.release(conn);
138
1391 if(err)
1400 return next(err);
141
1421 if(!file)
1430 return next(404);
144
145 // hide others' files from non-admin users
146 // if(!file || (!req.user.admin && files.user_id !== req.user.id))
147 // return next(404);
148
1491 res.set('Content-Type', file.mimetype);
1501 res.set('Content-Length', file.size);
1511 res.set('Content-Disposition', 'attachment; filename="' + file.name + '"');
1521 return res.send(file.data);
153 });
154 });
155 },
156 update: function(req, res, next) {
1571 if(!req.user)
1580 return next(401);
159
160 // sanitize the input
1611 sanitize(req.body);
162
163 // validate request against schema
1641 var err = resources.validator.validate('file', req.body, {checkRequired: false});
1651 if(err)
1660 return next({code: 400, message: err});
167
168 // make sure data is not replaced
1691 delete req.body.data;
170
171 // set timestamps
1721 delete req.body.created;
1731 req.body.updated = r.now();
174
1751 resources.db.acquire(function(err, conn) {
1761 if(err)
1770 return next(err);
178
179 // get files from the DB
1801 r.table('files').get(req.params.file).update(req.body, {returnChanges: true})('changes').nth(0)('new_val').without('data').run(conn, function(err, file){
1811 resources.db.release(conn);
182
1831 if(err)
1840 return next(err);
185
1861 return res.send(file);
187 });
188 });
189 },
190 replace: function(req, res, next) {
1912 if(!req.user)
1920 return next(401);
193
1942 if(!req.user.admin)
1951 return next(403, 'Only administrators may replace a file.');
196
197 // sanitize the input
1981 sanitize(req.body);
199
200 // validate request against schema
2011 var err = resources.validator.validate('file', req.body, {useDefault: true});
2021 if(err)
2030 return next({code: 400, message: err});
204
205 // inject ID
2061 req.body.id = req.params.file;
207
208 // make sure data is not replaced
2091 req.body.data = r.row('data');
210
211 // set timestamps
2121 req.body.created = r.row('created');
2131 req.body.updated = r.now();
214
215 // transform dates
2161 if(typeof req.body.open === 'string')
2170 req.body.open = new Date(req.body.open);
218
2191 if(typeof req.body.close === 'string')
2200 req.body.close = new Date(req.body.close);
221
2221 resources.db.acquire(function(err, conn) {
2231 if(err)
2240 return next(err);
225
226 // get files from the DB
2271 r.table('files').get(req.params.file).replace(req.body, {returnChanges: true})('changes').nth(0)('new_val').without('data').run(conn, function(err, file){
2281 resources.db.release(conn);
229
2301 if(err)
2310 return next(err);
232
2331 return res.send(file);
234 });
235 });
236 },
237 destroy: function(req, res, next) {
2382 if(!req.user)
2390 return next(401);
240
241 // if(!req.user.admin)
242 // return next(403, 'Only administrators may delete a file.');
243
244 // TODO: check that there are no projects that depend on this file
245
2462 resources.db.acquire(function(err, conn) {
2472 if(err)
2480 return next(err);
249
250 // get files from the DB
2512 r.table('files').get(req.params.file).without('data').run(conn, function(err, file){
2522 if(err) {
2530 resources.db.release(conn);
2540 return next(err);
255 }
256
2572 if(!file) {
2580 resources.db.release(conn);
2590 return next(404);
260 }
261
2622 if(file.user_id === req.user.id && !req.user.admin) {
2631 resources.db.release(conn);
2641 return next(403);
265 }
266
2671 if(file.lock) {
2680 resources.db.release(conn);
2690 return next(423);
270 }
271
272 // destroy the db entry
2731 r.table('files').get(req.params.file).delete().run(conn, function(err, result){
2741 resources.db.release(conn);
275
2761 if(err)
2770 return next(err);
278
2791 res.set('Content-Type', file.mimetype);
2801 res.set('Content-Length', file.size);
2811 res.set('Content-Disposition', 'attachment; filename="' + file.name + '"');
2821 return res.send(file);
283 });
284 });
285 });
286 }
287 };
288}
289

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/api/controllers/projects.js

62%
223
140
83
LineHitsSource
11'use strict';
2
31var url = require('url');
41var _ = require('lodash');
51var r = require('rethinkdb');
6
71var controller = require('../controller.js');
8
91module.exports = function(config, resources) {
10
111 function sanitize(o) {
12 // strip locks
1314 _.each(o.flow, function(stage){ delete stage.lock; })
143 delete o.href;
153 delete o.id;
16 }
17
181 function getRole(user, project, cycle) {
194 if(project.users[user.id] && project.users[user.id].role && cycle.roles[project.users[user.id].role])
201 return cycle.roles[project.users[user.id].role];
21
223 if(cycle.users[user.id] && cycle.users[user.id].role && cycle.roles[cycle.users[user.id].role])
233 return cycle.roles[cycle.users[user.id].role];
24
250 return null;
26 }
27
281 function buildEvents(project, cycle) {
293 var events = {};
30
313 _.each(cycle.events, function(event, id){
3227 var value = event.conditions.some(function(conditions){
3327 return conditions.every(function(condition){
3430 var tester = resources.testers[condition.name]
35
3630 if(!tester)
370 return false;
38
3930 return tester(condition.options, project);
40 });
41 });
42
43 // nothing's changed
4427 if(project.events[id] && project.events[id][0] && project.events[id][0].value === value)
4518 return;
46
47 // TODO: process listeners
48
49 // prepend the event to an existing list
509 if(project.events[id])
510 return events[id] = r.row('events')(id).prepend({
52 value: value,
53 date: r.now()
54 });
55
56 // add the first event
579 events[id] = [{
58 value: value,
59 date: r.now()
60 }];
61 });
62
633 return events;
64 }
65
661 function applyLocks(project, cycle) {
674 var locks = {};
684 _.each(cycle.flow, function(stage, id){
69
70 // open
7144 if(stage.lock.open.some(function(event){
7248 if(!project.events[event] || !project.events[event][0] || !project.events[event][0].value)
7331 return locks[id] = cycle.events[event] ? cycle.events[event].messages[0] : 'The event "'+event+'" has not yet opened this stage.';
7431 })) return;
75
76 // close
7713 if(stage.lock.close.some(function(event){
7839 if(project.events[event] && project.events[event][0] && project.events[event][0].value)
7913 return locks[id] = cycle.events[event] ? cycle.events[event].messages[1] : 'The event "'+event+'" has closed this stage.';
8013 })) return;
81
820 locks[id] = null;
83 });
84
854 return locks;
86 }
87
881 function Project(role, project, cycle) {
894 if(!project.flow)
902 project.flow = {};
91
92 // apply locks
934 _.each(applyLocks(project, cycle), function(lock, id) {
9444 project.flow[id] = project.flow[id] || {id: id, data: {}, status: 'none'};
9544 project.flow[id].lock = lock;
96 });
97
98 // TODO: apply role restrictions
99
100 // hide users
101
102 // hide flow stages
103
104 // hide invitations
105
1064 return project;
107 }
108
1091 return {
110 create: function(req, res, next) {
1111 if(!req.user)
1120 return next(401);
113
114 // sanitize request
1151 sanitize(req.body);
116
117 // validate request against schema
1181 var err = resources.validator.validate('project', req.body, {useDefault: true});
1191 if(err)
1200 return next({code: 400, message: err});
121
122 // set timestamps
1231 req.body.created = req.body.updated = r.now();
124
125 // transform dates
1261 if(typeof req.body.open === 'string')
1270 req.body.open = new Date(req.body.open);
128
1291 if(typeof req.body.close === 'string')
1300 req.body.close = new Date(req.body.close);
131
1321 resources.db.acquire(function(err, conn) {
1331 if(err)
1340 return next(err);
135
1361 r.table('cycles').get(req.body.cycle_id).run(conn, function(err, cycle){
1371 if(err){
1380 resources.db.release(conn);
1390 return next(err);
140 }
141
1421 if(!cycle){
1430 resources.db.release(conn);
1440 return next(400, 'No such cycle.');
145 }
146
147 // restrictions for non-admin users
1481 if(!req.user.admin){
149
150 // TODO: verify that project is open
151
152 // add current user with default role
1531 req.body.users[req.user.id] = {
154 id: req.user.id,
155 role: cycle.defaults.role
156 }
157
158 // TODO: verify that any other users are of allowable roles
159
160 // apply default status
1611 req.body.status = cycle.defaults.status;
162 }
163
164 // process the events
1651 req.body.events = buildEvents(req.body, cycle)
166
167 // insert the project
1681 r.table('projects').insert(req.body, {returnChanges: true}).run(conn, function(err, result){
1691 resources.db.release(conn);
170
1711 if(err)
1720 return next(err);
173
1741 var project = result.changes[0].new_val;
175
1761 return res.status(201).send(project);
177 });
178 });
179 });
180 },
181 list: function(req, res, next) {
1820 if(!req.user)
1830 return next(401);
184
1850 resources.db.acquire(function(err, conn) {
1860 if(err)
1870 return next(err);
188
1890 var query = r.table('projects');
1900 var per_page = parseInt(req.query.per_page, 10) || 50;
1910 var page = parseInt(req.query.page, 10) || 1;
192
193 // restrict to user
194 // TODO: make sure to only return users visible to the current user
1950 if(req.params.user)
1960 return r.table('cycles').hasFields({users: req.params.user})('id').run(conn, function(err, cursor){
1970 cursor.toArray(function(err, cycleIds){
1980 query = query.filter(r.expr(cycleIds).contains(r.row('cycle_id')).or(r.row('users').hasFields(req.params.user)));
1990 next();
200 });
201 });
202
203 // all projects
2040 return next();
205
2060 function next(){
207
208 // filter
2090 query = controller.filter(req, query);
210
211 // search
2120 if(req.query.search && req.query.search !== '')
2130 query = query.filter(r.row('title').downcase().match(req.query.search.toLowerCase()));
214
215 // sort
2160 query = controller.sort(req, query);
217
2180 r.expr({
219 // get the total results count
220 total: query.count(),
221 // get the results for this page
222 projects: query.skip(per_page * (page - 1)).limit(per_page).coerceTo('array')
223 }).run(conn, function(err, results){
2240 resources.db.release(conn);
225
2260 if(err)
2270 return next(err);
228
2290 var projects = results.projects;
230
231 // set pagination headers
2320 controller.paginate(req, res, page, per_page, results.total);
233
2340 res.send(projects);
235 });
236 }
237 });
238 },
239 show: function(req, res, next) {
2402 if(!req.user)
2410 return next(401);
242
243 // get the project & cycle
2442 resources.db.acquire(function(err, conn) {
2452 if(err)
2460 return next(err);
247
248 // get cycles from the DB
2492 var query = r.table('projects').get(req.params.project);
2502 r.expr({
251 project: query,
252 cycle: r.branch(query, r.table('cycles').get(query('cycle_id')), null)
253 }).run(conn, function(err, result){
2542 resources.db.release(conn);
255
2562 if(err)
2570 return next(err);
258
2592 var project = result.project;
2602 var cycle = result.cycle;
261
2622 if(!project || !cycle)
2630 return next(404);
264
265 // restrict to admin and assigned users
2662 var role = getRole(req.user, project, cycle);
2672 if(!role && !req.user.admin)
2680 return next(403);
269
2702 return res.send(Project(role, project, cycle));
271 });
272 });
273
274 },
275 update: function(req, res, next) {
2761 if(!req.user)
2770 return next(401);
278
279 // sanitize request
2801 sanitize(req.body);
281
282 // validate request against schema
2831 var err = resources.validator.validate('project', req.body, {checkRequired: false});
2841 if(err)
2850 return next({code: 400, message: err});
286
2871 resources.db.acquire(function(err, conn) {
2881 if(err)
2890 return next(err);
290
2911 var query = r.table('projects').get(req.params.project);
2921 r.expr({
293 project: query,
294 cycle: r.branch(query, r.table('cycles').get(query('cycle_id')), null)
295 }).run(conn, function(err, result){
2961 if(err){
2970 resources.db.release(conn);
2980 return next(err);
299 }
300
3011 var project = result.project;
3021 var cycle = result.cycle;
303
3041 if(!project || !cycle)
3050 return next(404);
306
307 // restrict to admin and assigned users
3081 var role = getRole(req.user, project, cycle);
3091 if(!role && !req.user.admin)
3100 return next(403);
311
312 // set timestamps
3131 delete req.body.created;
3141 req.body.updated = r.now();
315
316 // remove indexed objects with null values
3171 var without = {};
3181 _.each(['users'], function(index){
3191 _.each(req.body[index], function(value, id){
3200 if(value !== null)
3210 return;
322
3230 without[index] = without[index] || {};
3240 without[index][id] = true;
3250 delete req.body[index][id];
326 });
327 });
328
329 // TODO: process components individually
330
331 // TODO: prevent from updating certain fields
332
333 // update the record
3341 r.table('projects').get(req.params.project).replace(r.row.without(without).merge(req.body), {returnChanges: true}).run(conn, function(err, result){
3351 if(err){
3360 resources.db.release(conn);
3370 return next(err);
338 }
339
3401 var project = result.changes[0].new_val;
341
342 // process the events
3431 var events = buildEvents(project, cycle);
344
345 // no events to update
3461 if(!Object.keys(events).length){
3471 resources.db.release(conn);
3481 return res.send(Project(role, project, cycle));
349 }
350
351 // update the events
3520 r.table('projects').get(req.params.project).update({events: events}, {returnChanges: true}).run(conn, function(err, result){
3530 resources.db.release(conn);
354
3550 if(err)
3560 return next(err);
357
3580 var project = result.new_val;
359
3600 return res.send(Project(role, project, cycle));
361 });
362 });
363 });
364 });
365
366 // // TODO: validate against schema
367
368 // // TODO: get and process the current cycle and project
369 // var cycle = {};
370 // var project = {};
371
372 // // restrict to admin and assigned users
373 // var role = getRole(req.user, project, cycle);
374 // if(!role && !req.user.admin)
375 // return next(403);
376
377 // // if we aren't processing a component
378 // if(!req.body.flow)
379 // return next();
380
381 // // build the list of component processes to run
382 // var tasks = {};
383 // _.each(req.body.flow, function(data, id){
384 // if(!resources.components[cycle.flow[id].component.name])
385 // throw new Error('no such component!');
386
387 // tasks[id] = async.apply(
388 // resources.components[cycle.flow[id].component.name].update,
389 // data,
390 // role,
391 // cycle.flow[id].component.options,
392 // project
393 // )
394 // });
395
396 // // components are in charge of making
397 // // the db calls to update their stages,
398 // // so we're done with the flow
399 // delete req.body.flow;
400
401
402 // async.parallel(tasks, function(err, flow){
403 // if(err)
404 // return next(err);
405
406 // // TODO: what to do w/ result?
407
408 // return next()
409 // });
410
411
412 // function next(){
413 // if(!req.user.admin)
414 // return next();
415
416 // r.table('projects').get(req.params.project).update(req.body)
417
418 // function next(){
419
420 // // get and build the project
421
422 // res.status(200).send(Project(role, project, cycle));
423 // }
424 // }
425 },
426 replace: function(req, res, next) {
4272 if(!req.user)
4280 return next(401);
429
4302 if(!req.user.admin)
4311 return next(403, 'Only administrators may replace a project.');
432
433 // sanitize request
4341 sanitize(req.body);
435
436 // validate request against schema
4371 var err = resources.validator.validate('project', req.body, {useDefault: true});
4381 if(err)
4390 return next({code: 400, message: err});
440
441 // inject ID
4421 req.body.id = req.params.project;
443
444 // set timestamps
4451 req.body.created = r.row('created');
4461 req.body.updated = r.now();
447
4481 resources.db.acquire(function(err, conn) {
4491 if(err)
4500 return next(err);
451
4521 r.table('cycles').get(req.body.cycle_id).run(conn, function(err, cycle){
4531 if(err){
4540 resources.db.release(conn);
4550 return next(err);
456 }
457
4581 if(!cycle){
4590 resources.db.release(conn);
4600 return next(400, 'No such cycle.');
461 }
462
463 // replace the record in the db
4641 r.table('projects').get(req.params.project).replace(req.body, {returnChanges: true}).run(conn, function(err, result){
4651 if(err){
4660 resources.db.release(conn);
4670 return next(err);
468 }
469
4701 var project = result.changes[0].new_val;
4711 var role = getRole(req.user, project, cycle);
472
473 // process the events
4741 var events = buildEvents(project, cycle);
475
476 // no events to update
4771 if(!Object.keys(events).length){
4781 resources.db.release(conn);
4791 return res.send(Project(role, project, cycle));
480 }
481
482 // update the events
4830 r.table('projects').get(req.params.project).update({events: events}, {returnChanges: true}).run(conn, function(err, result){
4840 resources.db.release(conn);
485
4860 if(err)
4870 return next(err);
488
4890 var project = result.changes[0].new_val;
490
4910 return res.send(Project(role, project, cycle));
492 });
493 });
494 });
495 });
496 },
497 destroy: function(req, res, next) {
4982 if(!req.user)
4990 return next(401);
500
5012 if(!req.user.admin)
5021 return next(403, 'Only administrators may delete a project.');
503
5041 resources.db.acquire(function(err, conn) {
5051 if(err)
5060 return next(err);
507
5081 return r.table('projects').get(req.params.project).delete({returnChanges: true}).run(conn, function(err, result){
5091 resources.db.release(conn);
510
5111 if(err)
5120 return next(err);
513
5141 var project = result.old_val;
515
5161 return res.send(project);
517 });
518 });
519 }
520 };
521}
522

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/api/controllers/users.js

64%
208
134
74
LineHitsSource
11'use strict';
2
31var _ = require('lodash');
41var r = require('rethinkdb');
51var crypto = require('crypto');
61var passport = require('passport');
71var scrypt = require('scrypt');
81 scrypt.hash.config.keyEncoding = scrypt.verify.config.keyEncoding = 'utf8';
91 scrypt.hash.config.outputEncoding = scrypt.verify.config.outputEncoding = 'base64';
10
111var controller = require('../controller.js');
12
131var blacklist = ['password','recovery_token'];
141var whitelist = ['id', 'email', 'name', 'href', 'admin', 'created','updated'];
15
161function prepare(o, p){
1767 if(_.isArray(o))
1856 return _.map(o, function(o){return prepare(o, p);});
19
2061 return _.assign(
21 (p === true || p === o.id) ? _.omit(o, blacklist) : _.pick(o, whitelist),
22 {href: '/api/users/' + o.id}
23 );
24}
25
261function sanitize(o){
279 delete o.href;
289 delete o.id;
29}
30
311module.exports = function(config, resources) {
321 return {
33 create: function(req, res, next){
346 var email = 'welcome';
35
36 // generate random password if not set
376 req.body.password = req.body.password || crypto.randomBytes(256).toString('base64');
38
39 // validate request against schema
406 var err = resources.validator.validate('user', req.body, {useDefault: true});
416 if(err)
421 return next({code: 400, message: err});
43
445 var privilige = true;
45
46 // sanitize the input
475 sanitize(req.body);
48
495 passport.authenticate('bearer', { session: false }, function(err, user) {
50
51 // only allow admins to create a new admin user
525 if (req.body.admin && (err || !user || !user.admin))
532 return next({code: 403, message: 'You are not authorized to create admin accounts.'});
54
55 // add timestamps
563 req.body.created = req.body.updated = r.now();
57
58 // encrypt the password
593 req.body.password = scrypt.hash(req.body.password, scrypt.params(0.1));
60
613 resources.db.acquire(function(err, conn) {
623 if(err)
630 return next(err);
64
65 // make the email case insensitive
663 req.body.email = req.body.email.toLowerCase();
67
68 // insert the user
693 r.branch(r.table('users').filter({email: req.body.email}).limit(1).count().eq(1),
70 {'$$ERROR$$': {'code': 409, 'message': 'An account already exists with this email'}},
71 r.table('users').insert(req.body, {returnChanges: true})
72 ).run(conn, function(err, result){
733 resources.db.release(conn);
74
753 if(err)
760 return next(err);
77
783 if(result['$$ERROR$$'])
791 return next(result['$$ERROR$$']);
80
812 var user = result.changes[0].new_val;
82
832 return res.status(201).send(prepare(user, privilige));
84 });
85 });
86 })(req, res);
87 },
88 list: function(req, res, next){
896 if(!req.user)
900 return next(401);
91
926 var privilige = req.user.admin || req.user.id;
93
946 resources.db.acquire(function(err, conn) {
956 if(err)
960 return next(err);
97
986 function getUsers(ids){
99
1006 var per_page = parseInt(req.query.per_page, 10) || 50;
1016 var page = parseInt(req.query.page, 10) || 1;
102
103 // if we aren't getting any results
1046 if(ids && ids.length === 0)
1050 return res.send([]);
106
107 // get users from the DB
1086 var query = r.table('users');
109
110 // restrict to ids
1116 if(ids)
1120 query = query.getAll.apply(query, ids);
113
114 // filter
1156 query = controller.filter(req, query);
116
117 // search
1186 if(req.query.search && req.query.search !== '')
1190 query = query.filter(
120 r.row('email').downcase().match(req.query.search.toLowerCase())
121 .or(r.row('name').downcase().match(req.query.search.toLowerCase()))
122 );
123
124 // sort
1256 query = controller.sort(req, query);
126
1276 r.expr({
128 total: query.count(), // get the total results count
129 users: query.skip(per_page * (page - 1)).limit(per_page).coerceTo('array') // get the results for this page
130 }).run(conn, function(err, results){
1316 resources.db.release(conn);
132
1336 if(err)
1340 return next(err);
135
1366 var users = results.users;
137
138 // set pagination headers
1396 controller.paginate(req, res, page, per_page, results.total);
140
1416 res.send(prepare(users));
142 });
143 }
144
145 // get users by cycle
1466 if(req.params.cycle){
1470 return r.table('cycles').get(req.params.cycle).run(conn, function(err, cycle){
1480 if(err){
1490 resources.db.release(conn);
1500 return next(err);
151 }
152
1530 if(!cycle){
1540 resources.db.release(conn);
1550 return next(404);
156 }
157
1580 var role, ids = [];
159
160 // (admin) fetch all users
1610 if(req.user.admin){
1620 Object.keys(cycle.users).forEach(function(id){
1630 ids.push(id);
164 });
165
1660 return getUsers(ids);
167 }
168
169 // (role) fetch all users visible to the current user's role on the cycle
1700 if(cycle.users[req.user.id] && (role = cycle.users[req.user.id].role) && cycle.roles[role]){
1710 _.each(cycle.users, function(data, id){
1720 if(_.indexOf(cycle.roles[role].visible, data.role) !== -1)
1730 ids.push(id);
174 });
175
1760 return getUsers(ids);
177 }
178
179 // empty array for this user
1800 return res.send([]);
181 });
182 }
183
184 // get users by project
1856 if(req.params.project){
1860 return r.table('projects').get(req.params.project).run(conn, function(err, project){
1870 if(err){
1880 resources.db.release(conn);
1890 return next(err);
190 }
191
1920 if(!project){
1930 resources.db.release(conn);
1940 return next(404);
195 }
196
1970 return r.table('cycles').get(project.cycle_id).run(conn, function(err, cycle){
1980 if(err){
1990 resources.db.release(conn);
2000 return next(err);
201 }
202
2030 if(!cycle){
2040 resources.db.release(conn);
2050 return next(404);
206 }
207
2080 var role, ids = [];
209
210 // (admin) fetch all users
2110 if(req.user.admin){
2120 Object.keys(cycle.users).forEach(function(id){
2130 ids.push(id);
214 });
215
2160 Object.keys(project.users).forEach(function(id){
2170 ids.push(id);
218 });
219
2200 return getUsers(_.unique(ids));
221 }
222
223 // (role) fetch all users visible to the current user's role on the project or cycle
2240 if(
225 (project.users[req.user.id] && (role = project.users[req.user.id].role) && cycle.roles[role]) // role on project
226 || (cycle.users[req.user.id] && (role = cycle.users[req.user.id].role) && cycle.roles[role]) // role on cycle
227 ){
2280 _.each(cycle.users, function(data, id){
2290 if(_.indexOf(cycle.roles[role].visible, data.role) !== -1)
2300 ids.push(id);
231 });
232
2330 return getUsers(ids);
234 }
235
236 // empty array for this user
2370 return res.send([]);
238 });
239 });
240 }
241
2426 return getUsers();
243 });
244 },
245 show: function(req, res, next){
2465 if(!req.user)
2470 return next(401);
248
2495 var id;
250
2515 if(req.params.user) // get by ID
2525 id = req.params.user;
2530 else if(req.params.file) // get by file
2540 id = r.table('files').get(req.params.file)('user_id');
255 else
2560 return next(400);
257
2585 resources.db.acquire(function(err, conn) {
2595 if(err)
2600 return next(err);
261
262 // get users from the DB
2635 r.table('users').get(id).run(conn, function(err, user){
2645 resources.db.release(conn);
265
2665 if(err)
2670 return next(err);
268
2695 if(!user)
2700 return next(404);
271
2725 var privilige = req.user.admin || req.user.id === user.id;
273
2745 return res.send(prepare(user, privilige));
275 });
276 });
277 },
278 update: function(req, res, next){
2795 if(!req.user)
2800 return next(401);
281
2825 if(!req.user.admin && req.user.id !== req.params.user)
2831 return next({code: 403, message: 'Non-admin users may only update themselves.'});
284
2854 if(!req.user.admin && req.body.admin)
2861 return next({code: 403, message: 'You are not authorized to give admin status.'});
287
2883 var privilige = true;
289
290 // sanitize the input
2913 sanitize(req.body);
292
293 // validate request against schema
2943 var err = resources.validator.validate('user', req.body, {checkRequired: false});
2953 if(err)
2961 return next({code: 400, message: err});
297
298 // add timestamps
2992 req.body.updated = r.now();
300
301 // encrypt the password
3022 if(req.body.password)
3030 req.body.password = scrypt.hash(req.body.password, scrypt.params(0.1));
304
3052 resources.db.acquire(function(err, conn) {
3062 if(err)
3070 return next(err);
308
309 // build the query
3102 var query = r.table('users').get(req.params.user).update(req.body, {returnChanges: true});
311
312 // verify email is not already taken by a different user
3132 if(typeof req.body.email !== 'undefined')
3140 query = r.do(r.table('users').filter({email: req.body.email}).limit(1).nth(0).default(null), function(conflict){
3150 return r.branch(conflict.eq(null).not().and(conflict('id').eq(req.params.user).not()),
316 {'$$ERROR$$': {'code': 409, 'message': 'An account already exists with this email'}},
317 query
318 );
319 });
320
321 // update the user
3222 query.run(conn, function(err, result){
3232 resources.db.release(conn);
324
3252 if(err)
3260 return next(err);
327
3282 if(result['$$ERROR$$'])
3290 return next(result['$$ERROR$$']);
330
3312 var user = result.changes[0].new_val;
332
3332 return res.send(prepare(user, privilige));
334 });
335
336 });
337 },
338 replace: function(req, res, next){
3392 if(!req.user)
3400 return next(401);
341
3422 if(!req.user.admin)
3431 return next({code: 403, message: 'Only admins can replace a user record.'});
344
3451 var privilige = true;
346
347 // sanitize the input
3481 sanitize(req.body);
349
350 // validate request against schema
3511 var err = resources.validator.validate('user', req.body, {useDefault: true});
3521 if(err)
3530 return next({code: 400, message: err});
354
3551 resources.db.acquire(function(err, conn) {
3561 if(err)
3570 return next(err);
358
359 // build the query
3601 var query = r.table('users').get(req.params.user).replace(function(row){
361
362 // inject ID
3631 req.body.id = req.params.user;
364
365 // add timestamps
3661 req.body.created = row('created');
3671 req.body.updated = r.now();
368
369 // encrypt the password or use old one
3701 req.body.password = req.body.password ? scrypt.hash(req.body.password, scrypt.params(0.1)) : row('password');
371
3721 return req.body;
373 }, {returnChanges: true});
374
375 // verify email is not already taken by a different user
3761 if(typeof req.body.email !== 'undefined')
3771 query = r.do(r.table('users').filter({email: req.body.email}).limit(1).nth(0).default(null), function(conflict){
3781 return r.branch(conflict.eq(null).not().and(conflict('id').eq(req.params.user).not()),
379 {'$$ERROR$$': {'code': 409, 'message': 'An account already exists with this email'}},
380 query
381 );
382 });
383
384 // replace the user
3851 query.run(conn, function(err, result){
3861 resources.db.release(conn);
387
3881 if(err)
3890 return next(err);
390
3911 if(result['$$ERROR$$'])
3920 return next(result['$$ERROR$$']);
393
3941 var user = result.changes[0].new_val;
395
3961 return res.send(prepare(user, privilige));
397 });
398 });
399 },
400 destroy: function(req, res, next){
401
4023 if(!req.user || !req.user.admin)
4031 return next(403, 'Only administrators may delete a user.');
404
4052 if(req.user.id == req.params.user)
4061 return next(400, 'You cannot delete your own admin account.');
407
4081 var privilige = true;
409
4101 resources.db.acquire(function(err, conn) {
4111 if(err)
4120 return next(err);
413
414 // get users from the DB
4151 r.table('users').get(req.params.user).delete({returnChanges: true}).run(conn, function(err, result){
4161 resources.db.release(conn);
417
4181 if(err)
4190 return next(err);
420
4211 var user = result.changes[0].old_val;
422
4231 return res.send(prepare(user, privilige));
424 });
425 });
426 }
427 };
428};
429

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/api/endpoints/cycles.js

100%
11
11
0
LineHitsSource
11'use strict';
2
31var passport = require('passport');
4
51module.exports = function(config, router, resources){
6
71 var controller = require('../controllers/cycles')(config, resources);
8
9 // create
10 // ------
111 router.post(
12 '/api/cycles',
13 passport.authenticate('bearer', { session: false }),
14 controller.create
15 );
16
17 // list
18 // ----
191 router.get(
20 '/api/cycles',
21 passport.authenticate('bearer', { session: false }),
22 controller.list
23 );
24
25 // show
26 // ----
271 router.get(
28 '/api/cycles/:cycle',
29 passport.authenticate('bearer', { session: false }),
30 controller.show
31 );
32
33 // show (by project)
34 // -----------------
351 router.get(
36 '/api/projects/:project/cycle',
37 passport.authenticate('bearer', { session: false }),
38 controller.show
39 );
40
41 // update
42 // ------
431 router.patch(
44 '/api/cycles/:cycle',
45 passport.authenticate('bearer', { session: false }),
46 controller.update
47 );
48
49 // replace
50 // ------
511 router.put(
52 '/api/cycles/:cycle',
53 passport.authenticate('bearer', { session: false }),
54 controller.replace
55 );
56
57 // destroy
58 // -------
591 router.delete(
60 '/api/cycles/:cycle',
61 passport.authenticate('bearer', { session: false }),
62 controller.destroy
63 );
64};
65

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/api/endpoints/files.js

100%
12
12
0
LineHitsSource
11'use strict';
2
31var multer = require('multer');
41var passport = require('passport');
5
61module.exports = function(config, router, resources){
7
81 var controller = require('../controllers/files')(config, resources);
9
10 // create
11 // ------
121 router.post(
13 '/api/files',
14 passport.authenticate('bearer', { session: false }),
15 multer({ dest: config.files.directory }),
16 controller.create
17 );
18
19 // list
20 // ----
211 router.get(
22 '/api/files',
23 passport.authenticate('bearer', { session: false }),
24 controller.list
25 );
26
27 // list (by user)
28 // --------------
291 router.get(
30 '/api/users/:user/files',
31 passport.authenticate('bearer', { session: false }),
32 controller.list
33 );
34
35 // show
36 // ----
371 router.get(
38 '/api/files/:file',
39 // passport.authenticate('bearer', { session: false }),
40 controller.show
41 );
42
43 // update
44 // ------
451 router.patch(
46 '/api/files/:file',
47 passport.authenticate('bearer', { session: false }),
48 controller.update
49 );
50
51 // replace
52 // ------
531 router.put(
54 '/api/files/:file',
55 passport.authenticate('bearer', { session: false }),
56 controller.replace
57 );
58
59 // destroy
60 // -------
611 router.delete(
62 '/api/files/:file',
63 passport.authenticate('bearer', { session: false }),
64 controller.destroy
65 );
66};
67

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/api/endpoints/projects.js

100%
12
12
0
LineHitsSource
11'use strict';
2
31var passport = require('passport');
4
51module.exports = function(config, router, resources){
6
71 var controller = require('../controllers/projects')(config, resources);
8
9 // create
10 // ------
111 router.post(
12 '/api/projects',
13 passport.authenticate('bearer', { session: false }),
14 controller.create
15 );
16
17 // list
18 // ----
191 router.get(
20 '/api/projects',
21 passport.authenticate('bearer', { session: false }),
22 controller.list
23 );
24
25 // list (by cycle)
26 // ---------------
271 router.get(
28 '/api/cycles/:cycle/projects',
29 passport.authenticate('bearer', { session: false }),
30 controller.list
31 );
32
33 // list (by user)
34 // -----------------
351 router.get(
36 '/api/users/:user/projects',
37 passport.authenticate('bearer', { session: false }),
38 controller.list
39 );
40
41 // show
42 // ----
431 router.get(
44 '/api/projects/:project',
45 passport.authenticate('bearer', { session: false }),
46 controller.show
47 );
48
49 // update
50 // ------
511 router.patch(
52 '/api/projects/:project',
53 passport.authenticate('bearer', { session: false }),
54 controller.update
55 );
56
57 // replace
58 // ------
591 router.put(
60 '/api/projects/:project',
61 passport.authenticate('bearer', { session: false }),
62 controller.replace
63 );
64
65 // destroy
66 // -------
671 router.delete(
68 '/api/projects/:project',
69 passport.authenticate('bearer', { session: false }),
70 controller.destroy
71 );
72};
73

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/api/endpoints/tokens.js

70%
74
52
22
LineHitsSource
11'use strict';
2
31var _ = require('lodash');
41var r = require('rethinkdb');
51var jwt = require('jsonwebtoken');
61var crypto = require('crypto');
71var scrypt = require('scrypt');
81 scrypt.hash.config.keyEncoding = scrypt.verify.config.keyEncoding = 'utf8';
91 scrypt.hash.config.outputEncoding = scrypt.verify.config.outputEncoding = 'base64';
10
111module.exports = function(config, router, resources){
121 router.post('/api/tokens', function(req, res, next){
13
14 // validate request
1529 if (!req.body.email)
161 return res.error(400, 'Email is required to generate a token');
17
18 // make the email case insensitive
1928 req.body.email = req.body.email.toLowerCase();
20
21 // grab a db conn
2228 resources.db.acquire(function(err, conn) {
2328 if(err)
240 return res.error(err);
25
26 // look for a user by email
2728 r.table('users').filter({email: req.body.email}).limit(1).run(conn, function(err, cursor){
2828 if(err){
290 resources.db.release(conn);
300 return res.error(err);
31 }
32
3328 cursor.toArray(function(err, users){
3428 resources.db.release(conn);
35
3628 if(err)
370 return res.error(err);
38
3928 if(!users.length)
403 return res.error(404, 'User not found.');
41
4225 var user = users[0];
43
44 // exchange authentication token
4525 if(typeof req.body.token !== 'undefined'){
46
474 if(typeof req.body.token !== 'string')
480 return res.error(401, 'Invalid token.');
49
504 return jwt.verify(req.body.token, config.auth.secret, function(err, decoded) {
51
524 if(err)
531 return res.error(401, 'Invalid token.');
54
55 // exchange for same user or an admin
563 if(user.id !== decoded.sub && !decoded.admin)
571 return res.error(403);
58
59 // send the user a token
602 return res.status(201).send({
61 token: jwt.sign({admin: user.admin}, config.auth.secret, { expiresInMinutes: 24*60, subject: user.id })
62 });
63 });
64 }
65
66 // authenticate by password
6721 if(typeof req.body.password != 'undefined'){
68
6916 if(typeof req.body.password !== 'string' || req.body.password == '')
700 return res.error(401, 'Invalid password.');
71
7216 return scrypt.verify(new Buffer(user.password, 'base64'), req.body.password, function(err){
7316 if(err)
741 return res.error(401, 'Incorrect password.');
75
76 // send the user a token
7715 return res.status(201).send({
78 token: jwt.sign({admin: user.admin}, config.auth.secret, { expiresInMinutes: 24*60, subject: user.id })
79 });
80 });
81 }
82
83 // authenticate by recovery token
845 if(typeof req.body.recovery_token != 'undefined'){
855 if(typeof user.recovery_token != 'string')
861 return res.error(401, 'No recovery token set.');
87
884 return scrypt.verify(new Buffer(user.recovery_token, 'base64'), req.body.recovery_token, function(err){
894 if(err)
902 return res.error(401, 'Invalid recovery token.');
91
922 var recovery_token = JSON.parse(new Buffer(req.body.recovery_token, 'base64').toString('utf8'));
93
942 if(!recovery_token.expiration || recovery_token.expiration < Date.now())
951 return res.error(401, 'Expired recovery token.');
96
97 // authenticate by email (recovery token)
981 return resources.db.acquire(function(err, conn) {
991 if(err)
1000 return res.error(err);
101
102 // update the user
1031 r.table('users').get(user.id).update({recovery_token: null}).run(conn, function(err, results){
1041 resources.db.release(conn);
105
1061 if(err)
1070 return res.error(err);
108
1091 return res.status(201).send({
110 token: jwt.sign({admin: user.admin}, config.auth.secret, { expiresInMinutes: 24*60, subject: user.id })
111 });
112 });
113 });
114 });
115 }
116
117 // authenticate by email (recovery token)
1180 resources.db.acquire(function(err, conn) {
1190 if(err)
1200 return res.error(err);
121
122 // generate the token
1230 var recovery_token = new Buffer(JSON.stringify({
124 email: user.email,
125 expiration: Date.now() + 3600000, // 1 hour from now
126 secret: crypto.randomBytes(256).toString('base64')
127 })).toString('base64');
128
129 // update the user
1300 r.table('users').get(user.id).update({
131 recovery_token: scrypt.hash(recovery_token, scrypt.params(0.1))
132 }, {returnChanges: true}).run(conn, function(err, results){
1330 resources.db.release(conn);
134
1350 if(err)
1360 return res.error(err);
137
1380 if(!results.new_val || !results.new_val.recovery_token)
1390 return res.error(500, 'Failed to update user with recovery token');
140
141 // email recovery token to user
1420 resources.mail({
143 to: user.email,
144 subject: 'Your Recovery Token',
145 html: resources.emailTemplates.recovery({
146 user: results.new_val,
147 link: req.protocol + '://' + (req.headers.host || req.hostname) + '/#/recovery?recovery_token=' + recovery_token
148 }),
149 }, function(err, info){
1500 if(err)
1510 return res.error(err);
152
1530 return res.status(201).send();
154 });
155 });
156 });
157 });
158 });
159 });
160 });
161};
162

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/api/endpoints/users.js

100%
13
13
0
LineHitsSource
11'use strict';
2
31var passport = require('passport');
4
51module.exports = function(config, router, resources){
6
71 var controller = require('../controllers/users')(config, resources);
8
9 // create
10 // ------
111 router.post(
12 '/api/users',
13 controller.create
14 );
15
16 // list
17 // ----
181 router.get(
19 '/api/users',
20 passport.authenticate('bearer', { session: false }),
21 controller.list
22 );
23
24 // list (by cycle)
25 // ---------------
261 router.get(
27 '/api/cycles/:cycle/users',
28 passport.authenticate('bearer', { session: false }),
29 controller.list
30 );
31
32 // list (by project)
33 // -----------------
341 router.get(
35 '/api/projects/:project/users',
36 passport.authenticate('bearer', { session: false }),
37 controller.list
38 );
39
40 // show
41 // ----
421 router.get(
43 '/api/users/:user',
44 passport.authenticate('bearer', { session: false }),
45 controller.show
46 );
47
48 // show (by file)
49 // --------------
501 router.get(
51 '/api/files/:file/user',
52 passport.authenticate('bearer', { session: false }),
53 controller.show
54 );
55
56 // update
57 // ------
581 router.patch(
59 '/api/users/:user',
60 passport.authenticate('bearer', { session: false }),
61 controller.update
62 );
63
64 // replace
65 // ------
661 router.put(
67 '/api/users/:user',
68 passport.authenticate('bearer', { session: false }),
69 controller.replace
70 );
71
72 // destroy
73 // -------
741 router.delete(
75 '/api/users/:user',
76 passport.authenticate('bearer', { session: false }),
77 controller.destroy
78 );
79};
80

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/api/middleware/auth.js

78%
23
18
5
LineHitsSource
11'use strict';
2
31var r = require('rethinkdb');
41var jwt = require('jsonwebtoken');
51var passport = require('passport');
61var BearerStrategy = require('passport-http-bearer');
7
8
91module.exports = function(config, resources){
10
11 // configure passport
121 passport.use(new BearerStrategy(function(token, done) {
1352 jwt.verify(token, config.secret, function(err, decoded) {
14
1552 if(err)
160 return done(err);
17
1852 if(!decoded.sub)
190 return done('No subject encoded in token.');
20
2152 resources.db.acquire(function(err, conn) {
2252 if(err)
230 return done(err);
24
25 // get users from the DB
2652 r.table('users').get(decoded.sub).run(conn, function(err, user){
2752 resources.db.release(conn);
28
2952 if(err)
300 return done(err);
31
3252 if(!user)
330 return done('Invalid subject encoded in token.');
34
3552 return done(null, user);
36 });
37 });
38 });
39 }));
40
41 // return the initialized middleware
421 return passport.initialize();
43};
44

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/api/utils/db.js

85%
7
6
1
LineHitsSource
11'use strict';
2
31var Pool = require('generic-pool').Pool;
41var r = require('rethinkdb');
5
61module.exports = function(options, max, min, idleTimeoutMillis) {
71 return Pool({
8 name: 'rethinkdb',
9 create: function(callback) {
102 return r.connect(options, callback);
11 },
12 destroy: function(connection) {
130 return connection.close();
14 },
15 log: false,
16 min: min || 2,
17 max: max || 10,
18 idleTimeoutMillis: idleTimeoutMillis || 30000
19 });
20};

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/index.js

85%
62
53
9
LineHitsSource
11'use strict';
2
31var fs = require('fs');
41var _ = require('lodash');
51var express = require('express');
61var bodyParser = require('body-parser');
7
81module.exports = function(config, app){
91 var router = express.Router();
101 var transporter = require('nodemailer').createTransport(config.mail.transport);
11
12 /*************
13 * Resources *
14 *************/
15
16 // add resources
171 var resources = {
18 db: require('./api/utils/db.js')(config.db),
19 mail: function(data, callback) {
200 _.defaults(data, config.mail.defaults);
210 return transporter.sendMail(data, callback);
22 },
23 validator: require('jjv')(),
24 testers: {},
25 listeners: {},
26 components: {},
27 emailTemplates: {}
28 };
29
30 // customize validator
311 resources.validator.addType('date', function (v) {
326 return !isNaN(Date.parse(v));
33 });
34
351 resources.validator.addTypeCoercion('date', function (v) {
360 return new Date(v);
37 });
38
39 // add schemas
401 fs.readdirSync(__dirname + '/api/schemas/').forEach(function(file){
414 if(file.indexOf('.json') !== (file.length - 5))
420 return;
43
444 var schema = require(__dirname + '/api/schemas/' + file);
454 resources.validator.addSchema(schema.id, schema);
46 });
47
48 /*************************
49 * Dependency Management *
50 *************************/
51
52 // make sure bower directory exists
532 try { fs.mkdirSync(__dirname + '/app/assets/bower'); } catch(e) {}
54
55 // add gandhi modules
561 var bowerJson = require('../bower.json');
571 config.modules.forEach(function(directory){
58
59 // add client side
6011 if(fs.existsSync(directory + '/bower.json')){
617 var json = require(directory + '/bower.json');
62
63 // symlink into bower
6414 try { fs.unlinkSync(__dirname + '/app/assets/bower/' + json.name); } catch(e) {}
657 fs.symlinkSync(directory, __dirname + '/app/assets/bower/' + json.name, 'dir');
66
677 bowerJson.dependencies[json.name] = __dirname + '/app/assets/bower/' + json.name;
68
69 // add the angular module
707 if(typeof json.module == 'string')
717 bowerJson.modules.push(json.module);
72 }
73
74 // add server side
7511 if(fs.existsSync(directory + '/package.json')){
766 require(directory)(router, resources);
77 }
78 });
79
80 // install dependencies
811 require('bower').commands.install(undefined, undefined, { cwd: __dirname + '/../' });
82
83 // get all the bower deps
841 var deps = require('wiredep')({
85 bowerJson: bowerJson,
86 cwd: __dirname + '/../'
87 });
881 var main = '';
89
90 // require all the js files
911 main += deps.js.map(function(file){
9224 return fs.readFileSync(file);
93 }).join('\n') + '\n';
94
95 // build the gandhi module
961 main += 'angular.module("gandhi", ["' + bowerJson.modules.join('","') + '"]);' + '\n';
97
98 // require source files
991 main += bowerJson.main.map(function(file){
1009 return fs.readFileSync(__dirname + '/app/' + file);
101 }).join('\n') + '\n';
102
103 // require all the css files
1041 var styles = deps.css.map(function(file){
1054 return 'document.write(\'<link rel="stylesheet" type="text/css" href="' + file.replace(new RegExp('^'+__dirname+'/app/'), '') + '" />\');';
106 }).join('\n') + '\n';
107
108
109 /***********
110 * Routing *
111 ***********/
112
113 // add middleware
1141 router.use('/api', bodyParser.urlencoded({extended: true}));
1151 router.use('/api', bodyParser.json());
1161 router.use('/api', require('res-error'));
1171 router.use('/api', require('./api/middleware/auth.js')(config.auth, resources));
118
119 // add the endpoints
1201 fs.readdirSync(__dirname + '/api/endpoints/').forEach(function(file){
1215 if(file.indexOf('.js') === (file.length - 3))
1225 require(__dirname + '/api/endpoints/' + file)(config, router, resources);
123 });
124
125 // serve main.js
1261 router.get('/main.js', function(req, res){
1270 res.set('Content-Type','text/javascript');
1280 res.send(main);
129 });
130
131 // serve styles.js
1321 router.get('/styles.js', function(req, res){
1330 res.set('Content-Type','text/javascript');
1340 res.send(styles);
135 });
136
137 // serve static files
1381 router.use('/modules', express.static(__dirname + '/modules'));
1391 router.use(express.static(__dirname + '/app'));
140
141 // apply the router
1421 app.use(config.root, router);
143
144 // error handling
1451 app.use(function(err, req, res, next){
14620 var e = res.error(err);
147
14820 if(e.code === 500)
1490 console.error(e.code, e.message, e.err);
150 });
151};
152

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/modules/gandhi-component-form/api/index.js

100%
4
4
0
LineHitsSource
11'use strict';
2
31var _ = require('lodash');
4
51module.exports = function(router, resources){
61 resources.components['form'] = {
7 create: function() {
8
9 },
10 read: function() {
11
12 },
13 update: function() {
14
15 },
16 destroy: function() {
17
18 }
19 }
20}
21

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/modules/gandhi-component-message/api/index.js

100%
4
4
0
LineHitsSource
11'use strict';
2
31var _ = require('lodash');
4
51module.exports = function(router, resources){
61 resources.components['message'] = {
7 create: function(data, cycle) {
8
9 },
10 read: function() {
11
12 },
13 update: function() {
14
15 },
16 destroy: function() {
17
18 }
19 }
20}
21

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/modules/gandhi-component-start/api/index.js

100%
4
4
0
LineHitsSource
11'use strict';
2
31var _ = require('lodash');
4
51module.exports = function(router, resources){
61 resources.components['start'] = {
7 create: function() {
8
9 },
10 read: function() {
11
12 },
13 update: function() {
14
15 },
16 destroy: function() {
17
18 }
19 }
20}
21

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/modules/gandhi-emailTemplate-recovery/index.js

100%
5
5
0
LineHitsSource
11'use strict';
2
31var fs = require('fs');
41var handlebars = require('handlebars');
5
61module.exports = function(router, resources){
71 resources.emailTemplates.recovery = handlebars.compile(fs.readFileSync(__dirname + '/template.html').toString('utf8'));
8}
9

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/modules/gandhi-tester-date/index.js

100%
6
6
0
LineHitsSource
11'use strict';
2
31var pointer = require('json-pointer');
4
51module.exports = function(router, resources){
61 resources.testers.date = function(options, project){
79 var res = new Date() > new Date(options.date);
89 return options.mode === 'before' ? !res : !!res;
9 }
10}
11

/Users/mikemarcacci/Documents/Repositories/gandhi/lib/modules/gandhi-tester-regex/index.js

100%
9
9
0
LineHitsSource
11'use strict';
2
31var pointer = require('json-pointer');
4
51module.exports = function(router, resources){
61 resources.testers.regex = function(options, project){
721 try {
821 var data = pointer.get(project, options.pointer);
99 var res = typeof data === 'string' ? data.match(new RegExp(options.regex)) : false;
109 return options.invert ? !res : !!res;
11 } catch(e){
1212 return !!options.invert;
13 }
14 }
15}
16