diff --git a/README.md b/README.md index 46945d6..1f3cc26 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ npm install Create a `.env` file in the root directory and add the following line: ```bash +ADMIN_USERNAME=username of admins seprarated by "," + MONGODB_URI=mongodb://localhost:27017/bloglog EMAIL_USERNAME= EMAIL_APP_PASSWORD= diff --git a/server/middlewares/admin.js b/server/middlewares/admin.js new file mode 100644 index 0000000..3ef9947 --- /dev/null +++ b/server/middlewares/admin.js @@ -0,0 +1,12 @@ +module.exports = (req, res, next) => { + try { + if (req.user.admin) { + next(); + } else { + res.status(403).json({ message: 'Forbidden' }); + } + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Internal Server Error' }); + } +} \ No newline at end of file diff --git a/server/middlewares/auth.js b/server/middlewares/auth.js new file mode 100644 index 0000000..729bc3e --- /dev/null +++ b/server/middlewares/auth.js @@ -0,0 +1,19 @@ +const jwt = require('jsonwebtoken'); +const jwtSecret = process.env.JWT_SECRET; + + +module.exports = (req, res, next) => { + const token = req.cookies.user.token; + + if (!token) { + return res.status(401).json({ message: 'Unauthorized' }); + } + + try { + const decoded = jwt.verify(token, jwtSecret); + req.user = { id: decoded.id, username: decoded.username, admin: decoded.admin }; + next(); + } catch (error) { + res.status(401).json({ message: 'Unauthorized' }); + } +} \ No newline at end of file diff --git a/server/validations/authValidator.js b/server/middlewares/authValidator.js similarity index 100% rename from server/validations/authValidator.js rename to server/middlewares/authValidator.js diff --git a/server/middlewares/restrictAuthRoute.js b/server/middlewares/restrictAuthRoute.js new file mode 100644 index 0000000..41218d7 --- /dev/null +++ b/server/middlewares/restrictAuthRoute.js @@ -0,0 +1,5 @@ +module.exports = (req, res, next) => { + const token = req.cookies.token; + if (!token) return next(); + return res.status(200).redirect('/') +} \ No newline at end of file diff --git a/server/routes/admin.js b/server/routes/admin.js index d920302..ef65a3c 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -1,18 +1,17 @@ const express = require('express'); const router = express.Router(); const Post = require('../models/Post'); -const User = require('../models/User'); -const bcrypt = require('bcrypt'); -const jwt = require('jsonwebtoken'); -const passport = require('passport'); // Added passport import -const { validateRegistration, validatePost } = require('../validations/authValidator'); + +const { validatePost } = require('../middlewares/authValidator'); +const { CloudinaryStorage } = require('multer-storage-cloudinary'); + const adminLayout = '../views/layouts/admin'; -const jwtSecret = process.env.JWT_SECRET; const multer = require('multer'); const cloudinary = require('cloudinary').v2; -const { CloudinaryStorage } = require('multer-storage-cloudinary'); +const authMiddleware = require('../middlewares/auth'); +const adminMiddleware = require('../middlewares/admin'); cloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, @@ -35,37 +34,6 @@ const storage = new CloudinaryStorage({ const upload = multer({ storage }); -/** - * Check whether the user is signed in or not - * and makes the /admin route available only those users who are NOT logged - */ - -const restrictAuthRouteMiddleware = (req, res, next) => { - const token = req.cookies.token; - if (!token) return next(); - return res.status(200).redirect('/') -} - - -/** - * Authentication Middleware - */ -const authMiddleware = (req, res, next) => { - const token = req.cookies.token; - - if (!token) { - return res.status(401).json({ message: 'Unauthorized' }); - } - - try { - const decoded = jwt.verify(token, jwtSecret); - req.userId = decoded.userId; - next(); - } catch (error) { - res.status(401).json({ message: 'Unauthorized' }); - } -}; - /** * GET / * Admin - Login Page @@ -76,49 +44,12 @@ router.use((req, res, next) => { next(); // Call the next middleware or route handler }); -router.get('/admin', restrictAuthRouteMiddleware, async (req, res) => { - try { - const locals = { - title: 'Admin', - description: 'Simple Blog created with NodeJs, Express & MongoDb.', - }; - - res.render('admin/index', { locals, layout: adminLayout }); - } catch (error) { - console.log(error); - } -}); - -/** - * POST /admin - * Admin Login Route with Passport Authentication - */ -router.post('/admin', async (req, res, next) => { - passport.authenticate('local', async (err, user, info) => { - if (err) { - return res.status(500).json({ message: 'Internal server error' }); - } - if (!user) { - return res.status(401).json({ message: 'Unauthorized' }); - } - req.logIn(user, async (err) => { - if (err) { - return res.status(500).json({ message: 'Error logging in' }); - } - - const token = jwt.sign({ userId: user._id }, jwtSecret, { expiresIn: '1h' }); - res.cookie('token', token, { httpOnly: true }); - return res.redirect('/dashboard'); // Now redirect to dashboard - }); - })(req, res, next); -}); - /** * GET /dashboard * Admin Dashboard Route */ -router.get('/dashboard', authMiddleware, async (req, res) => { +router.get('/dashboard', authMiddleware, adminMiddleware, async (req, res) => { const locals = { title: 'Dashboard', user: req.cookies.token, @@ -127,7 +58,7 @@ router.get('/dashboard', authMiddleware, async (req, res) => { const posts = await Post.find(); // Fetch all posts - res.render('admin/dashboard', { locals, posts }); // Pass 'posts' to the template + res.render('admin/dashboard', { locals, posts }); }); @@ -135,8 +66,9 @@ router.get('/dashboard', authMiddleware, async (req, res) => { * GET /add-post * Admin Add Post Route */ -router.get('/add-post', authMiddleware, async (req, res) => { +router.get('/add-post', authMiddleware, adminMiddleware, async (req, res) => { const token = req.cookies.token; + try { const locals = { title: 'Add Post', @@ -154,7 +86,7 @@ router.get('/add-post', authMiddleware, async (req, res) => { * POST /add-post * Admin Create New Post Route */ -router.post('/add-post', upload.single('poster'), authMiddleware, validatePost, async (req, res) => { +router.post('/add-post', upload.single('poster'), authMiddleware, adminMiddleware, validatePost, async (req, res) => { try { const token = req.cookies.token @@ -177,7 +109,7 @@ router.post('/add-post', upload.single('poster'), authMiddleware, validatePost, * GET /edit-post/:id * Admin Edit Post Route */ -router.get('/edit-post/:id', authMiddleware, async (req, res) => { +router.get('/edit-post/:id', authMiddleware, adminMiddleware, async (req, res) => { try { const locals = { title: 'Edit Post', @@ -197,7 +129,7 @@ router.get('/edit-post/:id', authMiddleware, async (req, res) => { * PUT /edit-post/:id * Admin Update Post Route */ -router.put('/edit-post/:id', upload.single('poster'), authMiddleware, validatePost, async (req, res) => { +router.put('/edit-post/:id', upload.single('poster'), authMiddleware, adminMiddleware, validatePost, async (req, res) => { try { await Post.findByIdAndUpdate(req.params.id, { title: req.body.title, @@ -217,7 +149,7 @@ router.put('/edit-post/:id', upload.single('poster'), authMiddleware, validatePo * DELETE /delete-post/:id * Admin Delete Post Route */ -router.delete('/delete-post/:id', authMiddleware, async (req, res) => { +router.delete('/delete-post/:id', authMiddleware, adminMiddleware, async (req, res) => { try { await Post.deleteOne({ _id: req.params.id }); res.redirect('/dashboard'); @@ -226,89 +158,5 @@ router.delete('/delete-post/:id', authMiddleware, async (req, res) => { } }); -/** - * POST /register - * Admin Registration Route - */ - -/** - * GET /register - * Admin - Registration Page -*/ -// Example of admin.js route handling -router.get('/register',restrictAuthRouteMiddleware, (req, res) => { - // Initialize messages object, you can adjust it according to your error handling logic - const locals = { - title: 'Admin', - description: 'Simple Blog created with NodeJs, Express & MongoDb.', - }; - - res.render('admin/register', { locals, layout: adminLayout }); // Pass messages to the template -}); - - - -router.post('/register',validateRegistration, async (req, res) => { - const { username, password } = req.body; - - // Simple validation - if (!username || !password) { - req.flash('error', 'All fields are required'); - return res.redirect('/register'); // Change to '/register' - } - - if (!/^[a-zA-Z0-9]+$/.test(username) || username.length < 3) { - req.flash('error', 'Username must be at least 3 characters long and contain only alphanumeric characters.'); - return res.redirect('/register'); - } - - if (password.length < 8 || !/\d/.test(password) || !/[!@#$%^&*]/.test(password)) { - req.flash('error', 'Password must be at least 8 characters long, contain a number, and a special character.'); - return res.redirect('/register'); - } - - try { - const existingUser = await User.findOne({ username }); - - if (existingUser) { - req.flash('error', 'Username already taken'); - return res.redirect('/register'); // Change to '/register' - } - - // Hash password and create new user - const hashedPassword = await bcrypt.hash(password, 10); - const user = new User({ username, password: hashedPassword }); - await user.save(); - - // Automatically log the user in - req.login(user, (err) => { - if (err) return res.status(500).json({ message: 'Error logging in after registration' }); - - const token = jwt.sign({ userId: user._id }, jwtSecret, { expiresIn: '1h' }); - res.cookie('token', token, { httpOnly: true }); - - return res.redirect('/dashboard'); - }); - } catch (error) { - console.log(error); - res.status(500).json({ message: 'Internal server error' }); - } -}); - - -/** - * GET /logout - * Admin Logout Route - */ -router.get('/logout', (req, res) => { - - req.logout((err) => { - if (err) { - return next(err); - } - res.clearCookie('token'); - res.redirect('/'); - }); -}); module.exports = router; diff --git a/server/routes/main.js b/server/routes/main.js index e471d54..df40bbc 100644 --- a/server/routes/main.js +++ b/server/routes/main.js @@ -1,15 +1,19 @@ const express = require('express'); +const passport = require('passport'); + const router = express.Router(); + const Post = require('../models/Post'); +const User = require('../models/User'); const ContactMessage = require('../models/contactMessage'); + const transporter = require('../config/nodemailerConfig'); -const { validateContact } = require('../validations/authValidator'); +const { validateContact, validateRegistration } = require('../middlewares/authValidator'); +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const jwtSecret = process.env.JWT_SECRET; -/** - * GET / - * HOME -*/ router.use((req, res, next) => { res.locals.layout = './layouts/main'; // Set the layout for the response @@ -21,7 +25,7 @@ router.get('/posts', async (req, res) => { try { const locals = { title: "All Posts", - user: req.cookies.token, + user: req.cookies.user, description: "Made with ❤️" }; @@ -29,10 +33,10 @@ router.get('/posts', async (req, res) => { const page = parseInt(req.query.page) || 1; let query = {}; - + if (req.query.search) { const searchNoSpecialChar = req.query.search.replace(/[^a-zA-Z0-9 ]/g, ""); - + query = { $or: [ { title: { $regex: new RegExp(searchNoSpecialChar, 'i') } }, @@ -70,16 +74,15 @@ router.get('/', async (req, res) => { try { const locals = { title: "BlogLog", - user : req.cookies.token, + user: req.cookies.user, description: "Made with ❤️" } + const data = await Post.aggregate([{ $sort: { createdAt: -1 } }]) + .limit(5) + .exec(); - const data = await Post.aggregate([ { $sort: { createdAt: -1 } } ]) - .limit(5) - .exec(); - - res.render('index', { + res.render('index', { locals, data, currentRoute: '/' @@ -91,21 +94,6 @@ router.get('/', async (req, res) => { }); -// router.get('', async (req, res) => { -// const locals = { -// title: "NodeJs Blog", -// description: "Simple Blog created with NodeJs, Express & MongoDb." -// } - -// try { -// const data = await Post.find(); -// res.render('index', { locals, data }); -// } catch (error) { -// console.log(error); -// } - -// }); - /** * GET / @@ -119,11 +107,11 @@ router.get('/post/:id', async (req, res) => { const locals = { title: data.title, - user : req.cookies.token, + user: req.cookies.user, description: "Simple Blog created with NodeJs, Express & MongoDb.", } - res.render('post', { + res.render('post', { locals, data, currentRoute: `/post/${slug}` @@ -152,24 +140,25 @@ router.get('/about', (req, res) => { */ router.get('/contact', (req, res) => { res.render('contact', { - user : req.cookies.token, + user: req.cookies.token, currentRoute: '/contact' }); }); -router.post('/send-message',validateContact ,async (req, res) => { +router.post('/send-message', validateContact, async (req, res) => { const { name, email, message } = req.body; - try {`` + try { + `` // Create a new contact message const newMessage = new ContactMessage({ name, email, message }); await newMessage.save(); - // Send an email notification - const mailOptions = { - from: `"BlogLog Contact Form" <${email}>`, - to: process.env.EMAIL_USERNAME, - subject: `New Contact Message from ${name} - BlogLog`, + // Send an email notification + const mailOptions = { + from: `"BlogLog Contact Form" <${email}>`, + to: process.env.EMAIL_USERNAME, + subject: `New Contact Message from ${name} - BlogLog`, html: `

