jonathanwexler / get-programming-with-nodejs Goto Github PK
View Code? Open in Web Editor NEWCode samples for Get Programming with Node.js (See verhagen's VM setup for exercises in this book: https://github.com/verhagen/get-programming-with-nodejs)
Code samples for Get Programming with Node.js (See verhagen's VM setup for exercises in this book: https://github.com/verhagen/get-programming-with-nodejs)
If you sign up with an email with a dot (eg [email protected]), the dot is removed in usersController.validate
before being saved to the database.
If you later try to log in with that email with a dot, since the email does not get sanitized in usersController.validate
and thus the dot is not removed, the input email and the database email don't match, so you can't log in. The error flash message just says "Failed to login.".
If you add some code in usersController.authenticate
you can see that the info param in passport.authenticate
callback is "[IncorrectUsernameError]: Password or username is incorrect".
One option to fix this is to run the express-validator code in /users/login like is done in /users/create:
router.post(
'/users/login',
usersController.validate, // <-- add this
usersController.authenticate
)
Here I implemented the passport local strategy independent of mongoose database which is helpful for implementing your authentication method. we need four functions. The rest done by Passport itself
1-verifyCallback: it is a function which receive three arguments (username,password,done)
2-serializeCallback:it is a function which receive two arguments (user,done)
3-deserializeCallback:it is a function which receive two arguments (userId,done)
4-createStrategy:this function create an strategy to use by passport.
Here I implemented very basic of this fours:
const LocalStrategy=require("passport-local").Strategy;
const verifyCallback=(username,password,done)=>{
//Here I only return the case of authenticating for implementing a
//true logic you can first check user and password combination to be true
//according to saved data in database. in case of success you should return
//done(null,returnedUser). In case of mismatch return done(null,false,{message:""})
//and in case of Error in process return done(error); in other two case
// the first argument is null
return done(null,{id:1,username:"ardeshir",age:"33",fullname:"Ashraf Hakimi"});
}
const createStrategy=()=>new LocalStrategy({
usernameField:"username",
passwordField:"password"
},verifyCallback);
const serializeCallBack=(user,done)=>{
//you can serialize every field of user which you like
//normally user_id or username or email which is unique to find user
//quickly in deserialize callback
return done(null,user);
}
const desrializeCallBack=(userId,done)=>{
//Here you can implement a logic to return full user data from user id and then return it
//instead of userId itself!
return done(null,userId);
}
module.exports={createStrategy,serializeCallBack,desrializeCallBack}
then you need to configure passport to use this 4 functions:
passport.use(customeAuthModule.createStrategy());
passport.serializeUser(customeAuthModule.serializeCallBack);
passport.deserializeUser(customeAuthModule.desrializeCallBack);
Then we need to introduce passport to express:
app.use(passport.initialize());
app.use(passport.session());
And then define the routing middlewares as followes:
const customAuthRouter=express.Router();
customAuthRouter.get("/login",(req,res,next)=>{res.render("custome-auth/custome-login");});
customAuthRouter.post("/login",passport.authenticate("local",{
successRedirect:"/",failureRedirect:"/custome-auth/login",successFlash:"Successfull",failureFlash:true
}));
module.exports=customAuthRouter;
and use this router in your main app. You can design your login form whatever you like including username and password inputs!
Any suggestions?
Trying to work through the lessons, but keep getting this console error that wastes 1.5 hours per lesson. Am 99% certain it comes from Express. Don't know what causes the error because it comes and goes inexplicably. Source code doesn't seem to be the problem. The requested page always renders perfectly, the data set logs in terminal, then the verbose terminal error logs. Here are 4 things I've done to "fix" the problem:
res.render("subscribers", { subscribers: req.data });
, seems to be redundant to the same call in getAllSubscribers.rm -rf node_modules
then npm i
. Problem disappeared, again no code changes.node main.js
or npm start
, it doesn't matter. Went to http://localhost:3000/subscribers - errors re-appear.Not fun. This is the error copied from my terminal:
`ERROT state: Error: Failed to lookup view "404 | The page does not exist!" in views directory "/home/myHome/develop/recipe_app/views"
at Function.render (/home/myHome/develop/recipe_app/node_modules/express/lib/application.js:580:17)
at ServerResponse.render (/home/myHome/develop/recipe_app/node_modules/express/lib/response.js:1012:7)
at ServerResponse.res.render (/home/myHome/develop/recipe_app/node_modules/express-ejs-layouts/lib/express-layouts.js:77:18)
at exports.respondNoResourceFound (/home/myHome/develop/recipe_app/controllers/errorController.js:19:8)
at Layer.handle [as handle_request] (/home/myHome/develop/recipe_app/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/home/myHome/develop/recipe_app/node_modules/express/lib/router/index.js:317:13)
at /home/myHome/develop/recipe_app/node_modules/express/lib/router/index.js:284:7
at Function.process_params (/home/myHome/develop/recipe_app/node_modules/express/lib/router/index.js:335:12)
at next (/home/myHome/develop/recipe_app/node_modules/express/lib/router/index.js:275:10)
at Layer.handle [as handle_request] (/home/myHome/develop/recipe_app/node_modules/express/lib/router/layer.js:91:12)
at trim_prefix (/home/myHome/develop/recipe_app/node_modules/express/lib/router/index.js:317:13)
at /home/myHome/develop/recipe_app/node_modules/express/lib/router/index.js:284:7
at Function.process_params (/home/myHome/develop/recipe_app/node_modules/express/lib/router/index.js:335:12)
at next (/home/myHome/develop/recipe_app/node_modules/express/lib/router/index.js:275:10)
at jsonParser (/home/myHome/develop/recipe_app/node_modules/body-parser/lib/types/json.js:110:7)
at Layer.handle [as handle_request] (/home/myHome/develop/recipe_app/node_modules/express/lib/router/layer.js:95:5)
TypeError [ERR_INVALID_ARG_VALUE]: The argument 'id' must be a non-empty string. Received ''
at Module.require (internal/modules/cjs/loader.js:1037:11)
at require (internal/modules/cjs/helpers.js:77:18)
at new View (/home/myHome/develop/recipe_app/node_modules/express/lib/view.js:81:14)
at Function.render (/home/myHome/develop/recipe_app/node_modules/express/lib/application.js:570:12)
at ServerResponse.render (/home/myHome/develop/recipe_app/node_modules/express/lib/response.js:1012:7)
at ServerResponse.res.render (/home/myHome/develop/recipe_app/node_modules/express-ejs-layouts/lib/express-layouts.js:77:18)
at exports.respondInternalError (/home/myHome/develop/recipe_app/controllers/errorController.js:29:8)
at Layer.handle_error (/home/myHome/develop/recipe_app/node_modules/express/lib/router/layer.js:71:5)
at trim_prefix (/home/myHome/develop/recipe_app/node_modules/express/lib/router/index.js:315:13)
at /home/myHome/develop/recipe_app/node_modules/express/lib/router/index.js:284:7
at Function.process_params (/home/myHome/develop/recipe_app/node_modules/express/lib/router/index.js:335:12)
at next (/home/myHome/develop/recipe_app/node_modules/express/lib/router/index.js:275:10)
at /home/myHome/develop/recipe_app/node_modules/express-ejs-layouts/lib/express-layouts.js:80:40
at Function.render (/home/myHome/develop/recipe_app/node_modules/express/lib/application.js:582:14)
at ServerResponse.render (/home/myHome/develop/recipe_app/node_modules/express/lib/response.js:1012:7)
at ServerResponse.res.render (/home/myHome/develop/recipe_app/node_modules/express-ejs-layouts/lib/express-layouts.js:77:18)
`
solve this Error=> Error: Failed to lookup view "index" in views directory
should set path "views" on your project:
add this code for solve: app.set("views",__dirname+'\name you directory');
After you join the courses event listener that added for the button remain unchanged which make errors if you try to click on them!
But if use the dynamic binding of event in JQuery this problem doesnt raise.
consider the original course:
let addJoinButtonListener=()=>{
$(".join-button").click(event => {
let $button = $(event.target),
courseId = $button.data("id");
debugger;
$.get(`/_api/courses/${courseId}/join`, (data) => {
if (data.success) {
$button
.text("Joined")
.addClass("joined-button")
.removeClass("join-button");
} else {
$button.text("Try again");
}
});
});
}
The first problem here is that the button should be added to content (As mentioned in the book). But the main problem is that this event remain intact after joining to course.
consider the improved version of could below:
//this dynamicly set event when needed and can do without the content of modal
let addJoinButtonListenerDynamic=()=>{
$("#myModal").on("click","button.join-button",function(event) {
// let $button=$(event.target);
let $button=$(this);
let courseId=$button.data("id");
debugger;
$.get(`/_api/courses/${courseId}/join`,data=>{
if (data.success) {
$button.text("Joined")
.addClass("joined-button")
.removeClass("join-button");
} else {
$button.text("Try Again");
}
});
});
}
first this add everywhere in the code even during page load! secondly and mainly after removing the join-button the event doesn't fire if we click at the button because there is only one event an it is on Modal and dynamicly decide to work on every click if it was join-button!
add this code to project for read static files
app.use(express.static(__dirname+'/public'));
Unfortunately the express validator mechanism has changed and the book solution doesn't work because in new versions, every validator is a middle-ware so you can use them directly in route definitions or as I do, use the following approach.
1- Define Validation Rules almost similar to past in an array. I do this in user controller and I import express validator first:
`
const expressValidator=require("express-validator");
const validationsRules=[
expressValidator.sanitizeBody("email")
.normalizeEmail({
all_lowercase:true
}).trim(),
expressValidator.body("email","Email is Invalid").isEmail(),
expressValidator.body("zipCode","Zip Code is Invalid")
.isInt().notEmpty().isLength({min:5,max:5}),
expressValidator.body("password","Password Is Required")
.notEmpty()
];
`
2- Due to asynchronous nature of middle-wars you can define and use one of the following middle-wares (both worked true in my case!)
validateAsync:async(req,res,next)=>{
await Promise.all(validationsRules.map(v=>v.run(req)));
const err=expressValidator.validationResult(req);
if (err.isEmpty()) {
next();
} else {
const message=err.array().map(e=>e.msg).join(" and ");
req.flash("error",message);
res.redirect("/users/new");
}
},
validate:(req,res,next)=>{
Promise.all(validationsRules.map(v=>v.run(req)))
.then((result)=>{
const err=expressValidator.validationResult(req);
if (err.isEmpty()) {
next();
} else {
const message=err.array().map(e=>e.msg).join(" and ");
req.flash("error",message);
res.redirect("/users/new");
}
})
.catch(err=>{
next(err);
});
},
According to the MongoDB/Mongoose docs, the usage of callback functions has been deprecated in Mongoose 7. x. Therefore, the sample code needs to be updated to comply with the changes especially in lesson_13, lesson_14, and possibly lesson_15, if callback functions is still used in the API calls.
Unfortunately with Heroku I can not setup my mongo db service from MongoLab because They dont provide service for Heroku. There is not no free other services there. Altough Microsoft Azure provide similar services which can be search easily from internet.
I just ended the development phase of project and I am really and absolutely greatefull for this comprehensive book. It covers (Altough abstract) all the matter related to backend development including routing/ database/api/authentication/cookie and session/ static and dynamic html files/ work with files and streams/ web socket/ json web token/ cookie based Auth and importantly MVC structure in Express JS which is really valuable.
Thanks again
Because JWT signing middle-ware is post and needs email and password it may seems hard to use JWT in client side. The point is in changing that middle-war for Authenticated User.
We first change that middle-ware as follows:
apiAuthenticate:(req,res,next)=>{
if (req.isAuthenticated()) {
let user=req.user;
if (user) {
let signedToken=JWT.sign({
data:user._id,
exp:(new Date()).setMinutes((new Date()).getMinutes()+5)
},jwtSecret);
res.status(statusCodes.OK);
return res.json({
status:statusCodes.OK,
success:true,
token:signedToken
})
} else {
res.status(statusCodes.UNAUTHORIZED);
return res.json({
status:statusCodes.UNAUTHORIZED,
success:false,
message:"Couldn't Authenticate User!"
});
}
}
passport.authenticate("local",(err,user)=>{
if (user) {
let signedToken=JWT.sign({
data:user._id,
exp:(new Date()).setDate((new Date()).getDate()+1)
},jwtSecret);
res.status(statusCodes.OK);
return res.json({
status:statusCodes.OK,
success:true,
token:signedToken
})
} else {
res.status(statusCodes.UNAUTHORIZED);
return res.json({
status:statusCodes.UNAUTHORIZED,
success:false,
message:"Couldn't Authenticate User!"
});
}
})(req,res,next);
}
Then this api return a token to browser too. we can save the generated token in browser and use it second times or you can request it every time need it (which needs more resource in server!) or even you can use it in session too. By the way I choose to request it with every web request I send. So I change my Ajax codes as follow:
$(function() {
$("#modal-button").on('click',function() {
// let apiToken=0;
// if ($("#apiToken").length) {
// apiToken=$("#apiToken").val();
// }
// $("#myModal .modal-body").load("/courses?format=json");
$("#myModal .modal-body").html('');
$.post("/_api/login",(data)=>{
let apiToken=data.token;
$.ajax({
url:`/_api/courses`,
headers:{token:apiToken},
success:(result)=>{
result.data.forEach(course => {
$("#myModal .modal-body").append(
`<div class="course">
<span class="course-title">
${course.title}
</span>
<button class="${'button'+" "+(course.isJoined?'joined-button':'join-button')}" data-id="${course._id}" >${course.isJoined?"Leave":"Join"}</button>
<div class="course-description">
${course.description}
</div>
</div>
`
);
});
addJoinButtonListenerDynamic(apiToken);
},
error:(response,status,xhr)=>{console.log(status);}
});
});
});
})
//this dynamicly set event when needed and can do without the content of modal
let addJoinButtonListenerDynamic=(apiToken)=>{
// let apiToken=0;
// if ($("#apiToken").length) {
// apiToken=$("#apiToken").val();
// }
$("#myModal").on("click","button.join-button",function(event) {
// let $button=$(event.target);
let $button=$(this);
let courseId=$button.data("id");
$.ajax({
url:`/_api/courses/${courseId}/join`,
headers:{token:apiToken},
success:data=>{
if (data.success) {
$button.text("Joined")
.addClass("joined-button")
.removeClass("join-button");
} else {
$button.text("Try Again");
}
},
error:(response,status,xhr)=>{console.log(status);}
});
});
$("#myModal").on("click","button.joined-button",function(event) {
// let $button=$(event.target);
let $button=$(this);
let courseId=$button.data("id");
$.ajax({
url:`/_api/courses/${courseId}/leave`,
headers:{token:apiToken},
success:data=>{
if (data.success) {
$button.text("Join")
.removeClass("joined-button")
.addClass("join-button");
} else {
$button.text("Try Again");
}
},
error:(response,status,xhr)=>{console.log(status);}
});
});
}
The book suggests using middle-ware to add a isJoind property to courses. We can here move some code to course instance methods. We add an instance method to course which receives a users and then return a true if that user joined that course and return false in other cases. The code below added to Course Schema:
courseSchema.methods.isUserJoined=function(user) {
const course=this;
if (!user) {return false;}
return user.courses.indexOf(course._id.toString())>-1;
}
Then we add it to the middle-ware as follows:
res.locals.courses=res.locals.courses
.map(course=>
Object.assign(course.toObject(),
{isJoined:course.isUserJoined(req.user)})
);
As the book suggests here we should use toObject
to first convert mongoose document to POCO.
In Unit 5 lesson 23 You presented the pre("save")
hook for Hashing password which unfortunately doesn't work in Update case as I find out after a thorough search and attempts. If users try to edit their password it saves unhashed in database!
Similar approach proposed by many people in the internet which all rely in the assumption that this hook fire before update too which is not the case. I really disappointed from MongoDB and Mongoose!
Similar conversation:
Github
https://stackoverflow.com/questions/14588032/mongoose-password-hashing
I change my code in order to updating works as expected
updateUser:(req,res,next)=>{
let userId=req.params.id;
User.findById(userId)
.then(user=>{
user.name={
first:req.body.first,
last:req.body.last
};
user.email=req.body.email;
user.password=req.body.password;
user.zipCode=req.body.zipCode;
return user.save();
})
.then(u=>{
res.locals.user=u;
next();
})
.catch(err=>{
res.locals.errorMessage=err.message;
res.render("error");
});
There is a parameter named new
in findByIdAndUpdate
method which change the returned value in then
part of Method. Al tough we don't encounter this in the exact copy of your code but it needs attention. consider the following case with small change from lesson 21:
Course.findByIdAndUpdate(courseId, {
$set: courseParams
},{new:true})
.then(course => {
res.locals.course = course;
res.render("courses/show");
})
.catch(error => {
console.log(`Error updating course by ID: ${error.message}`);
next(error);
});
Here if you use not the new:true
the redirected view (with direct res.render
and not with res.redirect)
doesnt show the edited version of course!
In this method for authentication I send my requests with an extra headers as follows:
$.post("/_api/socket/login",(data)=>{
console.log(data);
let apiToken=data.token;
const socket=io({extraHeaders:{token:apiToken},auth:{token:apiToken}});
});
With the first post request we get a token (It is done in express so we easily can get a token in the case of user is authenticated) and then I initialize my socket with an extraHeaders (I can't get auth in express middlewares! Altough I think it dont different because of nature of tokens). Then I add the following middlewares in my io Initialization in server:
socketGetToken:(req,res,next)=>{
if (req.isAuthenticated()) {
let user=req.user;
if (user) {
let signedToken=JWT.sign({
data:user._id,
exp:(new Date()).setMinutes((new Date()).getMinutes()+5)
},jwtSecret);
res.status(statusCodes.OK);
return res.json({
status:statusCodes.OK,
success:true,
token:signedToken
})
} else {
res.status(statusCodes.UNAUTHORIZED);
return res.json({
status:statusCodes.UNAUTHORIZED,
success:false,
message:"Couldn't Authenticate User!"
});
}
}
passport.authenticate("local",(err,user)=>{
if (user) {
let signedToken=JWT.sign({
data:user._id,
exp:(new Date()).setDate((new Date()).getDate()+1)
},jwtSecret);
res.status(statusCodes.OK);
return res.json({
status:statusCodes.OK,
success:true,
token:signedToken
})
} else {
res.status(statusCodes.UNAUTHORIZED);
return res.json({
status:statusCodes.UNAUTHORIZED,
success:false,
message:"Couldn't Authenticate User!"
});
}
})(req,res,next);
},
verifyJWTSocket:(req,res,next)=>{
console.log(req)
let jwt=req.headers.token;
if (!jwt) {
next(new Error("UnAuthorized!"));
}
JWT.verify(jwt,jwtSecret,(err,payload)=>{
if (!payload) {
next(new Error("UnAuthorized!"));
}
User.findById(payload.data)
.then(user=>{
// req.logIn(user,err=>{});//configure current user for using in API for example enrolment
req.user=user; //This is better in case of no session or cookie!
next();
})
.catch(err=>{
next(new Error(err.message));
})
})
},
const wrapper=middleware=>(client,next)=>middleware(client.request,{},next);
module.exports=(io)=>{
io.use(wrapper(userController.verifyJWTSocket));
io.use((client,next)=>{
if (client.request.user) {
next();
}
else {
next(new Error("UnAuthorized!"));
}
});
io.on("connection",client=>{
console.log(client.request.user)
console.log("Socket Connected");
});
}
As you can see we have current user too according the requestee of token and we can be assure that no one can don't fake itself instead of Others! (which is simply possible using hidden fields in html!)
As you see the socket.io in server doesnt know sessions and it doesn't place in the middleware chains so you can get them authenticated or not which is not very secure. For this reason, there is methods and tools to authenticated them.
One way is using passport as socket.io suggestes. But I try different methods including manual authentication using call back but I always get error that there is no credentials. But after some search I find methods which the following method is simple. It needs a package named passport.socketio. Its idea is to share session used in express with socket. so If you authenticated in express you authenticated at socket and vice versa.
They suggest not to use memory store for session but as we use it for learning I found a method to use it. First I create an store using the following code:
var sessionStore= new MemoryStore();
Then I introduce it to express-session. (I declare it in main.js but you can declare them in separate module and then export and use them!)
app.use(expressSession({
store:sessionStore,
secret:app.get("secret"),
cookie:{
maxAge:4000000
},
saveUninitialized:false,
resave:false
}));
Then I use the package in chatController in socket initialization function as a socket middlerware
io.use(passportSocketIo.authorize({
cookieParser: cookieParser, // the same middleware you registrer in express
key: 'connect.sid', // the name of the cookie where express/connect stores its session_id
secret: secret, // the session_secret to parse the cookie
store: sessionStore // we NEED to use a sessionstore. no memorystore please
// success: onAuthorizeSuccess, // *optional* callback on success - read more below
// fail: onAuthorizeFail, // *optional* callback on fail/error - read more below
}));
io.use((socket, next) => {
if (socket.request.isAuthenticated()) {
next();
} else {
next(new Error("unauthorized"))
}
});
for using same session I define same session.id, same store and same secret as you see.
Then everything is authenticated as they say!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.