Error-handling - 404 for remoteMethods - node.js

I have been able to easily set up a basic node.js api with the help of strongloop. I have been able to add custom routes using remoteMethods. However, I am a bit confused in setting up 404 for those routes. I have one route for model category named mature that takes one argument(categorId) and fetches all games under that category that have boolean value set to true for mature. The endpoint url is: http://localhost:3000/api/Categories/1004/games/mature. If I place a non existent categorId, it breaks. What would be the best way to setup routes to handle 404 for errors? For example, display "no such category id". Github REPO
common/models/category.js
Category.mature = function(id, limit) {
var app = this.app;
var Games = app.models.Games;
Category.findById(id, {}, function(err, category){
if (err) return callback(err);
//set limit
if (limit && limit > 5){
limit = 5;
}else if(limit === undefined){
limit = 5;
}
Games.find({
"where": {
categoryId: id,
mature: true,
gameId: {gt: hashids.decode(after)}
},
"limit": limit
}, function(err, gameArray) {
if (err) return callback(err);
callback(null, gameArr);
});
)};
Category.remoteMethod(
'mature', {
accepts: [
{arg: 'id', type: 'number', required: true},
{arg: 'limit',type: 'number',required: false}
],
// mixing ':id' into the rest url allows $owner to be determined and used for access control
http: {
path: '/:id/games/mature',
verb: 'get'
},
returns: {
root: true,
type: 'object'
}
}
);
};

Set err.statusCode to 404 before you call callback(err):
if(!category) {
var err = new Error('Category ID ' + id + ' does not exist.');
err.statusCode = 404;
callback(err);
}
This will result in:

Related

How to read an image and text at the same time in Loopback/NodeJs

I am doing a remote method with Loopback to show some text and display an image. I am getting the path of the image and the fields by an sql query and data is showed correctly. What I want to do is to transform the path showed in the result to display the image itself.
This is my remote method so far :
cm_comediens.getprofile1 = function (options, cb) {
const token = options && options.accessToken;
const userId = token && token.userId;
var ds = app.datasources.mydb;
var sql = "SELECT comedien_perso_nom,comedien_perso_prenom,nationalite,photoscomedien.path FROM cm_comediens INNER JOIN photoscomedien ON cm_comediens.id_comedien=photoscomedien.id_comedien WHERE cm_comediens.id_comedien IN ( SELECT id_comedien FROM (SELECT id_comedien FROM cm_comediens WHERE id_utilisateur= '" + userId + "') as MakeitWork) AND photoscomedien.photo_profile=1 ";
ds.connector.execute(sql, [], function (err, cm_comedienss) {
if(err) {console.error(err);}
cb(err, cm_comedienss);
});
}
cm_comediens.remoteMethod(
'getprofile1', {
http: {verb: 'GET'},
description: 'Get Comediens infos',
accepts: [{arg: "options","type": "object","http": "optionsFromRequest"},],
returns: {arg: 'data',type: ['cm_comediens'],root: true,}
}
);
This is what I am getting so far and what I want to do exactly is to change the path to an image
Result
I tried to add the fs.readfile but weird result showed up. I changed the remote method as follows :
ds.connector.execute(sql, [], function (err, cm_comedienss) {
fs.readFile(cm_comedienss[0].path, function(err, cm_comedienss) {
if(err) {console.error(err);}
cb(err, cm_comedienss);
});
});
}
This is the result I got after adding the readfile :
after adding fs.readfile
The remoting metadata for your new method are describing the return value as a JSON data. The actual value is Buffer, which is converted by LoopBack to the value you shown on your screenshot.
{
"$type": "base64"
"$data": "(base64-encoded data of your image)"
}
If you want your API to return image, you need to make two changes:
tell LoopBack to set a different Content-Type header, e.g. image/png
tell LoopBack to treat the Buffer value as the raw response body
cm_comediens.getprofile1 = function (options, cb) {
ds.connector.execute(sql, [], function (err, found) {
if (err) return cb(err);
fs.readFile(found[0].path, function(err, image) {
if(err) return cb(err);
cb(null, image, 'image/png');
});
});
};
cm_comediens('getprofile1', {
http: {verb: 'GET'},
description: 'Get Comediens infos',
accepts: [
{arg: "options","type": "object","http": "optionsFromRequest"},
],
returns: [
{ arg: 'body', type: 'file', root: true },
{ arg: 'Content-Type', type: 'string', http: { target: 'header' } },
],
});

Mongoose Subdocument will not update