New Contact Message from BlogLog

@@ -182,7 +171,7 @@ router.post('/send-message',validateContact ,async (req, res) => {
`, } - await transporter.sendMail(mailOptions); + await transporter.sendMail(mailOptions); // Render the contact page with a success message res.render('contact', { @@ -198,6 +187,131 @@ router.post('/send-message',validateContact ,async (req, res) => { } }); + +/* Authentication */ + +router.get('/login', async (req, res) => { + try { + const locals = { + title: 'Login', + description: 'Simple Blog created with NodeJs, Express & MongoDb.', + }; + + res.render('login', { locals, currentRoute: '/login' }); + } catch (error) { + console.log(error); + } +}); + +router.post('/login', async (req, res, next) => { + passport.authenticate('local', async (err, user, info) => { + if (err) { + return res.status(500).json({ message: 'Internal server error' }); + } + if (!user) { + return res.status(401).json({ message: 'Unauthorized' }); + } + req.logIn(user, async (err) => { + if (err) { + return res.status(500).json({ message: 'Error logging in' }); + } + + const data = { + id: user._id, + username: user.username, + admin: process.env.ADMIN_USERNAME.split(",").some(x => x === user.username), + } + + const token = jwt.sign(data, jwtSecret, { expiresIn: '1h' }); + res.cookie('user', Object.assign(data, { token })) + + return res.redirect('/'); + }); + })(req, res, next); +}); + + +router.get('/register', (req, res) => { + // Initialize messages object, you can adjust it according to your error handling logic + const locals = { + title: 'Admin', + description: 'Simple Blog created with NodeJs, Express & MongoDb.', + }; + + res.render('register', { locals, currentRoute: '/register' }); +}); + + + +router.post('/register', validateRegistration, async (req, res) => { + const { username, password } = req.body; + + // Simple validation + if (!username || !password) { + req.flash('error', 'All fields are required'); + return res.redirect('/register'); // Change to '/register' + } + + if (!/^[a-zA-Z0-9]+$/.test(username) || username.length < 3) { + req.flash('error', 'Username must be at least 3 characters long and contain only alphanumeric characters.'); + return res.redirect('/register'); + } + + if (password.length < 8 || !/\d/.test(password) || !/[!@#$%^&*]/.test(password)) { + req.flash('error', 'Password must be at least 8 characters long, contain a number, and a special character.'); + return res.redirect('/register'); + } + + try { + const existingUser = await User.findOne({ username }); + + if (existingUser) { + req.flash('error', 'Username already taken'); + return res.redirect('/register'); // Change to '/register' + } + + // Hash password and create new user + const hashedPassword = await bcrypt.hash(password, 10); + const user = new User({ username, password: hashedPassword }); + await user.save(); + + // Automatically log the user in + req.login(user, (err) => { + if (err) return res.status(500).json({ message: 'Error logging in after registration' }); + + const data = { + id: user._id, + username: user.username, + admin: process.env.ADMIN_USERNAME.split(",").some(x => x === user.username), + } + + const token = jwt.sign(data, jwtSecret, { expiresIn: '1h' }); + res.cookie('user', Object.assign(data, { token }), { httpOnly: true }); + + return res.redirect('/'); + }); + } catch (error) { + console.log(error); + res.status(500).json({ message: 'Internal server error' }); + } +}); + + +/** + * GET /logout + * Admin Logout Route + */ +router.get('/logout', (req, res) => { + + req.logout((err) => { + if (err) { + return next(err); + } + res.clearCookie('user'); + res.redirect('/'); + }); +}); + // function insertPostData() { // Post.insertMany([ // { diff --git a/views/admin/index.ejs b/views/admin/index.ejs deleted file mode 100644 index 0ee19f0..0000000 --- a/views/admin/index.ejs +++ /dev/null @@ -1,44 +0,0 @@ -

Sign In

- -
- - - - - -
- - -
- - -
- Don't have an account? Sign up -
- - - - - diff --git a/views/index.ejs b/views/index.ejs index 5783677..bed9dfb 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -22,6 +22,8 @@ <% }) %> - +<% if (locals.user?.admin) { %> + +<% } %> diff --git a/views/login.ejs b/views/login.ejs new file mode 100644 index 0000000..ac9f88e --- /dev/null +++ b/views/login.ejs @@ -0,0 +1,45 @@ +

Sign In

+ +
+ + + + + +
+ + +
+ + +
+ Don't have an account? Sign up +
+ + + + \ No newline at end of file diff --git a/views/partials/header.ejs b/views/partials/header.ejs index 5557b8e..45411e7 100644 --- a/views/partials/header.ejs +++ b/views/partials/header.ejs @@ -13,7 +13,11 @@ Contact
  • - Post + <% if(locals.user?.admin) { %> + Post + <% } else { %> + Posts + <% } %>
  • @@ -24,7 +28,7 @@ <% if (locals.user) { %> <% } else { %> - + <% } %> diff --git a/views/admin/register.ejs b/views/register.ejs similarity index 98% rename from views/admin/register.ejs rename to views/register.ejs index d5ac374..0249e4f 100644 --- a/views/admin/register.ejs +++ b/views/register.ejs @@ -1,5 +1,5 @@

    Register

    -
    +