├── README.md ├── frontend ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── blog.svg │ ├── forgot.svg │ ├── icon.svg │ ├── login-pana.svg │ ├── login.svg │ ├── reset.svg │ └── signup.svg ├── src │ ├── App.css │ ├── App.jsx │ ├── authFolder │ │ ├── Forgot.jsx │ │ ├── Login.jsx │ │ ├── NotVerified.jsx │ │ ├── Reset.jsx │ │ ├── Signup.jsx │ │ ├── auth.css │ │ └── verify.jsx │ ├── blogFolder │ │ ├── Create.jsx │ │ └── homeBlog │ │ │ └── homeBlog.jsx │ ├── homeFolder │ │ ├── Home.jsx │ │ ├── home.css │ │ ├── home.jpg │ │ ├── homy.jpg │ │ ├── test3.jpg │ │ └── test4.jpg │ ├── index.css │ └── main.jsx └── vite.config.js ├── img ├── home.png ├── login.png └── signup.png └── server ├── index.js ├── middleware └── authMiddleware.js ├── model ├── Admin.js ├── blog.js └── user.js ├── package-lock.json ├── package.json └── routes ├── auth.js └── data.js /README.md: -------------------------------------------------------------------------------- 1 | # MERN Blog App 2 | 3 | ## Description 4 | 5 | The MERN Blog App is a full-stack blogging application built using the MERN stack (MongoDB, Express.js, React, Node.js). This application allows users to create, read, update, and delete blog posts. It also includes user authentication and authorization, allowing users to register and log in to manage their own posts.. 6 | 7 | ## Images 8 | 9 | ### Login Form 10 | 11 | ![Image Three](img/login.png) 12 | 13 | ### Signup Form 14 | 15 | ![Image Two](img/signup.png) 16 | 17 | ### Home Page 18 | 19 | ![Image One](img/home.png) 20 | 21 | 22 | ## Features 23 | 24 | - **User Authentication:** Register, Login, Logout 25 | - **CRUD Operations:** Create, Read, Update, and Delete blog posts 26 | - **User-specific Post Management:** Users can manage their own posts 27 | - **Responsive Design:** Ensures usability across various devices 28 | 29 | ## Technologies Used 30 | 31 | ### Frontend 32 | 33 | - **React:** A JavaScript library for building user interfaces 34 | - **Redux:** State management library 35 | - **Axios:** Promise-based HTTP client 36 | - **CSS:** Styling 37 | 38 | ### Backend 39 | 40 | - **Node.js:** JavaScript runtime built on Chrome's V8 JavaScript engine 41 | - **Express.js:** Web framework for Node.js 42 | 43 | ### Database 44 | 45 | - **MongoDB:** NoSQL database 46 | - **Mongoose:** MongoDB object modeling tool 47 | 48 | ### Authentication 49 | 50 | - **JSON Web Tokens (JWT):** For secure user authentication 51 | - **bcrypt:** For password hashing 52 | 53 | ## Installation 54 | 55 | ### Prerequisites 56 | 57 | - **Node.js:** Install from [Node.js official website](https://nodejs.org/) 58 | - **MongoDB:** Install from [MongoDB official website](https://www.mongodb.com/) 59 | 60 | ### Steps 61 | 62 | 1. **Clone the repository:** 63 | 64 | ```sh 65 | git clone https://github.com/DeveloperWilliams/MERN-Blog-App.git 66 | cd mern-blog-app 67 | 68 | ``` 69 | 70 | 2. **Install dependencies for both frontend and backend:** 71 | 72 | ```sh 73 | cd client 74 | npm install 75 | cd ../server 76 | npm install 77 | ``` 78 | 79 | 3. **Set up environment variables:** 80 | 81 | PORT=5000 82 | MONGODB_URI= 83 | JWT_SECRET= 84 | 85 | 4. **Run the application:** 86 | 87 | ```sh 88 | cd server 89 | npm install 90 | npm start 91 | ``` 92 | 93 | Terminal two 94 | 95 | ```sh 96 | cd server 97 | npm install 98 | npm start 99 | ``` 100 | 101 | 102 | ## Contributing 103 | 104 | We welcome contributions from the community! If you'd like to contribute, please follow these steps: 105 | 106 | 1. Fork the repository to your own GitHub account. 107 | 2. Clone the forked repository to your local machine. 108 | 3. Create a new branch with a descriptive name for your feature or bug fix. 109 | 4. Make your changes and commit them with clear and descriptive commit messages 110 | 5. Push your changes to your forked repository. 111 | 6. Open a Pull Request to the main repository, providing a detailed description of your changes and the problem they solve. 112 | 113 | Please make sure your code adheres to my coding standards and passes all tests. Thanks! 114 | 115 | 116 | ## License 117 | 118 | This project is licensed under the MIT License 119 | -------------------------------------------------------------------------------- /frontend/.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/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /frontend/.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 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blog 8 | 9 | 10 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 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 | "axios": "^1.7.2", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-router-dom": "^6.23.1", 17 | "react-toastify": "^10.0.5" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^18.2.66", 21 | "@types/react-dom": "^18.2.22", 22 | "@vitejs/plugin-react": "^4.2.1", 23 | "eslint": "^8.57.0", 24 | "eslint-plugin-react": "^7.34.1", 25 | "eslint-plugin-react-hooks": "^4.6.0", 26 | "eslint-plugin-react-refresh": "^0.4.6", 27 | "vite": "^5.2.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/public/blog.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/forgot.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/reset.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Route, Routes} from "react-router-dom" 3 | import Login from './authFolder/Login' 4 | import Signup from './authFolder/Signup' 5 | import Forgot from './authFolder/Forgot' 6 | import Reset from './authFolder/Reset' 7 | import Verify from './authFolder/verify' 8 | import Home from './homeFolder/Home' 9 | import Create from './blogFolder/Create' 10 | import NotVerified from './authFolder/NotVerified' 11 | import HomeBlog from './blogFolder/homeBlog/homeBlog' 12 | 13 | function App() { 14 | return ( 15 | 16 | }> 17 | }> 18 | }> 19 | }> 20 | }> 21 | }> 22 | }> 23 | }> 24 | }> 25 | 26 | ) 27 | } 28 | 29 | export default App -------------------------------------------------------------------------------- /frontend/src/authFolder/Forgot.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../App.css"; 3 | import "./auth.css"; 4 | import { Link } from "react-router-dom"; 5 | import { useState } from "react"; 6 | import axios from "axios"; 7 | import { ToastContainer, toast, Bounce } from "react-toastify"; 8 | import "react-toastify/dist/ReactToastify.css"; 9 | 10 | const Forgot = () => { 11 | const [email, setEmail] = useState(""); 12 | const [loading, setLoading] = useState(false); 13 | 14 | const handleSubmit = async (e) => { 15 | e.preventDefault(); 16 | setLoading(true); 17 | 18 | try { 19 | const response = await axios.post("http://localhost:5000/auth/forgot", { 20 | email, 21 | }); 22 | console.log(response.data.message); 23 | 24 | if (response.data.message === "Reset Link Sent") { 25 | toast.success("Reset Link Sent to Email"); 26 | } 27 | } catch (error) { 28 | toast.error(error.response.data.message || error.message); 29 | } finally { 30 | setLoading(false); 31 | setEmail(""); 32 | } 33 | }; 34 | 35 | return ( 36 | <> 37 |
38 | 50 |
51 |
52 | 53 |
54 |
55 |

