initial commit

This commit is contained in:
Harikrishnan Gopal
2024-12-03 15:53:50 +05:30
commit 956cf14c53
26 changed files with 22820 additions and 0 deletions

488
client/src/App.css Normal file
View 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
View 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;

View 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;

View 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
View 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;

View 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;

View 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
View 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
View 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();

View 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
View 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';