auth, integrated
This commit is contained in:
parent
9d579385d7
commit
aaef6e883b
1
env.example
Normal file
1
env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
REACT_APP_API_URL=http://localhost:5000/api
|
||||||
27
package-lock.json
generated
27
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
80
src/App.tsx
80
src/App.tsx
@ -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>
|
||||||
|
<AuthInitializer>
|
||||||
<Router>
|
<Router>
|
||||||
<div className="App">
|
<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={
|
||||||
|
<ProtectedRoute requiredRole="vendor">
|
||||||
<Layout>
|
<Layout>
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/resellers" element={
|
<Route path="/resellers" element={
|
||||||
|
<ProtectedRoute requiredRole="vendor">
|
||||||
<Layout>
|
<Layout>
|
||||||
<ResellersPage />
|
<ResellersPage />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/partnerships" element={
|
<Route path="/partnerships" element={
|
||||||
|
<ProtectedRoute requiredRole="vendor">
|
||||||
<Layout>
|
<Layout>
|
||||||
<PartnershipsPage />
|
<PartnershipsPage />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/deals" element={
|
<Route path="/deals" element={
|
||||||
|
<ProtectedRoute requiredRole="vendor">
|
||||||
<Layout>
|
<Layout>
|
||||||
<DealsPage />
|
<DealsPage />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/commissions" element={
|
<Route path="/commissions" element={
|
||||||
|
<ProtectedRoute requiredRole="vendor">
|
||||||
<Layout>
|
<Layout>
|
||||||
<CommissionsPage />
|
<CommissionsPage />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/product-management" element={
|
<Route path="/product-management" element={
|
||||||
|
<ProtectedRoute requiredRole="vendor">
|
||||||
<Layout>
|
<Layout>
|
||||||
<ProductManagement />
|
<ProductManagement />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/training" element={
|
<Route path="/training" element={
|
||||||
|
<ProtectedRoute requiredRole="vendor">
|
||||||
<Layout>
|
<Layout>
|
||||||
<Training />
|
<Training />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/support" element={
|
<Route path="/support" element={
|
||||||
|
<ProtectedRoute requiredRole="vendor">
|
||||||
<Layout>
|
<Layout>
|
||||||
<Support />
|
<Support />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/analytics" element={
|
<Route path="/analytics" element={
|
||||||
|
<ProtectedRoute requiredRole="vendor">
|
||||||
<Layout>
|
<Layout>
|
||||||
<Analytics />
|
<Analytics />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/reports" element={
|
<Route path="/reports" element={
|
||||||
|
<ProtectedRoute requiredRole="vendor">
|
||||||
<Layout>
|
<Layout>
|
||||||
<Reports />
|
<Reports />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/settings" element={
|
<Route path="/settings" element={
|
||||||
|
<ProtectedRoute requiredRole="vendor">
|
||||||
<Layout>
|
<Layout>
|
||||||
<Settings />
|
<Settings />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/targets" element={
|
<Route path="/targets" element={
|
||||||
|
<ProtectedRoute requiredRole="vendor">
|
||||||
<Layout>
|
<Layout>
|
||||||
<PlaceholderPage title="Targets" description="Set and track performance targets" />
|
<PlaceholderPage title="Targets" description="Set and track performance targets" />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/performance" element={
|
<Route path="/performance" element={
|
||||||
|
<ProtectedRoute requiredRole="vendor">
|
||||||
<Layout>
|
<Layout>
|
||||||
<PlaceholderPage title="Performance" description="Performance metrics and KPIs" />
|
<PlaceholderPage title="Performance" description="Performance metrics and KPIs" />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/marketplace" element={
|
<Route path="/marketplace" element={
|
||||||
|
<ProtectedRoute>
|
||||||
<Layout>
|
<Layout>
|
||||||
<PlaceholderPage title="Marketplace" description="Browse and manage marketplace offerings" />
|
<PlaceholderPage title="Marketplace" description="Browse and manage marketplace offerings" />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/certifications" element={
|
<Route path="/certifications" element={
|
||||||
|
<ProtectedRoute>
|
||||||
<Layout>
|
<Layout>
|
||||||
<PlaceholderPage title="Certifications" description="Manage certifications and badges" />
|
<PlaceholderPage title="Certifications" description="Manage certifications and badges" />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/knowledge-base" element={
|
<Route path="/knowledge-base" element={
|
||||||
|
<ProtectedRoute>
|
||||||
<Layout>
|
<Layout>
|
||||||
<PlaceholderPage title="Knowledge Base" description="Access documentation and resources" />
|
<PlaceholderPage title="Knowledge Base" description="Access documentation and resources" />
|
||||||
</Layout>
|
</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={
|
||||||
|
<ProtectedRoute requiredRole="reseller">
|
||||||
<ResellerLayout>
|
<ResellerLayout>
|
||||||
<ResellerDashboardMain />
|
<ResellerDashboardMain />
|
||||||
</ResellerLayout>
|
</ResellerLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/reseller-dashboard/customers" element={
|
<Route path="/reseller-dashboard/customers" element={
|
||||||
|
<ProtectedRoute requiredRole="reseller">
|
||||||
<ResellerLayout>
|
<ResellerLayout>
|
||||||
<ResellerDashboardCustomers />
|
<ResellerDashboardCustomers />
|
||||||
</ResellerLayout>
|
</ResellerLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/reseller-dashboard/instances" element={
|
<Route path="/reseller-dashboard/instances" element={
|
||||||
|
<ProtectedRoute requiredRole="reseller">
|
||||||
<ResellerLayout>
|
<ResellerLayout>
|
||||||
<ResellerDashboardInstances />
|
<ResellerDashboardInstances />
|
||||||
</ResellerLayout>
|
</ResellerLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/reseller-dashboard/billing" element={
|
<Route path="/reseller-dashboard/billing" element={
|
||||||
|
<ProtectedRoute requiredRole="reseller">
|
||||||
<ResellerLayout>
|
<ResellerLayout>
|
||||||
<PlaceholderPage title="Billing & Payments" description="Manage your billing, invoices, and payment methods" />
|
<PlaceholderPage title="Billing & Payments" description="Manage your billing, invoices, and payment methods" />
|
||||||
</ResellerLayout>
|
</ResellerLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/reseller-dashboard/support" element={
|
<Route path="/reseller-dashboard/support" element={
|
||||||
|
<ProtectedRoute requiredRole="reseller">
|
||||||
<ResellerLayout>
|
<ResellerLayout>
|
||||||
<Support />
|
<Support />
|
||||||
</ResellerLayout>
|
</ResellerLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/reseller-dashboard/reports" element={
|
<Route path="/reseller-dashboard/reports" element={
|
||||||
|
<ProtectedRoute requiredRole="reseller">
|
||||||
<ResellerLayout>
|
<ResellerLayout>
|
||||||
<PlaceholderPage title="Reports & Analytics" description="View detailed reports and analytics" />
|
<PlaceholderPage title="Reports & Analytics" description="View detailed reports and analytics" />
|
||||||
</ResellerLayout>
|
</ResellerLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/reseller-dashboard/wallet" element={
|
<Route path="/reseller-dashboard/wallet" element={
|
||||||
|
<ProtectedRoute requiredRole="reseller">
|
||||||
<ResellerLayout>
|
<ResellerLayout>
|
||||||
<PlaceholderPage title="Wallet Management" description="Manage your funds, transactions, and payment methods" />
|
<PlaceholderPage title="Wallet Management" description="Manage your funds, transactions, and payment methods" />
|
||||||
</ResellerLayout>
|
</ResellerLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/reseller-dashboard/training" element={
|
<Route path="/reseller-dashboard/training" element={
|
||||||
|
<ProtectedRoute requiredRole="reseller">
|
||||||
<ResellerLayout>
|
<ResellerLayout>
|
||||||
<ResellerTraining />
|
<ResellerTraining />
|
||||||
</ResellerLayout>
|
</ResellerLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/reseller-dashboard/marketplace" element={
|
<Route path="/reseller-dashboard/marketplace" element={
|
||||||
|
<ProtectedRoute requiredRole="reseller">
|
||||||
<ResellerLayout>
|
<ResellerLayout>
|
||||||
<PlaceholderPage title="Marketplace" description="Browse and purchase cloud services and solutions" />
|
<PlaceholderPage title="Marketplace" description="Browse and purchase cloud services and solutions" />
|
||||||
</ResellerLayout>
|
</ResellerLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/reseller-dashboard/certifications" element={
|
<Route path="/reseller-dashboard/certifications" element={
|
||||||
|
<ProtectedRoute requiredRole="reseller">
|
||||||
<ResellerLayout>
|
<ResellerLayout>
|
||||||
<PlaceholderPage title="Certifications" description="Manage your professional certifications and achievements" />
|
<PlaceholderPage title="Certifications" description="Manage your professional certifications and achievements" />
|
||||||
</ResellerLayout>
|
</ResellerLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/reseller-dashboard/knowledge-base" element={
|
<Route path="/reseller-dashboard/knowledge-base" element={
|
||||||
|
<ProtectedRoute requiredRole="reseller">
|
||||||
<ResellerLayout>
|
<ResellerLayout>
|
||||||
<PlaceholderPage title="Knowledge Base" description="Access documentation, guides, and helpful resources" />
|
<PlaceholderPage title="Knowledge Base" description="Access documentation, guides, and helpful resources" />
|
||||||
</ResellerLayout>
|
</ResellerLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/reseller-dashboard/settings" element={
|
<Route path="/reseller-dashboard/settings" element={
|
||||||
|
<ProtectedRoute requiredRole="reseller">
|
||||||
<ResellerLayout>
|
<ResellerLayout>
|
||||||
<PlaceholderPage title="Settings" description="Configure your account preferences and system settings" />
|
<PlaceholderPage title="Settings" description="Configure your account preferences and system settings" />
|
||||||
</ResellerLayout>
|
</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 />
|
||||||
|
<Toast />
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
</AuthInitializer>
|
||||||
</CookiesProvider>
|
</CookiesProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
56
src/components/AuthInitializer.tsx
Normal file
56
src/components/AuthInitializer.tsx
Normal 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;
|
||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
76
src/components/ProtectedRoute.tsx
Normal file
76
src/components/ProtectedRoute.tsx
Normal 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
57
src/components/Toast.tsx
Normal 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;
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
onClick={handleLogout}
|
||||||
|
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",
|
"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"
|
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>
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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,40 +30,58 @@ 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();
|
||||||
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',
|
|
||||||
email: email,
|
|
||||||
name: 'Yasha Khandelwal',
|
|
||||||
role: 'channel_partner',
|
|
||||||
company: 'Tech4biz Solutions',
|
|
||||||
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face',
|
|
||||||
tier: 'platinum',
|
|
||||||
isVerified: true,
|
|
||||||
twoFactorEnabled: true,
|
|
||||||
region: 'North India',
|
|
||||||
commissionRate: 15,
|
|
||||||
},
|
|
||||||
token: 'mock-token',
|
|
||||||
refreshToken: 'mock-refresh-token'
|
|
||||||
}));
|
|
||||||
|
|
||||||
navigate('/');
|
// Determine redirect path based on user role
|
||||||
} catch (err) {
|
const user = result.user;
|
||||||
setError('Invalid email or password. Please try again.');
|
let redirectPath = '/';
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
// Check if user has roles property and it's an array
|
||||||
|
if (user.roles && Array.isArray(user.roles)) {
|
||||||
|
const hasResellerRole = user.roles.some(role => role.name === 'reseller');
|
||||||
|
if (hasResellerRole) {
|
||||||
|
redirectPath = '/reseller-dashboard';
|
||||||
|
}
|
||||||
|
} else if (user.role) {
|
||||||
|
// Fallback: check the simple role property
|
||||||
|
if (user.role === 'reseller') {
|
||||||
|
redirectPath = '/reseller-dashboard';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
|||||||
@ -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;
|
||||||
@ -61,31 +91,26 @@ 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: {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
81
src/pages/Unauthorized.tsx
Normal file
81
src/pages/Unauthorized.tsx
Normal 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
251
src/pages/VerifyEmail.tsx
Normal 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;
|
||||||
@ -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">
|
||||||
|
|||||||
@ -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',
|
|
||||||
email: email,
|
|
||||||
name: 'John Reseller',
|
|
||||||
role: 'reseller_admin',
|
|
||||||
company: 'Tech Solutions Inc',
|
|
||||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face',
|
|
||||||
tier: 'gold',
|
|
||||||
isVerified: true,
|
|
||||||
twoFactorEnabled: false,
|
|
||||||
region: 'North America',
|
|
||||||
commissionRate: 12,
|
|
||||||
},
|
|
||||||
token: 'mock-token',
|
|
||||||
refreshToken: 'mock-refresh-token'
|
|
||||||
}));
|
|
||||||
|
|
||||||
navigate('/reseller');
|
// Determine redirect path based on user role
|
||||||
} catch (err) {
|
const user = result.user;
|
||||||
setError('Invalid email or password. Please try again.');
|
let redirectPath = '/reseller-dashboard'; // Default for reseller login
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
// Check if user has roles property and it's an array
|
||||||
|
if (user.roles && Array.isArray(user.roles)) {
|
||||||
|
const hasVendorRole = user.roles.some(role => role.name === 'vendor');
|
||||||
|
if (hasVendorRole) {
|
||||||
|
redirectPath = '/'; // Redirect vendors to vendor dashboard
|
||||||
|
}
|
||||||
|
} else if (user.role) {
|
||||||
|
// Fallback: check the simple role property
|
||||||
|
if (user.role === 'vendor') {
|
||||||
|
redirectPath = '/'; // Redirect vendors to vendor dashboard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|||||||
@ -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
|
|
||||||
dispatch(loginSuccess({
|
|
||||||
user: {
|
|
||||||
id: '1',
|
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
name: `${formData.firstName} ${formData.lastName}`,
|
password: formData.password,
|
||||||
role: formData.userType,
|
phone: formData.phone,
|
||||||
company: formData.company,
|
company: formData.company,
|
||||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face',
|
role: 'reseller'
|
||||||
tier: 'silver',
|
})).unwrap();
|
||||||
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
212
src/services/api.ts
Normal 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;
|
||||||
@ -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;
|
||||||
204
src/store/slices/authThunks.ts
Normal file
204
src/store/slices/authThunks.ts
Normal 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
29
src/utils/validation.ts
Normal 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;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user