diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index 035a40c..65abe51 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -1,33 +1,168 @@ +// const User = require('../models/User'); +// const jwt = require('jsonwebtoken'); +// const bcrypt = require('bcryptjs'); +// const crypto = require('crypto'); +// // Function to generate JWT +// const generateToken = (id) => { +// return jwt.sign({ id }, process.env.JWT_SECRET, { +// expiresIn: '30d', +// }); +// }; + +// // @desc Register a new user +// // @route POST /api/auth/signup +// // @access Public +// const signup = async (req, res) => { +// const { email, password } = req.body; + +// if (!email || !password) { +// return res.status(400).json({ message: 'Please enter all fields' }); +// } + +// try { +// const userExists = await User.findOne({ email }); + +// if (userExists) { +// return res.status(400).json({ message: 'User already exists' }); +// } + +// const user = await User.create({ +// email, +// password, +// }); + +// if (user) { +// res.status(201).json({ +// _id: user._id, +// email: user.email, +// token: generateToken(user._id), +// }); +// } else { +// res.status(400).json({ message: 'Invalid user data' }); +// } +// } catch (error) { +// res.status(500).json({ message: 'Server Error', error: error.message }); +// } +// }; + +// // @desc Authenticate user & get token +// // @route POST /api/auth/login +// // @access Public +// const login = async (req, res) => { +// const { email, password } = req.body; + +// try { +// const user = await User.findOne({ email }); + +// if (user && (await bcrypt.compare(password, user.password))) { +// res.json({ +// _id: user._id, +// email: user.email, +// token: generateToken(user._id), +// isSetupComplete: user.isSetupComplete, +// defaultCurrency: user.defaultCurrency, +// }); +// } else { +// res.status(401).json({ message: 'Invalid email or password' }); +// } +// } catch (error) { +// res.status(500).json({ message: 'Server Error', error: error.message }); +// } +// }; + +// // @desc Get user data +// // @route GET /api/auth/me +// // @access Private +// const getMe = async (req, res) => { +// // The user is already available in req.user +// res.status(200).json(req.user); +// }; + +// // @desc Complete user setup +// // @route PUT /api/auth/setup +// // @access Private +// const completeSetup = async (req, res) => { +// const { defaultCurrency } = req.body; + +// if (!defaultCurrency) { +// return res.status(400).json({ message: 'Default currency is required' }); +// } + +// try { +// const user = await User.findByIdAndUpdate( +// req.user._id, +// { +// defaultCurrency, +// isSetupComplete: true +// }, +// { new: true } +// ); + +// if (!user) { +// return res.status(404).json({ message: 'User not found' }); +// } + +// res.status(200).json({ +// _id: user._id, +// email: user.email, +// defaultCurrency: user.defaultCurrency, +// isSetupComplete: user.isSetupComplete, +// }); +// } catch (error) { +// res.status(500).json({ message: 'Server Error', error: error.message }); +// } +// }; + +// module.exports = { +// signup, +// login, +// getMe, +// completeSetup, +// }; + const User = require('../models/User'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); +const crypto = require('crypto'); + +// ============================================================ +// Generate JWT +// ============================================================ -// Function to generate JWT const generateToken = (id) => { return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: '30d', }); }; -// @desc Register a new user -// @route POST /api/auth/signup -// @access Public +// ============================================================ +// Register a new user +// POST /api/auth/signup +// Public +// ============================================================ + const signup = async (req, res) => { const { email, password } = req.body; if (!email || !password) { - return res.status(400).json({ message: 'Please enter all fields' }); + return res.status(400).json({ + message: 'Please enter all fields', + }); } try { - const userExists = await User.findOne({ email }); + const userExists = await User.findOne({ + email: email.toLowerCase(), + }); if (userExists) { - return res.status(400).json({ message: 'User already exists' }); + return res.status(400).json({ + message: 'User already exists', + }); } const user = await User.create({ - email, + email: email.toLowerCase(), password, }); @@ -36,23 +171,37 @@ const signup = async (req, res) => { _id: user._id, email: user.email, token: generateToken(user._id), + isSetupComplete: user.isSetupComplete, + defaultCurrency: user.defaultCurrency, }); } else { - res.status(400).json({ message: 'Invalid user data' }); + res.status(400).json({ + message: 'Invalid user data', + }); } } catch (error) { - res.status(500).json({ message: 'Server Error', error: error.message }); + console.error('Signup error:', error); + + res.status(500).json({ + message: 'Server Error', + error: error.message, + }); } }; -// @desc Authenticate user & get token -// @route POST /api/auth/login -// @access Public +// ============================================================ +// Authenticate user +// POST /api/auth/login +// Public +// ============================================================ + const login = async (req, res) => { const { email, password } = req.body; try { - const user = await User.findOne({ email }); + const user = await User.findOne({ + email: email.toLowerCase(), + }); if (user && (await bcrypt.compare(password, user.password))) { res.json({ @@ -63,43 +212,61 @@ const login = async (req, res) => { defaultCurrency: user.defaultCurrency, }); } else { - res.status(401).json({ message: 'Invalid email or password' }); + res.status(401).json({ + message: 'Invalid email or password', + }); } } catch (error) { - res.status(500).json({ message: 'Server Error', error: error.message }); + console.error('Login error:', error); + + res.status(500).json({ + message: 'Server Error', + error: error.message, + }); } }; -// @desc Get user data -// @route GET /api/auth/me -// @access Private +// ============================================================ +// Get user data +// GET /api/auth/me +// Private +// ============================================================ + const getMe = async (req, res) => { - // The user is already available in req.user res.status(200).json(req.user); }; -// @desc Complete user setup -// @route PUT /api/auth/setup -// @access Private +// ============================================================ +// Complete user setup +// PUT /api/auth/setup +// Private +// ============================================================ + const completeSetup = async (req, res) => { const { defaultCurrency } = req.body; if (!defaultCurrency) { - return res.status(400).json({ message: 'Default currency is required' }); + return res.status(400).json({ + message: 'Default currency is required', + }); } try { const user = await User.findByIdAndUpdate( req.user._id, - { + { defaultCurrency, - isSetupComplete: true + isSetupComplete: true, }, - { new: true } + { + new: true, + } ); if (!user) { - return res.status(404).json({ message: 'User not found' }); + return res.status(404).json({ + message: 'User not found', + }); } res.status(200).json({ @@ -109,13 +276,163 @@ const completeSetup = async (req, res) => { isSetupComplete: user.isSetupComplete, }); } catch (error) { - res.status(500).json({ message: 'Server Error', error: error.message }); + console.error('Setup error:', error); + + res.status(500).json({ + message: 'Server Error', + error: error.message, + }); } }; +// ============================================================ +// Forgot Password +// POST /api/auth/forgot-password +// Public +// ============================================================ + +const forgotPassword = async (req, res) => { + const { email } = req.body; + + if (!email) { + return res.status(400).json({ + message: 'Email is required', + }); + } + + try { + const user = await User.findOne({ + email: email.toLowerCase(), + }); + + // Don't reveal whether the email exists + if (!user) { + return res.status(200).json({ + message: + 'If an account exists with this email, a password reset link has been sent.', + }); + } + + // Generate a secure random token + const resetToken = crypto.randomBytes(32).toString('hex'); + + // Hash the token before storing it + const hashedToken = crypto + .createHash('sha256') + .update(resetToken) + .digest('hex'); + + user.resetPasswordToken = hashedToken; + + // Token expires after 15 minutes + user.resetPasswordExpire = new Date( + Date.now() + 15 * 60 * 1000 + ); + + await user.save(); + + // Temporary reset URL for local development + const resetUrl = + `http://localhost:5173/reset-password?token=${resetToken}`; + + // Show reset link in backend terminal for now + console.log(''); + console.log('=========================================='); + console.log('PASSWORD RESET LINK'); + console.log('=========================================='); + console.log(resetUrl); + console.log('=========================================='); + console.log(''); + + res.status(200).json({ + message: + 'If an account exists with this email, a password reset link has been generated.', + resetUrl, + }); + } catch (error) { + console.error('Forgot password error:', error); + + res.status(500).json({ + message: 'Server Error', + error: error.message, + }); + } +}; + +// ============================================================ +// Reset Password +// POST /api/auth/reset-password +// Public +// ============================================================ + +const resetPassword = async (req, res) => { + const { token, password } = req.body; + + if (!token || !password) { + return res.status(400).json({ + message: 'Token and new password are required', + }); + } + + if (password.length < 6) { + return res.status(400).json({ + message: 'Password must be at least 6 characters long', + }); + } + + try { + // Hash the token received from the frontend + const hashedToken = crypto + .createHash('sha256') + .update(token) + .digest('hex'); + + // Find user with valid, non-expired reset token + const user = await User.findOne({ + resetPasswordToken: hashedToken, + resetPasswordExpire: { + $gt: new Date(), + }, + }); + + if (!user) { + return res.status(400).json({ + message: 'Password reset token is invalid or has expired', + }); + } + + // Set new password. + // User.js pre-save middleware will automatically hash it. + user.password = password; + + // Delete reset token so it cannot be reused + user.resetPasswordToken = null; + user.resetPasswordExpire = null; + + await user.save(); + + res.status(200).json({ + message: 'Password reset successful. You can now log in.', + }); + } catch (error) { + console.error('Reset password error:', error); + + res.status(500).json({ + message: 'Server Error', + error: error.message, + }); + } +}; + +// ============================================================ +// Export Controllers +// ============================================================ + module.exports = { signup, login, getMe, completeSetup, + forgotPassword, + resetPassword, }; \ No newline at end of file diff --git a/backend/models/User.js b/backend/models/User.js index ce98e5a..4cd38a0 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -1,36 +1,95 @@ +// const mongoose = require('mongoose'); +// const bcrypt = require('bcryptjs'); + +// const userSchema = new mongoose.Schema({ +// email: { +// type: String, +// required: true, +// unique: true, +// lowercase: true, +// }, +// password: { +// type: String, +// required: true, +// }, +// defaultCurrency: { +// type: String, +// default: 'USD', +// }, +// isSetupComplete: { +// type: Boolean, +// default: false, +// }, +// }, { +// timestamps: true, +// }); + +// // Middleware to hash password before saving +// userSchema.pre('save', async function (next) { +// if (!this.isModified('password')) { +// return next(); +// } +// const salt = await bcrypt.genSalt(10); +// this.password = await bcrypt.hash(this.password, salt); +// next(); +// }); + +// const User = mongoose.model('User', userSchema); + +// module.exports = User; + const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); -const userSchema = new mongoose.Schema({ - email: { - type: String, - required: true, - unique: true, - lowercase: true, - }, - password: { - type: String, - required: true, - }, - defaultCurrency: { - type: String, - default: 'USD', - }, - isSetupComplete: { - type: Boolean, - default: false, +const userSchema = new mongoose.Schema( + { + email: { + type: String, + required: true, + unique: true, + lowercase: true, + }, + + password: { + type: String, + required: true, + }, + + defaultCurrency: { + type: String, + default: 'USD', + }, + + isSetupComplete: { + type: Boolean, + default: false, + }, + + // Password reset fields + resetPasswordToken: { + type: String, + default: null, + }, + + resetPasswordExpire: { + type: Date, + default: null, + }, }, -}, { - timestamps: true, -}); + { + timestamps: true, + } +); -// Middleware to hash password before saving +// Hash password before saving userSchema.pre('save', async function (next) { if (!this.isModified('password')) { return next(); } + const salt = await bcrypt.genSalt(10); this.password = await bcrypt.hash(this.password, salt); + next(); }); diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js index 4c4679f..3103960 100644 --- a/backend/routes/authRoutes.js +++ b/backend/routes/authRoutes.js @@ -1,12 +1,50 @@ +// const express = require('express'); +// const router = express.Router(); +// const { signup, login, getMe, completeSetup } = require('../controllers/authController'); +// const { protect } = require('../middleware/authMiddleware'); +// const { validateRegistration } = require('../middleware/validationMiddleware'); + +// router.post('/signup', validateRegistration, signup); +// router.post('/login', login); +// router.get('/me', protect, getMe); +// router.put('/setup', protect, completeSetup); + +// module.exports = router; + + const express = require('express'); + const router = express.Router(); -const { signup, login, getMe, completeSetup } = require('../controllers/authController'); + +const { + signup, + login, + getMe, + completeSetup, + forgotPassword, + resetPassword, +} = require('../controllers/authController'); + const { protect } = require('../middleware/authMiddleware'); + const { validateRegistration } = require('../middleware/validationMiddleware'); +// Register router.post('/signup', validateRegistration, signup); + +// Login router.post('/login', login); + +// Get current user router.get('/me', protect, getMe); + +// Complete setup router.put('/setup', protect, completeSetup); +// Forgot password +router.post('/forgot-password', forgotPassword); + +// Reset password +router.post('/reset-password', resetPassword); + module.exports = router; \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c45c3fc..861ff0b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,8 +1,74 @@ +// import { ToastContainer } from 'react-toastify'; +// import 'react-toastify/dist/ReactToastify.css'; +// import { Routes, Route } from 'react-router-dom'; +// import LoginPage from './pages/LoginPage'; +// import RegisterPage from './pages/RegisterPage'; +// import SetupPage from './pages/SetupPage'; +// import DashboardPage from './pages/DashboardPage'; +// import TransactionsPage from './pages/TransactionsPage'; +// import ReceiptsPage from './pages/ReceiptsPage'; +// import WelcomePage from './pages/WelcomePage'; +// import SettingsPage from './pages/SettingsPage'; +// import NotFound from './pages/NotFound'; +// import Budgets from './pages/Budgets'; +// import ContactUs from './pages/ContactUs'; +// import Layout from './components/Layout'; +// import ProtectedRoute from './components/ProtectedRoute'; +// import SetupProtectedRoute from './components/SetupProtectedRoute'; +// import RecurringTransactions from './pages/RecurringTransactions'; + +// function App() { +// return ( +// <> +// +// {/* Public Routes */} +// } /> +// } /> +// } /> +// } /> +// {/* Protected Routes */} +// +// +// +// } +// /> +// {/* Protected Routes Wrapper */} +// +// +// +// } +// > +// } /> +// } /> +// } /> +// } /> +// } /> +// } +// /> +// +// } /> +// +// +// +// ); +// } + +// export default App; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { Routes, Route } from 'react-router-dom'; + import LoginPage from './pages/LoginPage'; import RegisterPage from './pages/RegisterPage'; +import ForgotPasswordPage from './pages/ForgotPasswordPage'; + import SetupPage from './pages/SetupPage'; import DashboardPage from './pages/DashboardPage'; import TransactionsPage from './pages/TransactionsPage'; @@ -12,21 +78,25 @@ import SettingsPage from './pages/SettingsPage'; import NotFound from './pages/NotFound'; import Budgets from './pages/Budgets'; import ContactUs from './pages/ContactUs'; +import RecurringTransactions from './pages/RecurringTransactions'; + import Layout from './components/Layout'; import ProtectedRoute from './components/ProtectedRoute'; import SetupProtectedRoute from './components/SetupProtectedRoute'; -import RecurringTransactions from './pages/RecurringTransactions'; function App() { return ( <> - {/* Public Routes */} + + {/* ==================== Public Routes ==================== */} } /> } /> } /> + } /> } /> - {/* Protected Routes */} + + {/* ==================== Setup Route ==================== */} } /> - {/* Protected Routes Wrapper */} + + {/* ==================== Protected Routes ==================== */} @@ -53,11 +124,15 @@ function App() { element={} /> + + {/* ==================== 404 Route ==================== */} } /> + + ); } -export default App; +export default App; \ No newline at end of file diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index a82e76b..78df0a3 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -1,3 +1,121 @@ +// import React, { createContext, useState, useEffect } from 'react'; +// import { toast } from 'react-toastify'; +// import { useNavigate } from 'react-router-dom'; +// import api from '../api/axios'; + +// const AuthContext = createContext(); + +// export const AuthProvider = ({ children }) => { +// const [user, setUser] = useState(null); +// const [token, setToken] = useState(localStorage.getItem('token')); +// const [loading, setLoading] = useState(true); +// const [pendingToast, setPendingToast] = useState(null); +// const navigate = useNavigate(); + +// useEffect(() => { +// const verifyUser = async () => { +// const storedToken = localStorage.getItem('token'); +// if (storedToken) { +// setToken(storedToken); +// api.defaults.headers.common['Authorization'] = `Bearer ${storedToken}`; +// try { +// // Check if the token is valid by fetching user data +// const response = await api.get('/auth/me'); +// setUser(response.data); +// } catch (error) { +// // Clear invalid token +// console.error("Token verification failed", error); +// localStorage.removeItem('token'); +// setUser(null); +// setToken(null); +// } +// } +// setLoading(false); // Finished checking +// }; +// verifyUser(); +// }, []); + +// // Show pending toast after redirect +// useEffect(() => { +// if (pendingToast) { +// toast[pendingToast.type](pendingToast.message); +// setPendingToast(null); +// } +// }, [pendingToast]); + +// const login = async (email, password) => { +// try { +// const response = await api.post('/auth/login', { email, password }); +// const { token: newToken, ...userData } = response.data; + +// setToken(newToken); +// setUser(userData); +// localStorage.setItem('token', newToken); +// api.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; + +// setPendingToast({ type: 'success', message: 'Login successful!' }); +// if (!userData.isSetupComplete) { +// navigate('/setup'); +// } else { +// navigate('/dashboard'); +// } +// } catch (error) { +// console.error('Login failed', error.response?.data); +// setPendingToast({ type: 'error', message: error.response?.data?.message || 'Login failed. Please try again.' }); +// throw new Error(error.response?.data?.message || 'Login failed. Please try again.'); +// } +// }; + +// const signup = async (email, password) => { +// try { +// const response = await api.post('/auth/signup', { email, password }); +// const { token: newToken, ...userData } = response.data; + +// setToken(newToken); +// setUser(userData); +// localStorage.setItem('token', newToken); +// api.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; + +// setPendingToast({ type: 'success', message: 'Signup successful!' }); +// navigate('/setup'); +// } catch (error) { +// console.error('Signup failed', error.response?.data); +// setPendingToast({ type: 'error', message: error.response?.data?.message || 'Signup failed. Please try again.' }); +// throw new Error(error.response?.data?.message || 'Signup failed. Please try again.'); +// } +// }; + +// const logout = () => { +// setPendingToast({ type: 'info', message: 'Logged out successfully.' }); +// setUser(null); +// setToken(null); +// localStorage.removeItem('token'); +// delete api.defaults.headers.common['Authorization']; +// navigate('/login'); +// }; + +// const setup = async (defaultCurrency) => { +// try { +// const response = await api.put('/auth/setup', { defaultCurrency }); + +// setUser(response.data); + +// navigate('/dashboard'); +// } catch (error) { +// console.error('Setup failed', error); +// throw new Error(error.response?.data?.message || 'Setup failed. Please try again.'); +// } +// }; + +// return ( +// +// {children} +// +// ); +// }; + +// export default AuthContext; + import React, { createContext, useState, useEffect } from 'react'; import { toast } from 'react-toastify'; import { useNavigate } from 'react-router-dom'; @@ -15,23 +133,28 @@ export const AuthProvider = ({ children }) => { useEffect(() => { const verifyUser = async () => { const storedToken = localStorage.getItem('token'); + if (storedToken) { setToken(storedToken); api.defaults.headers.common['Authorization'] = `Bearer ${storedToken}`; + try { - // Check if the token is valid by fetching user data const response = await api.get('/auth/me'); setUser(response.data); } catch (error) { - // Clear invalid token - console.error("Token verification failed", error); + console.error('Token verification failed', error); + localStorage.removeItem('token'); setUser(null); setToken(null); + + delete api.defaults.headers.common['Authorization']; } } - setLoading(false); // Finished checking + + setLoading(false); }; + verifyUser(); }, []); @@ -43,17 +166,29 @@ export const AuthProvider = ({ children }) => { } }, [pendingToast]); + // ==================== LOGIN ==================== + const login = async (email, password) => { try { - const response = await api.post('/auth/login', { email, password }); + const response = await api.post('/auth/login', { + email, + password, + }); + const { token: newToken, ...userData } = response.data; setToken(newToken); setUser(userData); + localStorage.setItem('token', newToken); + api.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; - setPendingToast({ type: 'success', message: 'Login successful!' }); + setPendingToast({ + type: 'success', + message: 'Login successful!', + }); + if (!userData.isSetupComplete) { navigate('/setup'); } else { @@ -61,54 +196,150 @@ export const AuthProvider = ({ children }) => { } } catch (error) { console.error('Login failed', error.response?.data); - setPendingToast({ type: 'error', message: error.response?.data?.message || 'Login failed. Please try again.' }); - throw new Error(error.response?.data?.message || 'Login failed. Please try again.'); + + setPendingToast({ + type: 'error', + message: + error.response?.data?.message || + 'Login failed. Please try again.', + }); + + throw new Error( + error.response?.data?.message || + 'Login failed. Please try again.' + ); } }; + // ==================== SIGNUP ==================== + const signup = async (email, password) => { try { - const response = await api.post('/auth/signup', { email, password }); + const response = await api.post('/auth/signup', { + email, + password, + }); + const { token: newToken, ...userData } = response.data; setToken(newToken); setUser(userData); + localStorage.setItem('token', newToken); + api.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; - setPendingToast({ type: 'success', message: 'Signup successful!' }); + setPendingToast({ + type: 'success', + message: 'Signup successful!', + }); + navigate('/setup'); } catch (error) { console.error('Signup failed', error.response?.data); - setPendingToast({ type: 'error', message: error.response?.data?.message || 'Signup failed. Please try again.' }); - throw new Error(error.response?.data?.message || 'Signup failed. Please try again.'); + + setPendingToast({ + type: 'error', + message: + error.response?.data?.message || + 'Signup failed. Please try again.', + }); + + throw new Error( + error.response?.data?.message || + 'Signup failed. Please try again.' + ); } }; + // ==================== FORGOT PASSWORD ==================== + + const forgotPassword = async (email) => { + try { + const response = await api.post('/auth/forgot-password', { + email, + }); + + setPendingToast({ + type: 'success', + message: + response.data?.message || + 'If an account exists with this email, a password reset link has been sent.', + }); + + return response.data; + } catch (error) { + console.error( + 'Forgot password failed', + error.response?.data + ); + + setPendingToast({ + type: 'error', + message: + error.response?.data?.message || + 'Unable to send password reset link. Please try again.', + }); + + throw new Error( + error.response?.data?.message || + 'Unable to send password reset link. Please try again.' + ); + } + }; + + // ==================== LOGOUT ==================== + const logout = () => { - setPendingToast({ type: 'info', message: 'Logged out successfully.' }); + setPendingToast({ + type: 'info', + message: 'Logged out successfully.', + }); + setUser(null); setToken(null); + localStorage.removeItem('token'); + delete api.defaults.headers.common['Authorization']; + navigate('/login'); }; + // ==================== SETUP ==================== + const setup = async (defaultCurrency) => { try { - const response = await api.put('/auth/setup', { defaultCurrency }); + const response = await api.put('/auth/setup', { + defaultCurrency, + }); setUser(response.data); navigate('/dashboard'); } catch (error) { console.error('Setup failed', error); - throw new Error(error.response?.data?.message || 'Setup failed. Please try again.'); + + throw new Error( + error.response?.data?.message || + 'Setup failed. Please try again.' + ); } }; return ( - + {children} ); diff --git a/frontend/src/pages/ForgotPasswordPage.jsx b/frontend/src/pages/ForgotPasswordPage.jsx new file mode 100644 index 0000000..6cf9b3d --- /dev/null +++ b/frontend/src/pages/ForgotPasswordPage.jsx @@ -0,0 +1,443 @@ +// // import React, { useState } from 'react'; +// // import { Link } from 'react-router-dom'; +// // import { HiMail, HiArrowLeft } from 'react-icons/hi'; + +// // export default function ForgotPasswordPage() { +// // const [email, setEmail] = useState(''); +// // const [message, setMessage] = useState(''); +// // const [error, setError] = useState(''); +// // const [isLoading, setIsLoading] = useState(false); + +// // const handleSubmit = async (e) => { +// // e.preventDefault(); + +// // setError(''); +// // setMessage(''); +// // setIsLoading(true); + +// // try { +// // // Replace this with your actual backend/API call +// // const response = await fetch('/api/auth/forgot-password', { +// // method: 'POST', +// // headers: { +// // 'Content-Type': 'application/json', +// // }, +// // body: JSON.stringify({ email }), +// // }); + +// // const data = await response.json(); + +// // if (!response.ok) { +// // throw new Error(data.message || 'Unable to send reset email'); +// // } + +// // setMessage( +// // 'If an account exists with this email, a password reset link has been sent.' +// // ); +// // } catch (error) { +// // setError(error.message); +// // } finally { +// // setIsLoading(false); +// // } +// // }; + +// // return ( +// //
+ +// // {/* Logo */} +// // +// // Paisable +// // + +// // {/* Card */} +// //
+ +// // {/* Header */} +// //
+// //

