feat : add signin ,signout, forgot password features

This commit is contained in:
ghazall-ag
2025-10-29 22:43:48 +03:30
parent 46a1fae510
commit 5079a5bc56
63 changed files with 2920 additions and 125 deletions

View File

@@ -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>
);
};

View File

@@ -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

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

View File

@@ -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>

View File

@@ -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
View 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 }),
}));