12 apps in 12 weeks - App 2

11 min. read

Intro

So this app was a big challenge for me. This is like a little social app, so with a NOSQL db this is not the much easy want to do. So I learn a lot about working with the populate method etc and make relations with document. I learn to how make a reset password + forgot password features, something I never did before. I take this course for help me grasp MongoDB and I recommended it at 110%.

Why this app ?

When I was student at Thinkful I get struggle to find an idea for project to build. I found this website http://www.ideaswatch.com/ where people share idea and people comment. I found this idea great but this website don’t look like what I like. So I decided to cloned it with a bit more ux/ui.

Stacks

I use React with Redux for this project on the front-end. I really like to work with React. Make me feel have superpower, but this is not comming from React. Much more from redux, yes I build Redux app with React not the inverse. This is something I learn by building some little app. “Think in Redux way first, after build React component”. For the css framework I use Semantic-UI and I love it.

For the backend I use the famous Express.JS with Mongoose who is the driver for MongoDB database.

Features

  • Auth

    • Login user
    • Signup user
    • Forgot password
    • Reset password
    • JWT token
    • Sign out user if token expires
  • Idea

    • Create idea
    • Create comment belongs idea
    • See the author of the comments
    • Can follow idea
  • UI/UX

    • Can see how many followers each idea have
    • Can see how many comments each idea have

Some code

So code have done for the first time and was a good learning experience.

CheckToken

This code is the checkToken helpers. This is for be sure user get sign out if the token expires.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import jwt from 'jsonwebtoken';
import serverConfig from '../config/serverConfig';
import { generateToken, setUserInfo } from './index';
import { User } from '../modules';
export const checkToken = (req, res) => {
const { token } = req.body;
if (!token) {
return res.status(401).json({ success: false, message: 'Must pass token' });
}
jwt.verify(token.replace(/^JWT\s/, ''), serverConfig.JWT_SECRET, (err, decoded) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(422).json({
success: false,
expireTime: true,
message: 'Token expires plz log again, this is for your security!'
});
}
return res.status(422).json({ success: false, message: 'Token problem' });
}
User.findById(decoded.sub)
.then(user => res.status(201).json({
success: true,
message: 'Token refresh!',
token: `JWT ${generateToken(user)}`,
user: setUserInfo(user)
}))
.catch(error => res.status(401).json({ success: false, message: 'Something wrong happen with the token!', error }));
});
};

You can see I need to replace the JWT word cause of jsonwebtoken packages not want it but Passport.JS yes. I found in the error object coming from jsonwebtoken the name TokenExpiredError so I just handle it easy with a response to the front end.

If the token is not expires I give it back a new one to the user. So a user who play with the app can stay log all time.

Slug the title

Have id in the url is not user friendly. So because my title is unique in my schema I use the slug feature to make it.

1
2
3
4
5
6
7
8
9
export const slugifyText = text =>
text
.toString()
.toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/[^\w-]+/g, '') // Remove all non-word chars
.replace(/\-\-+/g, '-') // Replace multiple - with single -
.replace(/^-+/, '') // Trim - from start of text
.replace(/-+$/, '');

Simple code here but, we lowercase the title and we replace all space with - and don’t forget to trim at the end.

Async Email and Username

With Redux-Form we can handle async validation and this how I deal in the backend.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export const asyncEmail = (req, res) => {
const { email } = req.body;
User.findOne({ 'local.email': email })
.then(user => {
// if user we send object { exist: true } who is use in the front end
if (user) {
return res.json({ message: 'Email taken!', exist: true });
}
return res.json({ message: 'Email available', exist: false });
})
.catch(err => res.json({ err }));
};
export const asyncUsername = (req, res) => {
const { username } = req.body;
User.findOne({ username })
.then(user => {
// if user we send object { exist: true } who is use in the front end
if (user) {
return res.json({ message: 'Username taken!', exist: true });
}
return res.json({ message: 'Username available', exist: false });
})
.catch(err => res.json({ err }));
};

Forgot Password

That was one of the biggest challenge of this app. I was stuck cause I never really think of the idea behind this. This is the first I really touch full authentication in a app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export const forgotPassword = (req, res) => {
const { email } = req.body;
User.findOne({ 'local.email': email })
.then(user => {
if (!user) { return res.status(422).json({ success: false, message: 'This email not exist try again!' }); }
crypto.randomBytes(48, (err, buffer) => {
if (err) { return res.status(400).json({ success: false, message: 'Something wrong happen', error: err }); }
const resetToken = buffer.toString('hex');
user.resetPasswordToken = resetToken; // eslint-disable-line
user.resetPasswordExpires = Date.now() + (60000 * 60); // eslint-disable-line
user.save()
.then(u => {
emailHelpers(u, 'GiveMeThatIdea - Forgot Password', null, resetToken, err => { // eslint-disable-line
if (err) { return console.log(err); }
return res.status(201).json({ success: true, message: 'Message on the way!' });
});
})
.catch(err => console.log(err)); // eslint-disable-line
});
})
.catch(error => res.status(400).json({ success: false, message: 'Something wrong happen', error }));
};