+// // Forgot Password? +// //

+ +// //

+// // Enter your email address and we'll send you a link to reset your password. +// //

+// //
+ +// // {/* Success Message */} +// // {message && ( +// //
+// //

+// // {message} +// //

+// //
+// // )} + +// // {/* Error Message */} +// // {error && ( +// //
+// //

+// // {error} +// //

+// //
+// // )} + +// // {/* Form */} +// //
+ +// // {/* Email */} +// //
+// // + +// //
+// //
+// // +// //
+ +// // setEmail(e.target.value)} +// // required +// // className="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" +// // /> +// //
+// //
+ +// // {/* Submit */} +// // + +// // {/* Back to Login */} +// //
+// // +// // +// // Back to Sign In +// // +// //
+ +// //
+// //
+ +// // {/* Footer */} +// //

+// // Protected by industry-standard security +// //

+// //
+// // ); +// // } + + +// import React, { useState } from 'react'; +// import { Link } from 'react-router-dom'; +// import { HiMail, HiArrowLeft } from 'react-icons/hi'; +// import { useAuth } from '../hooks/useAuth'; + +// export default function ForgotPasswordPage() { +// const [email, setEmail] = useState(''); +// const [message, setMessage] = useState(''); +// const [error, setError] = useState(''); +// const [isLoading, setIsLoading] = useState(false); + +// const { forgotPassword } = useAuth(); + +// const handleSubmit = async (e) => { +// e.preventDefault(); + +// setError(''); +// setMessage(''); + +// if (!email.trim()) { +// setError('Please enter your email address.'); +// return; +// } + +// setIsLoading(true); + +// try { +// await forgotPassword(email.trim()); + +// setMessage( +// 'If an account exists with this email, a password reset link has been sent.' +// ); +// } catch (error) { +// setError( +// error.message || +// 'Unable to send password reset link. Please try again.' +// ); +// } finally { +// setIsLoading(false); +// } +// }; + +// return ( +//
+//
+ +// {/* Logo / Brand */} +//
+//

