Improvements in REST API

This commit is contained in:
vjrj 2018-06-15 06:39:09 +02:00
parent d980a31c05
commit b8ab47e867
2 changed files with 300 additions and 288 deletions

View file

@ -31,8 +31,8 @@ function defaultFailParams(e) {
} }
function checkAuthToken(token) { function checkAuthToken(token) {
if (token !== Meteor.settings.private.internalApiToken) { if (!Meteor.settings.private.internalApiToken || token !== Meteor.settings.private.internalApiToken) {
const message = `Unauthorized auth token '${token}' in REST API`; const message = 'Unauthorized auth token in REST API';
console.warn(message); console.warn(message);
return restivusError(401, message); return restivusError(401, message);
} }
@ -45,18 +45,20 @@ function checkLatLonDist(km, lat, lng) {
check(km, NumberBetween(0, Meteor.isDevelopment ? 1000 : 100)); check(km, NumberBetween(0, Meteor.isDevelopment ? 1000 : 100));
} }
if (!Meteor.settings.private.internalApiToken) {
console.warn('Meteor.settings.private.internalApiToken is not configured so we don\'t enable our REST API');
} else {
// export // export
const apiV1 = new Restivus({ const apiV1 = new Restivus({
useDefaultAuth: true, useDefaultAuth: true,
apiPath: 'api', apiPath: 'api',
version: 'v1', version: 'v1',
prettyJson: true prettyJson: true
}); });
// Generates: POST on /api/users and GET, DELETE /api/users/:id for // Generates: POST on /api/users and GET, DELETE /api/users/:id for
// Meteor.users collection // Meteor.users collection
/* apiV1.addCollection(Meteor.users, { /* apiV1.addCollection(Meteor.users, {
* excludedEndpoints: ['getAll', 'put', 'delete', 'patch', 'get'], * excludedEndpoints: ['getAll', 'put', 'delete', 'patch', 'get'],
* routeOptions: { * routeOptions: {
* authRequired: true * authRequired: true
@ -71,137 +73,57 @@ const apiV1 = new Restivus({
* } * }
* }); */ * }); */
apiV1.addCollection(Fires, { apiV1.addCollection(Fires, {
excludedEndpoints: ['put', 'post', 'patch', 'delete'], excludedEndpoints: ['put', 'post', 'patch', 'delete'],
// excludedEndpoints: ['getAll', 'put', 'post', 'patch', 'delete'], // excludedEndpoints: ['getAll', 'put', 'post', 'patch', 'delete'],
routeOptions: { routeOptions: {
authRequired: false authRequired: false
}, },
endpoints: { endpoints: {
get: { get: {
action: function getFire() { action: function getFire() {
return Fires.findOne(new Meteor.Collection.ObjectID(this.urlParams.id)); return Fires.findOne(new Meteor.Collection.ObjectID(this.urlParams.id));
}
} }
} }
}
});
// Maps to: /api/v1/status/last-fire-check
apiV1.addRoute('status/last-fire-check', { authRequired: false }, {
get: function get() {
return SiteSettings.findOne({ name: 'last-fire-check' });
}
});
// Maps to: /api/v1/status/last-fire-detected
apiV1.addRoute('status/last-fire-detected', { authRequired: false }, {
get: function get() {
return ActiveFiresCollection.findOne({}, { sort: { when: -1 } });
}
});
// Maps to: /api/v1/status/active-fires-count
apiV1.addRoute('status/active-fires-count', { authRequired: false }, {
get: function get() {
return { total: ActiveFiresCollection.find({}).count() };
}
});
// Maps to: /api/v1/status/uptime
apiV1.addRoute('status/uptime', { authRequired: false }, {
get: function get() {
return { ms: new Date() - uptime };
}
});
function getFires(route, full) {
const lat = Number(route.urlParams.lat);
const lng = Number(route.urlParams.lng);
const km = Number(route.urlParams.km);
const { token } = route.urlParams;
try {
checkLatLonDist(km, lat, lng);
check(token, String);
} catch (e) {
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
if (debug) console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`);
const fires = ActiveFiresCollection.find({
ourid: {
$near: {
$geometry: {
type: 'Point',
coordinates: [lng, lat]
},
$maxDistance: km * 1000,
$minDistance: 0
}
}
}, {
fields: {
lat: 1,
lon: 1,
when: 1,
scan: 1
}
}); });
const result = { total: fires.count(), real: countRealFires(fires) }; // Maps to: /api/v1/status/last-fire-check
apiV1.addRoute('status/last-fire-check', { authRequired: false }, {
get: function get() {
return SiteSettings.findOne({ name: 'last-fire-check' });
}
});
if (!full) { // Maps to: /api/v1/status/last-fire-detected
return result; apiV1.addRoute('status/last-fire-detected', { authRequired: false }, {
} get: function get() {
return ActiveFiresCollection.findOne({}, { sort: { when: -1 } });
}
});
// TODO only get real // Maps to: /api/v1/status/active-fires-count
const firesA = fires.fetch(); apiV1.addRoute('status/active-fires-count', { authRequired: false }, {
if (firesA.length > 0) { get: function get() {
const union = firesUnion(fires); return { total: ActiveFiresCollection.find({}).count() };
const falsePos = whichAreFalsePositives(FalsePositives, union); }
const industries = whichAreFalsePositives(Industries, union); });
result.fires = firesA;
result.industries = industries.fetch();
result.falsePos = falsePos.fetch();
return result;
}
// otherwise // Maps to: /api/v1/status/uptime
return { apiV1.addRoute('status/uptime', { authRequired: false }, {
total: 0, real: 0, fires: [], falsePos: [], industries: [] get: function get() {
}; return { ms: new Date() - uptime };
} }
});
// Maps to: /api/v1/fires-in/:lat/:lng/:km function getFires(route, full) {
// 100 km max const lat = Number(route.urlParams.lat);
// Ex: http://127.0.0.1:3000/api/v1/fires-in/token/38.736946/-9.142685/100 const lng = Number(route.urlParams.lng);
apiV1.addRoute('fires-in/:token/:lat/:lng/:km', { authRequired: false }, { const km = Number(route.urlParams.km);
get: function get() { const { token } = route.urlParams;
return getFires(this, false);
}
});
apiV1.addRoute('fires-in-full/:token/:lat/:lng/:km', { authRequired: false }, {
get: function get() {
return getFires(this, true);
}
});
// Add mobile user:
// curl -X POST http://localhost:3000/api/v1/users/mobile -d "token: thisAppAutToken" -d "mobileToken: user-mobile-firebase-token"
// Response:
//
// https://docs.meteor.com/api/passwords.html#Accounts-createUser
apiV1.addRoute('mobile/users', { authRequired: false }, {
post: function post() {
const { token, mobileToken, lang } = this.bodyParams;
try { try {
checkLatLonDist(km, lat, lng);
check(token, String); check(token, String);
check(lang, String);
check(mobileToken, String);
} catch (e) { } catch (e) {
return defaultFailParams(e); return defaultFailParams(e);
} }
@ -209,158 +131,250 @@ apiV1.addRoute('mobile/users', { authRequired: false }, {
const failed = checkAuthToken(token); const failed = checkAuthToken(token);
if (failed) return failed; if (failed) return failed;
let username; if (debug) console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`);
do { const fires = ActiveFiresCollection.find({
username = Random.id(15); ourid: {
} while (Meteor.users.find({ username }).count() !== 0); $near: {
$geometry: {
// FIXME check valid lang type: 'Point',
coordinates: [lng, lat]
const now = new Date(); },
// Accounts.createUser({ username: mobileToken, lang: 'FIXME' profile: {} }); $maxDistance: km * 1000,
const result = Meteor.users.upsert({ $minDistance: 0
fireBaseToken: mobileToken }
}
}, { }, {
$set: { fields: {
username, lat: 1,
fireBaseToken: mobileToken, lon: 1,
lang, when: 1,
profile: { }, scan: 1
createdAt: now,
updatedAt: now
} }
}); });
if (debug) { const result = { total: fires.count(), real: countRealFires(fires) };
console.log(this.bodyParams);
console.log(this.urlParams); if (!full) {
console.log(this.queryParams); return result;
} }
const newUser = Meteor.users.findOne({ username }); // TODO only get real
const firesA = fires.fetch();
if (firesA.length > 0) {
const union = firesUnion(fires);
const falsePos = whichAreFalsePositives(FalsePositives, union);
const industries = whichAreFalsePositives(Industries, union);
result.fires = firesA;
result.industries = industries.fetch();
result.falsePos = falsePos.fetch();
return result;
}
return jsend.success({ // otherwise
upsertResult: result, return {
username, total: 0, real: 0, fires: [], falsePos: [], industries: []
userId: newUser._id, };
lang: newUser.lang,
mobileToken: newUser.fireBaseToken
});
} }
}); // Maps to: /api/v1/fires-in/:lat/:lng/:km
// 100 km max
// Ex: http://127.0.0.1:3000/api/v1/fires-in/token/38.736946/-9.142685/100
apiV1.addRoute('fires-in/:token/:lat/:lng/:km', { authRequired: false }, {
get: function get() {
return getFires(this, false);
}
});
// max sitance: 100km apiV1.addRoute('fires-in-full/:token/:lat/:lng/:km', { authRequired: false }, {
apiV1.addRoute('mobile/subscriptions', { authRequired: false }, { get: function get() {
post: function post() { return getFires(this, true);
const { }
token, });
mobileToken,
lat, // Add mobile user:
lon, // curl -X POST http://localhost:3000/api/v1/users/mobile -d "token: thisAppAutToken" -d "mobileToken: user-mobile-firebase-token"
distance // Response:
} = this.bodyParams; //
try { // https://docs.meteor.com/api/passwords.html#Accounts-createUser
check(token, String); apiV1.addRoute('mobile/users', { authRequired: false }, {
check(mobileToken, String); post: function post() {
check(lat, Number); const { token, mobileToken, lang } = this.bodyParams;
check(lon, Number); try {
check(distance, Number); check(token, String);
checkLatLonDist(distance, lat, lon); check(lang, String);
} catch (e) { check(mobileToken, String);
return defaultFailParams(e); } catch (e) {
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
let username;
const already = Meteor.users.find({ fireBaseToken: mobileToken });
if (already.count() > 1) {
return restivusError(500, 'Unexpected error in REST call: several users with that mobile token?');
} else if (already.count() === 1) {
username = already.fetch()[0].username;
} else {
do {
username = Random.id(15);
} while (Meteor.users.find({ username }).count() !== 0);
}
// FIXME check valid lang
const now = new Date();
const result = Meteor.users.upsert({
fireBaseToken: mobileToken
}, {
$set: {
username,
fireBaseToken: mobileToken,
lang,
profile: { },
createdAt: now,
updatedAt: now
}
});
if (debug) {
console.log(this.bodyParams);
console.log(this.urlParams);
console.log(this.queryParams);
}
const upsertUser = Meteor.users.findOne({ username });
if (debug) console.log(upsertUser);
return jsend.success({
upsertResult: result,
username,
userId: upsertUser._id,
lang: upsertUser.lang,
mobileToken: upsertUser.fireBaseToken
});
} }
const failed = checkAuthToken(token); });
if (failed) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); // max sitance: 100km
if (!user) return failed; apiV1.addRoute('mobile/subscriptions', { authRequired: false }, {
post: function post() {
const {
token,
mobileToken,
lat,
lon,
distance
} = this.bodyParams;
try {
check(token, String);
check(mobileToken, String);
check(lat, Number);
check(lon, Number);
check(distance, Number);
checkLatLonDist(distance, lat, lon);
} catch (e) {
return defaultFailParams(e);
}
const newSubs = {}; const failed = checkAuthToken(token);
newSubs.location = {}; if (failed) return failed;
newSubs.location.lat = lat;
newSubs.location.lon = lon;
newSubs.distance = distance;
let result; const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
try { if (!user) return failed;
result = subscriptionsInsert(newSubs, user._id, 'mobile');
} catch (e) { const newSubs = {};
return fail(e); newSubs.location = {};
newSubs.location.lat = lat;
newSubs.location.lon = lon;
newSubs.distance = distance;
let result;
try {
result = subscriptionsInsert(newSubs, user._id, 'mobile');
} catch (e) {
return fail(e);
}
return jsend.success({ subsId: result._str });
},
delete: function del() {
const {
token,
mobileToken,
subsId
} = this.bodyParams;
try {
check(token, String);
check(mobileToken, String);
check(subsId, String);
} catch (e) {
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failed;
try {
Subscriptions.remove({ owner: user._id, _id: new Meteor.Collection.ObjectID(subsId) });
} catch (e) {
return fail(e);
}
return jsend.success({});
} }
return jsend.success({ subsId: result._str }); });
},
delete: function del() { apiV1.addRoute('mobile/subscriptions/all', { authRequired: false }, {
const { get: function get() {
token, const { token, mobileToken } = this.bodyParams;
mobileToken, try {
subsId check(token, String);
} = this.bodyParams; check(mobileToken, String);
try { } catch (e) {
check(token, String); return defaultFailParams(e);
check(mobileToken, String); }
check(subsId, String);
} catch (e) { const failed = checkAuthToken(token);
return defaultFailParams(e); if (failed) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failed;
const result = Subscriptions.find({ owner: user._id });
return jsend.success({ subscriptions: result.fetch(), count: result.count() });
},
delete: function delAll() {
const { token, mobileToken } = this.bodyParams;
try {
check(token, String);
check(mobileToken, String);
} catch (e) {
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
if (Meteor.users.find({ fireBaseToken: mobileToken }).count() !== 1) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failed;
const toRemove = Subscriptions.find({ owner: user._id }).count();
Subscriptions.remove({ owner: user._id });
return jsend.success({ count: toRemove });
} }
});
const failed = checkAuthToken(token); }
if (failed) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failed;
try {
Subscriptions.remove({ owner: user._id, _id: new Meteor.Collection.ObjectID(subsId) });
} catch (e) {
return fail(e);
}
return jsend.success({});
}
});
apiV1.addRoute('mobile/subscriptions/all', { authRequired: false }, {
get: function get() {
const { token, mobileToken } = this.bodyParams;
try {
check(token, String);
check(mobileToken, String);
} catch (e) {
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failed;
const result = Subscriptions.find({ owner: user._id });
return jsend.success({ subscriptions: result.fetch(), count: result.count() });
},
delete: function delAll() {
const { token, mobileToken } = this.bodyParams;
try {
check(token, String);
check(mobileToken, String);
} catch (e) {
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
if (Meteor.users.find({ fireBaseToken: mobileToken }).count() !== 1) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failed;
const toRemove = Subscriptions.find({ owner: user._id }).count();
Subscriptions.remove({ owner: user._id });
return jsend.success({ count: toRemove });
}
});

View file

@ -270,24 +270,22 @@ describe('basic api v1 returns', () => {
}); });
it('should del all mobile user subscriptions', async (done) => { it('should del all mobile user subscriptions', async (done) => {
// Add two // Add subs
addSubs(() => {}); addSubs(() => {
addSubs(() => {}); HTTP.del(url('api/v1/mobile/subscriptions/all'), {
HTTP.del(url('api/v1/mobile/subscriptions/all'), { data: {
data: { token,
token, mobileToken
mobileToken }
} }, (error, result) => {
}, (error, result) => { chai.expect(error, null);
chai.expect(error, null); chai.expect(result.statusCode).equal(200);
chai.expect(result.statusCode).equal(200); const jsendResult = result.data;
const jsendResult = result.data; chai.expect(jsendResult.status).equal('success');
chai.expect(jsendResult.status).equal('success'); chai.expect(typeof jsendResult.data.count).to.equal('number');
chai.expect(typeof jsendResult.data.count).to.equal('number'); chai.expect(jsendResult.data.count).to.equal(1);
chai.expect(jsendResult.data.count).to.equal(2); done();
done(); });
}); });
}); });
// TODO remove all subs
}); });