├── .gitignore ├── api ├── controllers │ ├── auth.controller.js │ ├── comment.controller.js │ ├── post.controller.js │ └── user.controller.js ├── index.js ├── models │ ├── comment.model.js │ ├── post.model.js │ └── user.model.js ├── routes │ ├── auth.route.js │ ├── comment.route.js │ ├── post.route.js │ └── user.route.js └── utils │ ├── error.js │ └── verifyUser.js ├── client ├── .eslintrc.cjs ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── src │ ├── App.jsx │ ├── components │ │ ├── CallToAction.jsx │ │ ├── Comment.jsx │ │ ├── CommentSection.jsx │ │ ├── DashComments.jsx │ │ ├── DashPosts.jsx │ │ ├── DashProfile.jsx │ │ ├── DashSidebar.jsx │ │ ├── DashUsers.jsx │ │ ├── DashboardComp.jsx │ │ ├── Footer.jsx │ │ ├── Header.jsx │ │ ├── OAuth.jsx │ │ ├── OnlyAdminPrivateRoute.jsx │ │ ├── PostCard.jsx │ │ ├── PrivateRoute.jsx │ │ ├── ScrollToTop.jsx │ │ └── ThemeProvider.jsx │ ├── firebase.js │ ├── index.css │ ├── main.jsx │ ├── pages │ │ ├── About.jsx │ │ ├── CreatePost.jsx │ │ ├── Dashboard.jsx │ │ ├── Home.jsx │ │ ├── PostPage.jsx │ │ ├── Projects.jsx │ │ ├── Search.jsx │ │ ├── SignIn.jsx │ │ ├── SignUp.jsx │ │ └── UpdatePost.jsx │ └── redux │ │ ├── store.js │ │ ├── theme │ │ └── themeSlice.js │ │ └── user │ │ └── userSlice.js ├── tailwind.config.js └── vite.config.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .env 26 | -------------------------------------------------------------------------------- /api/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | import User from '../models/user.model.js'; 2 | import bcryptjs from 'bcryptjs'; 3 | import { errorHandler } from '../utils/error.js'; 4 | import jwt from 'jsonwebtoken'; 5 | 6 | export const signup = async (req, res, next) => { 7 | const { username, email, password } = req.body; 8 | 9 | if ( 10 | !username || 11 | !email || 12 | !password || 13 | username === '' || 14 | email === '' || 15 | password === '' 16 | ) { 17 | next(errorHandler(400, 'All fields are required')); 18 | } 19 | 20 | const hashedPassword = bcryptjs.hashSync(password, 10); 21 | 22 | const newUser = new User({ 23 | username, 24 | email, 25 | password: hashedPassword, 26 | }); 27 | 28 | try { 29 | await newUser.save(); 30 | res.json('Signup successful'); 31 | } catch (error) { 32 | next(error); 33 | } 34 | }; 35 | 36 | export const signin = async (req, res, next) => { 37 | const { email, password } = req.body; 38 | 39 | if (!email || !password || email === '' || password === '') { 40 | next(errorHandler(400, 'All fields are required')); 41 | } 42 | 43 | try { 44 | const validUser = await User.findOne({ email }); 45 | if (!validUser) { 46 | return next(errorHandler(404, 'User not found')); 47 | } 48 | const validPassword = bcryptjs.compareSync(password, validUser.password); 49 | if (!validPassword) { 50 | return next(errorHandler(400, 'Invalid password')); 51 | } 52 | const token = jwt.sign( 53 | { id: validUser._id, isAdmin: validUser.isAdmin }, 54 | process.env.JWT_SECRET 55 | ); 56 | 57 | const { password: pass, ...rest } = validUser._doc; 58 | 59 | res 60 | .status(200) 61 | .cookie('access_token', token, { 62 | httpOnly: true, 63 | }) 64 | .json(rest); 65 | } catch (error) { 66 | next(error); 67 | } 68 | }; 69 | 70 | export const google = async (req, res, next) => { 71 | const { email, name, googlePhotoUrl } = req.body; 72 | try { 73 | const user = await User.findOne({ email }); 74 | if (user) { 75 | const token = jwt.sign( 76 | { id: user._id, isAdmin: user.isAdmin }, 77 | process.env.JWT_SECRET 78 | ); 79 | const { password, ...rest } = user._doc; 80 | res 81 | .status(200) 82 | .cookie('access_token', token, { 83 | httpOnly: true, 84 | }) 85 | .json(rest); 86 | } else { 87 | const generatedPassword = 88 | Math.random().toString(36).slice(-8) + 89 | Math.random().toString(36).slice(-8); 90 | const hashedPassword = bcryptjs.hashSync(generatedPassword, 10); 91 | const newUser = new User({ 92 | username: 93 | name.toLowerCase().split(' ').join('') + 94 | Math.random().toString(9).slice(-4), 95 | email, 96 | password: hashedPassword, 97 | profilePicture: googlePhotoUrl, 98 | }); 99 | await newUser.save(); 100 | const token = jwt.sign( 101 | { id: newUser._id, isAdmin: newUser.isAdmin }, 102 | process.env.JWT_SECRET 103 | ); 104 | const { password, ...rest } = newUser._doc; 105 | res 106 | .status(200) 107 | .cookie('access_token', token, { 108 | httpOnly: true, 109 | }) 110 | .json(rest); 111 | } 112 | } catch (error) { 113 | next(error); 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /api/controllers/comment.controller.js: -------------------------------------------------------------------------------- 1 | import Comment from '../models/comment.model.js'; 2 | 3 | export const createComment = async (req, res, next) => { 4 | try { 5 | const { content, postId, userId } = req.body; 6 | 7 | if (userId !== req.user.id) { 8 | return next( 9 | errorHandler(403, 'You are not allowed to create this comment') 10 | ); 11 | } 12 | 13 | const newComment = new Comment({ 14 | content, 15 | postId, 16 | userId, 17 | }); 18 | await newComment.save(); 19 | 20 | res.status(200).json(newComment); 21 | } catch (error) { 22 | next(error); 23 | } 24 | }; 25 | 26 | export const getPostComments = async (req, res, next) => { 27 | try { 28 | const comments = await Comment.find({ postId: req.params.postId }).sort({ 29 | createdAt: -1, 30 | }); 31 | res.status(200).json(comments); 32 | } catch (error) { 33 | next(error); 34 | } 35 | }; 36 | 37 | export const likeComment = async (req, res, next) => { 38 | try { 39 | const comment = await Comment.findById(req.params.commentId); 40 | if (!comment) { 41 | return next(errorHandler(404, 'Comment not found')); 42 | } 43 | const userIndex = comment.likes.indexOf(req.user.id); 44 | if (userIndex === -1) { 45 | comment.numberOfLikes += 1; 46 | comment.likes.push(req.user.id); 47 | } else { 48 | comment.numberOfLikes -= 1; 49 | comment.likes.splice(userIndex, 1); 50 | } 51 | await comment.save(); 52 | res.status(200).json(comment); 53 | } catch (error) { 54 | next(error); 55 | } 56 | }; 57 | 58 | export const editComment = async (req, res, next) => { 59 | try { 60 | const comment = await Comment.findById(req.params.commentId); 61 | if (!comment) { 62 | return next(errorHandler(404, 'Comment not found')); 63 | } 64 | if (comment.userId !== req.user.id && !req.user.isAdmin) { 65 | return next( 66 | errorHandler(403, 'You are not allowed to edit this comment') 67 | ); 68 | } 69 | 70 | const editedComment = await Comment.findByIdAndUpdate( 71 | req.params.commentId, 72 | { 73 | content: req.body.content, 74 | }, 75 | { new: true } 76 | ); 77 | res.status(200).json(editedComment); 78 | } catch (error) { 79 | next(error); 80 | } 81 | }; 82 | 83 | export const deleteComment = async (req, res, next) => { 84 | try { 85 | const comment = await Comment.findById(req.params.commentId); 86 | if (!comment) { 87 | return next(errorHandler(404, 'Comment not found')); 88 | } 89 | if (comment.userId !== req.user.id && !req.user.isAdmin) { 90 | return next( 91 | errorHandler(403, 'You are not allowed to delete this comment') 92 | ); 93 | } 94 | await Comment.findByIdAndDelete(req.params.commentId); 95 | res.status(200).json('Comment has been deleted'); 96 | } catch (error) { 97 | next(error); 98 | } 99 | }; 100 | 101 | export const getcomments = async (req, res, next) => { 102 | if (!req.user.isAdmin) 103 | return next(errorHandler(403, 'You are not allowed to get all comments')); 104 | try { 105 | const startIndex = parseInt(req.query.startIndex) || 0; 106 | const limit = parseInt(req.query.limit) || 9; 107 | const sortDirection = req.query.sort === 'desc' ? -1 : 1; 108 | const comments = await Comment.find() 109 | .sort({ createdAt: sortDirection }) 110 | .skip(startIndex) 111 | .limit(limit); 112 | const totalComments = await Comment.countDocuments(); 113 | const now = new Date(); 114 | const oneMonthAgo = new Date( 115 | now.getFullYear(), 116 | now.getMonth() - 1, 117 | now.getDate() 118 | ); 119 | const lastMonthComments = await Comment.countDocuments({ 120 | createdAt: { $gte: oneMonthAgo }, 121 | }); 122 | res.status(200).json({ comments, totalComments, lastMonthComments }); 123 | } catch (error) { 124 | next(error); 125 | } 126 | }; 127 | -------------------------------------------------------------------------------- /api/controllers/post.controller.js: -------------------------------------------------------------------------------- 1 | import Post from '../models/post.model.js'; 2 | import { errorHandler } from '../utils/error.js'; 3 | 4 | export const create = async (req, res, next) => { 5 | if (!req.user.isAdmin) { 6 | return next(errorHandler(403, 'You are not allowed to create a post')); 7 | } 8 | if (!req.body.title || !req.body.content) { 9 | return next(errorHandler(400, 'Please provide all required fields')); 10 | } 11 | const slug = req.body.title 12 | .split(' ') 13 | .join('-') 14 | .toLowerCase() 15 | .replace(/[^a-zA-Z0-9-]/g, ''); 16 | const newPost = new Post({ 17 | ...req.body, 18 | slug, 19 | userId: req.user.id, 20 | }); 21 | try { 22 | const savedPost = await newPost.save(); 23 | res.status(201).json(savedPost); 24 | } catch (error) { 25 | next(error); 26 | } 27 | }; 28 | 29 | export const getposts = async (req, res, next) => { 30 | try { 31 | const startIndex = parseInt(req.query.startIndex) || 0; 32 | const limit = parseInt(req.query.limit) || 9; 33 | const sortDirection = req.query.order === 'asc' ? 1 : -1; 34 | const posts = await Post.find({ 35 | ...(req.query.userId && { userId: req.query.userId }), 36 | ...(req.query.category && { category: req.query.category }), 37 | ...(req.query.slug && { slug: req.query.slug }), 38 | ...(req.query.postId && { _id: req.query.postId }), 39 | ...(req.query.searchTerm && { 40 | $or: [ 41 | { title: { $regex: req.query.searchTerm, $options: 'i' } }, 42 | { content: { $regex: req.query.searchTerm, $options: 'i' } }, 43 | ], 44 | }), 45 | }) 46 | .sort({ updatedAt: sortDirection }) 47 | .skip(startIndex) 48 | .limit(limit); 49 | 50 | const totalPosts = await Post.countDocuments(); 51 | 52 | const now = new Date(); 53 | 54 | const oneMonthAgo = new Date( 55 | now.getFullYear(), 56 | now.getMonth() - 1, 57 | now.getDate() 58 | ); 59 | 60 | const lastMonthPosts = await Post.countDocuments({ 61 | createdAt: { $gte: oneMonthAgo }, 62 | }); 63 | 64 | res.status(200).json({ 65 | posts, 66 | totalPosts, 67 | lastMonthPosts, 68 | }); 69 | } catch (error) { 70 | next(error); 71 | } 72 | }; 73 | 74 | export const deletepost = async (req, res, next) => { 75 | if (!req.user.isAdmin || req.user.id !== req.params.userId) { 76 | return next(errorHandler(403, 'You are not allowed to delete this post')); 77 | } 78 | try { 79 | await Post.findByIdAndDelete(req.params.postId); 80 | res.status(200).json('The post has been deleted'); 81 | } catch (error) { 82 | next(error); 83 | } 84 | }; 85 | 86 | export const updatepost = async (req, res, next) => { 87 | if (!req.user.isAdmin || req.user.id !== req.params.userId) { 88 | return next(errorHandler(403, 'You are not allowed to update this post')); 89 | } 90 | try { 91 | const updatedPost = await Post.findByIdAndUpdate( 92 | req.params.postId, 93 | { 94 | $set: { 95 | title: req.body.title, 96 | content: req.body.content, 97 | category: req.body.category, 98 | image: req.body.image, 99 | }, 100 | }, 101 | { new: true } 102 | ); 103 | res.status(200).json(updatedPost); 104 | } catch (error) { 105 | next(error); 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /api/controllers/user.controller.js: -------------------------------------------------------------------------------- 1 | import bcryptjs from 'bcryptjs'; 2 | import { errorHandler } from '../utils/error.js'; 3 | import User from '../models/user.model.js'; 4 | 5 | export const test = (req, res) => { 6 | res.json({ message: 'API is working!' }); 7 | }; 8 | 9 | export const updateUser = async (req, res, next) => { 10 | if (req.user.id !== req.params.userId) { 11 | return next(errorHandler(403, 'You are not allowed to update this user')); 12 | } 13 | if (req.body.password) { 14 | if (req.body.password.length < 6) { 15 | return next(errorHandler(400, 'Password must be at least 6 characters')); 16 | } 17 | req.body.password = bcryptjs.hashSync(req.body.password, 10); 18 | } 19 | if (req.body.username) { 20 | if (req.body.username.length < 7 || req.body.username.length > 20) { 21 | return next( 22 | errorHandler(400, 'Username must be between 7 and 20 characters') 23 | ); 24 | } 25 | if (req.body.username.includes(' ')) { 26 | return next(errorHandler(400, 'Username cannot contain spaces')); 27 | } 28 | if (req.body.username !== req.body.username.toLowerCase()) { 29 | return next(errorHandler(400, 'Username must be lowercase')); 30 | } 31 | if (!req.body.username.match(/^[a-zA-Z0-9]+$/)) { 32 | return next( 33 | errorHandler(400, 'Username can only contain letters and numbers') 34 | ); 35 | } 36 | } 37 | try { 38 | const updatedUser = await User.findByIdAndUpdate( 39 | req.params.userId, 40 | { 41 | $set: { 42 | username: req.body.username, 43 | email: req.body.email, 44 | profilePicture: req.body.profilePicture, 45 | password: req.body.password, 46 | }, 47 | }, 48 | { new: true } 49 | ); 50 | const { password, ...rest } = updatedUser._doc; 51 | res.status(200).json(rest); 52 | } catch (error) { 53 | next(error); 54 | } 55 | }; 56 | 57 | export const deleteUser = async (req, res, next) => { 58 | if (!req.user.isAdmin && req.user.id !== req.params.userId) { 59 | return next(errorHandler(403, 'You are not allowed to delete this user')); 60 | } 61 | try { 62 | await User.findByIdAndDelete(req.params.userId); 63 | res.status(200).json('User has been deleted'); 64 | } catch (error) { 65 | next(error); 66 | } 67 | }; 68 | 69 | export const signout = (req, res, next) => { 70 | try { 71 | res 72 | .clearCookie('access_token') 73 | .status(200) 74 | .json('User has been signed out'); 75 | } catch (error) { 76 | next(error); 77 | } 78 | }; 79 | 80 | export const getUsers = async (req, res, next) => { 81 | if (!req.user.isAdmin) { 82 | return next(errorHandler(403, 'You are not allowed to see all users')); 83 | } 84 | try { 85 | const startIndex = parseInt(req.query.startIndex) || 0; 86 | const limit = parseInt(req.query.limit) || 9; 87 | const sortDirection = req.query.sort === 'asc' ? 1 : -1; 88 | 89 | const users = await User.find() 90 | .sort({ createdAt: sortDirection }) 91 | .skip(startIndex) 92 | .limit(limit); 93 | 94 | const usersWithoutPassword = users.map((user) => { 95 | const { password, ...rest } = user._doc; 96 | return rest; 97 | }); 98 | 99 | const totalUsers = await User.countDocuments(); 100 | 101 | const now = new Date(); 102 | 103 | const oneMonthAgo = new Date( 104 | now.getFullYear(), 105 | now.getMonth() - 1, 106 | now.getDate() 107 | ); 108 | const lastMonthUsers = await User.countDocuments({ 109 | createdAt: { $gte: oneMonthAgo }, 110 | }); 111 | 112 | res.status(200).json({ 113 | users: usersWithoutPassword, 114 | totalUsers, 115 | lastMonthUsers, 116 | }); 117 | } catch (error) { 118 | next(error); 119 | } 120 | }; 121 | 122 | export const getUser = async (req, res, next) => { 123 | try { 124 | const user = await User.findById(req.params.userId); 125 | if (!user) { 126 | return next(errorHandler(404, 'User not found')); 127 | } 128 | const { password, ...rest } = user._doc; 129 | res.status(200).json(rest); 130 | } catch (error) { 131 | next(error); 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import mongoose from 'mongoose'; 3 | import dotenv from 'dotenv'; 4 | import userRoutes from './routes/user.route.js'; 5 | import authRoutes from './routes/auth.route.js'; 6 | import postRoutes from './routes/post.route.js'; 7 | import commentRoutes from './routes/comment.route.js'; 8 | import cookieParser from 'cookie-parser'; 9 | import path from 'path'; 10 | 11 | dotenv.config(); 12 | 13 | mongoose 14 | .connect(process.env.MONGO) 15 | .then(() => { 16 | console.log('MongoDb is connected'); 17 | }) 18 | .catch((err) => { 19 | console.log(err); 20 | }); 21 | 22 | const __dirname = path.resolve(); 23 | 24 | const app = express(); 25 | 26 | app.use(express.json()); 27 | app.use(cookieParser()); 28 | 29 | app.listen(3000, () => { 30 | console.log('Server is running on port 3000!'); 31 | }); 32 | 33 | app.use('/api/user', userRoutes); 34 | app.use('/api/auth', authRoutes); 35 | app.use('/api/post', postRoutes); 36 | app.use('/api/comment', commentRoutes); 37 | 38 | app.use(express.static(path.join(__dirname, '/client/dist'))); 39 | 40 | app.get('*', (req, res) => { 41 | res.sendFile(path.join(__dirname, 'client', 'dist', 'index.html')); 42 | }); 43 | 44 | app.use((err, req, res, next) => { 45 | const statusCode = err.statusCode || 500; 46 | const message = err.message || 'Internal Server Error'; 47 | res.status(statusCode).json({ 48 | success: false, 49 | statusCode, 50 | message, 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /api/models/comment.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const commentSchema = new mongoose.Schema( 4 | { 5 | content: { 6 | type: String, 7 | required: true, 8 | }, 9 | postId: { 10 | type: String, 11 | required: true, 12 | }, 13 | userId: { 14 | type: String, 15 | required: true, 16 | }, 17 | likes: { 18 | type: Array, 19 | default: [], 20 | }, 21 | numberOfLikes: { 22 | type: Number, 23 | default: 0, 24 | }, 25 | }, 26 | { timestamps: true } 27 | ); 28 | 29 | const Comment = mongoose.model('Comment', commentSchema); 30 | 31 | export default Comment; 32 | -------------------------------------------------------------------------------- /api/models/post.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const postSchema = new mongoose.Schema( 4 | { 5 | userId: { 6 | type: String, 7 | required: true, 8 | }, 9 | content: { 10 | type: String, 11 | required: true, 12 | }, 13 | title: { 14 | type: String, 15 | required: true, 16 | unique: true, 17 | }, 18 | image: { 19 | type: String, 20 | default: 21 | 'https://www.hostinger.com/tutorials/wp-content/uploads/sites/2/2021/09/how-to-write-a-blog-post.png', 22 | }, 23 | category: { 24 | type: String, 25 | default: 'uncategorized', 26 | }, 27 | slug: { 28 | type: String, 29 | required: true, 30 | unique: true, 31 | }, 32 | }, 33 | { timestamps: true } 34 | ); 35 | 36 | const Post = mongoose.model('Post', postSchema); 37 | 38 | export default Post; 39 | -------------------------------------------------------------------------------- /api/models/user.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const userSchema = new mongoose.Schema( 4 | { 5 | username: { 6 | type: String, 7 | required: true, 8 | unique: true, 9 | }, 10 | email: { 11 | type: String, 12 | required: true, 13 | unique: true, 14 | }, 15 | password: { 16 | type: String, 17 | required: true, 18 | }, 19 | profilePicture: { 20 | type: String, 21 | default: 22 | 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png', 23 | }, 24 | isAdmin: { 25 | type: Boolean, 26 | default: false, 27 | }, 28 | }, 29 | { timestamps: true } 30 | ); 31 | 32 | const User = mongoose.model('User', userSchema); 33 | 34 | export default User; 35 | -------------------------------------------------------------------------------- /api/routes/auth.route.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { google, signin, signup } from '../controllers/auth.controller.js'; 3 | 4 | const router = express.Router(); 5 | 6 | 7 | router.post('/signup', signup); 8 | router.post('/signin', signin); 9 | router.post('/google', google) 10 | 11 | export default router; -------------------------------------------------------------------------------- /api/routes/comment.route.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { verifyToken } from '../utils/verifyUser.js'; 3 | import { 4 | createComment, 5 | deleteComment, 6 | editComment, 7 | getPostComments, 8 | getcomments, 9 | likeComment, 10 | } from '../controllers/comment.controller.js'; 11 | 12 | const router = express.Router(); 13 | 14 | router.post('/create', verifyToken, createComment); 15 | router.get('/getPostComments/:postId', getPostComments); 16 | router.put('/likeComment/:commentId', verifyToken, likeComment); 17 | router.put('/editComment/:commentId', verifyToken, editComment); 18 | router.delete('/deleteComment/:commentId', verifyToken, deleteComment); 19 | router.get('/getcomments', verifyToken, getcomments); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /api/routes/post.route.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { verifyToken } from '../utils/verifyUser.js'; 3 | import { create, deletepost, getposts, updatepost } from '../controllers/post.controller.js'; 4 | 5 | const router = express.Router(); 6 | 7 | router.post('/create', verifyToken, create) 8 | router.get('/getposts', getposts) 9 | router.delete('/deletepost/:postId/:userId', verifyToken, deletepost) 10 | router.put('/updatepost/:postId/:userId', verifyToken, updatepost) 11 | 12 | 13 | export default router; -------------------------------------------------------------------------------- /api/routes/user.route.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { 3 | deleteUser, 4 | getUser, 5 | getUsers, 6 | signout, 7 | test, 8 | updateUser, 9 | } from '../controllers/user.controller.js'; 10 | import { verifyToken } from '../utils/verifyUser.js'; 11 | 12 | const router = express.Router(); 13 | 14 | router.get('/test', test); 15 | router.put('/update/:userId', verifyToken, updateUser); 16 | router.delete('/delete/:userId', verifyToken, deleteUser); 17 | router.post('/signout', signout); 18 | router.get('/getusers', verifyToken, getUsers); 19 | router.get('/:userId', getUser); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /api/utils/error.js: -------------------------------------------------------------------------------- 1 | export const errorHandler = (statusCode, message) => { 2 | const error = new Error(); 3 | error.statusCode = statusCode; 4 | error.message = message; 5 | return error; 6 | }; 7 | -------------------------------------------------------------------------------- /api/utils/verifyUser.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { errorHandler } from './error.js'; 3 | export const verifyToken = (req, res, next) => { 4 | const token = req.cookies.access_token; 5 | if (!token) { 6 | return next(errorHandler(401, 'Unauthorized')); 7 | } 8 | jwt.verify(token, process.env.JWT_SECRET, (err, user) => { 9 | if (err) { 10 | return next(errorHandler(401, 'Unauthorized')); 11 | } 12 | req.user = user; 13 | next(); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MERN Blog 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@reduxjs/toolkit": "^2.0.1", 14 | "caniuse-lite": "^1.0.30001707", 15 | "firebase": "^10.7.1", 16 | "flowbite-react": "^0.7.0", 17 | "moment": "^2.29.4", 18 | "react": "^18.2.0", 19 | "react-circular-progressbar": "^2.1.0", 20 | "react-dom": "^18.2.0", 21 | "react-icons": "^4.12.0", 22 | "react-quill": "^2.0.0", 23 | "react-redux": "^9.0.1", 24 | "react-router-dom": "^6.20.0", 25 | "redux-persist": "^6.0.0" 26 | }, 27 | "devDependencies": { 28 | "@tailwindcss/line-clamp": "^0.4.4", 29 | "@types/react": "^18.2.37", 30 | "@types/react-dom": "^18.2.15", 31 | "@vitejs/plugin-react-swc": "^3.5.0", 32 | "autoprefixer": "^10.4.16", 33 | "eslint": "^8.53.0", 34 | "eslint-plugin-react": "^7.33.2", 35 | "eslint-plugin-react-hooks": "^4.6.0", 36 | "eslint-plugin-react-refresh": "^0.4.4", 37 | "postcss": "^8.4.31", 38 | "tailwind-scrollbar": "^3.0.5", 39 | "tailwindcss": "^3.3.5", 40 | "vite": "^5.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 2 | import Home from './pages/Home'; 3 | import About from './pages/About'; 4 | import SignIn from './pages/SignIn'; 5 | import Dashboard from './pages/Dashboard'; 6 | import Projects from './pages/Projects'; 7 | import SignUp from './pages/SignUp'; 8 | import Header from './components/Header'; 9 | import Footer from './components/Footer'; 10 | import PrivateRoute from './components/PrivateRoute'; 11 | import OnlyAdminPrivateRoute from './components/OnlyAdminPrivateRoute'; 12 | import CreatePost from './pages/CreatePost'; 13 | import UpdatePost from './pages/UpdatePost'; 14 | import PostPage from './pages/PostPage'; 15 | import ScrollToTop from './components/ScrollToTop'; 16 | import Search from './pages/Search'; 17 | 18 | export default function App() { 19 | return ( 20 | 21 | 22 |
23 | 24 | } /> 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | }> 30 | } /> 31 | 32 | }> 33 | } /> 34 | } /> 35 | 36 | 37 | } /> 38 | } /> 39 | 40 |