diff --git a/.gitignore b/.gitignore index 3f8e1d990..1718e91dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ node_modules -.vscode/* \ No newline at end of file +.env +config/.env +.vscode +.history \ No newline at end of file diff --git a/config/.env b/config/.env deleted file mode 100644 index dbe126e0a..000000000 --- a/config/.env +++ /dev/null @@ -1,2 +0,0 @@ -PORT = 2121 -DB_STRING = mongodb+srv://demo:demo@cluster0.hcds1.mongodb.net/todos?retryWrites=true&w=majority \ No newline at end of file diff --git a/controllers/auth.js b/controllers/auth.js index c434c3c3a..bf5d1850f 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -15,6 +15,7 @@ const User = require('../models/User') 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 (validator.isEmpty(req.body.qAnswer)) validationErrors.push({ msg: 'Security answer cannot be blank.' })--- Wrong section, maybe used for secQ sign-in if (validationErrors.length) { req.flash('errors', validationErrors) @@ -36,15 +37,17 @@ const User = require('../models/User') })(req, res, next) } - exports.logout = (req, res) => { - req.logout(() => { - console.log('User has logged out.') - }) - req.session.destroy((err) => { - if (err) console.log('Error : Failed to destroy the session during logout.', err) - req.user = null - res.redirect('/') + exports.logout = (req, res, next) => { + // req.logout() --- Passport update, this is now async + req.logout(function(err) { + if (err) { return next(err); } + res.redirect('/') //--- causes error later with the session destroy }) + // req.session.destroy((err) => { + // if (err) console.log('Error : Failed to destroy the session during logout.', err) + // req.user = null + // res.redirect('/') + // }) } exports.getSignup = (req, res) => { @@ -61,6 +64,8 @@ const User = require('../models/User') 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' }) + // if (validator.isEmpty(req.body.secQuestion)) validationErrors.push({ msg: 'Must select a security question.' }) + // if (validator.isEmpty(req.body.qAnswer)) validationErrors.push({ msg: 'Security answer cannot be blank.' }) if (validationErrors.length) { req.flash('errors', validationErrors) @@ -69,9 +74,13 @@ const User = require('../models/User') req.body.email = validator.normalizeEmail(req.body.email, { gmail_remove_dots: false }) const user = new User({ + // firstName: req.body.firstName, + // lastName: req.body.lastName, userName: req.body.userName, email: req.body.email, - password: req.body.password + password: req.body.password, + // secQuestion: req.body.secQuestion, + // qAnswer: req.body.qAnswer }) User.findOne({$or: [ diff --git a/controllers/todos.js b/controllers/todos.js index b10950f93..865c35865 100644 --- a/controllers/todos.js +++ b/controllers/todos.js @@ -51,5 +51,30 @@ module.exports = { }catch(err){ console.log(err) } + }, + + editTodo: async(req, res) => { + try { + let todoId = req.params.id + let todoById = await Todo.findById({_id: todoId}) + res.render('editTodo.ejs', {title: 'Todo Manager', todoById}) + console.log(todoById) + } catch (error) { + console.log(error) + } + }, + + editTodoOnPost: async(req,res) =>{ + try { + req.body.user = req.body._id + let todoId = req.params.id + let todoById = await Todo.findByIdAndUpdate({_id: todoId}, {$set: { + todo: req.body.todo + }}) + res.redirect('/todos') + console.log(todoById) + } catch (error) { + res.status(500).send({message:error.message || 'error occurred'}) + } } } \ No newline at end of file diff --git a/middleware/auth.js b/middleware/auth.js index 8b92a4620..bc1bc4205 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -5,6 +5,13 @@ module.exports = { } else { res.redirect('/') } - } + }, + ensureGuest: function (req, res, next) { + if (!req.isAuthenticated()) { + return next() + } else { + res.redirect('/dashboard') + } + }, } \ No newline at end of file diff --git a/models/User.js b/models/User.js index db4b2d116..440900cc0 100644 --- a/models/User.js +++ b/models/User.js @@ -1,10 +1,19 @@ const bcrypt = require('bcrypt') const mongoose = require('mongoose') +// const UserSchema = new mongoose.Schema({ +// userName: { type: String, unique: true }, +// email: { type: String, unique: true }, +// password: String +// }) const UserSchema = new mongoose.Schema({ + // firstName: { type: String, unique: true }, + // lastName: { type: String, unique: true }, userName: { type: String, unique: true }, email: { type: String, unique: true }, - password: String + password: String, + // secQuestion: { type: String }, + // qAnswer: {type: String} }) @@ -23,6 +32,21 @@ const UserSchema = new mongoose.Schema({ }) }) +// Attempt at sec pass hash + +UserSchema.pre('save', function save(next) { + const user = this + if (!user.isModified('qAnswer')) { return next() } + bcrypt.genSalt(10, (err, salt) => { + if (err) { return next(err) } + bcrypt.hash(user.qAnswer, salt, (err, hash) => { + if (err) { return next(err) } + user.qAnswer = hash + next() + }) + }) +}) + // Helper method for validating user's password. diff --git a/package-lock.json b/package-lock.json index f5d8debf0..635c59eef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,8 @@ "passport": "^0.6.0", "passport-local": "^1.0.0", "validator": "^13.6.0" - } + }, + "devDependencies": {} }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.9", diff --git a/package.json b/package.json index 8c294b38a..e93d53340 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "todo-mvc-auth-local", "version": "1.0.0", - "description": "", + "description": "A Simple ToDo App is built using the MVC Architecture, we have also implemented \"authorization\" so folx can sign up, customize & personalize the app", "main": "server.js", "scripts": { "start": "nodemon server.js", @@ -24,5 +24,14 @@ "passport": "^0.6.0", "passport-local": "^1.0.0", "validator": "^13.6.0" - } + }, + "repository": { + "type": "git", + "url": "git+https://github.com/April-Yuen/todoAuth.git" + }, + "bugs": { + "url": "https://github.com/April-Yuen/todoAuth/issues" + }, + "homepage": "https://github.com/April-Yuen/todoAuth#readme", + "devDependencies": {} } diff --git a/public/css/style.css b/public/css/style.css index fbd6b929e..f235b1918 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,5 +1,6 @@ + h1{ - color: red; + color: white; } .completed{ text-decoration: line-through; @@ -7,4 +8,309 @@ h1{ } .not{ text-decoration: underline; +} + +@import url('https://fonts.googleapis.com/css?family=Poppins'); + +/* BASIC */ + +html { + font-size: 16px; + background-color: #0093E9; + background-image: linear-gradient(160deg, #0093E9 0%, #80D0C7 100%); + +} + +body { + font-family: "Poppins", sans-serif; + height: 100vh; +} + +a { + color: #92badd; + display:inline-block; + text-decoration: none; + font-weight: 400; +} + +h1 { + font-size: 5rem; + color: white; + margin-bottom: 3rem; + text-align: center; +} + +h2 { + text-align: center; + font-size: 16px; + font-weight: 600; + text-transform: uppercase; + display:inline-block; + color: #cccccc; + margin: 0 0.5rem; +} + + + +/* STRUCTURE */ + +.wrapper { + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; + width: 100vw; + height: 100vh; + max-width: 80rem; + margin: 0 auto; +} + +#formContent { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-content: center; + background-color: rgb(255,255,255); + width: 25%; + height:20rem; + -webkit-box-shadow: 0 30px 60px 0 rgba(0,0,0,0.3); + box-shadow: 0 30px 60px 0 rgba(0,0,0,0.3); + text-align: center; + border-radius: 1rem; + border: 1px solid red; +} + +#formContent2 { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-content: center; + background-color: rgb(255,255,255); + width: 50%; + height:30rem; + -webkit-box-shadow: 0 30px 60px 0 rgba(0,0,0,0.3); + box-shadow: 0 30px 60px 0 rgba(0,0,0,0.3); + text-align: center; + border-radius: 1rem; + border: 1px solid red; +} + +#formContent2 > form { /* Controls margin of items.*/ + margin: 2rem 0rem; +} + +/* TABS */ + +h2.inactive { + color: #cccccc; +} + +h2.active { + color: #0d0d0d; + border-bottom: 2px solid #5fbae9; +} + +/* FORM TYPOGRAPHY*/ + +input[type=button], input[type=submit], input[type=reset] { + background-color: #56baed; + border: none; + padding: 1rem 4rem; + margin: 1rem; + text-align: center; + text-decoration: none; + display: inline-block; + text-transform: uppercase; + font-size: 13px; + -webkit-box-shadow: 0 10px 30px 0 rgba(95,186,233,0.4); + box-shadow: 0 10px 30px 0 rgba(95,186,233,0.4); + -webkit-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; + -webkit-transition: all 0.3s ease-in-out; + -moz-transition: all 0.3s ease-in-out; + -ms-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; + color: white; +} + +input[type=button]:hover, input[type=submit]:hover, input[type=reset]:hover { + background-color: #39ace7; +} + +input[type=button]:active, input[type=submit]:active, input[type=reset]:active { + -moz-transform: scale(0.95); + -webkit-transform: scale(0.95); + -o-transform: scale(0.95); + -ms-transform: scale(0.95); + transform: scale(0.95); +} + +input[type=text], input[type=email], input[type=password] { + background-color: #f6f6f6; + border: none; + color: #0d0d0d; + padding: 1rem 2rem; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 5px; + width: 82%; + border: 2px solid #f6f6f6; + -webkit-transition: all 0.5s ease-in-out; + -moz-transition: all 0.5s ease-in-out; + -ms-transition: all 0.5s ease-in-out; + -o-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out; + -webkit-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; +} + +/* input[type=text] { + background-color: #f6f6f6; + border: none; + color: #0d0d0d; + padding: 1rem 1rem; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 5px; + width: 40%; + border: 2px solid #f6f6f6; + -webkit-transition: all 0.5s ease-in-out; + -moz-transition: all 0.5s ease-in-out; + -ms-transition: all 0.5s ease-in-out; + -o-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out; + -webkit-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; +} */ + +input[type=text]:focus { + background-color: #fff; + border-bottom: 2px solid #5fbae9; +} + +input[type=text]:placeholder { + color: #cccccc; +} + +/* ANIMATIONS */ + +/* Simple CSS3 Fade-in-down Animation */ +.fadeInDown { + -webkit-animation-name: fadeInDown; + animation-name: fadeInDown; + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +@-webkit-keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +/* Simple CSS3 Fade-in Animation */ +@-webkit-keyframes fadeIn { from { opacity:0; } to { opacity:1; } } +@-moz-keyframes fadeIn { from { opacity:0; } to { opacity:1; } } +@keyframes fadeIn { from { opacity:0; } to { opacity:1; } } + +.fadeIn { + opacity:0; + -webkit-animation:fadeIn ease-in 1; + -moz-animation:fadeIn ease-in 1; + animation:fadeIn ease-in 1; + + -webkit-animation-fill-mode:forwards; + -moz-animation-fill-mode:forwards; + animation-fill-mode:forwards; + + -webkit-animation-duration:1s; + -moz-animation-duration:1s; + animation-duration:1s; +} + +.fadeIn.first { + -webkit-animation-delay: 0.4s; + -moz-animation-delay: 0.4s; + animation-delay: 0.4s; +} + +.fadeIn.second { + -webkit-animation-delay: 0.6s; + -moz-animation-delay: 0.6s; + animation-delay: 0.6s; +} + +.fadeIn.third { + -webkit-animation-delay: 0.8s; + -moz-animation-delay: 0.8s; + animation-delay: 0.8s; +} + +.fadeIn.fourth { + -webkit-animation-delay: 1s; + -moz-animation-delay: 1s; + animation-delay: 1s; +} + +/* Simple CSS3 Fade-in Animation */ +.underlineHover:after { + display: block; + left: 0; + bottom: -10px; + width: 0; + height: 2px; + background-color: #56baed; + content: ""; + transition: width 0.2s; +} + +.underlineHover:hover { + color: #0d0d0d; +} + +.underlineHover:hover:after{ + width: 100%; +} + + + +/* OTHERS */ + +*:focus { + outline: none; +} + +#icon { + width:60%; +} + +* { + box-sizing: border-box; } \ No newline at end of file diff --git a/public/js/main.js b/public/js/main.js index b4cfee075..33676eee9 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -1,4 +1,4 @@ -const deleteBtn = document.querySelectorAll('.del') +const deleteBtn = document.querySelectorAll('.fa-solid fa-trash') const todoItem = document.querySelectorAll('span.not') const todoComplete = document.querySelectorAll('span.completed') diff --git a/routes/todos.js b/routes/todos.js index 03dcf42e4..4e87761be 100644 --- a/routes/todos.js +++ b/routes/todos.js @@ -1,7 +1,7 @@ const express = require('express') const router = express.Router() const todosController = require('../controllers/todos') -const { ensureAuth } = require('../middleware/auth') +const { ensureAuth, ensureGuest } = require('../middleware/auth') router.get('/', ensureAuth, todosController.getTodos) @@ -13,4 +13,8 @@ router.put('/markIncomplete', todosController.markIncomplete) router.delete('/deleteTodo', todosController.deleteTodo) +router.get('/editTodo/:id', todosController.editTodo) + +router.post('/editTodo/:id', todosController.editTodoOnPost) + module.exports = router \ No newline at end of file diff --git a/server.js b/server.js index b031c4cad..e0ef1748a 100644 --- a/server.js +++ b/server.js @@ -43,4 +43,11 @@ app.use('/todos', todoRoutes) app.listen(process.env.PORT, ()=>{ console.log('Server is running, you better catch it!') -}) \ No newline at end of file +}) + + +// Static Files +app.use(express.static('public')) +app.use('/css', express.static(__dirname + 'public/css')) +app.use('/js', express.static(__dirname + 'public/js')) +app.use('/img', express.static(__dirname + 'public/img')) \ No newline at end of file diff --git a/update.txt b/update.txt new file mode 100644 index 000000000..1394f9e15 --- /dev/null +++ b/update.txt @@ -0,0 +1,61 @@ +Possible sec qs { + First car make / colour + Teacher name + Childhood best friend + (First) Pet name + Fav show / film / book + +Modified User Schema to include the security question: -- models/Users, from line 9 +const UserSchema = new mongoose.Schema({ + userName: { type: String, unique: true }, + email: { type: String, unique: true }, + password: String, + secQuestion: { type: String }, + qAnswer: {type: String} +}) + +Added/copied hashing to include the sec answer: -- models/Users, from line 33 +UserSchema.pre('save', function save(next) { + const user = this + if (!user.isModified('qAnswer')) { return next() } + bcrypt.genSalt(10, (err, salt) => { + if (err) { return next(err) } + bcrypt.hash(user.qAnswer, salt, (err, hash) => { + if (err) { return next(err) } + user.qAnswer = hash + next() + }) + }) +}) + +Modified user creation to include sec q/answer -- controller/auth, from line 78 + const user = new User({ + userName: req.body.userName, + email: req.body.email, + password: req.body.password, + secQuestion: req.body.secQuestion, + qAnswer: req.body.qAnswer + }) + +Modified sign-up to include drop down of sec questions: --views/singup, from line 20 (submit input lowered beneath this section) + + + +Modified signup verification to include sec question: -- controller/auth, from line 62 + 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' }) + if (req.body.password !== req.body.confirmPassword) validationErrors.push({ msg: 'Passwords do not match' }) + if (validator.isEmpty(req.body.secQuestion)) validationErrors.push({ msg: 'Must select a security question.' }) + if (validator.isEmpty(req.body.qAnswer)) validationErrors.push({ msg: 'Security answer cannot be blank.' }) + // if (!req.body.qAnswer) validationErrors.push({msg: 'Please enter a valid answer'}) + diff --git a/views/editTodo.ejs b/views/editTodo.ejs new file mode 100644 index 000000000..69e172764 --- /dev/null +++ b/views/editTodo.ejs @@ -0,0 +1,11 @@ +