User Permission - Display edit in view (express, handlebars) - node.js

At the moment I have user profiles that are rendered using Handlbars as such:
exports.profile = function(req, res) {
User.findOne({username: req.params.username}).exec(function(err, user){
res.render('profile/view', {
user: req.user,
name: user.name,
username: user.username
});
});
};
On the rendered template at profile/view I would like to display an edit button if the user can edit the profile being viewed.
Edit your profile here
Additional information:
Every user us currently authenticated with passport-local strategy
Currently have some basic middleware on the route
Middleware
app.get('/:username', isAuth, user.profile);
function isAuth(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
req.flash('alert', 'First you need to log in');
res.redirect('/login');
}
I've looked into middleware for express such as connect-roles and I don't think its what I need?
What I'm thinking is that I should pass a boolean within the local variables of the res.render() method which then allow me to use handlebars to display the button or not.
{{#if profileOwner }}<a href="#">...
Any ideas?

The best way I think is creating an helper that check if userCan for you. Here the helper:
Handlebars.registerHelper('ifUserCan', function(action, options) {
var actions = action.split(/\s*,\s*/);
for (var i = 0; i < actions.length; i++) {
if (this.userCan(actions[i])) {
return options.fn(this);
}
}
return options.inverse(this);
});
You can use it as follow:
{{#ifUserCan 'show all store categories'}}
<li>{{__ 'Show categories'}}</li>
{{/ifUserCan}}

One way you could do this is by hydrating your template model with a profileOwner privilege as you suggested. Suppose you wanted to only allow users to edit their own profiles. Using connect-roles you could set up a rule like this:
user.use('profile owner', function (req, action) {
return req.isAuthenticated() &&
req.user.username === req.params.username;
})
And your route logic could then be:
exports.profile = function(req, res) {
User.findOne({username: req.params.username}).exec(function(err, user){
res.render('profile/view', {
user: req.user,
name: user.name,
username: user.username,
profileOwner: req.user.is('profile owner')
});
});
};
Then your mustache "if" syntax would look like this:
{{#profileOwner}}Tada{{/profileOwner}}
The negative case can be handled like so if you wish:
{{^profileOwner}}You are not a profile owner!{{/profileOwner}}

Related

Using two collection in MongoDB with Mongoose

I'm new to NodeJS, and I'm using the following:
Express
Mongoose
passport
HI I have two collections in MongoDB, they both have nothing to do with each other, they're called:
Post
and
User
In a get route for /home in an IF statement I am trying to query information from both collections and render them in an EJS page. My initial thoughts were create find for one model store in a var, then use it in another function when rendering the page.
Here is the relevant code:
app.get("/home", function(req, res){
console.log(req.user._id);
if (req.isAuthenticated()){
// //*Find current logged in user details
User.find({_id: req.user._id}, function(err, users){
var currentUserName = req.user.name
});
//*end
Post.find({}, function(err, posts){
res.render("home", {
startingContent: homeStartingContent,
posts: posts,
currentUser: currentUserName
});
});
} else {
res.redirect("login")
}
});
At the start of my js file, i have declared
var currentUserName
But its not displaying the value in the home.ejs for:
<p><%= currentUser %></p>
Its just blank.
What am I doing wrong here? I thought using var would make it a global variable and then can be used in another function? Other posts have suggested a aggregate, but these collections are completely seperate and have nothing to do with each other.
You have defined currentUserName inside User.find(){...} function and it is accessible only within that function. You need to define the variable that can be block-scoped, outside User.find(){...} function. Check the code snippet below:
app.get("/home", function(req, res){
console.log(req.user._id);
if (req.isAuthenticated()){
let currentUserName;
// //*Find current logged in user details
User.find({_id: req.user._id}, function(err, users){
currentUserName = req.user.name
});
//*end
Post.find({}, function(err, posts){
res.render("home", {
startingContent: homeStartingContent,
posts: posts,
currentUser: currentUserName
});
});
} else {
res.redirect("login")
}
});
Javascript best practices tells that it is better to use let instead of var. Variables declared by var keyword are scoped to the immediate function body i.e. User.find(){...} while let variables are scoped to the immediate enclosing block denoted by { }, i.e. if(req.isAuthenticated()){...}.
It won't work, because every variable has scope. Scope means access to use that var. In this case currentUserName has scope or can be used only in User.find() section. If you want to use it in Post.find(), then do it by declaring it in main function as below.
app.get("/home", function(req, res){
console.log(req.user._id);
let currentUserName;
if (req.isAuthenticated()){
User.find({_id: req.user._id}, function(err, users){
currentUserName = req.user.name
});
Post.find({}, function(err, posts){
res.render("home", {
startingContent: homeStartingContent,
posts: posts,
currentUser: currentUserName
});
});
} else {
res.redirect("login")
}
});
It will work as you require by declaring currentUserName in main function, so it will be used in whole main app.get() function

Update data in MongoDB using Mongoose and Node.js

I am trying to update certain info in a user collection, when the user is visiting a page.
But my method doesn't work. Can anyone help to get it fixed.
app.get('/add-your-accommodation/apartment-type', (req, res, next) => {
if (req.isAuthenticated()) {
res.render('apartment-type.ejs')
} else {
res.render('login.ejs')
}
var id = req.params.id
if(mongoose.Types.ObjectId.isValid(id)) {
User.findByIdAndUpdate(id, {$set: {accomtype: 'house'}},{new: true})
}
});
Your req.params.id is undefined since there is no mention of it in the route path. You can do this,
app.get('/add-your-accommodation/apartment-type', (req, res) => {
if (!req.isAuthenticated()) {
return res.render('login.ejs')
}
res.render('apartment-type.ejs')
var id = req.user._id //since you're using passport (LocalStrategy)
if(mongoose.Types.ObjectId.isValid(id)) {
User.findByIdAndUpdate(id, {$set: {accomtype: 'house'}})
}
})
Now when you call your API, do it like this,
GET /add-your-accommodation/apartment-type
I agree with #kedar-sedai, when you update/change something in your DB, you should not use a GET request. A good practise would be to use the PUT method, even if you have nothing to pass in the body. It makes it easier for you and other developers to understand what your code does at a glance.
Here are 4 HTTP requests that will work in most of the use cases :
GET
You want to retrieve information from your DB (ex: get users, get all the apartment types...)
POST
You want to add information (ex: register user, add an apartment, ...), or send information using the body of the POST request (ex: login, ...)
PUT
You want to update a value (ex: change username, change an apartment type, ...)
DELETE
You simply want to delete something in your DB (ex: delete a user...)
Try findOneAndUpdate. Also, use callback in your query function for getting the error or result.
app.get('/add-your-accommodation/apartment-type/:id', (req, res, next) => {
if (req.isAuthenticated()) {
res.render('apartment-type.ejs')
} else {
res.render('login.ejs')
}
var id = req.params.id
if(mongoose.Types.ObjectId.isValid(id)) {
User.findOneAndUpdate({_id: mongoose.Types.ObjectId(id)}, { $set: { accomtype:'house' } },(err, result)=>{
if (err) throw new Error(err);
console.log(result)
return
})
}
});

How can i print only current user in HBS template

I want to print the currently logged in user, by getting the username from my database.
In my model, the username property holds the following schema:
{
type: String,
required: true
}
index.js:
const users = require('../model/db');
router.get('/profiles/instructor', function (req, res, next) {
users.find({}, 'username', (err, doc)=>{
if(err){
console.log('err while finding on instructor at index.js => ' + err)
}else{
console.log(doc)
res.render('./profiles/instructor', {
title: 'Courstak | Instructor Profile',
name: doc
// etc......
})
ex.hbs:
<h5>{{name}}</h5>
The issue that I am having is when I open my website, it shows all of the users within the database, not just the currently logged in user:
Screenshot of error:
You mentioned in the comments of your OP that you're using the passport-local strategy, and it would appear that it very nicely connects up with express in the way you would expect, so req.user should have the information you are looking for, which would make your code like so (assuming you're following the documentation to properly use passport-local):
router.get('/profiles/instructor', function (req, res, next) {
res.render('./profiles/instructor', {
title: 'Courstak | Instructor Profile',
name: req.user.username
});
});
What I would recommend also is using some middleware such as connect-ensure-login to ensure that the user is logged in and has a valid session. You can add the middleware above like so:
var connectEnsureLogin = require('connect-ensure-login')
router.get('/profiles/instructor',connectEnsureLogin.ensureLoggedIn(), function (req, res, next) {
// ...
}

nodejs passport - use same routes in api but return different sets of data based on permissions

Not sure of a clean way to go about his. Let's say I have this endpoint:
GET /api/Books/
For the user on the webservice, this will return only the user's resources. It might look a little something like this:
exports.getBooks = function(req, res) {
// find all books for user
BookModel.find({ userId: req.user._id }, function(err, books) {
if (err)
res.send(err);
res.json(books);
});
};
The web service using the api needs the user to be logged in first. I can achieve this using a basic passport strategy to ensure authentication. But let's say I have an admin account that needs to see ALL books ever recorded. What's more is that the admin account and user accounts have completely different properties so assigning a Boolean for permissions is not enough. Using the same endpoint:
GET /api/Books
I see no reason to write another endpoint to achieve this. However the difference would look like this:
exports.getBooks = function(req, res) {
// find all books in the database
BookModel.find({}, function(err, books) {
if (err)
res.send(err);
res.json(books);
});
};
However I cannot come up with a clean way to achieve this while also using the passport middlewear as it is intended like so:
router.route('/books')
.post(authController.isAuthenticated, bookController.postBooks)
.get(authController.isAuthenticated, bookController.getBooks);
The function isAuthenticated will will only verify whether or not the user requesting resources has permission and does not change the way the controller behaves. I'm open to ideas.
ANSWER
The user #ZeroCho suggested to check user properties in req.user object to determine what should be sent back. This was more simple than I expected. In my implementation for passport.BasicAuth strategy, I check which table has a matching doc. Once the user is found in the common user or Admin user table all you do is add a property in the isMatch return object.
// Basic strategy for users
passport.use('basic', new BasicStrategy(
function(email, password, done) {
verifyUserPassword(email, password,
function(err, isMatch) {
if(err) { return done(err); }
// Password did not match
if(!isMatch) { return done(null, false); }
// Success
var userInfo = {
email: email,
isAdmin: isMatch.isAdmin || false,
businessID: isMatch.businessID || false
};
return done(null, userInfo);
});
})
);
Then you can check if .isAdmin or .businessID is valid in your requests.
Just separate your controller by if statement
exports.getBooks = function(req, res) {
if (req.user.isAdmin) { // or some other code to check user is admin
// find all books in the database
BookModel.find({}, function(err, books) {
if (err)
res.send(err);
res.json(books);
});
} else {
BookModel.find({ userId: req.user._id }, function(err, books) {
if (err)
res.send(err);
res.json(books);
});
}
};

expressjs: Best way to handle multiple optional route params

I'm using nodejs Express. I would like to have a router that receive id or username as request param as below:
router.get('/:id?/:username?', authMiddleware, function (req, res, next) {
models
.User
.findAll({
where: {
$or: {
id: req.params['id'],
username: req.params['username']
}
}
})
.then(function (rows) {
res.send(rows);
})
}
However, express seems to not understand my request on:
http://localhost:3000/api/1.0/users//david
Which I would like to query user by username instead of id
The only way works right now is defining a route as: /:id_or_username as below:
router.get('/:id_or_username', function (req, res, next) {
models
.User
.findAll({
where: {
$or: {
id: req.params['id_or_username'],
username: req.params['id_or_username']
}
}
})
.then(function (rows) {
res.send(rows);
})
}
But it is kind of dirty and I'm not happy with that code.
Is that possible to define router in this case using REST style instead of :idOrName param?
Assuming your id is an int, you can do something like this:
router.get('/:id_or_username', function (req, res, next) {
var where = {}
var param = req.params['id_or_username']
if (isNaN(param)) {
where.username = param
} else {
where.id = param
}
models.User.findAll({where}).then(function (rows) {
res.send(rows)
})
}
Now you can send the id or the username using the same paramether
I am not sure how your system is setup but this sounds like a better use for a req.query instead of req.param.
You should use req.query when you are not sure what the input might be, and use req.param when passing information you are sure will be there. There is probably a way to do this with req.param but I am not sure I can think of a reason why you'd want to do it that way.
This way you could do an action like:
action = "/users/"+ id ? id : username
and then req.query for it.

Resources