initial commit
This commit is contained in:
23
client/.gitignore
vendored
Normal file
23
client/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
18869
client/package-lock.json
generated
Normal file
18869
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
client/package.json
Normal file
46
client/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.6.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
"md5": "^2.3.0",
|
||||
"mongoose": "^8.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-toastify": "^10.0.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
client/public/favicon.png
Normal file
BIN
client/public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
18
client/public/index.html
Normal file
18
client/public/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
|
||||
<title>appointment_to_examiner</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
15
client/public/manifest.json
Normal file
15
client/public/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.svg",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
client/public/robots.txt
Normal file
3
client/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
488
client/src/App.css
Normal file
488
client/src/App.css
Normal file
@@ -0,0 +1,488 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for vertical */
|
||||
::-webkit-scrollbar-vertical {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for horizontal */
|
||||
::-webkit-scrollbar-horizontal {
|
||||
display: none;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Montserrat', sans-serif !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #96aefb;
|
||||
background: linear-gradient(to right, #dab8fc, #afc2ff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
body h1{
|
||||
font-size: 2rem ;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.LoginPageContainer,
|
||||
.HomePageContainer{
|
||||
background-color: #fff;
|
||||
border-radius: 35px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.35);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
width: 768px !important;
|
||||
min-height: 480px;
|
||||
}
|
||||
.LoginPageContainer p{
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.3px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.LoginPageContainer span{
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.LoginPageContainer a{
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
margin: 15px 0 10px;
|
||||
}
|
||||
|
||||
.LoginPageContainer button,
|
||||
.HomePageContainer button
|
||||
{
|
||||
background-color:rgb(122, 50, 199);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
padding: 10px 45px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.LoginPageContainer button:hover,
|
||||
.HomePageContainer button:hover
|
||||
{
|
||||
|
||||
background-color: #00a1ff;
|
||||
}
|
||||
.LoginPageContainer button:active,
|
||||
.HomePageContainer button:active
|
||||
{
|
||||
|
||||
background-color: #045d90;
|
||||
}
|
||||
|
||||
.LoginPageContainer button.hidden{
|
||||
background-color: transparent;
|
||||
transition: all 0.2sec ease !important;
|
||||
border-color: #fff;
|
||||
}
|
||||
.LoginPageContainer button.hidden:hover{
|
||||
box-shadow: 0 0 5px 1px white;
|
||||
}
|
||||
|
||||
|
||||
.LoginPageContainer form{
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 0 40px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.LoginPageContainer input{
|
||||
background-color: #eee;
|
||||
border: none;
|
||||
margin: 8px 0;
|
||||
padding: 10px 15px;
|
||||
font-size: 13px;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-container{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
transition: all 0.6s ease-in-out;
|
||||
}
|
||||
.SmallScreenBtn{
|
||||
display: none !important;
|
||||
}
|
||||
.sign-in{
|
||||
left: 0;
|
||||
width: 50%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.LoginPageContainer.active .sign-in{
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.sign-up{
|
||||
left: 0;
|
||||
width: 50%;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.OverlayAnimation{
|
||||
display: none !important;
|
||||
}
|
||||
.LoginPageContainer.active .sign-up{
|
||||
transform: translateX(100%);
|
||||
opacity: 1;
|
||||
z-index: 5;
|
||||
animation: move 0.6s;
|
||||
}
|
||||
|
||||
@keyframes move{
|
||||
0%, 49.99%{
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
50%, 100%{
|
||||
opacity: 1;
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
|
||||
.social-icons{
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.social-icons a{
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 20%;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 3px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.toggle-container{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: all 0.6s ease-in-out;
|
||||
border-radius: 150px 0 0 100px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.LoginPageContainer.active .toggle-container{
|
||||
transform: translateX(-100%);
|
||||
border-radius: 0 150px 100px 0;
|
||||
}
|
||||
|
||||
.toggle{
|
||||
background-color: linear-gradient(to left, #00a1ff, #00ff8f);
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, #C33764 , #1D2671);
|
||||
color: #fff;
|
||||
position: relative;
|
||||
left: -100%;
|
||||
height: 100%;
|
||||
width: 200%;
|
||||
transform: translateX(0);
|
||||
transition: all 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
.LoginPageContainer.active .toggle{
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.toggle-panel{
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 0 30px;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
transform: translateX(0);
|
||||
transition: all 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
.toggle-left{
|
||||
transform: translateX(-200%);
|
||||
}
|
||||
|
||||
.LoginPageContainer.active .toggle-left{
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.toggle-right{
|
||||
right: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.LoginPageContainer.active .toggle-right{
|
||||
transform: translateX(200%);
|
||||
}
|
||||
|
||||
.GoogleBtn{
|
||||
font-size: 14px !important;
|
||||
font-weight: 450 !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
padding: 10px 20px !important;
|
||||
background-color: #fff !important;
|
||||
border: 1px solid #ccc !important;
|
||||
border-radius: 20px !important;
|
||||
cursor: pointer !important;
|
||||
transition: background-color 0.3s ease !important;
|
||||
color: black !important;
|
||||
font-family: 'Roboto', sans-serif !important;
|
||||
text-transform:none !important;
|
||||
margin: 20px auto !important;
|
||||
}
|
||||
.GoogleBtn:hover {
|
||||
background-color: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
.GoogleBtn .icon {
|
||||
font-size:20px !important;
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.ProfileContainer,
|
||||
.profile{
|
||||
display: flex;
|
||||
text-align: center;
|
||||
gap:35px;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.profile-image img{
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
}
|
||||
.ResponseDiv{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.form-pw{
|
||||
|
||||
height:auto !important;
|
||||
}
|
||||
.ResponseDivButton{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.PwPage{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.PwPage p{
|
||||
text-align: center !important;
|
||||
}
|
||||
.PwPageContainer{
|
||||
min-height: 480px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.PwPage input{
|
||||
width: 350px !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@media (max-width:475px) {
|
||||
.PwPage input{
|
||||
width: 250px !important;
|
||||
}
|
||||
.profile-image img{
|
||||
height: 70px;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
}
|
||||
|
||||
.ProfileContainer,
|
||||
.profile{
|
||||
gap:25px;
|
||||
}
|
||||
.profile p{
|
||||
font-size: 12px ;
|
||||
font-weight: 500;
|
||||
}
|
||||
.toggle-container{
|
||||
display:none !important;
|
||||
}
|
||||
.LoginPageContainer,
|
||||
.HomePageContainer{
|
||||
max-width: 320px !important;
|
||||
min-height: 450px !important;
|
||||
border-radius: 19px !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
.LoginPageContainer button,
|
||||
.HomePageContainer button
|
||||
{
|
||||
font-size: 10px;
|
||||
padding: 8px 30px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
margin-top: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.LoginPageContainer p{
|
||||
font-size: 14px !important;
|
||||
}
|
||||
.LoginPageContainer span{
|
||||
font-size: 12px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
.form-container{
|
||||
width: 100% !important;
|
||||
}
|
||||
h1{
|
||||
font-size: 1.5rem !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.LoginPageContainer a{
|
||||
font-size: 12px;}
|
||||
.LoginPageContainer form{
|
||||
padding: 0 27px !important;
|
||||
}
|
||||
|
||||
.GoogleBtn{
|
||||
font-size: 11px !important;
|
||||
padding: 5px 10px !important;
|
||||
}
|
||||
.GoogleBtn .icon {
|
||||
font-size:15px !important;
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
.LoginPageContainer input{
|
||||
margin: 8px 0;
|
||||
padding: 8px 12px;
|
||||
font-size: 10px;
|
||||
width:85%;
|
||||
}
|
||||
.LoginPageContainer.active .sign-up{
|
||||
transform: translateY(6%) !important;
|
||||
}
|
||||
.LoginPageContainer .sign-up{
|
||||
transform: translateY(-5%) !important;
|
||||
}
|
||||
|
||||
.LoginPageContainer.active .sign-in{
|
||||
transform: translateY(40%) !important;
|
||||
}
|
||||
.LoginPageContainer .sign-in{
|
||||
transform: translateY(-5%) !important;
|
||||
}
|
||||
h1{
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.OverlayAnimation{
|
||||
transform: translateY(127%) !important;
|
||||
transition: all 1s ease !important;
|
||||
display: block !important;
|
||||
content: " ";
|
||||
overflow: hidden !important;
|
||||
border-radius: 20px !important;
|
||||
height: 300px !important;
|
||||
width:100% !important;
|
||||
z-index: 200 !important;
|
||||
color: white !important;
|
||||
background: linear-gradient(to right, #C33764 , #1D2671);
|
||||
}
|
||||
.LoginPageContainer.active .OverlayAnimation{
|
||||
transform: translateY(-65%) !important;
|
||||
display: flex !important;
|
||||
content: " ";
|
||||
height: 200px !important;
|
||||
border-radius: 20px !important;
|
||||
width:100% !important;
|
||||
z-index: 999 !important;
|
||||
background: linear-gradient(to right, #dd678c , #6370e7);
|
||||
}
|
||||
.OverlayAnimation{
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
margin-bottom: auto !important;
|
||||
margin-top: auto !important;
|
||||
position:relative;
|
||||
}
|
||||
.OverlayAnimation button{
|
||||
margin-top: -7px !important;
|
||||
}
|
||||
.togglebtnlogin{
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
position: absolute !important;
|
||||
flex-direction: column !important;
|
||||
top:1% !important;
|
||||
}
|
||||
|
||||
.LoginPageContainer.active .OverlayAnimation .togglebtnlogin{
|
||||
top:auto !important;
|
||||
bottom: 0 !important;
|
||||
|
||||
}
|
||||
.LoginPageContainer.active .OverlayAnimation button{
|
||||
/* padding: 6px 20px !important; */
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.LoginPageContainer.active .OverlayAnimation span{
|
||||
/* padding: 6px 20px !important; */
|
||||
margin-top: 4px !important;
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
.form-pw{
|
||||
|
||||
height:auto !important;
|
||||
}
|
||||
.PwPage p{
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
24
client/src/App.js
Normal file
24
client/src/App.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import {BrowserRouter as Router , Route , Routes } from "react-router-dom";
|
||||
import './App.css';
|
||||
import Welcome from "./Pages/Welcome"
|
||||
import AuthPage from "./Pages/Login";
|
||||
import HomePage from "./Pages/HomePage";
|
||||
import ForgetPwPage from "./Pages/ForgetPw";
|
||||
import ResetPwPage from "./Pages/ResetPw";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Welcome />}></Route>
|
||||
<Route path="/AuthpPage" element={<AuthPage />}></Route>
|
||||
<Route path="/Home" element={<HomePage />}></Route>
|
||||
<Route path="/ForgetPw" element={<ForgetPwPage />}></Route>
|
||||
<Route path="/ResetPw/:token" element={<ResetPwPage />}></Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
101
client/src/Pages/ForgetPw.jsx
Normal file
101
client/src/Pages/ForgetPw.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { useState } from "react";
|
||||
import { Container, Col, Row } from "react-bootstrap";
|
||||
import axios from "axios";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
|
||||
function ForgetPwPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const notifySuccess = (message) => {
|
||||
toast.success(message);
|
||||
};
|
||||
|
||||
const notifyError = (error) => {
|
||||
toast.error(error.message || "An error occurred");
|
||||
};
|
||||
|
||||
const notifyLoading = () => {
|
||||
toast.info("Sending verification link...");
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
notifyError("Please enter a valid email address");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
notifyLoading();
|
||||
try {
|
||||
const response = await axios.post(
|
||||
"http://localhost:8080/password/forgot-password",
|
||||
{ email }
|
||||
);
|
||||
setMessage(response.data.message);
|
||||
notifySuccess(response.data.message);
|
||||
} catch (error) {
|
||||
console.error("Forgot password error:", error);
|
||||
notifyError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenInbox = () => {
|
||||
const emailProviderUrl = "https://gmail.com/";
|
||||
window.open(emailProviderUrl, "_blank");
|
||||
};
|
||||
|
||||
const handleGoToLogin = () => {
|
||||
window.location.href = "/";
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToastContainer />
|
||||
<div className="LoginPage">
|
||||
<Container className="LoginPageContainer">
|
||||
<Row className="PwPageContainer">
|
||||
<Col md={12}>
|
||||
<div className="PwPage">
|
||||
<h1>Forgot Password</h1>
|
||||
<p>
|
||||
Enter your email address and we'll send you instructions on
|
||||
how to reset your password
|
||||
</p>
|
||||
<form className="form-pw" onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Email"
|
||||
required
|
||||
/>
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? "Sending..." : "Send Verification Link"}
|
||||
</button>
|
||||
</form>
|
||||
{message && (
|
||||
<div className="ResponseDiv">
|
||||
<p>{message}</p>
|
||||
<div className="ResponseDivButton">
|
||||
<button onClick={handleOpenInbox}>Open Gmail</button>
|
||||
<button onClick={handleGoToLogin}>Back to Login</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ForgetPwPage;
|
||||
107
client/src/Pages/HomePage.jsx
Normal file
107
client/src/Pages/HomePage.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Container, Col, Row, Button, Spinner } from "react-bootstrap";
|
||||
import axios from "axios";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
function HomePage(props) {
|
||||
|
||||
const notifyLoading = () => {
|
||||
toast.info("Logging Out Successfull..");
|
||||
};
|
||||
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
|
||||
const handleLogout = () => {
|
||||
axios
|
||||
.get("http://localhost:8080/auth/logout", {
|
||||
withCredentials: true,
|
||||
})
|
||||
.then(() => {
|
||||
window.location.href = "/";
|
||||
setUser(null);
|
||||
localStorage.removeItem("user");
|
||||
notifyLoading();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error logging out:", error);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchUser = async () => {
|
||||
const loggedInUser = localStorage.getItem("user");
|
||||
if (loggedInUser) {
|
||||
setUser(JSON.parse(loggedInUser));
|
||||
setLoading(false);
|
||||
} else {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://localhost:8080/api/user/profile/`,
|
||||
{
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
setUser(response.data.user);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error("Error fetching user data:", error);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchUser();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToastContainer />
|
||||
<div className="LoginPage">
|
||||
<Container className="HomePageContainer ProfileContainer">
|
||||
{loading ? (
|
||||
<div className="loader">
|
||||
<Spinner animation="border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</Spinner>
|
||||
</div>
|
||||
) : user ? (
|
||||
<>
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<h1>Welcome to MERN Auth App</h1>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<h1>Profile</h1>
|
||||
<div className="profile-info">
|
||||
<div className="profile-image">
|
||||
<img src={user.profilePicture} alt="Profile" />
|
||||
</div>
|
||||
<div className="profile-details">
|
||||
<p>Username: {user.username}</p>
|
||||
<p>Email: {user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleLogout}>Logout</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
) : (
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<h1>Logging out...</h1>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default HomePage;
|
||||
|
||||
248
client/src/Pages/Login.jsx
Normal file
248
client/src/Pages/Login.jsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import React, { useEffect , useState } from "react";
|
||||
import { Container, Col, Row } from "react-bootstrap";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
import axios from "axios";
|
||||
import md5 from "md5";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
function AuthPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
const [signin, setSignin] = useState(false);
|
||||
|
||||
const notifyError = (message) => {
|
||||
toast.error(message);
|
||||
};
|
||||
|
||||
|
||||
function ToggleSign(event) {
|
||||
event.preventDefault();
|
||||
setSignin(!signin);
|
||||
setFormData({
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
}
|
||||
|
||||
function handleInputChange(event) {
|
||||
const { name, value } = event.target;
|
||||
setFormData((prevData) => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}));
|
||||
}
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
if (!formData.username.trim() && signin) {
|
||||
notifyError("Username cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
if (!emailRegex.test(formData.email)) {
|
||||
notifyError("Enter a valid email address");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check password length
|
||||
if (formData.password.length < 8) {
|
||||
notifyError("Password must be at least 8 characters long");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`http://localhost:8080/api/${
|
||||
!signin ? "login" : "register"
|
||||
}`,
|
||||
formData
|
||||
);
|
||||
const { user } = response.data;
|
||||
delete user.password;
|
||||
const gravatarUrl = `https://www.gravatar.com/avatar/${md5(
|
||||
user.email
|
||||
)}?d=identicon`;
|
||||
user.profilePicture = gravatarUrl;
|
||||
|
||||
localStorage.setItem("user", JSON.stringify(user));
|
||||
window.location.href = "/Home";
|
||||
} catch (error) {
|
||||
console.error("Authentication error:", error);
|
||||
if (
|
||||
error.response &&
|
||||
error.response.status === 400 &&
|
||||
error.response.data.message === "User already exists"
|
||||
) {
|
||||
notifyError("User already exists");
|
||||
} else {
|
||||
notifyError(error.response?.data.message || "An error occurred");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleGoogleLogin = (event) => {
|
||||
event.preventDefault();
|
||||
window.location.href =
|
||||
"http://localhost:8080/auth/google";
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToastContainer />
|
||||
<div className="LoginPage">
|
||||
<Container className={`LoginPageContainer ${signin ? "active" : ""}`}>
|
||||
<Row>
|
||||
<Col xs={12} md={6}>
|
||||
<div className="form-container sign-up">
|
||||
<SignUpForm
|
||||
formData={formData}
|
||||
handleInputChange={handleInputChange}
|
||||
handleGoogleLogin={handleGoogleLogin}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-container sign-in">
|
||||
<SignInForm
|
||||
formData={formData}
|
||||
handleInputChange={handleInputChange}
|
||||
handleGoogleLogin={handleGoogleLogin}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<TogglerContainer signin={signin} ToggleSign={ToggleSign} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<div className="OverlayAnimation">
|
||||
{signin ? (
|
||||
<div className="togglebtnlogin">
|
||||
<button className="hidden" onClick={ToggleSign}>
|
||||
Sign In
|
||||
</button>
|
||||
<span>Already Have an Account?</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="togglebtnlogin">
|
||||
<span>Don't have an account? Create one</span>
|
||||
<button className="hidden" onClick={ToggleSign}>
|
||||
Sign Up
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Row>
|
||||
</Container>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TogglerContainer(props) {
|
||||
return (
|
||||
<>
|
||||
<div className="toggle-container">
|
||||
<div className="toggle">
|
||||
<div className="toggle-panel toggle-left">
|
||||
<h1>Welcome to MERN Auth App</h1>
|
||||
<p>Already Have an Account?</p>
|
||||
<button className="hidden" onClick={props.ToggleSign}>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
<div className="toggle-panel toggle-right">
|
||||
<h1>Welcome to MERN Auth App</h1>
|
||||
<p>Don't have an account? Create one</p>
|
||||
<button className="hidden" onClick={props.ToggleSign}>
|
||||
Sign Up
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SignUpForm(props) {
|
||||
return (
|
||||
<>
|
||||
<form>
|
||||
<h1>Create Account</h1>
|
||||
<div className="Googlediv">
|
||||
<button className="GoogleBtn" onClick={props.handleGoogleLogin}>
|
||||
<FcGoogle className="icon" /> Sign up with Google
|
||||
</button>
|
||||
</div>
|
||||
<span>or use your email for registration</span>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value={props.formData.username}
|
||||
onChange={props.handleInputChange}
|
||||
placeholder="Name"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={props.formData.email}
|
||||
onChange={props.handleInputChange}
|
||||
placeholder="Email"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={props.formData.password}
|
||||
onChange={props.handleInputChange}
|
||||
placeholder="Password"
|
||||
required
|
||||
/>
|
||||
<button onClick={props.handleSubmit}>Sign Up</button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SignInForm(props) {
|
||||
return (
|
||||
<>
|
||||
<form>
|
||||
<h1>Sign In</h1>
|
||||
<div>
|
||||
<button className="GoogleBtn" onClick={props.handleGoogleLogin}>
|
||||
<FcGoogle className="icon" /> Sign in with Google
|
||||
</button>
|
||||
</div>
|
||||
<span>or use your email password</span>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={props.formData.email}
|
||||
onChange={props.handleInputChange}
|
||||
placeholder="Email"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={props.formData.password}
|
||||
onChange={props.handleInputChange}
|
||||
placeholder="Password"
|
||||
required
|
||||
/>
|
||||
<a href="/ForgetPw">Forget Your Password?</a>
|
||||
<button onClick={props.handleSubmit}>Sign In</button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthPage;
|
||||
87
client/src/Pages/ResetPw.jsx
Normal file
87
client/src/Pages/ResetPw.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { useState } from "react";
|
||||
import { Container, Col, Row } from "react-bootstrap";
|
||||
import { useParams } from "react-router-dom";
|
||||
import axios from "axios";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
function ResetPwPage() {
|
||||
const { token } = useParams();
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const notifySuccess = (message) => {
|
||||
toast.success(message);
|
||||
};
|
||||
|
||||
const notifyError = (error) => {
|
||||
toast.error(error.message || "An error occurred");
|
||||
};
|
||||
|
||||
const notifyLoading = () => {
|
||||
toast.info("Sending Reset Request...");
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (newPassword.length < 8) {
|
||||
toast.error("Password must be at least 8 characters long");
|
||||
return;
|
||||
}
|
||||
notifyLoading();
|
||||
try {
|
||||
const response = await axios.post(
|
||||
"http://:8080/password/reset-password",
|
||||
{ resetToken: token, newPassword }
|
||||
);
|
||||
setMessage(response.data.message);
|
||||
notifySuccess(response.data.message);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Reset password error:",
|
||||
error.response ? error.response.data : error
|
||||
);
|
||||
notifyError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoToLogin = () => {
|
||||
window.location.href = "/";
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToastContainer />
|
||||
<div className="LoginPage">
|
||||
<Container className="LoginPageContainer">
|
||||
<Row className="PwPageContainer">
|
||||
<Col md={12}>
|
||||
<div className="PwPage">
|
||||
<h1>Reset Password</h1>
|
||||
<p>Enter your new password below</p>
|
||||
<form className="form-pw" onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="New Password"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Reset Password</button>
|
||||
</form>
|
||||
{message && (
|
||||
<div className="ResponseDiv">
|
||||
<p>{message}</p>
|
||||
<button onClick={handleGoToLogin}>Back to Login</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResetPwPage;
|
||||
28
client/src/Pages/Welcome.jsx
Normal file
28
client/src/Pages/Welcome.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const Welcome = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleRedirect = () => {
|
||||
navigate("/AuthpPage");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container text-center mt-5">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-6">
|
||||
<h1 className="mb-4">Welcome Page</h1>
|
||||
<button
|
||||
onClick={handleRedirect}
|
||||
className="btn btn-primary btn-lg"
|
||||
>
|
||||
SIGN IN / SIGN UP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Welcome;
|
||||
13
client/src/index.css
Normal file
13
client/src/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
15
client/src/index.js
Normal file
15
client/src/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
reportWebVitals();
|
||||
13
client/src/reportWebVitals.js
Normal file
13
client/src/reportWebVitals.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
5
client/src/setupTests.js
Normal file
5
client/src/setupTests.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
2
server/.gitignore
vendored
Normal file
2
server/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
.env
|
||||
12
server/ConnectionDb.js
Normal file
12
server/ConnectionDb.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const mongoose = require("mongoose");
|
||||
require("dotenv").config();
|
||||
exports.connectdb = () => {
|
||||
mongoose.connect(process.env.mongoURI);
|
||||
};
|
||||
|
||||
const db = mongoose.connection;
|
||||
|
||||
db.on("error", console.error.bind("Connection Error!"));
|
||||
db.once("open", function () {
|
||||
console.log("Connection Established!!!");
|
||||
});
|
||||
56
server/config/passport.js
Normal file
56
server/config/passport.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const passport = require("passport");
|
||||
const User = require("../models/User");
|
||||
require("dotenv").config();
|
||||
const GoogleStrategy = require("passport-google-oauth20").Strategy;
|
||||
|
||||
passport.use(
|
||||
new GoogleStrategy(
|
||||
{
|
||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
callbackURL: "http://localhost:8080/auth/google/callback",
|
||||
scope: ["profile", "email", "displayName"],
|
||||
},
|
||||
async (accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
// Check if a user with the same email already exists
|
||||
let user = await User.findOne({ email: profile.emails[0].value });
|
||||
|
||||
if (user) {
|
||||
// If the user exists, update their Google ID and profile information
|
||||
user.googleId = profile.id;
|
||||
user.username = profile.displayName;
|
||||
user.profilePicture = profile.photos[0].value;
|
||||
await user.save();
|
||||
return done(null, user);
|
||||
} else {
|
||||
// If the user doesn't exist, create a new user
|
||||
const newUser = new User({
|
||||
googleId: profile.id,
|
||||
username: profile.displayName,
|
||||
email: profile.emails[0].value,
|
||||
profilePicture: profile.photos[0].value,
|
||||
});
|
||||
|
||||
user = await newUser.save();
|
||||
return done(null, user);
|
||||
}
|
||||
} catch (error) {
|
||||
return done(error, false);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id); // Use user id for serialization
|
||||
});
|
||||
|
||||
passport.deserializeUser(async (id, done) => {
|
||||
try {
|
||||
const user = await User.findById(id); // Fetch user by id from MongoDB
|
||||
done(null, user); // Pass the user object to the next middleware
|
||||
} catch (error) {
|
||||
done(error, false);
|
||||
}
|
||||
});
|
||||
27
server/models/User.js
Normal file
27
server/models/User.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const mongoose = require("mongoose");
|
||||
const passportLocalMongoose = require("passport-local-mongoose");
|
||||
|
||||
const UserSchema = new mongoose.Schema(
|
||||
{
|
||||
googleId: String,
|
||||
username: String,
|
||||
email: String,
|
||||
password: String,
|
||||
profilePicture: String,
|
||||
resetPasswordToken: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
resetPasswordExpires: {
|
||||
type: Date,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
UserSchema.plugin(passportLocalMongoose);
|
||||
|
||||
module.exports = mongoose.model("User", UserSchema);
|
||||
2266
server/package-lock.json
generated
Normal file
2266
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
server/package.json
Normal file
42
server/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"description": "appointment_to_examiner",
|
||||
"main": "sever.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build" : "npm install --prefix ../client && npm run build --prefix ../client && npm install",
|
||||
"start": "nodemon server.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hk151109/appointment_to_examiner.git"
|
||||
},
|
||||
"author": "Harikrishnan",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/hk151109/appointment_to_examiner/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.20.2",
|
||||
"connect-mongo": "^5.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"express-session": "^1.18.0",
|
||||
"googleapis": "^134.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongoose": "^8.3.1",
|
||||
"mongoose-findorcreate": "^4.0.0",
|
||||
"nodemailer": "^6.9.13",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-local-mongoose": "^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.0"
|
||||
}
|
||||
}
|
||||
124
server/routes/authRoutes.js
Normal file
124
server/routes/authRoutes.js
Normal file
@@ -0,0 +1,124 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const bcrypt = require("bcryptjs");
|
||||
const User = require("../models/User");
|
||||
const crypto = require("crypto");
|
||||
const nodemailer = require("nodemailer");
|
||||
const { google } = require("googleapis");
|
||||
require("dotenv").config();
|
||||
|
||||
// Set up Google OAuth2 credentials
|
||||
const oauth2Client = new google.auth.OAuth2(
|
||||
process.env.GOOGLE_CLIENT_ID,
|
||||
process.env.GOOGLE_CLIENT_SECRET,
|
||||
"https://developers.google.com/oauthplayground" // Redirect URI
|
||||
);
|
||||
|
||||
oauth2Client.setCredentials({
|
||||
refresh_token: process.env.GOOGLE_REFRESH_TOKEN,
|
||||
});
|
||||
|
||||
// Nodemailer transporter using Google OAuth2
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: "gmail",
|
||||
auth: {
|
||||
type: "OAuth2",
|
||||
user: process.env.EMAIL_USERNAME,
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
refreshToken: process.env.GOOGLE_REFRESH_TOKEN,
|
||||
accessToken: async () => {
|
||||
const accessToken = await getAccessToken();
|
||||
return accessToken;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Function to refresh OAuth2 token
|
||||
async function getAccessToken() {
|
||||
try {
|
||||
const { token } = await oauth2Client.getAccessToken();
|
||||
return token;
|
||||
} catch (error) {
|
||||
console.error("Error refreshing access token:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Forgot Password
|
||||
router.post("/forgot-password", async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
const user = await User.findOne({ email });
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: "User not found" });
|
||||
}
|
||||
|
||||
// Generate reset token
|
||||
const resetToken = crypto.randomBytes(20).toString("hex");
|
||||
user.resetPasswordToken = resetToken;
|
||||
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour
|
||||
await user.save();
|
||||
|
||||
// Send reset email
|
||||
const mailOptions = {
|
||||
from: `"MERN Auth App" <${process.env.EMAIL_USERNAME}>`,
|
||||
to: email,
|
||||
subject: "Password Reset Verification",
|
||||
html: `
|
||||
<p>Hi there,</p>
|
||||
<br>
|
||||
<p>We received a request to reset the password for your account. To proceed with the password reset, please click on the link below:</p>
|
||||
<p>Click here : <a href="http://localhost:3000/ResetPw/${resetToken}">Reset Password</a></p>
|
||||
<br>
|
||||
<p>Please ensure you use this link within the next 1 hour as it will expire after that for security reasons.</p>
|
||||
<p>If you didn't request a password reset, please ignore this email.</p>
|
||||
|
||||
<br>
|
||||
<p>Best regards,</p>
|
||||
<p>MERN Auth App Team</p>
|
||||
`,
|
||||
};
|
||||
|
||||
transporter.sendMail(mailOptions, (error, info) => {
|
||||
if (error) {
|
||||
console.error("Error sending email:", error);
|
||||
return res.status(500).json({ message: "Error sending email" });
|
||||
}
|
||||
console.log("Email sent:", info.response);
|
||||
res.status(200).json({ message: "Reset password email sent" });
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Forgot password error:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Reset Password
|
||||
router.post("/reset-password", async (req, res) => {
|
||||
try {
|
||||
const { resetToken, newPassword } = req.body;
|
||||
const user = await User.findOne({
|
||||
resetPasswordToken: resetToken,
|
||||
resetPasswordExpires: { $gt: Date.now() },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(400).json({ message: "Invalid or expired token" });
|
||||
}
|
||||
|
||||
// Reset password
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
user.password = hashedPassword;
|
||||
user.resetPasswordToken = undefined;
|
||||
user.resetPasswordExpires = undefined;
|
||||
await user.save();
|
||||
|
||||
res.status(200).json({ message: "Password reset successful" });
|
||||
} catch (error) {
|
||||
console.error("Reset password error:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
188
server/server.js
Normal file
188
server/server.js
Normal file
@@ -0,0 +1,188 @@
|
||||
const express = require("express");
|
||||
const User = require("./models/User");
|
||||
const cors = require("cors");
|
||||
const passport = require("passport");
|
||||
const session = require("express-session");
|
||||
const bodyParser = require("body-parser");
|
||||
const bcrypt = require("bcryptjs");
|
||||
const LocalStrategy = require("passport-local").Strategy;
|
||||
const PasswordRouter = require("./routes/authRoutes");
|
||||
const crypto = require("crypto");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const path = require("path");
|
||||
|
||||
const { connectdb } = require("./ConnectionDb");
|
||||
connectdb();
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
app.use(
|
||||
session({
|
||||
secret: "secret",
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
})
|
||||
);
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
// CORS configuration
|
||||
app.use(
|
||||
cors({
|
||||
origin: "http://localhost:3000",
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
|
||||
// Passport configuration
|
||||
require("./config/passport");
|
||||
|
||||
passport.use(
|
||||
new LocalStrategy(
|
||||
{ usernameField: "email" },
|
||||
async (email, password, done) => {
|
||||
try {
|
||||
const user = await User.findOne({ email });
|
||||
if (!user) {
|
||||
return done(null, false, { message: "Incorrect email" });
|
||||
}
|
||||
const isMatch = await bcrypt.compare(password, user.password);
|
||||
if (isMatch) {
|
||||
return done(null, user);
|
||||
} else {
|
||||
return done(null, false, { message: "Incorrect password" });
|
||||
}
|
||||
} catch (error) {
|
||||
return done(error);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id); // Store user ID in the session
|
||||
});
|
||||
|
||||
passport.deserializeUser((id, done) => {
|
||||
User.findById(id, (err, user) => {
|
||||
done(err, user);
|
||||
});
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use("/password", PasswordRouter);
|
||||
|
||||
app.get(
|
||||
"/auth/google",
|
||||
passport.authenticate("google", { scope: ["profile", "email"] })
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/auth/google/callback",
|
||||
passport.authenticate("google", { failureRedirect: "/" }),
|
||||
function (req, res) {
|
||||
res.redirect("http://localhost:3000/Home");
|
||||
}
|
||||
);
|
||||
|
||||
app.post("/api/register", async (req, res) => {
|
||||
try {
|
||||
const { username, email, password } = req.body;
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
let user = await User.findOne({ email });
|
||||
|
||||
if (user) {
|
||||
if (user.googleId && user.password) {
|
||||
return res.status(400).json({ message: "User already exists" });
|
||||
}
|
||||
if (!user.googleId && user.password) {
|
||||
return res.status(400).json({ message: "User already exists" });
|
||||
}
|
||||
if (user.googleId && !user.password) {
|
||||
user.password = hashedPassword;
|
||||
await user.save();
|
||||
}
|
||||
} else {
|
||||
user = new User({ username, email, password: hashedPassword });
|
||||
await user.save();
|
||||
}
|
||||
|
||||
req.login(user, (err) => {
|
||||
if (err) {
|
||||
console.error("Error logging in user after registration:", err);
|
||||
return res.status(500).send("Internal server error");
|
||||
}
|
||||
return res.status(200).json({
|
||||
message: "Registered and logged in successfully",
|
||||
user,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error registering user:", error);
|
||||
res.status(400).send("Registration failed");
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/login", (req, res, next) => {
|
||||
passport.authenticate("local", (err, user, info) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
if (!user) {
|
||||
return res.status(401).json({ message: "Incorrect email or password" });
|
||||
}
|
||||
req.logIn(user, (err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
return res.status(200).json({ message: "Login successful", user });
|
||||
});
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
app.get("/auth/logout", function (req, res) {
|
||||
req.logout((err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return res.status(500).json({ message: "Error logging out" });
|
||||
}
|
||||
req.session.destroy(function (err) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return res.status(500).json({ message: "Error destroying session" });
|
||||
}
|
||||
res.json({ message: "Logout successful" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/api/user/profile", async (req, res) => {
|
||||
try {
|
||||
if (req.user) {
|
||||
return res.json({ user: req.user });
|
||||
} else {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching user data:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Serve static files
|
||||
app.use(express.static(path.join(__dirname, "../client/build")));
|
||||
|
||||
// Catch-all route to serve React app
|
||||
app.get("*", (req, res) =>
|
||||
res.sendFile(path.join(__dirname, "../client/build/index.html"))
|
||||
);
|
||||
|
||||
const Port = 8080;
|
||||
app.listen(Port, () => {
|
||||
console.log(`Server is Running at port ${Port}`);
|
||||
});
|
||||
Reference in New Issue
Block a user