auth, integrated

This commit is contained in:
rohit 2025-08-06 02:40:24 +05:30
parent 9d579385d7
commit aaef6e883b
25 changed files with 1599 additions and 341 deletions

1
env.example Normal file
View File

@ -0,0 +1 @@
REACT_APP_API_URL=http://localhost:5000/api

27
package-lock.json generated
View File

@ -26,6 +26,7 @@
"react": "^19.1.1", "react": "^19.1.1",
"react-cookie": "^8.0.1", "react-cookie": "^8.0.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-hot-toast": "^2.5.2",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^6.30.1", "react-router-dom": "^6.30.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
@ -9301,6 +9302,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/goober": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
"integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -14589,6 +14599,23 @@
"integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==", "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-hot-toast": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz",
"integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",

View File

@ -21,6 +21,7 @@
"react": "^19.1.1", "react": "^19.1.1",
"react-cookie": "^8.0.1", "react-cookie": "^8.0.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-hot-toast": "^2.5.2",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^6.30.1", "react-router-dom": "^6.30.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",

View File

@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>Cloudtopiaa reseller Dashboard</title> <title>Cloudtopiaa Connect</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -1,9 +1,12 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { CookiesProvider } from 'react-cookie'; import { CookiesProvider } from 'react-cookie';
import { store } from './store'; import { store } from './store';
import { setTheme } from './store/slices/themeSlice'; import { setTheme } from './store/slices/themeSlice';
import ProtectedRoute from './components/ProtectedRoute';
import AuthInitializer from './components/AuthInitializer';
import Toast from './components/Toast';
// Channel Partner Components // Channel Partner Components
import Layout from './components/Layout/Layout'; import Layout from './components/Layout/Layout';
@ -20,6 +23,7 @@ import Reports from './pages/Reports';
import Settings from './pages/Settings'; import Settings from './pages/Settings';
import Login from './pages/Login'; import Login from './pages/Login';
import Signup from './pages/Signup'; import Signup from './pages/Signup';
import VerifyEmail from './pages/VerifyEmail';
// Reseller Components // Reseller Components
import ResellerLogin from './pages/reseller/Login'; import ResellerLogin from './pages/reseller/Login';
@ -32,6 +36,7 @@ import ResellerSupport from './pages/reseller/Support';
import ResellerReports from './pages/reseller/Reports'; import ResellerReports from './pages/reseller/Reports';
import ResellerTraining from './pages/reseller/Training'; import ResellerTraining from './pages/reseller/Training';
import ResellerLayout from './components/Layout/ResellerLayout'; import ResellerLayout from './components/Layout/ResellerLayout';
import Unauthorized from './pages/Unauthorized';
import CookieConsent from './components/CookieConsent'; import CookieConsent from './components/CookieConsent';
import './index.css'; import './index.css';
@ -84,91 +89,128 @@ function App() {
return ( return (
<Provider store={store}> <Provider store={store}>
<CookiesProvider> <CookiesProvider>
<Router> <AuthInitializer>
<div className="App"> <Router>
<div className="App">
<Routes> <Routes>
{/* Channel Partner Routes */} {/* Public Routes */}
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} /> <Route path="/signup" element={<Signup />} />
<Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/unauthorized" element={<Unauthorized />} />
{/* Protected Routes - Vendor Only */}
<Route path="/" element={ <Route path="/" element={
<Layout> <ProtectedRoute requiredRole="vendor">
<Dashboard /> <Layout>
</Layout> <Dashboard />
</Layout>
</ProtectedRoute>
} /> } />
<Route path="/resellers" element={ <Route path="/resellers" element={
<Layout> <ProtectedRoute requiredRole="vendor">
<ResellersPage /> <Layout>
</Layout> <ResellersPage />
</Layout>
</ProtectedRoute>
} /> } />
<Route path="/partnerships" element={ <Route path="/partnerships" element={
<Layout> <ProtectedRoute requiredRole="vendor">
<PartnershipsPage /> <Layout>
</Layout> <PartnershipsPage />
</Layout>
</ProtectedRoute>
} /> } />
<Route path="/deals" element={ <Route path="/deals" element={
<Layout> <ProtectedRoute requiredRole="vendor">
<DealsPage /> <Layout>
</Layout> <DealsPage />
</Layout>
</ProtectedRoute>
} /> } />
<Route path="/commissions" element={ <Route path="/commissions" element={
<Layout> <ProtectedRoute requiredRole="vendor">
<CommissionsPage /> <Layout>
</Layout> <CommissionsPage />
</Layout>
</ProtectedRoute>
} /> } />
<Route path="/product-management" element={ <Route path="/product-management" element={
<Layout> <ProtectedRoute requiredRole="vendor">
<ProductManagement /> <Layout>
</Layout> <ProductManagement />
</Layout>
</ProtectedRoute>
} /> } />
<Route path="/training" element={ <Route path="/training" element={
<Layout> <ProtectedRoute requiredRole="vendor">
<Training /> <Layout>
</Layout> <Training />
</Layout>
</ProtectedRoute>
} />
<Route path="/support" element={
<ProtectedRoute requiredRole="vendor">
<Layout>
<Support />
</Layout>
</ProtectedRoute>
} />
<Route path="/analytics" element={
<ProtectedRoute requiredRole="vendor">
<Layout>
<Analytics />
</Layout>
</ProtectedRoute>
} />
<Route path="/reports" element={
<ProtectedRoute requiredRole="vendor">
<Layout>
<Reports />
</Layout>
</ProtectedRoute>
} />
<Route path="/settings" element={
<ProtectedRoute requiredRole="vendor">
<Layout>
<Settings />
</Layout>
</ProtectedRoute>
} />
<Route path="/targets" element={
<ProtectedRoute requiredRole="vendor">
<Layout>
<PlaceholderPage title="Targets" description="Set and track performance targets" />
</Layout>
</ProtectedRoute>
} />
<Route path="/performance" element={
<ProtectedRoute requiredRole="vendor">
<Layout>
<PlaceholderPage title="Performance" description="Performance metrics and KPIs" />
</Layout>
</ProtectedRoute>
} /> } />
<Route path="/support" element={
<Layout>
<Support />
</Layout>
} />
<Route path="/analytics" element={
<Layout>
<Analytics />
</Layout>
} />
<Route path="/reports" element={
<Layout>
<Reports />
</Layout>
} />
<Route path="/settings" element={
<Layout>
<Settings />
</Layout>
} />
<Route path="/targets" element={
<Layout>
<PlaceholderPage title="Targets" description="Set and track performance targets" />
</Layout>
} />
<Route path="/performance" element={
<Layout>
<PlaceholderPage title="Performance" description="Performance metrics and KPIs" />
</Layout>
} />
<Route path="/marketplace" element={ <Route path="/marketplace" element={
<Layout> <ProtectedRoute>
<PlaceholderPage title="Marketplace" description="Browse and manage marketplace offerings" /> <Layout>
</Layout> <PlaceholderPage title="Marketplace" description="Browse and manage marketplace offerings" />
</Layout>
</ProtectedRoute>
} /> } />
<Route path="/certifications" element={ <Route path="/certifications" element={
<Layout> <ProtectedRoute>
<PlaceholderPage title="Certifications" description="Manage certifications and badges" /> <Layout>
</Layout> <PlaceholderPage title="Certifications" description="Manage certifications and badges" />
</Layout>
</ProtectedRoute>
} /> } />
<Route path="/knowledge-base" element={ <Route path="/knowledge-base" element={
<Layout> <ProtectedRoute>
<PlaceholderPage title="Knowledge Base" description="Access documentation and resources" /> <Layout>
</Layout> <PlaceholderPage title="Knowledge Base" description="Access documentation and resources" />
</Layout>
</ProtectedRoute>
} /> } />
{/* Reseller Routes */} {/* Reseller Routes */}
@ -207,64 +249,88 @@ function App() {
{/* Reseller Dashboard Routes (Separate Service) */} {/* Reseller Dashboard Routes (Separate Service) */}
<Route path="/reseller-dashboard" element={ <Route path="/reseller-dashboard" element={
<ResellerLayout> <ProtectedRoute requiredRole="reseller">
<ResellerDashboardMain /> <ResellerLayout>
</ResellerLayout> <ResellerDashboardMain />
</ResellerLayout>
</ProtectedRoute>
} /> } />
<Route path="/reseller-dashboard/customers" element={ <Route path="/reseller-dashboard/customers" element={
<ResellerLayout> <ProtectedRoute requiredRole="reseller">
<ResellerDashboardCustomers /> <ResellerLayout>
</ResellerLayout> <ResellerDashboardCustomers />
</ResellerLayout>
</ProtectedRoute>
} /> } />
<Route path="/reseller-dashboard/instances" element={ <Route path="/reseller-dashboard/instances" element={
<ResellerLayout> <ProtectedRoute requiredRole="reseller">
<ResellerDashboardInstances /> <ResellerLayout>
</ResellerLayout> <ResellerDashboardInstances />
</ResellerLayout>
</ProtectedRoute>
} /> } />
<Route path="/reseller-dashboard/billing" element={ <Route path="/reseller-dashboard/billing" element={
<ResellerLayout> <ProtectedRoute requiredRole="reseller">
<PlaceholderPage title="Billing & Payments" description="Manage your billing, invoices, and payment methods" /> <ResellerLayout>
</ResellerLayout> <PlaceholderPage title="Billing & Payments" description="Manage your billing, invoices, and payment methods" />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/support" element={
<ProtectedRoute requiredRole="reseller">
<ResellerLayout>
<Support />
</ResellerLayout>
</ProtectedRoute>
} /> } />
<Route path="/reseller-dashboard/support" element={
<ResellerLayout>
<Support />
</ResellerLayout>
} />
<Route path="/reseller-dashboard/reports" element={ <Route path="/reseller-dashboard/reports" element={
<ResellerLayout> <ProtectedRoute requiredRole="reseller">
<PlaceholderPage title="Reports & Analytics" description="View detailed reports and analytics" /> <ResellerLayout>
</ResellerLayout> <PlaceholderPage title="Reports & Analytics" description="View detailed reports and analytics" />
</ResellerLayout>
</ProtectedRoute>
} /> } />
<Route path="/reseller-dashboard/wallet" element={ <Route path="/reseller-dashboard/wallet" element={
<ResellerLayout> <ProtectedRoute requiredRole="reseller">
<PlaceholderPage title="Wallet Management" description="Manage your funds, transactions, and payment methods" /> <ResellerLayout>
</ResellerLayout> <PlaceholderPage title="Wallet Management" description="Manage your funds, transactions, and payment methods" />
</ResellerLayout>
</ProtectedRoute>
} /> } />
<Route path="/reseller-dashboard/training" element={ <Route path="/reseller-dashboard/training" element={
<ResellerLayout> <ProtectedRoute requiredRole="reseller">
<ResellerTraining /> <ResellerLayout>
</ResellerLayout> <ResellerTraining />
</ResellerLayout>
</ProtectedRoute>
} /> } />
<Route path="/reseller-dashboard/marketplace" element={ <Route path="/reseller-dashboard/marketplace" element={
<ResellerLayout> <ProtectedRoute requiredRole="reseller">
<PlaceholderPage title="Marketplace" description="Browse and purchase cloud services and solutions" /> <ResellerLayout>
</ResellerLayout> <PlaceholderPage title="Marketplace" description="Browse and purchase cloud services and solutions" />
</ResellerLayout>
</ProtectedRoute>
} /> } />
<Route path="/reseller-dashboard/certifications" element={ <Route path="/reseller-dashboard/certifications" element={
<ResellerLayout> <ProtectedRoute requiredRole="reseller">
<PlaceholderPage title="Certifications" description="Manage your professional certifications and achievements" /> <ResellerLayout>
</ResellerLayout> <PlaceholderPage title="Certifications" description="Manage your professional certifications and achievements" />
</ResellerLayout>
</ProtectedRoute>
} /> } />
<Route path="/reseller-dashboard/knowledge-base" element={ <Route path="/reseller-dashboard/knowledge-base" element={
<ResellerLayout> <ProtectedRoute requiredRole="reseller">
<PlaceholderPage title="Knowledge Base" description="Access documentation, guides, and helpful resources" /> <ResellerLayout>
</ResellerLayout> <PlaceholderPage title="Knowledge Base" description="Access documentation, guides, and helpful resources" />
</ResellerLayout>
</ProtectedRoute>
} /> } />
<Route path="/reseller-dashboard/settings" element={ <Route path="/reseller-dashboard/settings" element={
<ResellerLayout> <ProtectedRoute requiredRole="reseller">
<PlaceholderPage title="Settings" description="Configure your account preferences and system settings" /> <ResellerLayout>
</ResellerLayout> <PlaceholderPage title="Settings" description="Configure your account preferences and system settings" />
</ResellerLayout>
</ProtectedRoute>
} /> } />
<Route path="/reseller/wallet" element={ <Route path="/reseller/wallet" element={
<Layout> <Layout>
@ -297,16 +363,14 @@ function App() {
</Layout> </Layout>
} /> } />
{/* Default Route */} {/* Default Route - Redirect to login */}
<Route path="*" element={ <Route path="*" element={<Navigate to="/login" replace />} />
<Layout>
<Dashboard />
</Layout>
} />
</Routes> </Routes>
<CookieConsent /> <CookieConsent />
</div> <Toast />
</Router> </div>
</Router>
</AuthInitializer>
</CookiesProvider> </CookiesProvider>
</Provider> </Provider>
); );

View File

@ -0,0 +1,56 @@
import React, { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import { getCurrentUser, refreshUserToken } from '../store/slices/authThunks';
import { setTokens } from '../store/slices/authSlice';
const AuthInitializer: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const dispatch = useAppDispatch();
const { isAuthenticated, user } = useAppSelector((state) => state.auth);
useEffect(() => {
const initializeAuth = async () => {
try {
// Check for existing tokens in localStorage
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
const sessionId = localStorage.getItem('sessionId');
if (accessToken && refreshToken && sessionId) {
// Set tokens in store
dispatch(setTokens({
token: accessToken,
refreshToken,
sessionId,
}));
// Try to get current user
try {
await dispatch(getCurrentUser()).unwrap();
} catch (error) {
// If getting user fails, try to refresh token
console.log('Failed to get current user, trying to refresh token...');
try {
await dispatch(refreshUserToken()).unwrap();
// Try to get user again after token refresh
await dispatch(getCurrentUser()).unwrap();
} catch (refreshError) {
console.log('Token refresh failed, clearing auth data...');
// Clear invalid tokens
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('sessionId');
}
}
}
} catch (error) {
console.error('Auth initialization error:', error);
}
};
initializeAuth();
}, [dispatch]);
return <>{children}</>;
};
export default AuthInitializer;

View File

@ -23,12 +23,14 @@ import {
Handshake, Handshake,
Target, Target,
TrendingUp, TrendingUp,
Package Package,
User
} from 'lucide-react'; } from 'lucide-react';
import { RootState } from '../../store'; import { RootState } from '../../store';
import { toggleTheme } from '../../store/slices/themeSlice'; import { toggleTheme } from '../../store/slices/themeSlice';
import { logout } from '../../store/slices/authSlice'; import { logout } from '../../store/slices/authSlice';
import { cn } from '../../utils/cn'; import { cn } from '../../utils/cn';
import toast from 'react-hot-toast';
const navigation = [ const navigation = [
{ name: 'Dashboard', href: '/', icon: Home }, { name: 'Dashboard', href: '/', icon: Home },
@ -58,6 +60,7 @@ const Sidebar: React.FC = () => {
const handleLogout = () => { const handleLogout = () => {
dispatch(logout()); dispatch(logout());
toast.success('Logged out successfully');
}; };
return ( return (
@ -72,9 +75,14 @@ const Sidebar: React.FC = () => {
<div className="w-8 h-8 bg-gradient-to-r from-primary-600 to-primary-400 rounded-lg flex items-center justify-center"> <div className="w-8 h-8 bg-gradient-to-r from-primary-600 to-primary-400 rounded-lg flex items-center justify-center">
<Building className="w-5 h-5 text-white" /> <Building className="w-5 h-5 text-white" />
</div> </div>
<span className="text-lg font-semibold text-secondary-900 dark:text-white"> <a
Channel Partners href="https://cloudtopiaa.com/"
</span> target="_blank"
rel="noopener noreferrer"
className="text-lg font-semibold text-secondary-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
>
Cloudtopiaa
</a>
</div> </div>
)} )}
<button <button
@ -149,29 +157,19 @@ const Sidebar: React.FC = () => {
{user && ( {user && (
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<img <div className="w-8 h-8 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
className="w-8 h-8 rounded-full object-cover bg-secondary-200 dark:bg-secondary-700"
src={user.avatar}
alt={user.name}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
target.nextElementSibling?.classList.remove('hidden');
}}
/>
<div className="w-8 h-8 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center hidden">
<span className="text-xs font-medium text-primary-600 dark:text-primary-400"> <span className="text-xs font-medium text-primary-600 dark:text-primary-400">
{user.name.split(' ').map(n => n[0]).join('')} {`${user.firstName[0]}${user.lastName[0]}`}
</span> </span>
</div> </div>
</div> </div>
{!isCollapsed && ( {!isCollapsed && (
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-secondary-900 dark:text-white truncate"> <p className="text-sm font-medium text-secondary-900 dark:text-white truncate">
{user.name} {`${user.firstName} ${user.lastName}`}
</p> </p>
<p className="text-xs text-secondary-500 dark:text-secondary-400 truncate"> <p className="text-xs text-secondary-500 dark:text-secondary-400 truncate">
{user.company} {user.email}
</p> </p>
</div> </div>
)} )}

View File

@ -0,0 +1,76 @@
import React, { useEffect } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAppSelector, useAppDispatch } from '../store/hooks';
import { getCurrentUser } from '../store/slices/authThunks';
interface ProtectedRouteProps {
children: React.ReactNode;
requiredRole?: string;
fallbackPath?: string;
}
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
children,
requiredRole,
fallbackPath = '/login'
}) => {
const { isAuthenticated, user, isLoading } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const location = useLocation();
useEffect(() => {
// Check if user is authenticated but no user data
if (isAuthenticated && !user) {
dispatch(getCurrentUser());
}
}, [isAuthenticated, user, dispatch]);
// Show loading while checking authentication
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
</div>
);
}
// Redirect to login if not authenticated
if (!isAuthenticated) {
return <Navigate to={fallbackPath} state={{ from: location }} replace />;
}
// Check role-based access if required
if (requiredRole && user) {
let hasRequiredRole = false;
// Check if user has roles property and it's an array
if (user.roles && Array.isArray(user.roles)) {
hasRequiredRole = user.roles.some(role => role.name === requiredRole);
} else if (user.role) {
// Fallback: check the simple role property
hasRequiredRole = user.role === requiredRole;
} else {
console.warn('User roles not properly loaded:', user);
return <Navigate to="/unauthorized" replace />;
}
if (!hasRequiredRole) {
return <Navigate to="/unauthorized" replace />;
}
}
// Check if email is verified
// TODO: Uncomment when email service is configured
// if (user && !user.emailVerified) {
// return <Navigate to="/verify-email" replace />;
// }
// Check if account is active
if (user && user.status !== 'active') {
return <Navigate to="/account-pending" replace />;
}
return <>{children}</>;
};
export default ProtectedRoute;

57
src/components/Toast.tsx Normal file
View File

@ -0,0 +1,57 @@
import { Toaster } from 'react-hot-toast';
const Toast: React.FC = () => {
return (
<Toaster
position="top-right"
toastOptions={{
duration: 4000,
style: {
background: '#363636',
color: '#fff',
borderRadius: '8px',
padding: '12px 16px',
fontSize: '14px',
fontWeight: '500',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
border: '1px solid transparent',
},
success: {
iconTheme: {
primary: '#10b981',
secondary: '#fff',
},
style: {
background: '#f0fdf4',
color: '#166534',
border: '1px solid #bbf7d0',
},
},
error: {
iconTheme: {
primary: '#ef4444',
secondary: '#fff',
},
style: {
background: '#fef2f2',
color: '#991b1b',
border: '1px solid #fecaca',
},
},
loading: {
iconTheme: {
primary: '#3b82f6',
secondary: '#fff',
},
style: {
background: '#eff6ff',
color: '#1e40af',
border: '1px solid #bfdbfe',
},
},
}}
/>
);
};
export default Toast;

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useAppSelector } from '../../../store/hooks';
import ResellerSidebar from './ResellerSidebar'; import ResellerSidebar from './ResellerSidebar';
interface ResellerLayoutProps { interface ResellerLayoutProps {
@ -6,6 +7,8 @@ interface ResellerLayoutProps {
} }
const ResellerLayout: React.FC<ResellerLayoutProps> = ({ children }) => { const ResellerLayout: React.FC<ResellerLayoutProps> = ({ children }) => {
const { user } = useAppSelector((state) => state.auth);
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-emerald-50 to-teal-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900"> <div className="min-h-screen bg-gradient-to-br from-slate-50 via-emerald-50 to-teal-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
<ResellerSidebar /> <ResellerSidebar />
@ -61,11 +64,17 @@ const ResellerLayout: React.FC<ResellerLayoutProps> = ({ children }) => {
{/* User Menu */} {/* User Menu */}
<div className="flex items-center space-x-4 flex-shrink-0 group cursor-pointer"> <div className="flex items-center space-x-4 flex-shrink-0 group cursor-pointer">
<div className="text-right flex flex-col justify-center"> <div className="text-right flex flex-col justify-center">
<p className="text-sm font-semibold text-slate-900 dark:text-white leading-tight">John Reseller</p> <p className="text-sm font-semibold text-slate-900 dark:text-white leading-tight">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 leading-tight">Tech Solutions Inc</p> {user ? `${user.firstName} ${user.lastName}` : 'User'}
</p>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 leading-tight">
{user?.email || 'user@example.com'}
</p>
</div> </div>
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 via-teal-500 to-cyan-500 rounded-xl flex items-center justify-center flex-shrink-0 shadow-lg group-hover:shadow-xl transition-all duration-200"> <div className="w-10 h-10 bg-gradient-to-br from-emerald-500 via-teal-500 to-cyan-500 rounded-xl flex items-center justify-center flex-shrink-0 shadow-lg group-hover:shadow-xl transition-all duration-200">
<span className="text-white text-sm font-bold">JR</span> <span className="text-white text-sm font-bold">
{user ? `${user.firstName[0]}${user.lastName[0]}` : 'U'}
</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,9 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation, useNavigate } from 'react-router-dom';
import { useAppSelector, useAppDispatch } from '../../../store/hooks'; import { useAppSelector, useAppDispatch } from '../../../store/hooks';
import { toggleTheme } from '../../../store/slices/themeSlice'; import { toggleTheme } from '../../../store/slices/themeSlice';
import { logout } from '../../../store/slices/authSlice';
import toast from 'react-hot-toast';
import { import {
Home, Home,
Users, Users,
@ -21,17 +23,35 @@ import {
ChevronRight, ChevronRight,
LogOut, LogOut,
User, User,
Bell Bell,
Zap
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '../../../utils/cn'; import { cn } from '../../../utils/cn';
const ResellerSidebar: React.FC = () => { const ResellerSidebar: React.FC = () => {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const location = useLocation(); const location = useLocation();
const navigate = useNavigate();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { theme } = useAppSelector((state) => state.theme); const { theme } = useAppSelector((state) => state.theme);
const { user } = useAppSelector((state) => state.auth); const { user } = useAppSelector((state) => state.auth);
const handleLogout = () => {
// Clear localStorage
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('sessionId');
// Clear Redux state
dispatch(logout());
// Show success message
toast.success('Logged out successfully');
// Navigate to login
navigate('/reseller/login');
};
const navigation = [ const navigation = [
{ name: 'Dashboard', href: '/reseller-dashboard', icon: Home }, { name: 'Dashboard', href: '/reseller-dashboard', icon: Home },
{ name: 'Customers', href: '/reseller-dashboard/customers', icon: Users }, { name: 'Customers', href: '/reseller-dashboard/customers', icon: Users },
@ -53,29 +73,38 @@ const ResellerSidebar: React.FC = () => {
return ( return (
<div className={cn( <div className={cn(
"fixed left-0 top-0 h-full bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 border-r border-slate-700/50 transition-all duration-300 z-50", "fixed left-0 top-0 h-full bg-gradient-to-b from-gray-900 via-gray-800 to-gray-900 border-r border-gray-700/50 transition-all duration-300 z-50 shadow-2xl",
collapsed ? "w-16" : "w-64" collapsed ? "w-16" : "w-64"
)}> )}>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-6 py-4 h-16 border-b border-slate-700/50"> <div className="flex items-center justify-between px-6 py-4 h-16 border-b border-gray-700/50 bg-gray-900/50">
{!collapsed ? ( {!collapsed ? (
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-lg flex items-center justify-center flex-shrink-0"> <div className="relative w-8 h-8 bg-gradient-to-r from-purple-600 to-indigo-600 rounded-xl flex items-center justify-center flex-shrink-0 shadow-lg">
<Cloud className="w-5 h-5 text-white" /> <Cloud className="w-4 h-4 text-white" />
<Zap className="w-2 h-2 text-yellow-300 absolute -top-0.5 -right-0.5" />
</div> </div>
<div className="flex flex-col justify-center"> <div className="flex flex-col justify-center">
<h1 className="text-lg font-bold text-white leading-tight">Reseller Portal</h1> <a
<p className="text-xs text-slate-400 leading-tight">Cloud Services</p> href="https://cloudtopiaa.com/"
target="_blank"
rel="noopener noreferrer"
className="text-lg font-bold text-white leading-tight hover:text-purple-300 transition-colors"
>
Cloudtopiaa
</a>
<p className="text-xs text-gray-400 leading-tight">Reseller Portal</p>
</div> </div>
</div> </div>
) : ( ) : (
<div className="w-8 h-8 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-lg flex items-center justify-center flex-shrink-0"> <div className="relative w-8 h-8 bg-gradient-to-r from-purple-600 to-indigo-600 rounded-xl flex items-center justify-center flex-shrink-0 shadow-lg">
<Cloud className="w-5 h-5 text-white" /> <Cloud className="w-4 h-4 text-white" />
<Zap className="w-2 h-2 text-yellow-300 absolute -top-0.5 -right-0.5" />
</div> </div>
)} )}
<button <button
onClick={() => setCollapsed(!collapsed)} onClick={() => setCollapsed(!collapsed)}
className="p-1 rounded-lg bg-slate-800/50 hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors duration-200 flex-shrink-0" className="p-1 rounded-lg bg-gray-800/50 hover:bg-gray-700/50 text-gray-400 hover:text-white transition-colors duration-200 flex-shrink-0"
> >
{collapsed ? <ChevronRight className="w-5 h-5" /> : <ChevronLeft className="w-5 h-5" />} {collapsed ? <ChevronRight className="w-5 h-5" /> : <ChevronLeft className="w-5 h-5" />}
</button> </button>
@ -83,16 +112,16 @@ const ResellerSidebar: React.FC = () => {
{/* User Profile */} {/* User Profile */}
{!collapsed && ( {!collapsed && (
<div className="p-4 border-b border-slate-700/50"> <div className="p-4 border-b border-gray-700/50 bg-gray-800/30">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-full flex items-center justify-center"> <div className="w-10 h-10 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full flex items-center justify-center shadow-lg">
<User className="w-5 h-5 text-white" /> <User className="w-5 h-5 text-white" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white truncate">{user?.name || 'John Reseller'}</p> <p className="text-sm font-medium text-white truncate">{user ? `${user.firstName} ${user.lastName}` : 'John Reseller'}</p>
<p className="text-xs text-slate-400 truncate">{user?.company || 'Tech Solutions Inc'}</p> <p className="text-xs text-gray-400 truncate">{user?.email || 'reseller@example.com'}</p>
</div> </div>
<button className="p-1 rounded-lg bg-slate-800/50 hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors duration-200"> <button className="p-1 rounded-lg bg-gray-800/50 hover:bg-gray-700/50 text-gray-400 hover:text-white transition-colors duration-200">
<Bell className="w-5 h-5" /> <Bell className="w-5 h-5" />
</button> </button>
</div> </div>
@ -100,44 +129,48 @@ const ResellerSidebar: React.FC = () => {
)} )}
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 p-4 space-y-3 overflow-y-auto"> <nav className="flex-1 p-4 space-y-2 overflow-y-auto">
{navigation.map((item) => ( {navigation.map((item) => (
<Link <Link
key={item.name} key={item.name}
to={item.href} to={item.href}
className={cn( className={cn(
"flex items-center rounded-xl text-sm font-medium transition-all duration-200 group", "flex items-center rounded-xl text-sm font-medium transition-all duration-200 group relative overflow-hidden",
collapsed ? "justify-center px-3 py-4" : "px-4 py-3", collapsed ? "justify-center px-3 py-4" : "px-4 py-3",
isActive(item.href) isActive(item.href)
? "bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 border border-emerald-500/30 shadow-lg" ? "bg-gradient-to-r from-purple-500/20 to-indigo-500/20 text-purple-300 border border-purple-500/30 shadow-lg"
: "text-slate-300 hover:bg-slate-800/50 hover:text-white" : "text-gray-300 hover:bg-gray-800/50 hover:text-white"
)} )}
> >
<div className={cn(
"absolute inset-0 bg-gradient-to-r from-purple-500/5 to-indigo-500/5 opacity-0 transition-opacity duration-200",
isActive(item.href) ? "opacity-100" : "group-hover:opacity-100"
)} />
<item.icon className={cn( <item.icon className={cn(
"w-6 h-6 transition-colors duration-200", "w-6 h-6 transition-colors duration-200 relative z-10",
isActive(item.href) ? "text-emerald-400" : "text-slate-400 group-hover:text-white" isActive(item.href) ? "text-purple-300" : "text-gray-400 group-hover:text-white"
)} /> )} />
{!collapsed && ( {!collapsed && (
<span className="ml-3">{item.name}</span> <span className="ml-3 relative z-10">{item.name}</span>
)} )}
</Link> </Link>
))} ))}
</nav> </nav>
{/* Footer */} {/* Footer */}
<div className="p-4 border-t border-slate-700/50 space-y-2"> <div className="p-4 border-t border-gray-700/50 bg-gray-900/50 space-y-2">
{/* Theme Toggle */} {/* Theme Toggle */}
<button <button
onClick={() => dispatch(toggleTheme())} onClick={() => dispatch(toggleTheme())}
className={cn( className={cn(
"w-full flex items-center rounded-xl text-sm font-medium text-slate-300 hover:bg-slate-800/50 hover:text-white transition-all duration-200", "w-full flex items-center rounded-xl text-sm font-medium text-gray-300 hover:bg-gray-800/50 hover:text-white transition-all duration-200",
collapsed ? "justify-center px-3 py-4" : "px-4 py-3" collapsed ? "justify-center px-3 py-4" : "px-4 py-3"
)} )}
> >
{theme === 'dark' ? ( {theme === 'dark' ? (
<Sun className="w-6 h-6 text-amber-400" /> <Sun className="w-6 h-6 text-amber-400" />
) : ( ) : (
<Moon className="w-6 h-6 text-slate-400" /> <Moon className="w-6 h-6 text-gray-400" />
)} )}
{!collapsed && ( {!collapsed && (
<span className="ml-3"> <span className="ml-3">
@ -147,10 +180,13 @@ const ResellerSidebar: React.FC = () => {
</button> </button>
{/* Logout */} {/* Logout */}
<button className={cn( <button
"w-full flex items-center rounded-xl text-sm font-medium text-red-400 hover:bg-red-500/10 hover:text-red-300 transition-all duration-200", onClick={handleLogout}
collapsed ? "justify-center px-3 py-4" : "px-4 py-3" className={cn(
)}> "w-full flex items-center rounded-xl text-sm font-medium text-red-400 hover:bg-red-500/10 hover:text-red-300 transition-all duration-200",
collapsed ? "justify-center px-3 py-4" : "px-4 py-3"
)}
>
<LogOut className="w-6 h-6" /> <LogOut className="w-6 h-6" />
{!collapsed && <span className="ml-3">Logout</span>} {!collapsed && <span className="ml-3">Logout</span>}
</button> </button>

View File

@ -2,17 +2,29 @@ import { DashboardStats, RecentActivity, QuickAction } from '../store/slices/das
import { User } from '../store/slices/authSlice'; import { User } from '../store/slices/authSlice';
export const mockUser: User = { export const mockUser: User = {
id: '1', id: 1,
email: 'yasha.khandelwal@channelpartners.com', email: 'yasha.khandelwal@channelpartners.com',
name: 'Yasha Khandelwal', firstName: 'Yasha',
role: 'channel_partner', lastName: 'Khandelwal',
role: 'vendor',
company: 'Tech4biz Solutions', company: 'Tech4biz Solutions',
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face', status: 'active',
tier: 'platinum', emailVerified: true,
isVerified: true,
twoFactorEnabled: true, twoFactorEnabled: true,
region: 'North India', lastLogin: new Date().toISOString(),
commissionRate: 15, roles: [
{
id: 2,
name: 'vendor',
description: 'Vendor who can manage products and resellers',
permissions: [
'user:read', 'user:update',
'product:create', 'product:read', 'product:update', 'product:delete',
'commission:create', 'commission:read', 'commission:update',
'report:read', 'analytics:read'
]
}
]
}; };
export const mockDashboardStats: DashboardStats = { export const mockDashboardStats: DashboardStats = {

View File

@ -3,6 +3,66 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Dark mode input text color fix */
.dark input[type="text"],
.dark input[type="email"],
.dark input[type="password"],
.dark input[type="tel"] {
color: #cbd5e1 !important; /* slate-300 */
}
.dark input[type="text"]::placeholder,
.dark input[type="email"]::placeholder,
.dark input[type="password"]::placeholder,
.dark input[type="tel"]::placeholder {
color: #94a3b8 !important; /* slate-400 */
}
/* Remove all white colors in dark mode */
.dark {
color-scheme: dark;
}
/* Override all input backgrounds and colors in dark mode */
.dark input[type="text"],
.dark input[type="email"],
.dark input[type="password"],
.dark input[type="tel"],
.dark input[type="number"],
.dark input[type="search"],
.dark input[type="url"] {
background-color: #374151 !important; /* gray-700 */
border-color: #4b5563 !important; /* gray-600 */
color: #cbd5e1 !important; /* slate-300 */
}
.dark input[type="text"]:focus,
.dark input[type="email"]:focus,
.dark input[type="password"]:focus,
.dark input[type="tel"]:focus,
.dark input[type="number"]:focus,
.dark input[type="search"]:focus,
.dark input[type="url"]:focus {
background-color: #374151 !important; /* gray-700 */
border-color: #3b82f6 !important; /* blue-500 */
color: #cbd5e1 !important; /* slate-300 */
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2) !important;
}
/* Override form backgrounds */
.dark .bg-white\/80 {
background-color: #1f2937 !important; /* gray-800 */
}
.dark .border-white\/20 {
border-color: #374151 !important; /* gray-700 */
}
/* Override theme toggle button */
.dark .bg-white\/80.dark\:bg-slate-800 {
background-color: #1f2937 !important; /* gray-800 */
}
@layer base { @layer base {
* { * {
@apply border-secondary-200 dark:border-secondary-700; @apply border-secondary-200 dark:border-secondary-700;

View File

@ -30,14 +30,10 @@ const Dashboard: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const { stats, recentActivities, quickActions } = useAppSelector((state) => state.dashboard); const { stats, recentActivities, quickActions } = useAppSelector((state) => state.dashboard);
const { user } = useAppSelector((state) => state.auth);
useEffect(() => { useEffect(() => {
// Initialize with mock data // Initialize dashboard data
dispatch(loginSuccess({
user: mockUser,
token: 'mock-token',
refreshToken: 'mock-refresh-token'
}));
dispatch(setStats(mockDashboardStats)); dispatch(setStats(mockDashboardStats));
dispatch(setRecentActivities(mockRecentActivities)); dispatch(setRecentActivities(mockRecentActivities));
dispatch(setQuickActions(mockQuickActions)); dispatch(setQuickActions(mockQuickActions));
@ -111,7 +107,7 @@ const Dashboard: React.FC = () => {
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-4 sm:space-y-0"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-4 sm:space-y-0">
<div> <div>
<h1 className="text-2xl sm:text-3xl font-bold text-secondary-900 dark:text-white"> <h1 className="text-2xl sm:text-3xl font-bold text-secondary-900 dark:text-white">
Welcome back, Yasha! Welcome back, {user ? `${user.firstName} ${user.lastName}` : 'User'}!
</h1> </h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1"> <p className="text-secondary-600 dark:text-secondary-400 mt-1">
Here's your channel partner performance overview. Here's your channel partner performance overview.

View File

@ -1,7 +1,8 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate, useLocation } from 'react-router-dom';
import { useAppDispatch } from '../store/hooks'; import { useAppDispatch, useAppSelector } from '../store/hooks';
import { loginSuccess } from '../store/slices/authSlice'; import { loginUser } from '../store/slices/authThunks';
import toast from 'react-hot-toast';
import { import {
Eye, Eye,
EyeOff, EyeOff,
@ -12,9 +13,10 @@ import {
Moon, Moon,
ArrowRight, ArrowRight,
CheckCircle, CheckCircle,
AlertCircle AlertCircle,
Cloud,
Zap
} from 'lucide-react'; } from 'lucide-react';
import { useAppSelector } from '../store/hooks';
import { RootState } from '../store'; import { RootState } from '../store';
import { toggleTheme } from '../store/slices/themeSlice'; import { toggleTheme } from '../store/slices/themeSlice';
import { cn } from '../utils/cn'; import { cn } from '../utils/cn';
@ -28,8 +30,21 @@ const Login: React.FC = () => {
const [rememberMe, setRememberMe] = useState(false); const [rememberMe, setRememberMe] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { theme } = useAppSelector((state: RootState) => state.theme); const { theme } = useAppSelector((state: RootState) => state.theme);
// Check for success message from signup
const successMessage = location.state?.message;
// Show success message toast if available
useEffect(() => {
if (location.state?.message) {
toast.success(location.state.message);
// Clear the message from state
navigate(location.pathname, { replace: true });
}
}, [location.state, navigate, location.pathname]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -37,31 +52,36 @@ const Login: React.FC = () => {
setIsLoading(true); setIsLoading(true);
try { try {
// Simulate API call const result = await dispatch(loginUser({ email, password })).unwrap();
await new Promise(resolve => setTimeout(resolve, 1500));
// Mock login success // Show success toast
dispatch(loginSuccess({ toast.success('Login successful! Welcome back.');
user: {
id: '1', // Determine redirect path based on user role
email: email, const user = result.user;
name: 'Yasha Khandelwal', let redirectPath = '/';
role: 'channel_partner',
company: 'Tech4biz Solutions', if (user) {
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face', // Check if user has roles property and it's an array
tier: 'platinum', if (user.roles && Array.isArray(user.roles)) {
isVerified: true, const hasResellerRole = user.roles.some(role => role.name === 'reseller');
twoFactorEnabled: true, if (hasResellerRole) {
region: 'North India', redirectPath = '/reseller-dashboard';
commissionRate: 15, }
}, } else if (user.role) {
token: 'mock-token', // Fallback: check the simple role property
refreshToken: 'mock-refresh-token' if (user.role === 'reseller') {
})); redirectPath = '/reseller-dashboard';
}
navigate('/'); }
} catch (err) { }
setError('Invalid email or password. Please try again.');
// Navigate to appropriate dashboard
navigate(redirectPath, { replace: true });
} catch (err: any) {
const errorMessage = err.message || 'Invalid email or password. Please try again.';
setError(errorMessage);
toast.error(errorMessage);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -76,7 +96,7 @@ const Login: React.FC = () => {
{/* Theme Toggle */} {/* Theme Toggle */}
<button <button
onClick={handleThemeToggle} onClick={handleThemeToggle}
className="fixed top-6 right-6 p-3 rounded-full bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm border border-slate-200 dark:border-slate-700 shadow-lg hover:shadow-xl transition-all duration-300 z-50" className="fixed top-6 right-6 p-3 rounded-full bg-white/80 dark:bg-slate-800 backdrop-blur-sm border border-slate-200 dark:border-slate-700 shadow-lg hover:shadow-xl transition-all duration-300 z-50"
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'} title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
> >
{theme === 'dark' ? ( {theme === 'dark' ? (
@ -89,11 +109,17 @@ const Login: React.FC = () => {
<div className="w-full max-w-md"> <div className="w-full max-w-md">
{/* Logo and Header */} {/* Logo and Header */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl shadow-lg mb-6"> <div className="relative inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-blue-600 via-indigo-600 to-purple-600 rounded-3xl shadow-2xl mb-6 overflow-hidden">
<Building className="w-8 h-8 text-white" /> <div className="absolute inset-0 bg-gradient-to-br from-white/20 to-transparent"></div>
<div className="relative flex items-center justify-center">
<div className="relative">
<Cloud className="w-10 h-10 text-white drop-shadow-lg" />
<Zap className="w-5 h-5 text-yellow-300 absolute -top-1 -right-1 drop-shadow-lg" />
</div>
</div>
</div> </div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2"> <h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Channel Partners Cloudtopiaa Connect
</h1> </h1>
<p className="text-slate-600 dark:text-slate-400"> <p className="text-slate-600 dark:text-slate-400">
Welcome back! Please sign in to your account. Welcome back! Please sign in to your account.
@ -101,7 +127,7 @@ const Login: React.FC = () => {
</div> </div>
{/* Login Form */} {/* Login Form */}
<div className="bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-slate-700/20 p-8"> <div className="bg-white/80 dark:bg-slate-800 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-slate-700 p-8">
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{/* Email Field */} {/* Email Field */}
<div> <div>
@ -117,7 +143,7 @@ const Login: React.FC = () => {
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter your email" placeholder="Enter your email"
required required
/> />
@ -138,7 +164,7 @@ const Login: React.FC = () => {
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="block w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" className="block w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter your password" placeholder="Enter your password"
required required
/> />
@ -177,6 +203,14 @@ const Login: React.FC = () => {
</Link> </Link>
</div> </div>
{/* Success Message */}
{successMessage && (
<div className="flex items-center p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<CheckCircle className="h-5 w-5 text-green-500 mr-2 flex-shrink-0" />
<span className="text-sm text-green-700 dark:text-green-400">{successMessage}</span>
</div>
)}
{/* Error Message */} {/* Error Message */}
{error && ( {error && (
<div className="flex items-center p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg"> <div className="flex items-center p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
@ -250,7 +284,7 @@ const Login: React.FC = () => {
{/* Footer */} {/* Footer */}
<div className="mt-8 text-center"> <div className="mt-8 text-center">
<p className="text-xs text-slate-500 dark:text-slate-400"> <p className="text-xs text-slate-500 dark:text-slate-400">
© 2024 Channel Partners. All rights reserved. © 2025 Cloudtopiaa. All rights reserved.
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,7 +1,9 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useAppDispatch } from '../store/hooks'; import { useAppDispatch } from '../store/hooks';
import { loginSuccess } from '../store/slices/authSlice'; import { registerUser } from '../store/slices/authThunks';
import toast from 'react-hot-toast';
import { validatePhoneNumber, validateEmail, validatePassword, validateName } from '../utils/validation';
import { import {
Eye, Eye,
EyeOff, EyeOff,
@ -13,7 +15,9 @@ import {
Sun, Sun,
Moon, Moon,
ArrowRight, ArrowRight,
AlertCircle AlertCircle,
Cloud,
Zap
} from 'lucide-react'; } from 'lucide-react';
import { useAppSelector } from '../store/hooks'; import { useAppSelector } from '../store/hooks';
import { RootState } from '../store'; import { RootState } from '../store';
@ -48,11 +52,37 @@ const Signup: React.FC = () => {
e.preventDefault(); e.preventDefault();
setError(''); setError('');
// Validation
if (!validateName(formData.firstName)) {
setError('First name must be between 2 and 50 characters');
return;
}
if (!validateName(formData.lastName)) {
setError('Last name must be between 2 and 50 characters');
return;
}
if (!validateEmail(formData.email)) {
setError('Please enter a valid email address');
return;
}
if (!validatePassword(formData.password)) {
setError('Password must be at least 8 characters with uppercase, lowercase, number, and special character');
return;
}
if (formData.password !== formData.confirmPassword) { if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match'); setError('Passwords do not match');
return; return;
} }
if (formData.phone && !validatePhoneNumber(formData.phone)) {
setError('Please enter a valid phone number (7-15 digits, optional + prefix)');
return;
}
if (!agreedToTerms) { if (!agreedToTerms) {
setError('Please agree to the terms and conditions'); setError('Please agree to the terms and conditions');
return; return;
@ -60,35 +90,30 @@ const Signup: React.FC = () => {
setIsLoading(true); setIsLoading(true);
try { try {
// Simulate API call await dispatch(registerUser({
await new Promise(resolve => setTimeout(resolve, 2000)); firstName: formData.firstName,
lastName: formData.lastName,
// Mock signup success
dispatch(loginSuccess({
user: {
id: '1',
email: formData.email, email: formData.email,
name: `${formData.firstName} ${formData.lastName}`, password: formData.password,
role: 'channel_partner', phone: formData.phone,
company: formData.company, company: formData.company,
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face', role: 'vendor'
tier: 'platinum', })).unwrap();
isVerified: true,
twoFactorEnabled: true,
region: 'Global',
commissionRate: 15,
},
token: 'mock-token',
refreshToken: 'mock-refresh-token'
}));
navigate('/'); // Navigate to login page with success message
} catch (err) { navigate('/login', {
setError('An error occurred during signup. Please try again.'); state: {
} finally { message: 'Registration successful! You can now login.'
setIsLoading(false); }
} });
} catch (err: any) {
const errorMessage = err.message || 'An error occurred during signup. Please try again.';
setError(errorMessage);
toast.error(errorMessage);
} finally {
setIsLoading(false);
}
}; };
const handleThemeToggle = () => { const handleThemeToggle = () => {
@ -100,7 +125,7 @@ const Signup: React.FC = () => {
{/* Theme Toggle */} {/* Theme Toggle */}
<button <button
onClick={handleThemeToggle} onClick={handleThemeToggle}
className="fixed top-6 right-6 p-3 rounded-full bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm border border-slate-200 dark:border-slate-700 shadow-lg hover:shadow-xl transition-all duration-300 z-50" className="fixed top-6 right-6 p-3 rounded-full bg-white/80 dark:bg-slate-800 backdrop-blur-sm border border-slate-200 dark:border-slate-700 shadow-lg hover:shadow-xl transition-all duration-300 z-50"
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'} title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
> >
{theme === 'dark' ? ( {theme === 'dark' ? (
@ -113,19 +138,25 @@ const Signup: React.FC = () => {
<div className="w-full max-w-2xl"> <div className="w-full max-w-2xl">
{/* Logo and Header */} {/* Logo and Header */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl shadow-lg mb-6"> <div className="relative inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-blue-600 via-indigo-600 to-purple-600 rounded-3xl shadow-2xl mb-6 overflow-hidden">
<Building className="w-8 h-8 text-white" /> <div className="absolute inset-0 bg-gradient-to-br from-white/20 to-transparent"></div>
<div className="relative flex items-center justify-center">
<div className="relative">
<Cloud className="w-10 h-10 text-white drop-shadow-lg" />
<Zap className="w-5 h-5 text-yellow-300 absolute -top-1 -right-1 drop-shadow-lg" />
</div>
</div>
</div> </div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2"> <h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Join Channel Partners Join Cloudtopiaa Connect
</h1> </h1>
<p className="text-slate-600 dark:text-slate-400"> <p className="text-slate-600 dark:text-slate-400">
Create your channel partner account and start managing resellers. Partner with us to offer next-gen cloud services.
</p> </p>
</div> </div>
{/* Signup Form */} {/* Signup Form */}
<div className="bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-slate-700/20 p-8"> <div className="bg-white/80 dark:bg-slate-800 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-slate-700 p-8">
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{/* Name Fields */} {/* Name Fields */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@ -142,7 +173,7 @@ const Signup: React.FC = () => {
type="text" type="text"
value={formData.firstName} value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)} onChange={(e) => handleInputChange('firstName', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter first name" placeholder="Enter first name"
required required
/> />
@ -162,7 +193,7 @@ const Signup: React.FC = () => {
type="text" type="text"
value={formData.lastName} value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)} onChange={(e) => handleInputChange('lastName', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter last name" placeholder="Enter last name"
required required
/> />
@ -185,7 +216,7 @@ const Signup: React.FC = () => {
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)} onChange={(e) => handleInputChange('email', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter email address" placeholder="Enter email address"
required required
/> />
@ -205,7 +236,7 @@ const Signup: React.FC = () => {
type="tel" type="tel"
value={formData.phone} value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)} onChange={(e) => handleInputChange('phone', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter phone number" placeholder="Enter phone number"
required required
/> />
@ -227,7 +258,7 @@ const Signup: React.FC = () => {
type="text" type="text"
value={formData.company} value={formData.company}
onChange={(e) => handleInputChange('company', e.target.value)} onChange={(e) => handleInputChange('company', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter company name" placeholder="Enter company name"
required required
/> />
@ -249,7 +280,7 @@ const Signup: React.FC = () => {
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={formData.password} value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)} onChange={(e) => handleInputChange('password', e.target.value)}
className="block w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" className="block w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Create password" placeholder="Create password"
required required
/> />
@ -280,7 +311,7 @@ const Signup: React.FC = () => {
type={showConfirmPassword ? 'text' : 'password'} type={showConfirmPassword ? 'text' : 'password'}
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={(e) => handleInputChange('confirmPassword', e.target.value)} onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
className="block w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" className="block w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Confirm password" placeholder="Confirm password"
required required
/> />
@ -381,7 +412,7 @@ const Signup: React.FC = () => {
{/* Footer */} {/* Footer */}
<div className="mt-8 text-center"> <div className="mt-8 text-center">
<p className="text-xs text-slate-500 dark:text-slate-400"> <p className="text-xs text-slate-500 dark:text-slate-400">
© 2024 Channel Partners. All rights reserved. © 2025 Cloudtopiaa. All rights reserved.
</p> </p>
</div> </div>
</div> </div>

View File

@ -0,0 +1,81 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Shield, ArrowLeft, Home } from 'lucide-react';
import { useAppSelector } from '../store/hooks';
const Unauthorized: React.FC = () => {
const { user } = useAppSelector((state) => state.auth);
// Determine the correct dashboard based on user role
const getDashboardPath = () => {
if (!user) return '/login';
let hasResellerRole = false;
let hasVendorRole = false;
// Check if user has roles property and it's an array
if (user.roles && Array.isArray(user.roles)) {
hasResellerRole = user.roles.some(role => role.name === 'reseller');
hasVendorRole = user.roles.some(role => role.name === 'vendor');
} else if (user.role) {
// Fallback: check the simple role property
hasResellerRole = user.role === 'reseller';
hasVendorRole = user.role === 'vendor';
} else {
console.warn('User roles not properly loaded:', user);
return '/login';
}
if (hasResellerRole) return '/reseller-dashboard';
if (hasVendorRole) return '/';
return '/login';
};
return (
<div className="min-h-screen bg-gradient-to-br from-red-50 via-orange-50 to-yellow-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900 flex items-center justify-center p-4">
<div className="w-full max-w-md text-center">
{/* Icon */}
<div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-r from-red-500 to-orange-500 rounded-2xl shadow-lg mb-6">
<Shield className="w-10 h-10 text-white" />
</div>
{/* Content */}
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-4">
Access Denied
</h1>
<p className="text-slate-600 dark:text-slate-400 mb-8 text-lg">
You don't have permission to access this area. Please contact your administrator if you believe this is an error.
</p>
{/* Actions */}
<div className="space-y-4">
<Link
to={getDashboardPath()}
className="inline-flex items-center justify-center w-full px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-medium rounded-xl hover:from-blue-700 hover:to-blue-800 transition-all duration-200 shadow-lg hover:shadow-xl"
>
<Home className="w-5 h-5 mr-2" />
Go to Dashboard
</Link>
<button
onClick={() => window.history.back()}
className="inline-flex items-center justify-center w-full px-6 py-3 bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-medium rounded-xl hover:bg-slate-200 dark:hover:bg-slate-700 transition-all duration-200"
>
<ArrowLeft className="w-5 h-5 mr-2" />
Go Back
</button>
</div>
{/* Footer */}
<div className="mt-8 text-center">
<p className="text-xs text-slate-500 dark:text-slate-400">
© 2025 Cloudtopiaa. All rights reserved.
</p>
</div>
</div>
</div>
);
};
export default Unauthorized;

251
src/pages/VerifyEmail.tsx Normal file
View File

@ -0,0 +1,251 @@
import React, { useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAppDispatch } from '../store/hooks';
import { verifyEmail, resendVerificationEmail } from '../store/slices/authThunks';
import {
Mail,
CheckCircle,
AlertCircle,
ArrowLeft,
RefreshCw
} from 'lucide-react';
import { useAppSelector } from '../store/hooks';
import { RootState } from '../store';
import { toggleTheme } from '../store/slices/themeSlice';
import { cn } from '../utils/cn';
import { Link } from 'react-router-dom';
const VerifyEmail: React.FC = () => {
const [otp, setOtp] = useState('');
const [email, setEmail] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isResending, setIsResending] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const dispatch = useAppDispatch();
const { theme } = useAppSelector((state: RootState) => state.theme);
// Get email from URL params or localStorage
React.useEffect(() => {
const emailFromParams = searchParams.get('email');
const emailFromStorage = localStorage.getItem('pendingEmail');
if (emailFromParams) {
setEmail(emailFromParams);
} else if (emailFromStorage) {
setEmail(emailFromStorage);
}
}, [searchParams]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setSuccess('');
if (!email) {
setError('Email is required');
return;
}
if (!otp || otp.length !== 6) {
setError('Please enter a valid 6-digit OTP');
return;
}
setIsLoading(true);
try {
await dispatch(verifyEmail({ email, otp })).unwrap();
setSuccess('Email verified successfully! You can now login.');
// Clear pending email from localStorage
localStorage.removeItem('pendingEmail');
// Redirect to login after 2 seconds
setTimeout(() => {
navigate('/login');
}, 2000);
} catch (err: any) {
setError(err.message || 'Verification failed. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleResendOTP = async () => {
if (!email) {
setError('Email is required');
return;
}
setIsResending(true);
setError('');
try {
await dispatch(resendVerificationEmail(email)).unwrap();
setSuccess('Verification email sent successfully!');
} catch (err: any) {
setError(err.message || 'Failed to resend verification email.');
} finally {
setIsResending(false);
}
};
const handleThemeToggle = () => {
dispatch(toggleTheme());
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900 flex items-center justify-center p-4">
{/* Theme Toggle */}
<button
onClick={handleThemeToggle}
className="fixed top-6 right-6 p-3 rounded-full bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm border border-slate-200 dark:border-slate-700 shadow-lg hover:shadow-xl transition-all duration-300 z-50"
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
>
{theme === 'dark' ? (
<div className="w-5 h-5 text-amber-500"></div>
) : (
<div className="w-5 h-5 text-slate-600">🌙</div>
)}
</button>
<div className="w-full max-w-md">
{/* Logo and Header */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl shadow-lg mb-6">
<Mail className="w-8 h-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Verify Your Email
</h1>
<p className="text-slate-600 dark:text-slate-400">
We've sent a verification code to your email address.
</p>
</div>
{/* Success Message */}
{success && (
<div className="mb-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg flex items-center space-x-3">
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0" />
<p className="text-green-800 dark:text-green-200 text-sm">{success}</p>
</div>
)}
{/* Error Message */}
{error && (
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex items-center space-x-3">
<AlertCircle className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0" />
<p className="text-red-800 dark:text-red-200 text-sm">{error}</p>
</div>
)}
{/* Verification Form */}
<div className="card p-8">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Email Input */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Email Address
</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-800 dark:text-white"
placeholder="Enter your email"
required
/>
</div>
{/* OTP Input */}
<div>
<label htmlFor="otp" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Verification Code
</label>
<input
type="text"
id="otp"
value={otp}
onChange={(e) => setOtp(e.target.value.replace(/\D/g, '').slice(0, 6))}
className="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-800 dark:text-white text-center text-2xl tracking-widest"
placeholder="000000"
maxLength={6}
required
/>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
Enter the 6-digit code sent to your email
</p>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isLoading || !otp || otp.length !== 6}
className={cn(
"w-full py-3 px-4 rounded-lg font-medium transition-all duration-200 flex items-center justify-center space-x-2",
isLoading || !otp || otp.length !== 6
? "bg-slate-300 dark:bg-slate-700 text-slate-500 dark:text-slate-400 cursor-not-allowed"
: "bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
)}
>
{isLoading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<span>Verifying...</span>
</>
) : (
<>
<CheckCircle className="w-4 h-4" />
<span>Verify Email</span>
</>
)}
</button>
{/* Resend OTP Button */}
<button
type="button"
onClick={handleResendOTP}
disabled={isResending || !email}
className={cn(
"w-full py-2 px-4 rounded-lg font-medium transition-all duration-200 flex items-center justify-center space-x-2",
isResending || !email
? "text-slate-400 dark:text-slate-500 cursor-not-allowed"
: "text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
)}
>
{isResending ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
<span>Resending...</span>
</>
) : (
<>
<RefreshCw className="w-4 h-4" />
<span>Resend Code</span>
</>
)}
</button>
</form>
{/* Back to Login */}
<div className="mt-6 text-center">
<Link
to="/login"
className="inline-flex items-center space-x-2 text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
<span>Back to Login</span>
</Link>
</div>
</div>
</div>
</div>
);
};
export default VerifyEmail;

View File

@ -2,8 +2,7 @@ import React, { useEffect } from 'react';
import { useAppSelector, useAppDispatch } from '../../store/hooks'; import { useAppSelector, useAppDispatch } from '../../store/hooks';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { setStats, setRecentActivities, setQuickActions } from '../../store/slices/dashboardSlice'; import { setStats, setRecentActivities, setQuickActions } from '../../store/slices/dashboardSlice';
import { loginSuccess } from '../../store/slices/authSlice'; import { mockDashboardStats, mockResellerQuickActions, mockResellerRecentActivities } from '../../data/mockData';
import { mockDashboardStats, mockRecentActivities, mockResellerQuickActions, mockResellerRecentActivities, mockUser } from '../../data/mockData';
import { formatNumber, formatRelativeTime, formatPercentage } from '../../utils/format'; import { formatNumber, formatRelativeTime, formatPercentage } from '../../utils/format';
import RevenueChart from '../../components/charts/RevenueChart'; import RevenueChart from '../../components/charts/RevenueChart';
import ResellerPerformanceChart from '../../components/charts/ResellerPerformanceChart'; import ResellerPerformanceChart from '../../components/charts/ResellerPerformanceChart';
@ -38,18 +37,10 @@ const ResellerDashboard: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const { stats, recentActivities, quickActions } = useAppSelector((state) => state.dashboard); const { stats, recentActivities, quickActions } = useAppSelector((state) => state.dashboard);
const { user } = useAppSelector((state) => state.auth);
useEffect(() => { useEffect(() => {
// Initialize with mock data // Initialize dashboard data
dispatch(loginSuccess({
user: {
...mockUser,
role: 'reseller_admin',
company: 'Tech Solutions Inc'
},
token: 'mock-token',
refreshToken: 'mock-refresh-token'
}));
dispatch(setStats(mockDashboardStats)); dispatch(setStats(mockDashboardStats));
dispatch(setRecentActivities(mockResellerRecentActivities)); dispatch(setRecentActivities(mockResellerRecentActivities));
dispatch(setQuickActions(mockResellerQuickActions)); dispatch(setQuickActions(mockResellerQuickActions));
@ -103,7 +94,7 @@ const ResellerDashboard: React.FC = () => {
<div className="relative z-10"> <div className="relative z-10">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold mb-2">Welcome back, John! </h1> <h1 className="text-3xl font-bold mb-2">Welcome back, {user ? `${user.firstName} ${user.lastName}` : 'User'}! </h1>
<p className="text-emerald-100 text-lg">Here's what's happening with your cloud services business today.</p> <p className="text-emerald-100 text-lg">Here's what's happening with your cloud services business today.</p>
</div> </div>
<div className="hidden lg:flex items-center space-x-4"> <div className="hidden lg:flex items-center space-x-4">

View File

@ -1,7 +1,8 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate, useLocation } from 'react-router-dom';
import { useAppDispatch } from '../../store/hooks'; import { useAppDispatch } from '../../store/hooks';
import { loginSuccess } from '../../store/slices/authSlice'; import { loginUser } from '../../store/slices/authThunks';
import toast from 'react-hot-toast';
import { import {
Eye, Eye,
EyeOff, EyeOff,
@ -27,40 +28,55 @@ const ResellerLogin: React.FC = () => {
const [rememberMe, setRememberMe] = useState(false); const [rememberMe, setRememberMe] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { theme } = useAppSelector((state: RootState) => state.theme); const { theme } = useAppSelector((state: RootState) => state.theme);
// Show success message from signup
useEffect(() => {
if (location.state?.message) {
toast.success(location.state.message);
// Clear the message from state
navigate(location.pathname, { replace: true });
}
}, [location.state, navigate, location.pathname]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError(''); setError('');
setIsLoading(true); setIsLoading(true);
try { try {
// Simulate API call const result = await dispatch(loginUser({ email, password })).unwrap();
await new Promise(resolve => setTimeout(resolve, 1500));
// Mock login success // Show success toast
dispatch(loginSuccess({ toast.success('Login successful! Welcome back.');
user: {
id: '1', // Determine redirect path based on user role
email: email, const user = result.user;
name: 'John Reseller', let redirectPath = '/reseller-dashboard'; // Default for reseller login
role: 'reseller_admin',
company: 'Tech Solutions Inc', if (user) {
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face', // Check if user has roles property and it's an array
tier: 'gold', if (user.roles && Array.isArray(user.roles)) {
isVerified: true, const hasVendorRole = user.roles.some(role => role.name === 'vendor');
twoFactorEnabled: false, if (hasVendorRole) {
region: 'North America', redirectPath = '/'; // Redirect vendors to vendor dashboard
commissionRate: 12, }
}, } else if (user.role) {
token: 'mock-token', // Fallback: check the simple role property
refreshToken: 'mock-refresh-token' if (user.role === 'vendor') {
})); redirectPath = '/'; // Redirect vendors to vendor dashboard
}
navigate('/reseller'); }
} catch (err) { }
setError('Invalid email or password. Please try again.');
// Navigate to appropriate dashboard
navigate(redirectPath, { replace: true });
} catch (err: any) {
const errorMessage = err.message || 'Invalid email or password. Please try again.';
setError(errorMessage);
toast.error(errorMessage);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -92,7 +108,7 @@ const ResellerLogin: React.FC = () => {
<Users className="w-8 h-8 text-white" /> <Users className="w-8 h-8 text-white" />
</div> </div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2"> <h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Reseller Portal Cloudtopiaa Connect
</h1> </h1>
<p className="text-slate-600 dark:text-slate-400"> <p className="text-slate-600 dark:text-slate-400">
Welcome back! Please sign in to your account. Welcome back! Please sign in to your account.

View File

@ -1,7 +1,9 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useAppDispatch } from '../../store/hooks'; import { useAppDispatch } from '../../store/hooks';
import { loginSuccess } from '../../store/slices/authSlice'; import { registerUser } from '../../store/slices/authThunks';
import { validatePhoneNumber, validateEmail, validatePassword, validateName } from '../../utils/validation';
import toast from 'react-hot-toast';
import { import {
Eye, Eye,
EyeOff, EyeOff,
@ -76,11 +78,37 @@ const ResellerSignup: React.FC = () => {
e.preventDefault(); e.preventDefault();
setError(''); setError('');
// Validation
if (!validateName(formData.firstName)) {
setError('First name must be between 2 and 50 characters');
return;
}
if (!validateName(formData.lastName)) {
setError('Last name must be between 2 and 50 characters');
return;
}
if (!validateEmail(formData.email)) {
setError('Please enter a valid email address');
return;
}
if (!validatePassword(formData.password)) {
setError('Password must be at least 8 characters with uppercase, lowercase, number, and special character');
return;
}
if (formData.password !== formData.confirmPassword) { if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match'); setError('Passwords do not match');
return; return;
} }
if (formData.phone && !validatePhoneNumber(formData.phone)) {
setError('Please enter a valid phone number (7-15 digits, optional + prefix)');
return;
}
if (!agreedToTerms) { if (!agreedToTerms) {
setError('Please agree to the terms and conditions'); setError('Please agree to the terms and conditions');
return; return;
@ -94,31 +122,26 @@ const ResellerSignup: React.FC = () => {
setIsLoading(true); setIsLoading(true);
try { try {
// Simulate API call await dispatch(registerUser({
await new Promise(resolve => setTimeout(resolve, 2000)); firstName: formData.firstName,
lastName: formData.lastName,
// Mock signup success email: formData.email,
dispatch(loginSuccess({ password: formData.password,
user: { phone: formData.phone,
id: '1', company: formData.company,
email: formData.email, role: 'reseller'
name: `${formData.firstName} ${formData.lastName}`, })).unwrap();
role: formData.userType,
company: formData.company,
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face',
tier: 'silver',
isVerified: false,
twoFactorEnabled: false,
region: formData.region,
commissionRate: 10,
},
token: 'mock-token',
refreshToken: 'mock-refresh-token'
}));
navigate('/reseller'); // Navigate to common login page with success message
} catch (err) { navigate('/login', {
setError('An error occurred during signup. Please try again.'); state: {
message: 'Registration successful! You can now login.'
}
});
} catch (err: any) {
const errorMessage = err.message || 'An error occurred during signup. Please try again.';
setError(errorMessage);
toast.error(errorMessage);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -150,7 +173,7 @@ const ResellerSignup: React.FC = () => {
<Users className="w-8 h-8 text-white" /> <Users className="w-8 h-8 text-white" />
</div> </div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2"> <h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Join Our Network Join Cloudtopiaa Connect
</h1> </h1>
<p className="text-slate-600 dark:text-slate-400"> <p className="text-slate-600 dark:text-slate-400">
Create your reseller account and start your journey with us. Create your reseller account and start your journey with us.

212
src/services/api.ts Normal file
View File

@ -0,0 +1,212 @@
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';
export interface LoginRequest {
email: string;
password: string;
twoFactorCode?: string;
}
export interface RegisterRequest {
email: string;
password: string;
firstName: string;
lastName: string;
phone?: string;
company?: string;
role?: 'vendor' | 'reseller' | 'admin';
}
export interface AuthResponse {
success: boolean;
message: string;
data?: {
user: {
id: number;
email: string;
firstName: string;
lastName: string;
phone?: string;
company?: string;
role: string;
status: string;
emailVerified: boolean;
twoFactorEnabled: boolean;
lastLogin?: string;
roles: Array<{
id: number;
name: string;
description: string;
permissions: string[];
}>;
};
accessToken: string;
refreshToken: string;
sessionId: string;
};
}
export interface User {
id: number;
email: string;
firstName: string;
lastName: string;
phone?: string;
company?: string;
role: string;
status: string;
emailVerified: boolean;
twoFactorEnabled: boolean;
lastLogin?: string;
roles: Array<{
id: number;
name: string;
description: string;
permissions: string[];
}>;
}
class ApiService {
private baseURL: string;
constructor() {
this.baseURL = API_BASE_URL;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseURL}${endpoint}`;
const config: RequestInit = {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
};
// Add auth token if available
const token = localStorage.getItem('accessToken');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
try {
const response = await fetch(url, config);
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'API request failed');
}
return data;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
// Authentication endpoints
async login(credentials: LoginRequest): Promise<AuthResponse> {
return this.request<AuthResponse>('/auth/login', {
method: 'POST',
body: JSON.stringify(credentials),
});
}
async register(userData: RegisterRequest): Promise<AuthResponse> {
return this.request<AuthResponse>('/auth/register', {
method: 'POST',
body: JSON.stringify(userData),
});
}
async verifyEmail(email: string, otp: string): Promise<AuthResponse> {
return this.request<AuthResponse>('/auth/verify-email', {
method: 'POST',
body: JSON.stringify({ email, otp }),
});
}
async resendVerificationEmail(email: string): Promise<AuthResponse> {
return this.request<AuthResponse>('/auth/resend-verification', {
method: 'POST',
body: JSON.stringify({ email }),
});
}
async refreshToken(refreshToken: string): Promise<AuthResponse> {
return this.request<AuthResponse>('/auth/refresh-token', {
method: 'POST',
body: JSON.stringify({ refreshToken }),
});
}
async getCurrentUser(): Promise<{ success: boolean; data: User }> {
return this.request<{ success: boolean; data: User }>('/auth/me');
}
async logout(sessionId: string): Promise<AuthResponse> {
return this.request<AuthResponse>('/auth/logout', {
method: 'POST',
body: JSON.stringify({ sessionId }),
});
}
async forgotPassword(email: string): Promise<AuthResponse> {
return this.request<AuthResponse>('/auth/forgot-password', {
method: 'POST',
body: JSON.stringify({ email }),
});
}
async resetPassword(token: string, password: string): Promise<AuthResponse> {
return this.request<AuthResponse>('/auth/reset-password', {
method: 'POST',
body: JSON.stringify({ token, password }),
});
}
// Two-factor authentication
async setupTwoFactor(): Promise<AuthResponse> {
return this.request<AuthResponse>('/auth/setup-2fa', {
method: 'POST',
});
}
async enableTwoFactor(code: string): Promise<AuthResponse> {
return this.request<AuthResponse>('/auth/enable-2fa', {
method: 'POST',
body: JSON.stringify({ code }),
});
}
async disableTwoFactor(code: string): Promise<AuthResponse> {
return this.request<AuthResponse>('/auth/disable-2fa', {
method: 'POST',
body: JSON.stringify({ code }),
});
}
// Profile management
async updateProfile(profileData: Partial<User>): Promise<{ success: boolean; data: User }> {
return this.request<{ success: boolean; data: User }>('/auth/profile', {
method: 'PUT',
body: JSON.stringify(profileData),
});
}
async changePassword(currentPassword: string, newPassword: string): Promise<{ success: boolean; message: string }> {
return this.request<{ success: boolean; message: string }>('/auth/change-password', {
method: 'POST',
body: JSON.stringify({ currentPassword, newPassword }),
});
}
}
export const apiService = new ApiService();
export default apiService;

View File

@ -1,18 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { User } from '../../services/api';
export interface User {
id: string;
email: string;
name: string;
role: 'channel_partner' | 'sales_manager' | 'account_manager' | 'read_only' | 'reseller_admin' | 'sales_agent' | 'support_agent';
company: string;
avatar?: string;
tier: 'silver' | 'gold' | 'platinum';
isVerified: boolean;
twoFactorEnabled: boolean;
region: string;
commissionRate: number;
}
interface AuthState { interface AuthState {
user: User | null; user: User | null;
@ -21,6 +8,7 @@ interface AuthState {
error: string | null; error: string | null;
token: string | null; token: string | null;
refreshToken: string | null; refreshToken: string | null;
sessionId: string | null;
} }
const initialState: AuthState = { const initialState: AuthState = {
@ -30,6 +18,7 @@ const initialState: AuthState = {
error: null, error: null,
token: null, token: null,
refreshToken: null, refreshToken: null,
sessionId: null,
}; };
const authSlice = createSlice({ const authSlice = createSlice({
@ -42,10 +31,11 @@ const authSlice = createSlice({
setError: (state, action: PayloadAction<string | null>) => { setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload; state.error = action.payload;
}, },
loginSuccess: (state, action: PayloadAction<{ user: User; token: string; refreshToken: string }>) => { loginSuccess: (state, action: PayloadAction<{ user: User; token: string; refreshToken: string; sessionId: string }>) => {
state.user = action.payload.user; state.user = action.payload.user;
state.token = action.payload.token; state.token = action.payload.token;
state.refreshToken = action.payload.refreshToken; state.refreshToken = action.payload.refreshToken;
state.sessionId = action.payload.sessionId;
state.isAuthenticated = true; state.isAuthenticated = true;
state.error = null; state.error = null;
}, },
@ -53,6 +43,7 @@ const authSlice = createSlice({
state.user = null; state.user = null;
state.token = null; state.token = null;
state.refreshToken = null; state.refreshToken = null;
state.sessionId = null;
state.isAuthenticated = false; state.isAuthenticated = false;
state.error = null; state.error = null;
}, },
@ -61,12 +52,14 @@ const authSlice = createSlice({
state.user = { ...state.user, ...action.payload }; state.user = { ...state.user, ...action.payload };
} }
}, },
setTokens: (state, action: PayloadAction<{ token: string; refreshToken: string }>) => { setTokens: (state, action: PayloadAction<{ token: string; refreshToken: string; sessionId: string }>) => {
state.token = action.payload.token; state.token = action.payload.token;
state.refreshToken = action.payload.refreshToken; state.refreshToken = action.payload.refreshToken;
state.sessionId = action.payload.sessionId;
}, },
}, },
}); });
export const { setLoading, setError, loginSuccess, logout, updateUser, setTokens } = authSlice.actions; export const { setLoading, setError, loginSuccess, logout, updateUser, setTokens } = authSlice.actions;
export type { User };
export default authSlice.reducer; export default authSlice.reducer;

View File

@ -0,0 +1,204 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { apiService, LoginRequest, RegisterRequest } from '../../services/api';
import { setLoading, setError, loginSuccess, logout, setTokens } from './authSlice';
export const loginUser = createAsyncThunk(
'auth/login',
async (credentials: LoginRequest, { dispatch }) => {
try {
dispatch(setLoading(true));
dispatch(setError(null));
const response = await apiService.login(credentials);
if (response.success && response.data) {
// Store tokens in localStorage
localStorage.setItem('accessToken', response.data.accessToken);
localStorage.setItem('refreshToken', response.data.refreshToken);
localStorage.setItem('sessionId', response.data.sessionId);
// Dispatch login success
dispatch(loginSuccess({
user: response.data.user,
token: response.data.accessToken,
refreshToken: response.data.refreshToken,
sessionId: response.data.sessionId,
}));
return response.data;
} else {
throw new Error(response.message || 'Login failed');
}
} catch (error: any) {
const errorMessage = error.message || 'Login failed. Please try again.';
dispatch(setError(errorMessage));
throw error;
} finally {
dispatch(setLoading(false));
}
}
);
export const registerUser = createAsyncThunk(
'auth/register',
async (userData: RegisterRequest, { dispatch }) => {
try {
dispatch(setLoading(true));
dispatch(setError(null));
const response = await apiService.register(userData);
if (response.success) {
return response;
} else {
throw new Error(response.message || 'Registration failed');
}
} catch (error: any) {
const errorMessage = error.message || 'Registration failed. Please try again.';
dispatch(setError(errorMessage));
throw error;
} finally {
dispatch(setLoading(false));
}
}
);
export const verifyEmail = createAsyncThunk(
'auth/verifyEmail',
async ({ email, otp }: { email: string; otp: string }, { dispatch }) => {
try {
dispatch(setLoading(true));
dispatch(setError(null));
const response = await apiService.verifyEmail(email, otp);
if (response.success) {
return response;
} else {
throw new Error(response.message || 'Email verification failed');
}
} catch (error: any) {
const errorMessage = error.message || 'Email verification failed. Please try again.';
dispatch(setError(errorMessage));
throw error;
} finally {
dispatch(setLoading(false));
}
}
);
export const resendVerificationEmail = createAsyncThunk(
'auth/resendVerification',
async (email: string, { dispatch }) => {
try {
dispatch(setLoading(true));
dispatch(setError(null));
const response = await apiService.resendVerificationEmail(email);
if (response.success) {
return response;
} else {
throw new Error(response.message || 'Failed to resend verification email');
}
} catch (error: any) {
const errorMessage = error.message || 'Failed to resend verification email. Please try again.';
dispatch(setError(errorMessage));
throw error;
} finally {
dispatch(setLoading(false));
}
}
);
export const logoutUser = createAsyncThunk(
'auth/logout',
async (_, { dispatch, getState }) => {
try {
const state = getState() as any;
const sessionId = state.auth.sessionId;
if (sessionId) {
await apiService.logout(sessionId);
}
// Clear localStorage
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('sessionId');
// Dispatch logout
dispatch(logout());
} catch (error) {
console.error('Logout error:', error);
// Still logout locally even if API call fails
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('sessionId');
dispatch(logout());
}
}
);
export const getCurrentUser = createAsyncThunk(
'auth/getCurrentUser',
async (_, { dispatch }) => {
try {
dispatch(setLoading(true));
dispatch(setError(null));
const response = await apiService.getCurrentUser();
if (response.success) {
return response.data;
} else {
throw new Error('Failed to get current user');
}
} catch (error: any) {
const errorMessage = error.message || 'Failed to get current user';
dispatch(setError(errorMessage));
throw error;
} finally {
dispatch(setLoading(false));
}
}
);
export const refreshUserToken = createAsyncThunk(
'auth/refreshToken',
async (_, { dispatch, getState }) => {
try {
const state = getState() as any;
const refreshToken = state.auth.refreshToken;
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await apiService.refreshToken(refreshToken);
if (response.success && response.data) {
// Update tokens in localStorage
localStorage.setItem('accessToken', response.data.accessToken);
localStorage.setItem('refreshToken', response.data.refreshToken);
localStorage.setItem('sessionId', response.data.sessionId);
// Update tokens in store
dispatch(setTokens({
token: response.data.accessToken,
refreshToken: response.data.refreshToken,
sessionId: response.data.sessionId,
}));
return response.data;
} else {
throw new Error(response.message || 'Token refresh failed');
}
} catch (error: any) {
console.error('Token refresh failed:', error);
// If refresh fails, logout the user
dispatch(logoutUser());
throw error;
}
}
);

29
src/utils/validation.ts Normal file
View File

@ -0,0 +1,29 @@
// Phone number validation
export const validatePhoneNumber = (phone: string): boolean => {
// Allow international format with optional +, digits only, 7-15 digits total
const phoneRegex = /^[\+]?[1-9][\d]{6,14}$/;
return phoneRegex.test(phone);
};
// Email validation
export const validateEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
// Password validation
export const validatePassword = (password: string): boolean => {
// At least 8 characters, 1 uppercase, 1 lowercase, 1 number, 1 special character
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
return passwordRegex.test(password);
};
// Name validation
export const validateName = (name: string): boolean => {
return name.length >= 2 && name.length <= 50;
};
// Company validation
export const validateCompany = (company: string): boolean => {
return company.length <= 255;
};