56 | Hi 👋,
Forgot Password! 57 |

58 |
59 | setEmail(e.target.value)} 65 | /> 66 |
67 | 70 |

71 | 72 | Login Instead 73 | 74 |

75 |
76 |
77 |
78 |
79 |
80 | 81 | ); 82 | }; 83 | 84 | export default Forgot; 85 | -------------------------------------------------------------------------------- /frontend/src/authFolder/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "../App.css"; 3 | import './auth.css' 4 | import { Link } from "react-router-dom"; 5 | import axios from "axios"; 6 | import { useNavigate } from "react-router-dom"; 7 | import { ToastContainer, toast, Bounce } from "react-toastify"; 8 | import "react-toastify/dist/ReactToastify.css"; 9 | 10 | const Login = () => { 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const [loading, setLoading] = useState(false); 14 | const navigate = useNavigate(); 15 | 16 | const handleSubmit = async (e) => { 17 | e.preventDefault(); 18 | setLoading(true); 19 | try { 20 | const response = await axios.post("http://localhost:5000/auth/login", { 21 | email, 22 | password, 23 | }); 24 | 25 | if (response.data.message === "Login Successful") { 26 | navigate("/home"); 27 | toast.success("Login Succesfull") 28 | } 29 | } catch (error) { 30 | if (error.response) { 31 | const { message, redirect } = error.response.data; 32 | 33 | if (message === "Email Not Found") { 34 | toast.error("Email Not Found"); 35 | } else if (message === "Password Does Not Match") { 36 | toast.error("Incorrect Password"); 37 | } else if (message === "Email Not Verified") { 38 | navigate(redirect); 39 | } else { 40 | toast.error(message); 41 | } 42 | } else { 43 | toast.error(error.message); 44 | } 45 | } finally { 46 | setLoading(false); 47 | setEmail("") 48 | setPassword("") 49 | } 50 | }; 51 | 52 | return ( 53 |
54 | 66 |
67 |
68 | Login 69 |
70 |
71 |

72 | Hi 👋, Welcome Back! 73 |

74 |
75 | setEmail(e.target.value)} 82 | /> 83 |
84 | setPassword(e.target.value)} 91 | /> 92 | Reset Password 93 |
94 |
95 | 98 |

