Skip to content

Commit dee433f

Browse files
committed
Added lesson 4
1 parent aa12ad2 commit dee433f

21 files changed

+3804
-0
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
- 🔗 [dotenv](https://www.npmjs.com/package/dotenv)
6868
- 🔗 [MongooseJS](https://mongoosejs.com/)
6969
- 🔗 [mongoose-sequence](https://www.npmjs.com/package/mongoose-sequence)
70+
- 🔗 [express-async-handler](https://www.npmjs.com/package/express-async-handler)
71+
- 🔗 [bcrypt](https://www.npmjs.com/package/bcrypt)
7072

7173
### ⚙ VS Code Extensions I Use:
7274

@@ -81,3 +83,4 @@
8183
- 🔗 [Chapter 1 - Intro to MERN](https://github.com/gitdagray/mern_stack_course/tree/main/lesson_01)
8284
- 🔗 [Chapter 2 - MERN Middleware](https://github.com/gitdagray/mern_stack_course/tree/main/lesson_02)
8385
- 🔗 [Chapter 3 - MERN MongoDB](https://github.com/gitdagray/mern_stack_course/tree/main/lesson_03)
86+
- 🔗 [Chapter 4 - MERN Controllers](https://github.com/gitdagray/mern_stack_course/tree/main/lesson_04)

lesson_04/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
logs
3+
.env

lesson_04/UserStories.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# User Stories for techNotes
2+
3+
1. [ ] Replace current sticky note system
4+
2. [ ] Add a public facing page with basic contact info
5+
3. [ ] Add an employee login to the notes app
6+
4. [ ] Provide a welcome page after login
7+
5. [ ] Provide easy navigation
8+
6. [ ] Display current user and assigned role
9+
7. [ ] Provide a logout option
10+
8. [ ] Require users to login at least once per week
11+
9. [ ] Provide a way to remove user access asap if needed
12+
10. [ ] Notes are assigned to specific users
13+
11. [ ] Notes have a ticket #, title, note body, created & updated dates
14+
12. [ ] Notes are either OPEN or COMPLETED
15+
13. [ ] Users can be Employees, Managers, or Admins
16+
14. [ ] Notes can only be deleted by Managers or Admins
17+
15. [ ] Anyone can create a note (when customer checks-in)
18+
16. [ ] Employees can only view and edit their assigned notes
19+
17. [ ] Managers and Admins can view, edit, and delete all notes
20+
18. [ ] Only Managers and Admins can access User Settings
21+
19. [ ] Only Managers and Admins can create new users
22+
20. [ ] Desktop mode is most important but should be available in mobile

lesson_04/config/allowedOrigins.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const allowedOrigins = [
2+
'http://localhost:3000',
3+
'https://www.dandrepairshop.com',
4+
'https://dandrepairshop.com'
5+
]
6+
7+
module.exports = allowedOrigins

lesson_04/config/corsOptions.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const allowedOrigins = require('./allowedOrigins')
2+
3+
const corsOptions = {
4+
origin: (origin, callback) => {
5+
if (allowedOrigins.indexOf(origin) !== -1 || !origin) {
6+
callback(null, true)
7+
} else {
8+
callback(new Error('Not allowed by CORS'))
9+
}
10+
},
11+
credentials: true,
12+
optionsSuccessStatus: 200
13+
}
14+
15+
module.exports = corsOptions

lesson_04/config/dbConn.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const mongoose = require('mongoose')
2+
3+
const connectDB = async () => {
4+
try {
5+
await mongoose.connect(process.env.DATABASE_URI)
6+
} catch (err) {
7+
console.log(err)
8+
}
9+
}
10+
11+
module.exports = connectDB
+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
const Note = require('../models/Note')
2+
const User = require('../models/User')
3+
const asyncHandler = require('express-async-handler')
4+
5+
// @desc Get all notes
6+
// @route GET /notes
7+
// @access Private
8+
const getAllNotes = asyncHandler(async (req, res) => {
9+
// Get all notes from MongoDB
10+
const notes = await Note.find().lean()
11+
12+
// If no notes
13+
if (!notes?.length) {
14+
return res.status(400).json({ message: 'No notes found' })
15+
}
16+
17+
// Add username to each note before sending the response
18+
// See Promise.all with map() here: https://youtu.be/4lqJBBEpjRE
19+
// You could also do this with a for...of loop
20+
const notesWithUser = await Promise.all(notes.map(async (note) => {
21+
const user = await User.findById(note.user).lean().exec()
22+
return { ...note, username: user.username }
23+
}))
24+
25+
res.json(notesWithUser)
26+
})
27+
28+
// @desc Create new note
29+
// @route POST /notes
30+
// @access Private
31+
const createNewNote = asyncHandler(async (req, res) => {
32+
const { user, title, text } = req.body
33+
34+
// Confirm data
35+
if (!user || !title || !text) {
36+
return res.status(400).json({ message: 'All fields are required' })
37+
}
38+
39+
// Check for duplicate title
40+
const duplicate = await Note.findOne({ title }).lean().exec()
41+
42+
if (duplicate) {
43+
return res.status(409).json({ message: 'Duplicate note title' })
44+
}
45+
46+
// Create and store the new user
47+
const note = await Note.create({ user, title, text })
48+
49+
if (note) { // Created
50+
return res.status(201).json({ message: 'New note created' })
51+
} else {
52+
return res.status(400).json({ message: 'Invalid note data received' })
53+
}
54+
55+
})
56+
57+
// @desc Update a note
58+
// @route PATCH /notes
59+
// @access Private
60+
const updateNote = asyncHandler(async (req, res) => {
61+
const { id, user, title, text, completed } = req.body
62+
63+
// Confirm data
64+
if (!id || !user || !title || !text || typeof completed !== 'boolean') {
65+
return res.status(400).json({ message: 'All fields are required' })
66+
}
67+
68+
// Confirm note exists to update
69+
const note = await Note.findById(id).exec()
70+
71+
if (!note) {
72+
return res.status(400).json({ message: 'Note not found' })
73+
}
74+
75+
// Check for duplicate title
76+
const duplicate = await Note.findOne({ title }).lean().exec()
77+
78+
// Allow renaming of the original note
79+
if (duplicate && duplicate?._id.toString() !== id) {
80+
return res.status(409).json({ message: 'Duplicate note title' })
81+
}
82+
83+
note.user = user
84+
note.title = title
85+
note.text = text
86+
note.completed = completed
87+
88+
const updatedNote = await note.save()
89+
90+
res.json(`'${updatedNote.title}' updated`)
91+
})
92+
93+
// @desc Delete a note
94+
// @route DELETE /notes
95+
// @access Private
96+
const deleteNote = asyncHandler(async (req, res) => {
97+
const { id } = req.body
98+
99+
// Confirm data
100+
if (!id) {
101+
return res.status(400).json({ message: 'Note ID required' })
102+
}
103+
104+
// Confirm note exists to delete
105+
const note = await Note.findById(id).exec()
106+
107+
if (!note) {
108+
return res.status(400).json({ message: 'Note not found' })
109+
}
110+
111+
const result = await note.deleteOne()
112+
113+
const reply = `Note '${result.title}' with ID ${result._id} deleted`
114+
115+
res.json(reply)
116+
})
117+
118+
module.exports = {
119+
getAllNotes,
120+
createNewNote,
121+
updateNote,
122+
deleteNote
123+
}
+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
const User = require('../models/User')
2+
const Note = require('../models/Note')
3+
const asyncHandler = require('express-async-handler')
4+
const bcrypt = require('bcrypt')
5+
6+
// @desc Get all users
7+
// @route GET /users
8+
// @access Private
9+
const getAllUsers = asyncHandler(async (req, res) => {
10+
// Get all users from MongoDB
11+
const users = await User.find().select('-password').lean()
12+
13+
// If no users
14+
if (!users?.length) {
15+
return res.status(400).json({ message: 'No users found' })
16+
}
17+
18+
res.json(users)
19+
})
20+
21+
// @desc Create new user
22+
// @route POST /users
23+
// @access Private
24+
const createNewUser = asyncHandler(async (req, res) => {
25+
const { username, password, roles } = req.body
26+
27+
// Confirm data
28+
if (!username || !password || !Array.isArray(roles) || !roles.length) {
29+
return res.status(400).json({ message: 'All fields are required' })
30+
}
31+
32+
// Check for duplicate username
33+
const duplicate = await User.findOne({ username }).lean().exec()
34+
35+
if (duplicate) {
36+
return res.status(409).json({ message: 'Duplicate username' })
37+
}
38+
39+
// Hash password
40+
const hashedPwd = await bcrypt.hash(password, 10) // salt rounds
41+
42+
const userObject = { username, "password": hashedPwd, roles }
43+
44+
// Create and store new user
45+
const user = await User.create(userObject)
46+
47+
if (user) { //created
48+
res.status(201).json({ message: `New user ${username} created` })
49+
} else {
50+
res.status(400).json({ message: 'Invalid user data received' })
51+
}
52+
})
53+
54+
// @desc Update a user
55+
// @route PATCH /users
56+
// @access Private
57+
const updateUser = asyncHandler(async (req, res) => {
58+
const { id, username, roles, active, password } = req.body
59+
60+
// Confirm data
61+
if (!id || !username || !Array.isArray(roles) || !roles.length || typeof active !== 'boolean') {
62+
return res.status(400).json({ message: 'All fields except password are required' })
63+
}
64+
65+
// Does the user exist to update?
66+
const user = await User.findById(id).exec()
67+
68+
if (!user) {
69+
return res.status(400).json({ message: 'User not found' })
70+
}
71+
72+
// Check for duplicate
73+
const duplicate = await User.findOne({ username }).lean().exec()
74+
75+
// Allow updates to the original user
76+
if (duplicate && duplicate?._id.toString() !== id) {
77+
return res.status(409).json({ message: 'Duplicate username' })
78+
}
79+
80+
user.username = username
81+
user.roles = roles
82+
user.active = active
83+
84+
if (password) {
85+
// Hash password
86+
user.password = await bcrypt.hash(password, 10) // salt rounds
87+
}
88+
89+
const updatedUser = await user.save()
90+
91+
res.json({ message: `${updatedUser.username} updated` })
92+
})
93+
94+
// @desc Delete a user
95+
// @route DELETE /users
96+
// @access Private
97+
const deleteUser = asyncHandler(async (req, res) => {
98+
const { id } = req.body
99+
100+
// Confirm data
101+
if (!id) {
102+
return res.status(400).json({ message: 'User ID Required' })
103+
}
104+
105+
// Does the user still have assigned notes?
106+
const note = await Note.findOne({ user: id }).lean().exec()
107+
if (note) {
108+
return res.status(400).json({ message: 'User has assigned notes' })
109+
}
110+
111+
// Does the user exist to delete?
112+
const user = await User.findById(id).exec()
113+
114+
if (!user) {
115+
return res.status(400).json({ message: 'User not found' })
116+
}
117+
118+
const result = await user.deleteOne()
119+
120+
const reply = `Username ${result.username} with ID ${result._id} deleted`
121+
122+
res.json(reply)
123+
})
124+
125+
module.exports = {
126+
getAllUsers,
127+
createNewUser,
128+
updateUser,
129+
deleteUser
130+
}

lesson_04/middleware/errorHandler.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const { logEvents } = require('./logger')
2+
3+
const errorHandler = (err, req, res, next) => {
4+
logEvents(`${err.name}: ${err.message}\t${req.method}\t${req.url}\t${req.headers.origin}`, 'errLog.log')
5+
console.log(err.stack)
6+
7+
const status = res.statusCode ? res.statusCode : 500 // server error
8+
9+
res.status(status)
10+
11+
res.json({ message: err.message })
12+
}
13+
14+
module.exports = errorHandler

lesson_04/middleware/logger.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const { format } = require('date-fns')
2+
const { v4: uuid } = require('uuid')
3+
const fs = require('fs')
4+
const fsPromises = require('fs').promises
5+
const path = require('path')
6+
7+
const logEvents = async (message, logFileName) => {
8+
const dateTime = format(new Date(), 'yyyyMMdd\tHH:mm:ss')
9+
const logItem = `${dateTime}\t${uuid()}\t${message}\n`
10+
11+
try {
12+
if (!fs.existsSync(path.join(__dirname, '..', 'logs'))) {
13+
await fsPromises.mkdir(path.join(__dirname, '..', 'logs'))
14+
}
15+
await fsPromises.appendFile(path.join(__dirname, '..', 'logs', logFileName), logItem)
16+
} catch (err) {
17+
console.log(err)
18+
}
19+
}
20+
21+
const logger = (req, res, next) => {
22+
logEvents(`${req.method}\t${req.url}\t${req.headers.origin}`, 'reqLog.log')
23+
console.log(`${req.method} ${req.path}`)
24+
next()
25+
}
26+
27+
module.exports = { logEvents, logger }

0 commit comments

Comments
 (0)