feat : add signin ,signout, forgot password features
This commit is contained in:
21
src/App.jsx
21
src/App.jsx
@@ -1,16 +1,18 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { AuthProvider, useAuth } from './context/AuthContext';
|
||||
import { useAuthStore } from './store/authStore';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Navbar from './components/Navbar';
|
||||
import Login from './pages/Login';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Transactions from './pages/Transactions';
|
||||
import Settings from './pages/Settings';
|
||||
import ForgotPassword from './pages/ForgotPassword';
|
||||
|
||||
// Protected Route Component
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const { isAuthenticated, loading } = useAuth();
|
||||
const isLoggedIn = useAuthStore((s) => s.isLoggedIn);
|
||||
const loading = useAuthStore((s) => s.loading);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -20,7 +22,7 @@ const ProtectedRoute = ({ children }) => {
|
||||
);
|
||||
}
|
||||
|
||||
return isAuthenticated ? children : <Navigate to="/login" />;
|
||||
return isLoggedIn ? children : <Navigate to="/login" />;
|
||||
};
|
||||
|
||||
// Main Layout Component
|
||||
@@ -58,6 +60,7 @@ const AppRoutes = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
@@ -96,13 +99,11 @@ const AppRoutes = () => {
|
||||
// Main App Component
|
||||
const App = () => {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<div className="App">
|
||||
<AppRoutes />
|
||||
</div>
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
<Router>
|
||||
<div className="App">
|
||||
<AppRoutes />
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Menu, Sun, Moon, User, LogOut, ChevronDown } from 'lucide-react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { signOut } from '../services/api';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
|
||||
const Navbar = ({ onSidebarToggle }) => {
|
||||
const { user, logout } = useAuth();
|
||||
const isLoggedIn = useAuthStore((s) => s.isLoggedIn);
|
||||
const [isDarkMode, setIsDarkMode] = useState(() => {
|
||||
return localStorage.getItem('theme') === 'dark' ||
|
||||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
@@ -25,9 +26,14 @@ const Navbar = ({ onSidebarToggle }) => {
|
||||
setIsDarkMode(!isDarkMode);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
setIsUserMenuOpen(false);
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await signOut();
|
||||
} catch (e) {
|
||||
// no-op; interceptor handles redirect and state
|
||||
} finally {
|
||||
setIsUserMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -71,7 +77,7 @@ const Navbar = ({ onSidebarToggle }) => {
|
||||
<User className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<span className="hidden sm:block text-sm font-medium">
|
||||
{user?.name || 'Admin'}
|
||||
{isLoggedIn ? 'Admin' : 'Guest'}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -81,10 +87,10 @@ const Navbar = ({ onSidebarToggle }) => {
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50">
|
||||
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{user?.name || 'Admin User'}
|
||||
{isLoggedIn ? 'Admin User' : 'Guest'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{user?.email || 'admin@example.com'}
|
||||
{isLoggedIn ? 'admin@example.com' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
|
||||
100
src/pages/ForgotPassword.jsx
Normal file
100
src/pages/ForgotPassword.jsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { CreditCard } from 'lucide-react';
|
||||
import { forgotPassword } from '../services/api';
|
||||
|
||||
const ForgotPassword = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
|
||||
try {
|
||||
const res = await forgotPassword(email);
|
||||
setSuccess(res?.message || 'If the email exists, a reset link was sent.');
|
||||
} catch (err) {
|
||||
setError(typeof err === 'string' ? err : 'Request failed. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="flex justify-center">
|
||||
<div className="bg-primary-600 p-3 rounded-full">
|
||||
<CreditCard className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Forgot Password
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
Enter your email to receive a new password
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10 border border-gray-200 dark:border-gray-700">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 text-red-700 dark:text-red-300 px-4 py-3 rounded-md text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 text-green-700 dark:text-green-300 px-4 py-3 rounded-md text-sm">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="input-field"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
|
||||
>
|
||||
{loading ? 'Sending...' : 'Send new password'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<Link to="/login" className="text-primary-600 hover:text-primary-700">Back to Login</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { CreditCard, Eye, EyeOff } from 'lucide-react';
|
||||
import { login as apiLogin } from '../services/api';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
|
||||
const Login = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -10,7 +11,7 @@ const Login = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const { login } = useAuth();
|
||||
const setLoggedIn = useAuthStore((s) => s.setLoggedIn);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
@@ -19,14 +20,16 @@ const Login = () => {
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const result = await login(email, password);
|
||||
if (result.success) {
|
||||
const result = await apiLogin(email, password);
|
||||
// If server responds successfully, consider logged in (session cookie via withCredentials)
|
||||
if (result) {
|
||||
setLoggedIn(true);
|
||||
navigate('/');
|
||||
} else {
|
||||
setError('Invalid email or password');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Login failed. Please try again.');
|
||||
setError(typeof err === 'string' ? err : 'Login failed. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -140,8 +143,8 @@ const Login = () => {
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>Email: admin@example.com</p>
|
||||
<p>Password: password</p>
|
||||
<p>Forget Password? <a href="/forgot-password" className="text-primary-600 hover:text-primary-700">Reset Password</a></p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,43 +1,95 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// Create axios instance
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
|
||||
const BASE_URL = import.meta.env.DEV ? "/" : "https://khalijpay-core.qaserver.ir";
|
||||
|
||||
// ساخت instance از axios
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001/api',
|
||||
timeout: 10000,
|
||||
baseURL: BASE_URL,
|
||||
withCredentials: true, // ارسال و دریافت cookie/session
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor to add JWT token
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
// -----------------------------
|
||||
// Interceptor پاسخها
|
||||
// -----------------------------
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
if (error.response?.status === 401) {
|
||||
// session منقضی شده → هدایت به login
|
||||
const setLoggedIn = useAuthStore.getState().setLoggedIn;
|
||||
setLoggedIn(false);
|
||||
window.location.href = "/";
|
||||
}
|
||||
return Promise.reject(error.response?.data || error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor to handle token expiration
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Token expired or invalid
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
// -----------------------------
|
||||
// توابع API
|
||||
// -----------------------------
|
||||
|
||||
// Login
|
||||
export async function login(username, password) {
|
||||
try {
|
||||
const res = await api.post("/api/v1/Auth/SignIn", {
|
||||
// include common variants to satisfy different backends
|
||||
userName: username,
|
||||
username: username,
|
||||
email: username,
|
||||
password: password,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// خروج از سیستم
|
||||
export async function signOut() {
|
||||
try {
|
||||
const res = await api.post("/api/v1/Auth/SignOut");
|
||||
// پاک کردن وضعیت login در Zustand
|
||||
const setLoggedIn = useAuthStore.getState().setLoggedIn;
|
||||
setLoggedIn(false);
|
||||
window.location.href = "/";
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// فراموشی رمز عبور
|
||||
export async function forgotPassword(email) {
|
||||
try {
|
||||
const res = await api.post("/api/v1/Auth/ForgotPassword", { email });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// گرفتن دادههای محافظتشده
|
||||
export async function fetchProtectedData(endpoint) {
|
||||
try {
|
||||
const res = await api.get(endpoint);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Mock data for development
|
||||
const mockData = {
|
||||
@@ -99,62 +151,12 @@ const mockData = {
|
||||
]
|
||||
};
|
||||
|
||||
// API functions
|
||||
export const authAPI = {
|
||||
login: async (email, password) => {
|
||||
try {
|
||||
const response = await api.post('/auth/login', { email, password });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// Return mock data for demo
|
||||
return {
|
||||
token: 'mock-jwt-token-' + Date.now(),
|
||||
user: {
|
||||
id: 1,
|
||||
email: email,
|
||||
name: 'Admin User',
|
||||
role: 'admin'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Expose mock payment API for dashboard until real endpoints are integrated
|
||||
export const paymentsAPI = {
|
||||
getStats: async () => {
|
||||
try {
|
||||
const response = await api.get('/payments/stats');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// Return mock data for demo
|
||||
return mockData.stats;
|
||||
}
|
||||
async getStats() {
|
||||
return mockData.stats;
|
||||
},
|
||||
|
||||
getPayments: async (page = 1, limit = 10) => {
|
||||
try {
|
||||
const response = await api.get(`/payments?page=${page}&limit=${limit}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// Return mock data for demo
|
||||
return {
|
||||
data: mockData.payments,
|
||||
total: mockData.payments.length,
|
||||
page,
|
||||
limit
|
||||
};
|
||||
}
|
||||
async getChartData() {
|
||||
return mockData.chartData;
|
||||
},
|
||||
|
||||
getChartData: async () => {
|
||||
try {
|
||||
const response = await api.get('/payments/chart-data');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// Return mock data for demo
|
||||
return mockData.chartData;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
8
src/store/authStore.js
Normal file
8
src/store/authStore.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
export const useAuthStore = create((set) => ({
|
||||
isLoggedIn: false,
|
||||
loading: false,
|
||||
setLoggedIn: (status) => set({ isLoggedIn: status }),
|
||||
setLoading: (status) => set({ loading: status }),
|
||||
}));
|
||||
Reference in New Issue
Block a user