99 | Not Having Account? Signup 100 |

101 |
102 |
103 |
104 |
105 |
106 | ); 107 | }; 108 | 109 | export default Login; 110 | -------------------------------------------------------------------------------- /frontend/src/authFolder/NotVerified.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NotVerified = () => { 4 | return ( 5 | <> 6 |
18 |

23 | Email Not Verified 😞, 24 |
Please Verify Email First. 25 |

26 |
27 | 28 | ); 29 | }; 30 | 31 | export default NotVerified; 32 | -------------------------------------------------------------------------------- /frontend/src/authFolder/Reset.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "../App.css"; 3 | import { useParams, useNavigate } from "react-router-dom"; 4 | import "./auth.css"; 5 | import axios from "axios"; 6 | import { ToastContainer, toast, Bounce } from "react-toastify"; 7 | import "react-toastify/dist/ReactToastify.css"; 8 | 9 | const Signup = () => { 10 | const [password, setPassword] = useState(""); 11 | const [confirmedPassword, setConfirmedPassword] = useState(""); 12 | const { token } = useParams(); 13 | const navigate = useNavigate(); 14 | 15 | const handleSubmit = async (e) => { 16 | e.preventDefault(); 17 | 18 | try { 19 | const response = await axios.post( 20 | `http://localhost:5000/auth/reset/${token}`, 21 | { 22 | password, 23 | confirmedPassword, 24 | } 25 | ); 26 | 27 | if (response.data.message === "Password Updated") { 28 | navigate("/login"); 29 | } 30 | } catch (error) { 31 | toast.error(error.response.data.message || error.message); 32 | } 33 | }; 34 | 35 | return ( 36 | <> 37 |
38 | 50 |
51 |
52 | 53 |
54 |
55 |

56 | Hi 👋, Welcome! 57 |

58 |
59 |
60 | setPassword(e.target.value)} 66 | /> 67 |
68 |
69 | setConfirmedPassword(e.target.value)} 75 | /> 76 |
77 |
78 | 79 |
80 |
81 |
82 |
83 |
84 | 85 | ); 86 | }; 87 | 88 | export default Signup; 89 | -------------------------------------------------------------------------------- /frontend/src/authFolder/Signup.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "../App.css"; 3 | import { Link, useNavigate } from "react-router-dom"; 4 | import axios from "axios"; 5 | import "./auth.css"; 6 | import { ToastContainer, toast, Bounce } from "react-toastify"; 7 | import "react-toastify/dist/ReactToastify.css"; 8 | 9 | const Signup = () => { 10 | const [email, setEmail] = useState(""); 11 | const [password, setPassword] = useState(""); 12 | const [confirmedPassword, setConfirmedPassword] = useState(""); 13 | const [loading, setLoading] = useState(false); 14 | 15 | const navigate = useNavigate(); // useNavigate should be defined outside of handleSubmit 16 | 17 | const handleSubmit = async (e) => { 18 | e.preventDefault(); 19 | 20 | setLoading(true); 21 | 22 | try { 23 | const response = await axios.post("http://localhost:5000/auth/register", { 24 | email, 25 | password, 26 | confirmedPassword, 27 | }); 28 | if (response.data.message === "User Created") { 29 | toast.success("User Created Succesfully"); 30 | setTimeout(() => { 31 | navigate("/login"); 32 | }, 5000); 33 | } 34 | } catch (error) { 35 | toast.error(error.response?.data?.message || "Use Strong Password" ); 36 | } finally { 37 | setLoading(false); 38 | setEmail(""); 39 | setPassword(""); 40 | setConfirmedPassword(""); 41 | } 42 | }; 43 | 44 | return ( 45 | <> 46 |
47 | 59 |
60 |
61 | Signup Illustration 62 |
63 |
64 |

65 | Hi 👋, Welcome! 66 |

67 |
68 | setEmail(e.target.value)} 74 | /> 75 |
76 | setPassword(e.target.value)} 82 | /> 83 |
84 |
85 | setConfirmedPassword(e.target.value)} 91 | /> 92 |
93 |
94 | 97 |