+// Paisable +//

+ +//

+// Reset your password +//

+//
+ +// {/* Card */} +//
+ +//
+//

+// Forgot Password? +//

+ +//

+// Enter your email address and we'll send you a link to reset +// your password. +//

+//
+ +// {/* Success Message */} +// {message && ( +//
+// {message} +//
+// )} + +// {/* Error Message */} +// {error && ( +//
+// {error} +//
+// )} + +//
+ +// {/* Email */} +//
+// + +//
+// + +// setEmail(e.target.value)} +// placeholder="Enter your email" +// disabled={isLoading} +// className="w-full pl-11 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" +// required +// /> +//
+//
+ +// {/* Send Button */} +// +//
+ +// {/* Back to Login */} +//
+// +// +// Back to Sign In +// +//
+ +//
+//
+//
+// ); +// } + + +import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { HiMail, HiArrowLeft } from 'react-icons/hi'; +import useAuth from '../hooks/useAuth'; + +export default function ForgotPasswordPage() { + const [email, setEmail] = useState(''); + const [message, setMessage] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const { forgotPassword } = useAuth(); + + const handleSubmit = async (e) => { + e.preventDefault(); + + setError(''); + setMessage(''); + + if (!email.trim()) { + setError('Please enter your email address.'); + return; + } + + setIsLoading(true); + + try { + await forgotPassword(email.trim()); + + setMessage( + 'If an account exists with this email, a password reset link has been sent.' + ); + } catch (error) { + setError( + error.message || + 'Unable to send password reset link. Please try again.' + ); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ + {/* Logo / Brand */} +
+

