Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Work with a team to improve the Todo List App #173

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
52 changes: 20 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,32 @@
# Introduction
# Todo List App
Create your own todo list powered by Passport.js, EJS, Node, MongoDB, and Express.

A Simple ToDo App is built using the MVC Architecture, we have also implemented "authorization" so folx can sign up, customize & personalize the app
![todolist](https://github.com/user-attachments/assets/5b89df51-c4dc-4676-b4d9-6e832e1fcfdb)

---
# Features

> Be sure to add that lovely star 😀 and fork it for your own copy
- Custom user profiles and authentication powered by Passport.js
- Add, delete, and mark todo list items as complete

---
#Installation

# Objectives

- It's a beginner level app created to understand how MVC concept and logins are added
`npm install`
- Create a `.env` file and add the following as `key: value`
- PORT: 2121 (can be any port example: 3000)
- DB_STRING: `your database URI`

## Lessons Learned

---
The goal of rebuilding this project was to become more familiar with the structure of full-stack apps, particularly MVC architecture. Most of the code that connected the backend to MongoDB needed to be rewritten to work in 2024.

# Who is this for?
The biggest challenge was rewriting the authentication code that allowed Passport to create and maintain sessions. The order that middleware is introduced in the main Node server file is critical to authentication working properly.

- It's for beginners & intermediates with little more experience, to help understand the various aspects of building a node app with some complex features
## Future enhancements

---
- The signup form would benefit from email and password validation.
- Todo list items should be editable.
- The frontend would benefit from having a more responsive framework like React.

# Packages/Dependencies used
## How It's Made

bcrypt, connect-mongo, dotenv, ejs, express, express-flash, express-session, mongodb, mongoose, morgan, nodemon, passport, passport-local, validator

---

# Install all the dependencies or node packages used for development via Terminal

`npm install`

---

# Things to add

- Create a `.env` file and add the following as `key: value`
- PORT: 2121 (can be any port example: 3000)
- DB_STRING: `your database URI`
---

Have fun testing and improving it! 😎


5 changes: 3 additions & 2 deletions config/.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
PORT = 2121
DB_STRING = mongodb+srv://demo:demo@cluster0.hcds1.mongodb.net/todos?retryWrites=true&w=majority
PORT=8000
SECRET=kuromi black
DB_STRING=mongodb+srv://mayecarver:KjFr1KquaVWqQfSw@cluster0.uwnzbd7.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
4 changes: 0 additions & 4 deletions config/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ const mongoose = require('mongoose')
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.DB_STRING, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
useCreateIndex: true
})

console.log(`MongoDB Connected: ${conn.connection.host}`)
Expand Down
62 changes: 38 additions & 24 deletions config/passport.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,47 @@
const LocalStrategy = require('passport-local').Strategy
const mongoose = require('mongoose')
const User = require('../models/User')
const bcrypt = require('bcrypt')

module.exports = function (passport) {
passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
User.findOne({ email: email.toLowerCase() }, (err, user) => {
if (err) { return done(err) }
if (!user) {
return done(null, false, { msg: `Email ${email} not found.` })
}
if (!user.password) {
return done(null, false, { msg: 'Your account was registered using a sign-in provider. To enable password login, sign in using a provider, and then set a password under your user profile.' })
}
user.comparePassword(password, (err, isMatch) => {
if (err) { return done(err) }
if (isMatch) {
return done(null, user)

module.exports = function(passport) {
passport.use(
new LocalStrategy({ usernameField: 'email' }, async (email, password, done) => {
try {
// Find the user by email
const user = await User.findOne({ email });

// If user not found, return error
if (!user) {
return done(null, false, { message: 'Invalid email or password' });
}
return done(null, false, { msg: 'Invalid email or password.' })
})

// Check password
const isMatch = await bcrypt.compare(password, user.password);

if (!isMatch) {
return done(null, false, { message: 'Invalid email or password' });
}

return done(null, user);
} catch (err) {
return done(err);
}
})
}))

);

// Serialize user into session
passport.serializeUser((user, done) => {
done(null, user.id)
})
done(null, user.id);
});

passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => done(err, user))
})
}
// Deserialize user from session
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err);
}
});
};
125 changes: 59 additions & 66 deletions controllers/auth.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,33 @@
const passport = require('passport')
const validator = require('validator')
const User = require('../models/User')
const bcrypt = require('bcrypt')

