Skip to content

Commit 10d1e9e

Browse files
committed
Added lesson 13
1 parent 726d07b commit 10d1e9e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+34744
-0
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
- 🔗 [Redux Toolkit](https://redux-toolkit.js.org/)
6666
- 🔗 [FontAwesome Icons](https://fontawesome.com/docs/web/use-with/react/)
6767
- 🔗 [React Spinners](https://www.npmjs.com/package/react-spinners)
68+
- 🔗 [@fvilers/disable-react-devtools](https://www.npmjs.com/package/@fvilers/disable-react-devtools)
6869

6970
### 📚 Other Node.js REST API Dependencies
7071
- 🔗 [date-fns](https://www.npmjs.com/package/date-fns)
@@ -102,3 +103,5 @@
102103
- 🔗 [Chapter 11 - MERN User Role-Based Access Control and Permissions](https://github.com/gitdagray/mern_stack_course/tree/main/lesson_11-frontend)
103104
- 🔗 [Chapter 12 - Pt. 1 - MERN Review & Refactor - Backend Code](https://github.com/gitdagray/mern_stack_course/tree/main/lesson_12-backend)
104105
- 🔗 [Chapter 12 - Pt. 2 - MERN Review & Refactor - Frontend Code](https://github.com/gitdagray/mern_stack_course/tree/main/lesson_12-frontend)
106+
- 🔗 [Chapter 13 - Pt. 1 - MERN Deployment - Frontend Code](https://github.com/gitdagray/mern_stack_course/tree/main/lesson_13-frontend)
107+
- 🔗 [Chapter 13 - Pt. 2 - MERN Deployment - Backend Code](https://github.com/gitdagray/mern_stack_course/tree/main/lesson_13-backend)

lesson_13-backend/.gitignore

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

lesson_13-backend/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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const allowedOrigins = [
2+
'https://technotes.onrender.com'
3+
]
4+
5+
module.exports = allowedOrigins
+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_13-backend/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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
const User = require('../models/User')
2+
const bcrypt = require('bcrypt')
3+
const jwt = require('jsonwebtoken')
4+
5+
// @desc Login
6+
// @route POST /auth
7+
// @access Public
8+
const login = async (req, res) => {
9+
const { username, password } = req.body
10+
11+
if (!username || !password) {
12+
return res.status(400).json({ message: 'All fields are required' })
13+
}
14+
15+
const foundUser = await User.findOne({ username }).exec()
16+
17+
if (!foundUser || !foundUser.active) {
18+
return res.status(401).json({ message: 'Unauthorized' })
19+
}
20+
21+
const match = await bcrypt.compare(password, foundUser.password)
22+
23+
if (!match) return res.status(401).json({ message: 'Unauthorized' })
24+
25+
const accessToken = jwt.sign(
26+
{
27+
"UserInfo": {
28+
"username": foundUser.username,
29+
"roles": foundUser.roles
30+
}
31+
},
32+
process.env.ACCESS_TOKEN_SECRET,
33+
{ expiresIn: '15m' }
34+
)
35+
36+
const refreshToken = jwt.sign(
37+
{ "username": foundUser.username },
38+
process.env.REFRESH_TOKEN_SECRET,
39+
{ expiresIn: '7d' }
40+
)
41+
42+
// Create secure cookie with refresh token
43+
res.cookie('jwt', refreshToken, {
44+
httpOnly: true, //accessible only by web server
45+
secure: true, //https
46+
sameSite: 'None', //cross-site cookie
47+
maxAge: 7 * 24 * 60 * 60 * 1000 //cookie expiry: set to match rT
48+
})
49+
50+
// Send accessToken containing username and roles
51+
res.json({ accessToken })
52+
}
53+
54+
// @desc Refresh
55+
// @route GET /auth/refresh
56+
// @access Public - because access token has expired
57+
const refresh = (req, res) => {
58+
const cookies = req.cookies
59+
60+
if (!cookies?.jwt) return res.status(401).json({ message: 'Unauthorized' })
61+
62+
const refreshToken = cookies.jwt
63+
64+
jwt.verify(
65+
refreshToken,
66+
process.env.REFRESH_TOKEN_SECRET,
67+
async (err, decoded) => {
68+
if (err) return res.status(403).json({ message: 'Forbidden' })
69+
70+
const foundUser = await User.findOne({ username: decoded.username }).exec()
71+
72+
if (!foundUser) return res.status(401).json({ message: 'Unauthorized' })
73+
74+
const accessToken = jwt.sign(
75+
{
76+
"UserInfo": {
77+
"username": foundUser.username,
78+
"roles": foundUser.roles
79+
}
80+
},
81+
process.env.ACCESS_TOKEN_SECRET,
82+
{ expiresIn: '15m' }
83+
)
84+
85+
res.json({ accessToken })
86+
}
87+
)
88+
}
89+
90+
// @desc Logout
91+
// @route POST /auth/logout
92+
// @access Public - just to clear cookie if exists
93+
const logout = (req, res) => {
94+
const cookies = req.cookies
95+
if (!cookies?.jwt) return res.sendStatus(204) //No content
96+
res.clearCookie('jwt', { httpOnly: true, sameSite: 'None', secure: true })
97+
res.json({ message: 'Cookie cleared' })
98+
}
99+
100+
module.exports = {
101+
login,
102+
refresh,
103+
logout
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
const Note = require('../models/Note')
2+
const User = require('../models/User')
3+
4+
// @desc Get all notes
5+
// @route GET /notes
6+
// @access Private
7+
const getAllNotes = async (req, res) => {
8+
// Get all notes from MongoDB
9+
const notes = await Note.find().lean()
10+
11+
// If no notes
12+
if (!notes?.length) {
13+
return res.status(400).json({ message: 'No notes found' })
14+
}
15+
16+
// Add username to each note before sending the response
17+
// See Promise.all with map() here: https://youtu.be/4lqJBBEpjRE
18+
// You could also do this with a for...of loop
19+
const notesWithUser = await Promise.all(notes.map(async (note) => {
20+
const user = await User.findById(note.user).lean().exec()
21+
return { ...note, username: user.username }
22+
}))
23+
24+
res.json(notesWithUser)
25+
}
26+
27+
// @desc Create new note
28+
// @route POST /notes
29+
// @access Private
30+
const createNewNote = async (req, res) => {
31+
const { user, title, text } = req.body
32+
33+
// Confirm data
34+
if (!user || !title || !text) {
35+
return res.status(400).json({ message: 'All fields are required' })
36+
}
37+
38+
// Check for duplicate title
39+
const duplicate = await Note.findOne({ title }).collation({ locale: 'en', strength: 2 }).lean().exec()
40+
41+
if (duplicate) {
42+
return res.status(409).json({ message: 'Duplicate note title' })
43+
}
44+
45+
// Create and store the new user
46+
const note = await Note.create({ user, title, text })
47+
48+
if (note) { // Created
49+
return res.status(201).json({ message: 'New note created' })
50+
} else {
51+
return res.status(400).json({ message: 'Invalid note data received' })
52+
}
53+
54+
}
55+
56+
// @desc Update a note
57+
// @route PATCH /notes
58+
// @access Private
59+
const updateNote = async (req, res) => {
60+
const { id, user, title, text, completed } = req.body
61+
62+
// Confirm data
63+
if (!id || !user || !title || !text || typeof completed !== 'boolean') {
64+
return res.status(400).json({ message: 'All fields are required' })
65+
}
66+
67+
// Confirm note exists to update
68+
const note = await Note.findById(id).exec()
69+
70+
if (!note) {
71+
return res.status(400).json({ message: 'Note not found' })
72+
}
73+
74+
// Check for duplicate title
75+
const duplicate = await Note.findOne({ title }).collation({ locale: 'en', strength: 2 }).lean().exec()
76+
77+
// Allow renaming of the original note
78+
if (duplicate && duplicate?._id.toString() !== id) {
79+
return res.status(409).json({ message: 'Duplicate note title' })
80+
}
81+
82+
note.user = user
83+
note.title = title
84+
note.text = text
85+
note.completed = completed
86+
87+
const updatedNote = await note.save()
88+
89+
res.json(`'${updatedNote.title}' updated`)
90+
}
91+
92+
// @desc Delete a note
93+
// @route DELETE /notes
94+
// @access Private
95+
const deleteNote = async (req, res) => {
96+
const { id } = req.body
97+
98+
// Confirm data
99+
if (!id) {
100+
return res.status(400).json({ message: 'Note ID required' })
101+
}
102+
103+
// Confirm note exists to delete
104+
const note = await Note.findById(id).exec()
105+
106+
if (!note) {
107+
return res.status(400).json({ message: 'Note not found' })
108+
}
109+
110+
const result = await note.deleteOne()
111+
112+
const reply = `Note '${result.title}' with ID ${result._id} deleted`
113+
114+
res.json(reply)
115+
}
116+
117+
module.exports = {
118+
getAllNotes,
119+
createNewNote,
120+
updateNote,
121+
deleteNote
122+
}

0 commit comments

Comments
 (0)