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