98 | Already have an account? Login 99 |

100 |
101 |
102 |
103 |
104 |
105 | 106 | ); 107 | }; 108 | 109 | export default Signup; 110 | -------------------------------------------------------------------------------- /frontend/src/authFolder/auth.css: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | font-family: "Pacifico", cursive; 4 | font-family: "Kanit", sans-serif; 5 | font-family: "Roboto Slab", serif; 6 | font-family: "Oswald", sans-serif; 7 | */ 8 | .container { 9 | height: 100vh; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | gap: 20px; 14 | } 15 | .container form { 16 | height: 80%; 17 | width: 400px; 18 | } 19 | .container form img { 20 | height: 100%; 21 | width: 100%; 22 | display: flex; 23 | justify-content: center; 24 | font-family: "Roboto Slab", serif; 25 | font-family: "Kanit", sans-serif; 26 | } 27 | .myform { 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: center; 31 | padding: 20px; 32 | font-family: "Pacifico", cursive; 33 | gap: 10px; 34 | padding: 2px 0px; 35 | } 36 | .myform em { 37 | font-size: 30px; 38 | } 39 | .myform div { 40 | margin-top: 30px; 41 | display: flex; 42 | flex-direction: column; 43 | gap: 0px; 44 | } 45 | .myform .mydiv { 46 | display: flex; 47 | flex-direction: column; 48 | gap: 5px; 49 | font-size: 11px; 50 | text-align: right; 51 | font-family: "Pacifico", cursive; 52 | font-family: "Kanit", sans-serif; 53 | } 54 | .myform input { 55 | width: 90%; 56 | height: 40px; 57 | padding: 0px 10px; 58 | outline: none; 59 | font-family: "Pacifico", cursive; 60 | font-family: "Kanit", sans-serif; 61 | font-family: "Roboto Slab", serif; 62 | font-family: "Oswald", sans-serif; 63 | font-family: "Kanit", sans-serif; 64 | font-style: italic; 65 | font-size: 15px; 66 | border: 0px; 67 | border-bottom: 1px solid; 68 | transition: 0.5s ease; 69 | } 70 | .myform input:focus{ 71 | border-bottom: 2px solid; 72 | 73 | } 74 | .myform a { 75 | font-family: cursive; 76 | text-decoration: none; 77 | color: grey; 78 | font-style: italic; 79 | } 80 | .myform input::placeholder { 81 | color: rgb(44, 44, 1); 82 | font-family: "Pacifico", cursive; 83 | font-family: "Kanit", sans-serif; 84 | font-family: cursive; 85 | font-style: italic; 86 | font-size: 12px; 87 | } 88 | .myform .second-div { 89 | display: flex; 90 | gap: 8px; 91 | } 92 | .myform .second-div button { 93 | height: 35px; 94 | border: 0px; 95 | background-color: rgb(107, 107, 7); 96 | color: white; 97 | cursor: pointer; 98 | font-family: "Pacifico", cursive; 99 | font-family: "Kanit", sans-serif; 100 | } 101 | .myform .second-div p { 102 | text-align: right; 103 | } 104 | -------------------------------------------------------------------------------- /frontend/src/authFolder/verify.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useParams, useNavigate } from 'react-router-dom'; 3 | import axios from 'axios'; 4 | import { ToastContainer, toast, Bounce } from 'react-toastify'; 5 | import 'react-toastify/dist/ReactToastify.css'; 6 | 7 | const VerifyAccount = () => { 8 | const { token } = useParams(); 9 | const navigate = useNavigate(); 10 | const [verificationStatus, setVerificationStatus] = useState('Verifying...'); 11 | 12 | useEffect(() => { 13 | const verifyToken = async () => { 14 | try { 15 | const response = await axios.get(`http://localhost:5000/auth/verify/${token}`); 16 | console.log(token) 17 | if (response.data.message === "Invalid Token") { 18 | setVerificationStatus('Invalid Token'); 19 | toast.error('Invalid Token'); 20 | } else { 21 | setVerificationStatus('Verification successful'); 22 | toast.success('Verification successful'); 23 | setTimeout(() => navigate('/login'), 2000); // delay navigation for better UX 24 | } 25 | } catch (error) { 26 | console.error('Error verifying token:', error.response?.data || error.message); 27 | setVerificationStatus('Error verifying token'); 28 | toast.error(`Error verifying token: ${error.response?.data.message || error.message}`); 29 | } 30 | }; 31 | 32 | verifyToken(); 33 | }, [token, navigate]); 34 | 35 | return ( 36 |
37 | 49 |

{verificationStatus}

50 |
51 | ); 52 | }; 53 | 54 | export default VerifyAccount; 55 | -------------------------------------------------------------------------------- /frontend/src/blogFolder/Create.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Create = () => { 4 | return ( 5 | <> 6 |
7 | Creating Blogs 8 |
9 | 10 | ) 11 | } 12 | 13 | export default Create -------------------------------------------------------------------------------- /frontend/src/blogFolder/homeBlog/homeBlog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const HomeBlog = () => { 4 | return ( 5 |
homeBlog
6 | ) 7 | } 8 | 9 | export default HomeBlog 10 | -------------------------------------------------------------------------------- /frontend/src/homeFolder/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import "./home.css" 4 | 5 | //twill0 = L511X4FKR4CXU7X4HVNKJRH5 6 | 7 | const Home = () => { 8 | return ( 9 | <> 10 |
11 |
12 |

13 | ArchyBlog 14 |

15 | Signup Now 16 |
17 |
18 | 19 |
20 |
21 |

The Home for Best Blog, Signup Now!

22 |
23 |
24 | 25 | ); 26 | }; 27 | 28 | export default Home; 29 | -------------------------------------------------------------------------------- /frontend/src/homeFolder/home.css: -------------------------------------------------------------------------------- 1 | .home { 2 | height: 120vh; 3 | width: 100%; 4 | background: url("./test4.jpg"); 5 | display: flex; 6 | flex-direction: column; 7 | } 8 | .nav { 9 | display: flex; 10 | flex-direction: row; 11 | justify-content: space-between; 12 | padding: 20px 120px; 13 | z-index: 1000; 14 | top: 0; 15 | left: 0; 16 | right: 0; 17 | position: fixed; 18 | } 19 | h4 { 20 | font-family: "Pacifico", cursive; 21 | font-family: "Kanit", sans-serif; 22 | font-family: "Roboto Slab", serif; 23 | font-size: 20px; 24 | cursor: pointer; 25 | } 26 | h4 em { 27 | color: rgb(219, 16, 182); 28 | } 29 | .nav a { 30 | text-decoration: none; 31 | padding: 10px 18px; 32 | background-color: rgb(219, 16, 182); 33 | color: white; 34 | font-family: "Pacifico", cursive; 35 | font-family: "Kanit", sans-serif; 36 | font-family: "Roboto Slab", serif; 37 | font-family: "Oswald", sans-serif; 38 | } 39 | .body { 40 | padding-top: 30px; 41 | height: 90vh; 42 | display: flex; 43 | justify-content: center; 44 | align-items: start; 45 | z-index: 0; 46 | } 47 | .body img { 48 | height: 90vh; 49 | } 50 | .lower { 51 | text-align: center; 52 | font-family: "Pacifico", cursive; 53 | font-size: 34px; 54 | } 55 | .lower em { 56 | color: rgb(6, 188, 191); 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/homeFolder/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperWilliams/MERN-Blog-App/4d61d8d25d6f088b57bb82981392e3abc3c369e4/frontend/src/homeFolder/home.jpg -------------------------------------------------------------------------------- /frontend/src/homeFolder/homy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperWilliams/MERN-Blog-App/4d61d8d25d6f088b57bb82981392e3abc3c369e4/frontend/src/homeFolder/homy.jpg -------------------------------------------------------------------------------- /frontend/src/homeFolder/test3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperWilliams/MERN-Blog-App/4d61d8d25d6f088b57bb82981392e3abc3c369e4/frontend/src/homeFolder/test3.jpg -------------------------------------------------------------------------------- /frontend/src/homeFolder/test4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperWilliams/MERN-Blog-App/4d61d8d25d6f088b57bb82981392e3abc3c369e4/frontend/src/homeFolder/test4.jpg -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperWilliams/MERN-Blog-App/4d61d8d25d6f088b57bb82981392e3abc3c369e4/frontend/src/index.css -------------------------------------------------------------------------------- /frontend/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import "./index.css"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | 7 | ReactDOM.createRoot(document.getElementById("root")).render( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /img/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperWilliams/MERN-Blog-App/4d61d8d25d6f088b57bb82981392e3abc3c369e4/img/home.png -------------------------------------------------------------------------------- /img/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperWilliams/MERN-Blog-App/4d61d8d25d6f088b57bb82981392e3abc3c369e4/img/login.png -------------------------------------------------------------------------------- /img/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperWilliams/MERN-Blog-App/4d61d8d25d6f088b57bb82981392e3abc3c369e4/img/signup.png -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import dotenv from "dotenv"; 3 | import cors from "cors"; 4 | import cookieParser from "cookie-parser"; 5 | import mongoose from "mongoose"; 6 | 7 | // Initializing Dotenv 8 | dotenv.config(); 9 | 10 | // Declaring the app 11 | const app = express(); 12 | app.use(cors()); 13 | app.use(express.json()); 14 | app.use(cookieParser()); 15 | 16 | 17 | //ROUTES 18 | //import 19 | import authRoute from "./routes/auth.js" 20 | import blogRoute from "./routes/data.js" 21 | 22 | 23 | //using routes 24 | app.use('/auth', authRoute) 25 | app.use('/blog', blogRoute) 26 | 27 | // MongoDB 28 | mongoose 29 | .connect(process.env.MONGO_URL) 30 | .then(() => { 31 | console.log("DB connected"); 32 | }) 33 | .catch((err) => { 34 | console.error("DB connection error:", err.message); 35 | }); 36 | 37 | // Listening 38 | app.listen(process.env.PORT, () => { 39 | console.log(`Server running on port ${process.env.PORT}`); 40 | }); 41 | -------------------------------------------------------------------------------- /server/middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | const verifyToken = (req, res, next) => { 4 | const token = req.cookies.token; 5 | 6 | if (!token) { 7 | return res.status(401).json({ message: "Access Denied" }); 8 | } 9 | 10 | try { 11 | const verified = jwt.verify(token, process.env.JWT_SECRET); 12 | req.user = verified; 13 | next(); 14 | } catch (error) { 15 | res.status(401).json({ message: "Invalid token" }); 16 | } 17 | }; 18 | 19 | export default verifyToken; 20 | -------------------------------------------------------------------------------- /server/model/Admin.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const { Schema, model } = mongoose; 4 | 5 | const AdminSchema = new Schema( 6 | { 7 | email: { type: String, required: true, unique: true }, 8 | password: { type: String, required: true }, 9 | isVerified: { type: Boolean, default: false }, 10 | role: { type: String, default: "admin" }, 11 | }, 12 | { timestamps: true } 13 | ); 14 | 15 | export default model("AdminModel", AdminSchema); 16 | -------------------------------------------------------------------------------- /server/model/blog.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const { Schema, model } = mongoose; 4 | 5 | const BlogSchema = new Schema({ 6 | title: { type: String, required: true }, 7 | image: { type: String, required: true }, 8 | content: { type: String, required: true }, 9 | author: { type: mongoose.Schema.Types.ObjectId, ref: "UserModel", required: true }, 10 | }); 11 | 12 | export default model('Blog', BlogSchema) 13 | -------------------------------------------------------------------------------- /server/model/user.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const { Schema, model } = mongoose; 4 | 5 | const UserSchema = new Schema( 6 | { 7 | email: { type: String, required: true, unique: true }, 8 | password: { type: String, required: true }, 9 | isVerified: { type: Boolean, default: false }, 10 | role: { type: String, default: "user" }, 11 | }, 12 | { timestamps: true } 13 | ); 14 | 15 | export default model("Usermodel", UserSchema); 16 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon index.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "description": "", 14 | "dependencies": { 15 | "bcrypt": "^5.1.1", 16 | "cookie-parser": "^1.4.6", 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.4.5", 19 | "express": "^4.19.2", 20 | "express-validator": "^7.1.0", 21 | "helmet": "^7.1.0", 22 | "jsonwebtoken": "^9.0.2", 23 | "mongoose": "^8.4.1", 24 | "morgan": "^1.10.0", 25 | "nodemailer": "^6.9.13", 26 | "nodemon": "^3.1.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/routes/auth.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import bcrypt from "bcrypt"; 3 | import jwt from "jsonwebtoken"; 4 | import nodemailer from "nodemailer"; 5 | import Usermodel from "../model/User.js"; 6 | import { body, validationResult } from "express-validator"; 7 | import verifyToken from "../middleware/authMiddleware.js"; 8 | 9 | const router = express.Router(); 10 | 11 | // nodemailer setup 12 | const transporter = nodemailer.createTransport({ 13 | service: "gmail", 14 | auth: { 15 | user: "archywilliams2@gmail.com", 16 | pass: "kfsr ntuc uzkg wnen", 17 | }, 18 | }); 19 | 20 | // Register route 21 | router.post( 22 | "/register", 23 | [ 24 | body("email").isEmail().withMessage("Please enter a valid email"), 25 | body("password").isStrongPassword().withMessage("Use a strong Password"), 26 | body("confirmedPassword") 27 | .custom((value, { req }) => value === req.body.password) 28 | .withMessage("Passwords do not match"), 29 | ], 30 | async (req, res) => { 31 | const errors = validationResult(req); 32 | if (!errors.isEmpty()) { 33 | return res.status(400).json({ errors: errors.array() }); 34 | } 35 | 36 | const { email, password } = req.body; 37 | 38 | try { 39 | // Check if the email already exists. 40 | const existingUser = await Usermodel.findOne({ email }); 41 | if (existingUser) { 42 | return res.status(400).json({ message: "Email Already Exists" }); 43 | } 44 | 45 | const hashedPassword = await bcrypt.hash(password, 10); 46 | const user = new Usermodel({ email, password: hashedPassword }); 47 | await user.save(); 48 | 49 | const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { 50 | expiresIn: "1h", 51 | }); 52 | 53 | const url = `http://localhost:5173/verify/${token}`; 54 | 55 | await transporter.sendMail({ 56 | to: user.email, 57 | subject: `Verify Email`, 58 | html: `Please click on the link to verify: Here`, 59 | }); 60 | 61 | res.status(201).json({ message: "User Created, Verify Email Now" }); 62 | } catch (error) { 63 | res.status(500).json({ message: error.message }); 64 | } 65 | } 66 | ); 67 | 68 | // Token verifying route 69 | router.get(`/verify/:token`, async (req, res) => { 70 | try { 71 | const { token } = req.params; 72 | const { userId } = jwt.verify(token, process.env.JWT_SECRET); 73 | 74 | const user = await Usermodel.findById(userId); 75 | 76 | if (!user) { 77 | return res.status(400).json({ message: "Invalid Token" }); 78 | } 79 | 80 | user.isVerified = true; 81 | await user.save(); 82 | 83 | res.status(200).json({ message: "Verified", redirect: "/login" }); 84 | } catch (error) { 85 | res.status(500).json({ message: error.message }); 86 | } 87 | }); 88 | 89 | // Login route 90 | router.post( 91 | "/login", 92 | [ 93 | body("email").isEmail().withMessage("Please enter a valid email"), 94 | body("password").not().isEmpty().withMessage("Password is required"), 95 | ], 96 | async (req, res) => { 97 | const errors = validationResult(req); 98 | if (!errors.isEmpty()) { 99 | return res.status(400).json({ errors: errors.array() }); 100 | } 101 | 102 | 103 | const { email, password } = req.body; 104 | 105 | try { 106 | const user = await Usermodel.findOne({ email }); 107 | if (!user) { 108 | return res.status(404).json({ message: "Email Not Found" }); 109 | } 110 | 111 | if (!user.isVerified) { 112 | return res 113 | .status(400) 114 | .json({ message: "Email Not Verified", redirect: "/not-verified" }); 115 | } 116 | 117 | const isMatch = await bcrypt.compare(password, user.password); 118 | 119 | if (!isMatch) { 120 | return res.status(400).json({ message: "Password Does Not Match" }); 121 | } 122 | 123 | const token = jwt.sign( 124 | { userId: user._id, email: user.email }, 125 | process.env.JWT_SECRET, 126 | { expiresIn: "1h" } 127 | ); 128 | 129 | res.cookie("token", token, { httpOnly: true }); 130 | res.status(200).json({ message: "Login Successful", redirect: "/home" }); 131 | } catch (error) { 132 | res.status(500).json({ message: error.message }); 133 | } 134 | } 135 | ); 136 | 137 | // Forgot password route 138 | router.post("/forgot", async (req, res) => { 139 | const { email } = req.body; 140 | 141 | try { 142 | const user = await Usermodel.findOne({ email }); 143 | 144 | if (!user) { 145 | return res.status(400).json({ message: "Email Not Found" }); 146 | } 147 | 148 | const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { 149 | expiresIn: "1h", 150 | }); 151 | 152 | const url = `http://localhost:5173/reset-password/${token}`; 153 | 154 | await transporter.sendMail({ 155 | to: user.email, 156 | subject: `Reset Password`, 157 | html: `

Reset your password by clicking here

`, 158 | }); 159 | 160 | res.status(200).json({ message: "Reset Link Sent" }); 161 | } catch (error) { 162 | res.status(500).json({ message: error.message }); 163 | } 164 | }); 165 | 166 | // Reset password route 167 | router.post( 168 | "/reset/:token", 169 | [ 170 | body("password") 171 | .isLength({ min: 6 }) 172 | .withMessage("Password must be at least 6 characters long"), 173 | body("confirmedPassword") 174 | .custom((value, { req }) => value === req.body.password) 175 | .withMessage("Passwords do not match"), 176 | ], 177 | async (req, res) => { 178 | const errors = validationResult(req); 179 | if (!errors.isEmpty()) { 180 | return res.status(400).json({ errors: errors.array() }); 181 | } 182 | 183 | const { token } = req.params; 184 | const { password } = req.body; 185 | 186 | try { 187 | const { userId } = jwt.verify(token, process.env.JWT_SECRET); 188 | const user = await Usermodel.findById(userId); 189 | if (!user) { 190 | return res.status(400).json({ message: "Invalid Token" }); 191 | } 192 | 193 | const hashedPassword = await bcrypt.hash(password, 10); 194 | user.password = hashedPassword; 195 | await user.save(); 196 | 197 | res.status(200).json({ message: "Password Updated", redirect: "/login" }); 198 | } catch (error) { 199 | res.status(500).json({ message: error.message }); 200 | } 201 | } 202 | ); 203 | 204 | router.get("/protected", verifyToken, (req, res, next) => { 205 | res.status(200).json({ message: "This protected route", user: req.user }); 206 | }); 207 | 208 | export default router; 209 | -------------------------------------------------------------------------------- /server/routes/data.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import BlogModel from "../model/Blog.js"; 3 | 4 | const router = express.Router(); 5 | 6 | router.post("/create-blog/", async (req, res) => { 7 | const { title, image, content, author } = req.body; 8 | try { 9 | const blog = new BlogModel({ title, image, content, author }); 10 | await blog.save(); 11 | res.status(200).json({ message: "Success" }); 12 | } catch (error) { 13 | res.status(500).json({ message: error.message }); 14 | } 15 | }); 16 | 17 | router.post("/delete-blog/:id", async (req, res) => { 18 | const { id } = req.params; 19 | try { 20 | const blog = await BlogModel.findByIdAndDelete(id); 21 | if (!blog) { 22 | return res.status(404).json({ message: "Blog not found" }); 23 | } 24 | res.status(200).json({ message: "Blog Deleted" }); 25 | } catch (error) { 26 | res.status(500).json({ message: error.message }); 27 | } 28 | }); 29 | 30 | router.post("/update-blog/:id", async (req, res) => { 31 | const { id } = req.params; 32 | const { title, image, content } = req.body; 33 | try { 34 | const blog = await BlogModel.findById(id); 35 | if (!blog) { 36 | return res.status(404).json({ message: "Blog not found" }); 37 | } 38 | 39 | blog.title = title || blog.title; 40 | blog.image = image || blog.image; 41 | blog.content = content || blog.content; 42 | 43 | await blog.save(); 44 | res.status(200).json({ message: "Blog Updated" }); 45 | } catch (error) { 46 | res.status(500).json({ message: error.message }); 47 | } 48 | }); 49 | 50 | export default router; 51 | --------------------------------------------------------------------------------