exports.getLogin = (req, res) => {
if (req.user) {
return res.redirect('/todos')
}
res.render('login', {
title: 'Login'
})
}
// exports.getLogin = (req, res) => {
// if (req.user) {
// return res.redirect('/todos')
// }
// res.render('login', {
// title: 'Login'
// })
// }

exports.postLogin = (req, res, next) => {
const validationErrors = []
if (!validator.isEmail(req.body.email)) validationErrors.push({ msg: 'Please enter a valid email address.' })
if (validator.isEmpty(req.body.password)) validationErrors.push({ msg: 'Password cannot be blank.' })

if (validationErrors.length) {
req.flash('errors', validationErrors)
return res.redirect('/login')
}
req.body.email = validator.normalizeEmail(req.body.email, { gmail_remove_dots: false })

passport.authenticate('local', (err, user, info) => {
if (err) { return next(err) }
if (err) {
return next(err)
}

if (!user) {
req.flash('errors', info)
return res.redirect('/login')
return res.status(401).json({ message: info.message })
}
req.logIn(user, (err) => {
if (err) { return next(err) }
req.flash('success', { msg: 'Success! You are logged in.' })
res.redirect(req.session.returnTo || '/todos')

req.logIn(user, err => {
if (err) {
return next(err)
}

res.redirect('/todos')
})
})(req, res, next)
}
Expand All @@ -47,50 +43,47 @@ const User = require('../models/User')
})
}

exports.getSignup = (req, res) => {
if (req.user) {
return res.redirect('/todos')
}
res.render('signup', {
title: 'Create Account'
})
}

exports.postSignup = (req, res, next) => {
const validationErrors = []
if (!validator.isEmail(req.body.email)) validationErrors.push({ msg: 'Please enter a valid email address.' })
if (!validator.isLength(req.body.password, { min: 8 })) validationErrors.push({ msg: 'Password must be at least 8 characters long' })
if (req.body.password !== req.body.confirmPassword) validationErrors.push({ msg: 'Passwords do not match' })
// exports.getSignup = (req, res) => {
// if (req.user) {
// return res.redirect('/todos')
// }
// res.render('signup', {
// title: 'Create Account'
// })
// }

if (validationErrors.length) {
req.flash('errors', validationErrors)
return res.redirect('../signup')
}
req.body.email = validator.normalizeEmail(req.body.email, { gmail_remove_dots: false })
exports.postSignup = async(req, res) => {
const { userName, email, password } = req.body

const user = new User({
userName: req.body.userName,
email: req.body.email,
password: req.body.password
})
try {
// Check if user already exists
let user = await User.findOne({ email })

User.findOne({$or: [
{email: req.body.email},
{userName: req.body.userName}
]}, (err, existingUser) => {
if (err) { return next(err) }
if (existingUser) {
req.flash('errors', { msg: 'Account with that email address or username already exists.' })
return res.redirect('../signup')
if (user) {
return res.status(400).json({ message: 'User already exists' })
}
user.save((err) => {
if (err) { return next(err) }
req.logIn(user, (err) => {
if (err) {
return next(err)
}
res.redirect('/todos')
})

// Create new user
user = new User({ userName, email, password })

// Hash the password
const salt = await bcrypt.genSalt(10)
user.password = await bcrypt.hash(password, salt)

// Save user to DB
await user.save();

//res.json({ message: 'Registration successful' });

req.logIn(user, (err) => {
if (err) {
return next(err)
}
res.redirect('/todos')
})
})
//res.redirect('/home')
} catch (err) {
console.error(err)
res.status(500).json({ message: 'Server error' })
}
}
55 changes: 21 additions & 34 deletions models/User.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,23 @@
const bcrypt = require('bcrypt')
const mongoose = require('mongoose')
const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
userName: { type: String, unique: true },
email: { type: String, unique: true },
password: String
})


// Password hash middleware.

UserSchema.pre('save', function save(next) {
const user = this
if (!user.isModified('password')) { return next() }
bcrypt.genSalt(10, (err, salt) => {
if (err) { return next(err) }
bcrypt.hash(user.password, salt, (err, hash) => {
if (err) { return next(err) }
user.password = hash
next()
})
})
})


// Helper method for validating user's password.

UserSchema.methods.comparePassword = function comparePassword(candidatePassword, cb) {
bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
cb(err, isMatch)
})
}


module.exports = mongoose.model('User', UserSchema)
userName: {
type: String,
unique: true
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
createdAt: {
type: Date,
default: Date.now,
},
});

module.exports = mongoose.model('User', UserSchema);
Loading