I am having trouble figuring out if I designed the schema correctly because I am receiving a 500 error when attempting to PATCH changes of the roles property from a profile. (Note: The 500 error just responds with an empty {}, so it isn't really informative)
Below is the profile schema:
var ProfileSchema = new Schema({
name: {
type: String,
required: true
},
roles: [{
application: {
type: Schema.Types.ObjectId,
required: true,
ref: 'Application'
},
role: {
type: String,
required: true,
enum: [ 'admin', 'author', 'publisher' ]
}
}]
});
Each profile has a role for an application, and when I send the request to the controller action 'update', it fails:
profile update controller:
// Updates an existing Profile in the DB
export function update(req, res) {
try {
if (req.body._id) {
delete req.body._id;
}
console.log('ENDPOINT HIT...');
console.log(`REQUEST PARAM ID: ${req.params.id}`);
console.log('REQUEST BODY:');
console.log(req.body);
console.log('ENTIRE REQUEST: ');
return Profile.findByIdAsync(req.params.id)
.then(handleEntityNotFound(res))
.then(saveUpdates(req.body))
.then(respondWithResult(res))
.catch(handleError(res));
} catch(ex) {
console.error('FAILED TO UPDATE PROFILE');
return handleError(res);
}
}
I made sure that the id and body was being sent properly, and I am hitting the end point.
This is an example of the request body JSON:
{
_id: 57e58ad2781fd340563e29ff,
__updated: Thu Oct 27 2016 15:41:12 GMT-0400 (EDT),
__created: Fri Sep 23 2016 16:04:34 GMT-0400 (EDT),
name: 'test',
__v: 11,
roles:[
{ application: 57b70937c4b9fe460a235375,
role: 'admin',
_id: 58125858a36bd76d8111ba16 },
{ application: 581b299f0145b48adf8f57bd,
role: 'publisher',
_id: 581b481898eefb19ed8a73ee }
]
}
When I try to find the Profile by Id, the promise chain goes straight to the catch(handleError(res)); part of the code and shows an empty object in my console.
My handle error function:
function handleError(res, statusCode) {
console.error('HANDLE PROFILE ERROR: ', statusCode);
statusCode = statusCode || 500;
return function(err) {
console.error('PROFILE ERROR:');
console.error(JSON.stringify(err, null, 2));
res.status(statusCode).send(err);
};
}
UPDATE
I am realizing the code is breaking when it hits my saveUpdates function (Note: I am using lodash):
function saveUpdates(updates) {
/// the code is fine here ///
return function(entity) {
/// once it enters in here, is where it begins to break ///
var updated = _.merge(entity, updates);
if(updated.roles.length != updates.roles.length) {
updated.roles = updates.roles;
}
for(var i in updates.roles) {
updated.roles.set(i, updates.roles[i]);
}
return updated.saveAsync()
.then(updated => {
return updated;
});
};
}
Lesson learned: Read Documentation properly.
Since I am using bluebird promises for this application, I forgot to use .spread() within my saveUpdates() callback function.
Solution:
function saveUpdates(updates) {
return function(entity) {
var updated = _.merge(entity, updates);
if(updated.roles.length != updates.roles.length) {
updated.roles = updates.roles;
}
for(var i in updates.roles) {
updated.roles.set(i, updates.roles[i]);
}
return updated.saveAsync()
// use `.spread()` and not `.then()` //
.spread(updated => {
return updated;
});
};
}
I want to thank the following SOA that led to this conclusion: https://stackoverflow.com/a/25799076/5994486
Also, here is the link to the bluebird documentation in case anyone was curious on .spread(): http://bluebirdjs.com/docs/api/spread.html

sails js save many to many is only one way

i have two models:
user.js
module.exports = {
attributes: {
...
profile: {
model: 'Profile'
},
groups: {
collection: 'group',
via: 'users',
dominate: true
},
roles: {
collection: 'role',
via: 'users',
dominate: true
}
}};
and, group.js
module.exports = {
attributes: {
...
users: {
collection: 'user',
via: 'groups'
}
}};
when i try to add users to a group (when i select a group and add users to it), it works as it is supposed to,
var defer = q.defer();
baseDbContext.single(req, 'users')
.then(function(op){
if(!op.status || !op.obj) {
defer.resolve(notFound);
return;
}
op.obj.users = [];
_.each(req.users, function(item){
op.obj.users.add(item);
});
op.obj.save(function(err, obj){
if(err) defer.reject(operationResult().throwException(err));
else defer.resolve(operationResult().succeed());
});
});
return defer.promise;
but when i try to add groups to the user (when i select the user and add groups to it) it fails silently!!!
var defer = q.defer();
baseDbContext.single(req, 'groups')
.then(function(op){
if(!op.status || !op.obj) {
defer.resolve(notFound);
return;
}
op.obj.groups = [];
_.each(req.groups, function(item){
op.obj.groups.add(item);
});
op.obj.save(function(err, obj){
if(err) defer.reject(operationResult().throwException(err));
else defer.resolve(operationResult().succeed());
});
});
return defer.promise;
when i check it in sails console it shows :
throw new Error('Unknown rule: ' + ruleName);
Error: Unknown rule: dominate
this is a simple many to many insertion why would it fail?
(a note about code, the function baseDbContext.single finds a object based on its id and the second parameter is for populate)
Seems like you have a misprint, documentation says that the rule you need is writes as "dominant: true", not "dominate: true".

Soft delete in Sails/Waterline

Trying to delete a user model using:
//Hard Delete
User.destroy({id:userId}, function(err, res){
//Hard Delete
})
I need to do a soft delete on User model and currently setting a flag isDeleted to true on delete and updating document:
updateUser.isDeleted = true;
User.update({id:userId}, updateUser, function(err, res){
Update project
})
and while fetching documents I am doing a check If isDeleted - true or not.
Is there any In-built feature provided by Sails or Waterline which I can configure to perform a soft delete and avoid updating and then fetching based on isDeleted flag?
you can use beforeFind() life cycle function for filter of soft deleted records
model: parrot,js
module.exports = {
attributes: {
// e.g., "Polly"
name: {
type: 'string'
},
// e.g., 3.26
wingspan: {
type: 'float',
required: true
},
// e.g., "cm"
wingspanUnits: {
type: 'string',
enum: ['cm', 'in', 'm', 'mm'],
defaultsTo: 'cm'
},
// e.g., [{...}, {...}, ...]
knownDialects: {
collection: 'Dialect'
},
isDeleted:{
type:'boolean'
}
},
beforeFind: function(values, cb) {
values.isDeleted = false;
cb();
}
}
ParrotController.js
module.exports = {
// getting default parrots isDeleted = true
list: function (req, res) {
Parrot
.find()
.exec(function(err, parrots) {
if(err) return res.send({ flag:false, data:[], message:"Error." });
if(parrots && parrots.length){
return res.send({ flag:true, data:parrots, message:"Success." });
}
else{
return res.send({ flag:false, data:[], message:"Parrot list is empty." });
}
});
}
};
There is no soft-delete feature built into sails, and I doubt there will be.
Here's a challenge: why not write your own? Waterline supports class methods! Of course you would have to do it for each model or create a service... which might be even more effective.

Preventing Brute Force Using Node and Express JS

I'm building a website using Node and Express JS and would like to throttle invalid login attempts. Both to prevent online cracking and to reduce unnecessary database calls. What are some ways in which I can implement this?
rate-limiter-flexible package with Redis or Mongo for distributed apps and in-Memory or with Cluster helps
Here is example with Redis
const { RateLimiterRedis } = require('rate-limiter-flexible');
const Redis = require('ioredis');
const redisClient = new Redis({
options: {
enableOfflineQueue: false
}
});
const opts = {
redis: redisClient,
points: 5, // 5 points
duration: 15 * 60, // Per 15 minutes
blockDuration: 15 * 60, // block for 15 minutes if more than points consumed
};
const rateLimiter = new RateLimiterRedis(opts);
app.post('/auth', (req, res, next) => {
// Consume 1 point for each login attempt
rateLimiter.consume(req.connection.remoteAddress)
.then((data) => {
const loggedIn = loginUser();
if (!loggedIn) {
// Message to user
res.status(400).send(data.remainingPoints + ' attempts left');
} else {
// successful login
}
})
.catch((rejRes) => {
// Blocked
const secBeforeNext = Math.ceil(rejRes.msBeforeNext / 1000) || 1;
res.set('Retry-After', String(secBeforeNext));
res.status(429).send('Too Many Requests');
});
});
Maybe something like this might help you get started.
var failures = {};
function tryToLogin() {
var f = failures[remoteIp];
if (f && Date.now() < f.nextTry) {
// Throttled. Can't try yet.
return res.error();
}
// Otherwise do login
...
}
function onLoginFail() {
var f = failures[remoteIp] = failures[remoteIp] || {count: 0, nextTry: new Date()};
++f.count;
f.nextTry.setTime(Date.now() + 2000 * f.count); // Wait another two seconds for every failed attempt
}
function onLoginSuccess() { delete failures[remoteIp]; }
// Clean up people that have given up
var MINS10 = 600000, MINS30 = 3 * MINS10;
setInterval(function() {
for (var ip in failures) {
if (Date.now() - failures[ip].nextTry > MINS10) {
delete failures[ip];
}
}
}, MINS30);
So after doing some searching, I wasn't able to find a solution I liked so I wrote my own based on Trevor's solution and express-brute. You can find it here.
okk,i found the solution of max login attemp on wrong password in mongoose and expressjs.there is a solution.
*first we will define the user schema
*second we will define the max login on wrongpassword handler function.
*third when we will create the login api then we will check this function that how many times user login with wrong password.so be ready for code
var config = require('../config');
var userSchema = new mongoose.Schema({
email: { type: String, unique: true, required: true },
password: String,
verificationToken: { type: String, unique: true, required: true },
isVerified: { type: Boolean, required: true, default: false },
passwordResetToken: { type: String, unique: true },
passwordResetExpires: Date,
loginAttempts: { type: Number, required: true, default: 0 },
lockUntil: Number,
role: String
});
userSchema.virtual('isLocked').get(function() {
return !!(this.lockUntil && this.lockUntil > Date.now());
});
userSchema.methods.incrementLoginAttempts = function(callback) {
console.log("lock until",this.lockUntil)
// if we have a previous lock that has expired, restart at 1
var lockExpired = !!(this.lockUntil && this.lockUntil < Date.now());
console.log("lockExpired",lockExpired)
if (lockExpired) {
return this.update({
$set: { loginAttempts: 1 },
$unset: { lockUntil: 1 }
}, callback);
}
// otherwise we're incrementing
var updates = { $inc: { loginAttempts: 1 } };
// lock the account if we've reached max attempts and it's not locked already
var needToLock = !!(this.loginAttempts + 1 >= config.login.maxAttempts && !this.isLocked);
console.log("needToLock",needToLock)
console.log("loginAttempts",this.loginAttempts)
if (needToLock) {
updates.$set = { lockUntil: Date.now() + config.login.lockoutHours };
console.log("config.login.lockoutHours",Date.now() + config.login.lockoutHours)
}
//console.log("lockUntil",this.lockUntil)
return this.update(updates, callback);
};
here is my login function where we have checked the max login attempt on wrong password.so we will call this function
User.findOne({ email: email }, function(err, user) {
console.log("i am aurhebengdfhdbndbcxnvndcvb")
if (!user) {
return done(null, false, { msg: 'No user with the email ' + email + ' was found.' });
}
if (user.isLocked) {
return user.incrementLoginAttempts(function(err) {
if (err) {
return done(err);
}
return done(null, false, { msg: 'You have exceeded the maximum number of login attempts. Your account is locked until ' + moment(user.lockUntil).tz(config.server.timezone).format('LT z') + '. You may attempt to log in again after that time.' });
});
}
if (!user.isVerified) {
return done(null, false, { msg: 'Your email has not been verified. Check your inbox for a verification email.<p><i class="material-icons left">email</i>Re-send verification email</p>' });
}
user.comparePassword(password, function(err, isMatch) {
if (isMatch) {
return done(null, user);
}
else {
user.incrementLoginAttempts(function(err) {
if (err) {
return done(err);
}
return done(null, false, { msg: 'Invalid password. Please try again.' });
});
}
});
});
}));
Have a look on this: https://github.com/AdamPflug/express-brute
A brute-force protection middleware for express routes that rate-limits incoming requests, increasing the delay with each request in a fibonacci-like sequence.
I myself wondered how to tackle this, but I tried the following and I am not sure how good is it in terms of performance and good code.
Basically, I created a flag in my Schema called "login attempts" and set it to 0
Then in the login process, I do the following: compare the password, if it's okay then I log in. Else, I increment the login attempt flag in my DB each time the user enters the wrong password. If the login attempts exceed 3, I display an error message saying that you exceeded login attempts.
Now up to this point everything works, the next part is pretty much way of switching that flag to zero.
Now I used setTimeout function to run after 5 mins and switch that flag to 0 and it worked.
My main concern: Is it safe to use setTimeout like this.
the other concern is how is this going to affect the performance.
So in terms of getting the job done, it's working but in terms of performance and best method, I am not sure about that.

Resources