So this is my step:

  1. Get the user by is email. “Yes the user must remember it I hope haha”
  2. Make a randomToken using crypto
  3. Pass this token to the resetPasswordToken prop of the user.
  4. Add resetPasswordExpires with the data now + 1 hours for security.
  5. Send the user a email
  6. Put the link for the resetpassword with the token as params.

Reset Password

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const resetPassword = (req, res) => {
const { resetToken } = req.params;
if (!resetToken) {
return res.status(404).json({ success: false, message: 'Not supposed to be there' });
}
User.findOne({ resetPasswordToken: resetToken, resetPasswordExpires: { $gt: Date.now() } })
.then(user => {
if (!user) {
return res.status(422).json({ success: false, message: 'ERROR ' });
}
return res.status(200).json({ success: true, message: 'Work' });
});
};
  1. Get the token by the params
  2. Find the user not by his email but by the resetPasswordToken prop + be sure is valid again with the resetPasswordExpires prop.
  3. Now we let the user it this url. If this is a bad token or expires, in the front end I handle a redirect

Change Password

So after that time to change the password.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export const changePassword = (req, res) => {
const { password } = req.body;
const { resetToken } = req.params;
User.findOne({ resetPasswordToken: resetToken, resetPasswordExpires: { $gt: Date.now() } })
.then(user => {
if (!user) {
return res.status(422).json({ success: false, message: 'ERROR ' });
}
// no need anymore
user.resetPasswordToken = undefined; // eslint-disable-line
user.resetPasswordExpires = undefined; // eslint-disable-line
// make use of new password
user.local.password = password; // eslint-disable-line
user.save()
.then(u => {
emailHelpers(u, 'GiveMeThatIdea - New Password', null, null, err => { // eslint-disable-line
if (err) { return console.log(err); }
return res.status(201).json({ success: true, message: 'Message on the way!' });
});
})
.catch(err => console.log(err));
});
};
  1. Get the password from the body
  2. Find the user with the token in the params
  3. Resave the user with the new password.
1
2
3
4
5
6
7
8
9
10
11
12
13
UserSchema.pre('save', function(next) {
// check if it's new password or update one so we don't touch it
if (!this.isModified('local.password')) { return next(); }
// number of round for salt
const SALT_ROUNDS = 10;
// hash the password
hash(this.local.password, SALT_ROUNDS, (err, hashPasword) => {
if (err) { return next(err); }
// make the local.password the result of the hash
this.local.password = hashPasword;
next();
});
});

When I resave you can see the pre('save') method handle the hashing part.

Create a comment

Not sure if this is the best solution but that was a good challenge cause I need to it multiple model in same time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
export const createComment = (req, res) => {
const { text } = req.body;
const userId = req.user._id;
if (!text || !userId) {
return res.status(422).json({ success: false, message: 'Need title and author!' });
}
const ideaPromise = new Promise((resolve, reject) => {
return Idea.findById(req.params.id)
.then(
idea => resolve(idea),
error => reject(error)
);
});
const userPromise = new Promise((resolve, reject) => {
return User.findById(userId)
.then(
user => resolve(user),
error => reject(error)
);
});
const promiseAll = Promise.all([ideaPromise, userPromise])
.then(
values => {
const idea = values[0];
const user = values[1];
const newComment = new Comment({ text, author: user._id, idea: idea._id });
return newComment.save()
.then(comment => {
idea.comments.push(comment);
return idea.save()
.then(
() => {
user.comments.push(comment);
return user.save()
.then(
() => res.status(201).json({ success: true, comment: setCommentInfo(comment) }),
error => res.status(422).json({ success: false, error })
);
},
error => res.status(422).json({ success: false, error })
);
});
},
error => res.status(422).json({ success: false, error })
);
return promiseAll;
};
  1. Create a userPromise for get the user from the id in the jwt
  2. Get the idea by the params
  3. Create a a other promise who take the two one and make the comment by passing info after get the values of each.

Last Word

Hope this post was helpful for the one who like me never touch some of this features. I’m thinking about making a video tutorial on this challenge and post it on my Youtube Channel. Let me know it you think this is a good idea.

This is the link again for this app.