├── client ├── src │ ├── index.css │ ├── components │ │ ├── PrivateRoute.jsx │ │ ├── Header.jsx │ │ └── OAuth.jsx │ ├── main.jsx │ ├── firebase.js │ ├── redux │ │ ├── store.js │ │ └── user │ │ │ └── userSlice.js │ ├── App.jsx │ └── pages │ │ ├── About.jsx │ │ ├── Home.jsx │ │ ├── SignIn.jsx │ │ ├── SignUp.jsx │ │ └── Profile.jsx ├── postcss.config.js ├── tailwind.config.js ├── index.html ├── vite.config.js ├── README.md ├── .eslintrc.cjs └── package.json ├── api ├── utils │ ├── error.js │ └── verifyUser.js ├── routes │ ├── auth.route.js │ └── user.route.js ├── models │ └── user.model.js ├── index.js └── controllers │ ├── user.controller.js │ └── auth.controller.js ├── .gitignore └── package.json /client/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } -------------------------------------------------------------------------------- /client/src/components/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import {useSelector} from 'react-redux' 2 | import { Outlet, Navigate } from 'react-router-dom' 3 | 4 | export default function PrivateRoute() { 5 | const {currentUser} = useSelector(state => state.user) 6 | return currentUser ? : 7 | } 8 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Auth App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /api/routes/auth.route.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { signin, signup, google, signout } from '../controllers/auth.controller.js'; 3 | 4 | const router = express.Router(); 5 | 6 | router.post('/signup', signup); 7 | router.post('/signin', signin); 8 | router.post('/google', google); 9 | router.get('/signout', signout); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /client/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | server: { 7 | proxy: { 8 | '/api': { 9 | target: 'http://localhost:3000', 10 | secure: false, 11 | }, 12 | }, 13 | }, 14 | plugins: [react()], 15 | }); 16 | -------------------------------------------------------------------------------- /.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/routes/user.route.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { 3 | test, 4 | updateUser, 5 | deleteUser, 6 | } from '../controllers/user.controller.js'; 7 | import { verifyToken } from '../utils/verifyUser.js'; 8 | 9 | const router = express.Router(); 10 | 11 | router.get('/', test); 12 | router.post('/update/:id', verifyToken, updateUser); 13 | router.delete('/delete/:id', verifyToken, deleteUser); 14 | 15 | export default router; 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /api/utils/verifyUser.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { errorHandler } from './error.js'; 3 | 4 | export const verifyToken = (req, res, next) => { 5 | const token = req.cookies.access_token; 6 | 7 | if (!token) return next(errorHandler(401, 'You are not authenticated!')); 8 | 9 | jwt.verify(token, process.env.JWT_SECRET, (err, user) => { 10 | if (err) return next(errorHandler(403, 'Token is not valid!')); 11 | 12 | req.user = user; 13 | next(); 14 | }); 15 | 16 | 17 | } -------------------------------------------------------------------------------- /client/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 { persistor, store } from './redux/store.js'; 6 | import { Provider } from 'react-redux'; 7 | import { PersistGate } from 'redux-persist/integration/react'; 8 | 9 | ReactDOM.createRoot(document.getElementById('root')).render( 10 | 11 | 12 | 13 | 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-auth", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "dev": "nodemon api/index.js", 9 | "start": "node api/index.js", 10 | "build": "npm install && npm install --prefix client && npm run build --prefix client" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bcryptjs": "^2.4.3", 17 | "cookie-parser": "^1.4.6", 18 | "dotenv": "^16.3.1", 19 | "express": "^4.18.2", 20 | "jsonwebtoken": "^9.0.1", 21 | "mongoose": "^7.4.2", 22 | "nodemon": "^3.0.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/src/firebase.js: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { initializeApp } from 'firebase/app'; 3 | // TODO: Add SDKs for Firebase products that you want to use 4 | // https://firebase.google.com/docs/web/setup#available-libraries 5 | 6 | // Your web app's Firebase configuration 7 | const firebaseConfig = { 8 | apiKey: import.meta.env.VITE_FIREBASE_API_KEY, 9 | authDomain: 'mern-auth-1c4ae.firebaseapp.com', 10 | projectId: 'mern-auth-1c4ae', 11 | storageBucket: 'mern-auth-1c4ae.appspot.com', 12 | messagingSenderId: '277641423672', 13 | appId: '1:277641423672:web:2de25252aae022d51aafcd', 14 | }; 15 | 16 | // Initialize Firebase 17 | export const app = initializeApp(firebaseConfig); 18 | -------------------------------------------------------------------------------- /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://img.freepik.com/premium-vector/man-avatar-profile-picture-vector-illustration_268834-538.jpg', 23 | }, 24 | }, 25 | { timestamps: true } 26 | ); 27 | 28 | const User = mongoose.model('User', userSchema); 29 | 30 | export default User; 31 | -------------------------------------------------------------------------------- /client/src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from '@reduxjs/toolkit'; 2 | import userReducer from './user/userSlice.js'; 3 | import { persistReducer, persistStore } from 'redux-persist'; 4 | import storage from 'redux-persist/lib/storage'; 5 | 6 | const rootReducer = combineReducers({ user: userReducer }); 7 | 8 | const persistConfig = { 9 | key: 'root', 10 | version: 1, 11 | storage, 12 | }; 13 | 14 | const persistedReducer = persistReducer(persistConfig, rootReducer); 15 | 16 | export const store = configureStore({ 17 | reducer: persistedReducer, 18 | middleware: (getDefaultMiddleware) => 19 | getDefaultMiddleware({ 20 | serializableCheck: false, 21 | }), 22 | }); 23 | 24 | export const persistor = persistStore(store); 25 | -------------------------------------------------------------------------------- /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 SignUp from './pages/SignUp'; 6 | import Profile from './pages/Profile'; 7 | import Header from './components/Header'; 8 | import PrivateRoute from './components/PrivateRoute'; 9 | 10 | export default function App() { 11 | return ( 12 | 13 | {/* header */} 14 |
15 | 16 | } /> 17 | } /> 18 | } /> 19 | } /> 20 | }> 21 | } /> 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /client/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | export default function Header() { 5 | const { currentUser } = useSelector((state) => state.user); 6 | return ( 7 |
8 |
9 | 10 |

Auth App

11 | 12 |
    13 | 14 |
  • Home
  • 15 | 16 | 17 |
  • About
  • 18 | 19 | 20 | {currentUser ? ( 21 | profile 22 | ) : ( 23 |
  • Sign In
  • 24 | )} 25 | 26 |
27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /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": "^1.9.5", 14 | "firebase": "^10.1.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-redux": "^8.1.2", 18 | "react-router-dom": "^6.14.2", 19 | "redux-persist": "^6.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.2.15", 23 | "@types/react-dom": "^18.2.7", 24 | "@vitejs/plugin-react-swc": "^3.3.2", 25 | "autoprefixer": "^10.4.14", 26 | "eslint": "^8.45.0", 27 | "eslint-plugin-react": "^7.32.2", 28 | "eslint-plugin-react-hooks": "^4.6.0", 29 | "eslint-plugin-react-refresh": "^0.4.3", 30 | "postcss": "^8.4.27", 31 | "tailwindcss": "^3.3.3", 32 | "vite": "^4.4.5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /client/src/pages/About.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function About() { 4 | return ( 5 |
6 |

About

7 |

8 | This is a MERN (MongoDB, Express, React, Node.js) stack application with 9 | authentication. It allows users to sign up, log in, and log out, and 10 | provides access to protected routes only for authenticated users. 11 |

12 |

13 | The front-end of the application is built with React and uses React 14 | Router for client-side routing. The back-end is built with Node.js and 15 | Express, and uses MongoDB as the database. Authentication is implemented 16 | using JSON Web Tokens (JWT). 17 |

18 |

19 | This application is intended as a starting point for building full-stack 20 | web applications with authentication using the MERN stack. Feel free to 21 | use it as a template for your own projects! 22 |

23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /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 cookieParser from 'cookie-parser'; 7 | import path from 'path'; 8 | dotenv.config(); 9 | 10 | mongoose 11 | .connect(process.env.MONGO) 12 | .then(() => { 13 | console.log('Connected to MongoDB'); 14 | }) 15 | .catch((err) => { 16 | console.log(err); 17 | }); 18 | 19 | const __dirname = path.resolve(); 20 | 21 | const app = express(); 22 | 23 | app.use(express.static(path.join(__dirname, '/client/dist'))); 24 | 25 | app.get('*', (req, res) => { 26 | res.sendFile(path.join(__dirname, 'client', 'dist', 'index.html')); 27 | }); 28 | 29 | app.use(express.json()); 30 | 31 | app.use(cookieParser()); 32 | 33 | app.listen(3000, () => { 34 | console.log('Server listening on port 3000'); 35 | }); 36 | 37 | app.use('/api/user', userRoutes); 38 | app.use('/api/auth', authRoutes); 39 | 40 | app.use((err, req, res, next) => { 41 | const statusCode = err.statusCode || 500; 42 | const message = err.message || 'Internal Server Error'; 43 | return res.status(statusCode).json({ 44 | success: false, 45 | message, 46 | statusCode, 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /client/src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |

7 | Welcome to my Auth App! 8 |

9 |

10 | This is a full-stack web application built with the MERN (MongoDB, 11 | Express, React, Node.js) stack. It includes authentication features that 12 | allow users to sign up, log in, and log out, and provides access to 13 | protected routes only for authenticated users. 14 |

15 |

16 | The front-end of the application is built with React and uses React 17 | Router for client-side routing. The back-end is built with Node.js and 18 | Express, and uses MongoDB as the database. Authentication is implemented 19 | using JSON Web Tokens (JWT). 20 |

21 |

22 | This application is intended as a starting point for building full-stack 23 | web applications with authentication using the MERN stack. Feel free to 24 | use it as a template for your own projects! 25 |

26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /client/src/components/OAuth.jsx: -------------------------------------------------------------------------------- 1 | import { GoogleAuthProvider, signInWithPopup, getAuth } from 'firebase/auth'; 2 | import { app } from '../firebase'; 3 | import { useDispatch } from 'react-redux'; 4 | import { signInSuccess } from '../redux/user/userSlice'; 5 | import { useNavigate } from 'react-router-dom'; 6 | 7 | export default function OAuth() { 8 | const dispatch = useDispatch(); 9 | const navigate = useNavigate(); 10 | const handleGoogleClick = async () => { 11 | try { 12 | const provider = new GoogleAuthProvider(); 13 | const auth = getAuth(app); 14 | 15 | const result = await signInWithPopup(auth, provider); 16 | const res = await fetch('/api/auth/google', { 17 | method: 'POST', 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | }, 21 | body: JSON.stringify({ 22 | name: result.user.displayName, 23 | email: result.user.email, 24 | photo: result.user.photoURL, 25 | }), 26 | }); 27 | const data = await res.json(); 28 | console.log(data); 29 | dispatch(signInSuccess(data)); 30 | navigate('/'); 31 | } catch (error) { 32 | console.log('could not login with google', error); 33 | } 34 | }; 35 | return ( 36 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /api/controllers/user.controller.js: -------------------------------------------------------------------------------- 1 | import User from '../models/user.model.js'; 2 | import { errorHandler } from '../utils/error.js'; 3 | import bcryptjs from 'bcryptjs'; 4 | 5 | export const test = (req, res) => { 6 | res.json({ 7 | message: 'API is working!', 8 | }); 9 | }; 10 | 11 | // update user 12 | 13 | export const updateUser = async (req, res, next) => { 14 | if (req.user.id !== req.params.id) { 15 | return next(errorHandler(401, 'You can update only your account!')); 16 | } 17 | try { 18 | if (req.body.password) { 19 | req.body.password = bcryptjs.hashSync(req.body.password, 10); 20 | } 21 | 22 | const updatedUser = await User.findByIdAndUpdate( 23 | req.params.id, 24 | { 25 | $set: { 26 | username: req.body.username, 27 | email: req.body.email, 28 | password: req.body.password, 29 | profilePicture: req.body.profilePicture, 30 | }, 31 | }, 32 | { new: true } 33 | ); 34 | const { password, ...rest } = updatedUser._doc; 35 | res.status(200).json(rest); 36 | } catch (error) { 37 | next(error); 38 | } 39 | }; 40 | 41 | 42 | // delete user 43 | 44 | 45 | export const deleteUser = async (req, res, next) => { 46 | if (req.user.id !== req.params.id) { 47 | return next(errorHandler(401, 'You can delete only your account!')); 48 | } 49 | try { 50 | await User.findByIdAndDelete(req.params.id); 51 | res.status(200).json('User has been deleted...'); 52 | } catch (error) { 53 | next(error); 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /client/src/redux/user/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | currentUser: null, 5 | loading: false, 6 | error: false, 7 | }; 8 | 9 | const userSlice = createSlice({ 10 | name: 'user', 11 | initialState, 12 | reducers: { 13 | signInStart: (state) => { 14 | state.loading = true; 15 | }, 16 | signInSuccess: (state, action) => { 17 | state.currentUser = action.payload; 18 | state.loading = false; 19 | state.error = false; 20 | }, 21 | signInFailure: (state, action) => { 22 | state.loading = false; 23 | state.error = action.payload; 24 | }, 25 | updateUserStart: (state) => { 26 | state.loading = true; 27 | }, 28 | updateUserSuccess: (state, action) => { 29 | state.currentUser = action.payload; 30 | state.loading = false; 31 | state.error = false; 32 | }, 33 | updateUserFailure: (state, action) => { 34 | state.loading = false; 35 | state.error = action.payload; 36 | }, 37 | deleteUserStart: (state) => { 38 | state.loading = true; 39 | }, 40 | deleteUserSuccess: (state) => { 41 | state.currentUser = null; 42 | state.loading = false; 43 | state.error = false; 44 | }, 45 | deleteUserFailure: (state, action) => { 46 | state.loading = false; 47 | state.error = action.payload; 48 | }, 49 | signOut: (state) => { 50 | state.currentUser = null; 51 | state.loading = false; 52 | state.error = false; 53 | }, 54 | }, 55 | }); 56 | 57 | export const { 58 | signInStart, 59 | signInSuccess, 60 | signInFailure, 61 | updateUserFailure, 62 | updateUserStart, 63 | updateUserSuccess, 64 | deleteUserFailure, 65 | deleteUserStart, 66 | deleteUserSuccess, 67 | signOut, 68 | } = userSlice.actions; 69 | 70 | export default userSlice.reducer; 71 | -------------------------------------------------------------------------------- /client/src/pages/SignIn.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { 4 | signInStart, 5 | signInSuccess, 6 | signInFailure, 7 | } from '../redux/user/userSlice'; 8 | import { useDispatch, useSelector } from 'react-redux'; 9 | import OAuth from '../components/OAuth'; 10 | 11 | export default function SignIn() { 12 | const [formData, setFormData] = useState({}); 13 | const { loading, error } = useSelector((state) => state.user); 14 | 15 | const navigate = useNavigate(); 16 | const dispatch = useDispatch(); 17 | const handleChange = (e) => { 18 | setFormData({ ...formData, [e.target.id]: e.target.value }); 19 | }; 20 | 21 | const handleSubmit = async (e) => { 22 | e.preventDefault(); 23 | try { 24 | dispatch(signInStart()); 25 | const res = await fetch('/api/auth/signin', { 26 | method: 'POST', 27 | headers: { 28 | 'Content-Type': 'application/json', 29 | }, 30 | body: JSON.stringify(formData), 31 | }); 32 | const data = await res.json(); 33 | if (data.success === false) { 34 | dispatch(signInFailure(data)); 35 | return; 36 | } 37 | dispatch(signInSuccess(data)); 38 | navigate('/'); 39 | } catch (error) { 40 | dispatch(signInFailure(error)); 41 | } 42 | }; 43 | return ( 44 |
45 |

Sign In

46 |
47 | 54 | 61 | 67 | 68 | 69 |
70 |

Dont Have an account?

71 | 72 | Sign up 73 | 74 |
75 |

76 | {error ? error.message || 'Something went wrong!' : ''} 77 |

78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /client/src/pages/SignUp.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import OAuth from '../components/OAuth'; 4 | 5 | export default function SignUp() { 6 | const [formData, setFormData] = useState({}); 7 | const [error, setError] = useState(false); 8 | const [loading, setLoading] = useState(false); 9 | const navigate = useNavigate(); 10 | const handleChange = (e) => { 11 | setFormData({ ...formData, [e.target.id]: e.target.value }); 12 | }; 13 | 14 | const handleSubmit = async (e) => { 15 | e.preventDefault(); 16 | try { 17 | setLoading(true); 18 | setError(false); 19 | const res = await fetch('/api/auth/signup', { 20 | method: 'POST', 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | }, 24 | body: JSON.stringify(formData), 25 | }); 26 | const data = await res.json(); 27 | console.log(data); 28 | setLoading(false); 29 | if (data.success === false) { 30 | setError(true); 31 | return; 32 | } 33 | navigate('/sign-in'); 34 | } catch (error) { 35 | setLoading(false); 36 | setError(true); 37 | } 38 | }; 39 | return ( 40 |
41 |

Sign Up

42 |
43 | 50 | 57 | 64 | 70 | 71 | 72 |
73 |

Have an account?

74 | 75 | Sign in 76 | 77 |
78 |

{error && 'Something went wrong!'}

79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /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 | const hashedPassword = bcryptjs.hashSync(password, 10); 9 | const newUser = new User({ username, email, password: hashedPassword }); 10 | try { 11 | await newUser.save(); 12 | res.status(201).json({ message: 'User created successfully' }); 13 | } catch (error) { 14 | next(error); 15 | } 16 | }; 17 | 18 | export const signin = async (req, res, next) => { 19 | const { email, password } = req.body; 20 | try { 21 | const validUser = await User.findOne({ email }); 22 | if (!validUser) return next(errorHandler(404, 'User not found')); 23 | const validPassword = bcryptjs.compareSync(password, validUser.password); 24 | if (!validPassword) return next(errorHandler(401, 'wrong credentials')); 25 | const token = jwt.sign({ id: validUser._id }, process.env.JWT_SECRET); 26 | const { password: hashedPassword, ...rest } = validUser._doc; 27 | const expiryDate = new Date(Date.now() + 3600000); // 1 hour 28 | res 29 | .cookie('access_token', token, { httpOnly: true, expires: expiryDate }) 30 | .status(200) 31 | .json(rest); 32 | } catch (error) { 33 | next(error); 34 | } 35 | }; 36 | 37 | export const google = async (req, res, next) => { 38 | try { 39 | const user = await User.findOne({ email: req.body.email }); 40 | if (user) { 41 | const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET); 42 | const { password: hashedPassword, ...rest } = user._doc; 43 | const expiryDate = new Date(Date.now() + 3600000); // 1 hour 44 | res 45 | .cookie('access_token', token, { 46 | httpOnly: true, 47 | expires: expiryDate, 48 | }) 49 | .status(200) 50 | .json(rest); 51 | } else { 52 | const generatedPassword = 53 | Math.random().toString(36).slice(-8) + 54 | Math.random().toString(36).slice(-8); 55 | const hashedPassword = bcryptjs.hashSync(generatedPassword, 10); 56 | const newUser = new User({ 57 | username: 58 | req.body.name.split(' ').join('').toLowerCase() + 59 | Math.random().toString(36).slice(-8), 60 | email: req.body.email, 61 | password: hashedPassword, 62 | profilePicture: req.body.photo, 63 | }); 64 | await newUser.save(); 65 | const token = jwt.sign({ id: newUser._id }, process.env.JWT_SECRET); 66 | const { password: hashedPassword2, ...rest } = newUser._doc; 67 | const expiryDate = new Date(Date.now() + 3600000); // 1 hour 68 | res 69 | .cookie('access_token', token, { 70 | httpOnly: true, 71 | expires: expiryDate, 72 | }) 73 | .status(200) 74 | .json(rest); 75 | } 76 | } catch (error) { 77 | next(error); 78 | } 79 | }; 80 | 81 | export const signout = (req, res) => { 82 | res.clearCookie('access_token').status(200).json('Signout success!'); 83 | }; 84 | -------------------------------------------------------------------------------- /client/src/pages/Profile.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { useRef, useState, useEffect } from 'react'; 3 | import { 4 | getDownloadURL, 5 | getStorage, 6 | ref, 7 | uploadBytesResumable, 8 | } from 'firebase/storage'; 9 | import { app } from '../firebase'; 10 | import { useDispatch } from 'react-redux'; 11 | import { 12 | updateUserStart, 13 | updateUserSuccess, 14 | updateUserFailure, 15 | deleteUserStart, 16 | deleteUserSuccess, 17 | deleteUserFailure, 18 | signOut, 19 | } from '../redux/user/userSlice'; 20 | 21 | export default function Profile() { 22 | const dispatch = useDispatch(); 23 | const fileRef = useRef(null); 24 | const [image, setImage] = useState(undefined); 25 | const [imagePercent, setImagePercent] = useState(0); 26 | const [imageError, setImageError] = useState(false); 27 | const [formData, setFormData] = useState({}); 28 | const [updateSuccess, setUpdateSuccess] = useState(false); 29 | 30 | const { currentUser, loading, error } = useSelector((state) => state.user); 31 | useEffect(() => { 32 | if (image) { 33 | handleFileUpload(image); 34 | } 35 | }, [image]); 36 | const handleFileUpload = async (image) => { 37 | const storage = getStorage(app); 38 | const fileName = new Date().getTime() + image.name; 39 | const storageRef = ref(storage, fileName); 40 | const uploadTask = uploadBytesResumable(storageRef, image); 41 | uploadTask.on( 42 | 'state_changed', 43 | (snapshot) => { 44 | const progress = 45 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 46 | setImagePercent(Math.round(progress)); 47 | }, 48 | (error) => { 49 | setImageError(true); 50 | }, 51 | () => { 52 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => 53 | setFormData({ ...formData, profilePicture: downloadURL }) 54 | ); 55 | } 56 | ); 57 | }; 58 | const handleChange = (e) => { 59 | setFormData({ ...formData, [e.target.id]: e.target.value }); 60 | }; 61 | 62 | const handleSubmit = async (e) => { 63 | e.preventDefault(); 64 | try { 65 | dispatch(updateUserStart()); 66 | const res = await fetch(`/api/user/update/${currentUser._id}`, { 67 | method: 'POST', 68 | headers: { 69 | 'Content-Type': 'application/json', 70 | }, 71 | body: JSON.stringify(formData), 72 | }); 73 | const data = await res.json(); 74 | if (data.success === false) { 75 | dispatch(updateUserFailure(data)); 76 | return; 77 | } 78 | dispatch(updateUserSuccess(data)); 79 | setUpdateSuccess(true); 80 | } catch (error) { 81 | dispatch(updateUserFailure(error)); 82 | } 83 | }; 84 | 85 | const handleDeleteAccount = async () => { 86 | try { 87 | dispatch(deleteUserStart()); 88 | const res = await fetch(`/api/user/delete/${currentUser._id}`, { 89 | method: 'DELETE', 90 | }); 91 | const data = await res.json(); 92 | if (data.success === false) { 93 | dispatch(deleteUserFailure(data)); 94 | return; 95 | } 96 | dispatch(deleteUserSuccess(data)); 97 | } catch (error) { 98 | dispatch(deleteUserFailure(error)); 99 | } 100 | }; 101 | 102 | const handleSignOut = async () => { 103 | try { 104 | await fetch('/api/auth/signout'); 105 | dispatch(signOut()) 106 | } catch (error) { 107 | console.log(error); 108 | } 109 | }; 110 | return ( 111 |
112 |

Profile

113 |
114 | setImage(e.target.files[0])} 120 | /> 121 | {/* 122 | firebase storage rules: 123 | allow read; 124 | allow write: if 125 | request.resource.size < 2 * 1024 * 1024 && 126 | request.resource.contentType.matches('image/.*') */} 127 | profile fileRef.current.click()} 132 | /> 133 |

134 | {imageError ? ( 135 | 136 | Error uploading image (file size must be less than 2 MB) 137 | 138 | ) : imagePercent > 0 && imagePercent < 100 ? ( 139 | {`Uploading: ${imagePercent} %`} 140 | ) : imagePercent === 100 ? ( 141 | Image uploaded successfully 142 | ) : ( 143 | '' 144 | )} 145 |

146 | 154 | 162 | 169 | 172 |
173 |
174 | 178 | Delete Account 179 | 180 | 181 | Sign out 182 | 183 |
184 |

{error && 'Something went wrong!'}

185 |

186 | {updateSuccess && 'User is updated successfully!'} 187 |

188 |
189 | ); 190 | } 191 | --------------------------------------------------------------------------------