+ Paisable +

+ +

+ Reset your password +

+
+ + {/* Card */} +
+ +
+

+ Forgot Password? +

+ +

+ Enter your email address and we'll send you a link to reset + your password. +

+
+ + {/* Success Message */} + {message && ( +
+ {message} +
+ )} + + {/* Error Message */} + {error && ( +
+ {error} +
+ )} + +
+ + {/* Email */} +
+ + +
+ + + setEmail(e.target.value)} + placeholder="Enter your email" + disabled={isLoading} + className="w-full pl-11 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" + required + /> +
+
+ + {/* Send Button */} + +
+ + {/* Back to Login */} +
+ + + Back to Sign In + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index d3acd3b..1717979 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -1,24 +1,26 @@ + import React, { useState } from 'react'; import { Link } from 'react-router-dom'; import useAuth from '../hooks/useAuth'; import PasswordInput from '../components/PasswordInput'; import { HiMail, HiArrowRight } from 'react-icons/hi'; -/* // Demo user credentials const DEMO_EMAIL = 'test@example.com'; -const DEMO_PASSWORD = 'password123'; */ +const DEMO_PASSWORD = 'password123'; export default function LoginPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [serverError, setServerError] = useState(''); const [isLoading, setIsLoading] = useState(false); + const { login } = useAuth(); const handleSubmit = async (e) => { e.preventDefault(); - setServerError(''); // Clear previous server errors + setServerError(''); setIsLoading(true); + try { await login(email, password); // Toast handled globally in AuthContext @@ -37,10 +39,11 @@ export default function LoginPage() { return (
- {/* Logo/Brand */} - Paisable @@ -48,66 +51,121 @@ export default function LoginPage() { {/* Login Card */}
+ {/* Header */}
-

Welcome Back

-

Sign in to continue to your account

+

+ Welcome Back +

+ +

+ Sign in to continue to your account +

{/* Error Message */} {serverError && (
-

{serverError}

+

+ {serverError} +

)} {/* Form */}
+ {/* Email Field */}
-
{/* Password Field */}
-
{/* Submit Button */} - {/* Divider */}
+
+
New to Paisable?
+
{/* Register Link */}
- Create an account +
+
@@ -147,6 +212,8 @@ export default function LoginPage() {

Protected by industry-standard security

+
); -} \ No newline at end of file +} +