vulnnearable comments removed and source exposing to frobrowser disabled worknote XSS fixed
This commit is contained in:
parent
2fa52b90e3
commit
a16346effd
27
.env.local.backup
Normal file
27
.env.local.backup
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#Local
|
||||||
|
VITE_PUBLIC_VAPID_KEY=BBb78N3tSTEw6mPbBmvEDX2bhYEDKPc_zffL-vxPV8FBSmR1qSpy9gdV8zt-WFF-q2NPpVmL4BhbUzLSHVAPjcI
|
||||||
|
VITE_BASE_URL=http://localhost:3000
|
||||||
|
VITE_API_BASE_URL=http://localhost:3000/api/v1
|
||||||
|
VITE_OKTA_CLIENT_ID=0oa18b98aari6I6eo2p8
|
||||||
|
VITE_OKTA_DOMAIN=https://royalenfield.okta.com
|
||||||
|
|
||||||
|
#Development
|
||||||
|
# VITE_PUBLIC_VAPID_KEY=BBb78N3tSTEw6mPbBmvEDX2bhYEDKPc_zffL-vxPV8FBSmR1qSpy9gdV8zt-WFF-q2NPpVmL4BhbUzLSHVAPjcI
|
||||||
|
# VITE_BASE_URL=https://re-workflow-nt-dev.siplsolutions.com
|
||||||
|
# VITE_API_BASE_URL=https://re-workflow-nt-dev.siplsolutions.com/api/v1
|
||||||
|
# VITE_OKTA_CLIENT_ID=0oa18b98aari6I6eo2p8
|
||||||
|
# VITE_OKTA_DOMAIN=https://royalenfield.okta.com
|
||||||
|
|
||||||
|
#Uat
|
||||||
|
# VITE_PUBLIC_VAPID_KEY=BBb78N3tSTEw6mPbBmvEDX2bhYEDKPc_zffL-vxPV8FBSmR1qSpy9gdV8zt-WFF-q2NPpVmL4BhbUzLSHVAPjcI
|
||||||
|
# VITE_BASE_URL=https://reflow-uat.royalenfield.com
|
||||||
|
# VITE_API_BASE_URL=https://reflow-uat.royalenfield.com/api/v1/
|
||||||
|
# VITE_OKTA_CLIENT_ID=0oa2jgzvrpdwx2iqd0h8
|
||||||
|
# VITE_OKTA_DOMAIN=https://dev-830839.oktapreview.com
|
||||||
|
|
||||||
|
#Production
|
||||||
|
# VITE_PUBLIC_VAPID_KEY=BBb78N3tSTEw6mPbBmvEDX2bhYEDKPc_zffL-vxPV8FBSmR1qSpy9gdV8zt-WFF-q2NPpVmL4BhbUzLSHVAPjcI
|
||||||
|
# VITE_BASE_URL=https://reflow.royalenfield.com
|
||||||
|
# VITE_API_BASE_URL=https://reflow.royalenfield.com/api/v1
|
||||||
|
# VITE_OKTA_CLIENT_ID=0oa18b98aari6I6eo2p8
|
||||||
|
# VITE_OKTA_DOMAIN=https://royalenfield.okta.com
|
||||||
@ -10,7 +10,7 @@
|
|||||||
<meta name="theme-color" content="#2d4a3e" />
|
<meta name="theme-color" content="#2d4a3e" />
|
||||||
<title>Royal Enfield | Approval Portal</title>
|
<title>Royal Enfield | Approval Portal</title>
|
||||||
|
|
||||||
<!-- Preload critical fonts and icons -->
|
<!-- Preload essential fonts and icons -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
4
public/robots.txt
Normal file
4
public/robots.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /api/
|
||||||
|
|
||||||
|
Sitemap: https://reflow.royalenfield.com/sitemap.xml
|
||||||
9
public/sitemap.xml
Normal file
9
public/sitemap.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://reflow.royalenfield.com</loc>
|
||||||
|
<lastmod>2024-03-20T12:00:00+00:00</lastmod>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
41
src/App.tsx
41
src/App.tsx
@ -20,7 +20,7 @@ import { Profile } from '@/pages/Profile';
|
|||||||
import { Settings } from '@/pages/Settings';
|
import { Settings } from '@/pages/Settings';
|
||||||
import { Notifications } from '@/pages/Notifications';
|
import { Notifications } from '@/pages/Notifications';
|
||||||
import { DetailedReports } from '@/pages/DetailedReports';
|
import { DetailedReports } from '@/pages/DetailedReports';
|
||||||
import { Admin } from '@/pages/Admin';
|
|
||||||
import { AdminTemplatesList } from '@/pages/Admin/Templates/AdminTemplatesList';
|
import { AdminTemplatesList } from '@/pages/Admin/Templates/AdminTemplatesList';
|
||||||
import { CreateTemplate } from '@/pages/Admin/Templates/CreateTemplate';
|
import { CreateTemplate } from '@/pages/Admin/Templates/CreateTemplate';
|
||||||
import { CreateAdminRequest } from '@/pages/CreateAdminRequest/CreateAdminRequest';
|
import { CreateAdminRequest } from '@/pages/CreateAdminRequest/CreateAdminRequest';
|
||||||
@ -216,7 +216,7 @@ function AppRoutes({ onLogout }: AppProps) {
|
|||||||
name: 'Current User',
|
name: 'Current User',
|
||||||
role: requestData.initiatorRole || 'Employee',
|
role: requestData.initiatorRole || 'Employee',
|
||||||
department: requestData.department || 'General',
|
department: requestData.department || 'General',
|
||||||
email: 'current.user@{{API_DOMAIN}}',
|
email: 'current.user@royalenfield.com',
|
||||||
phone: '+91 98765 43290',
|
phone: '+91 98765 43290',
|
||||||
avatar: 'CU'
|
avatar: 'CU'
|
||||||
},
|
},
|
||||||
@ -462,44 +462,7 @@ function AppRoutes({ onLogout }: AppProps) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/admin"
|
|
||||||
element={
|
|
||||||
<PageLayout currentPage="admin" onNavigate={handleNavigate} onNewRequest={handleNewRequest} onLogout={onLogout}>
|
|
||||||
<Admin />
|
|
||||||
</PageLayout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Admin Routes Group with Shared Layout */}
|
|
||||||
<Route
|
|
||||||
element={
|
|
||||||
<PageLayout currentPage="admin-templates" onNavigate={handleNavigate} onNewRequest={handleNewRequest} onLogout={onLogout}>
|
|
||||||
<Outlet />
|
|
||||||
</PageLayout>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Route path="/admin/create-template" element={<CreateTemplate />} />
|
|
||||||
<Route path="/admin/edit-template/:templateId" element={<CreateTemplate />} />
|
|
||||||
<Route path="/admin/templates" element={<AdminTemplatesList />} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Create Request from Admin Template (Dedicated Flow) */}
|
|
||||||
<Route
|
|
||||||
path="/create-admin-request/:templateId"
|
|
||||||
element={
|
|
||||||
<CreateAdminRequest />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/admin"
|
|
||||||
element={
|
|
||||||
<PageLayout currentPage="admin" onNavigate={handleNavigate} onNewRequest={handleNewRequest} onLogout={onLogout}>
|
|
||||||
<Admin />
|
|
||||||
</PageLayout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Open Requests */}
|
{/* Open Requests */}
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@ -298,13 +298,13 @@ export function ApprovalWorkflowStep({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`p-4 rounded-lg border-2 transition-all ${approver.email
|
<div className={`p-4 rounded-lg border-2 transition-all ${approver.email
|
||||||
? 'border-green-200 bg-green-50'
|
? 'border-green-200 bg-green-50'
|
||||||
: 'border-gray-200 bg-gray-50'
|
: 'border-gray-200 bg-gray-50'
|
||||||
}`}>
|
}`}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${approver.email
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${approver.email
|
||||||
? 'bg-green-600'
|
? 'bg-green-600'
|
||||||
: 'bg-gray-400'
|
: 'bg-gray-400'
|
||||||
}`}>
|
}`}>
|
||||||
<span className="text-white font-semibold">{level}</span>
|
<span className="text-white font-semibold">{level}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -334,7 +334,7 @@ export function ApprovalWorkflowStep({
|
|||||||
<Input
|
<Input
|
||||||
id={`approver-${level}`}
|
id={`approver-${level}`}
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="approver@{{API_DOMAIN}}"
|
placeholder={`approver@${process.env.VITE_APP_DOMAIN}`}
|
||||||
value={approver.email || ''}
|
value={approver.email || ''}
|
||||||
onChange={(e) => handleApproverEmailChange(index, e.target.value)}
|
onChange={(e) => handleApproverEmailChange(index, e.target.value)}
|
||||||
className="h-10 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full"
|
className="h-10 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full"
|
||||||
|
|||||||
@ -129,7 +129,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PRIORITY 3: Skip auth check if on callback page - let callback handler process first
|
// PRIORITY 3: Skip auth check if on callback page - let callback handler process first
|
||||||
// This is critical for production mode where we need to exchange code for tokens
|
// This is essential for production mode where we need to exchange code for tokens
|
||||||
// before we can verify session with server
|
// before we can verify session with server
|
||||||
if (window.location.pathname === '/login/callback' || window.location.pathname === '/login/tanflow/callback') {
|
if (window.location.pathname === '/login/callback' || window.location.pathname === '/login/tanflow/callback') {
|
||||||
// Don't check auth status here - let the callback handler do its job
|
// Don't check auth status here - let the callback handler do its job
|
||||||
@ -149,14 +149,14 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// In production: Always verify with server (cookies are sent automatically)
|
// In production: Always verify with server (cookies are sent automatically)
|
||||||
// In development: Check local auth data first
|
// In development: Check local auth data first
|
||||||
if (isProductionMode) {
|
if (isProductionMode) {
|
||||||
// Production: Verify session with server via httpOnly cookie
|
// Prod: Verify session with server via httpOnly cookie
|
||||||
if (!isLoggingOut) {
|
if (!isLoggingOut) {
|
||||||
checkAuthStatus();
|
checkAuthStatus();
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Development: If no auth data exists, user is not authenticated
|
// Dev: If no auth data exists, user is not authenticated
|
||||||
if (!hasAuthData) {
|
if (!hasAuthData) {
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
@ -323,7 +323,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// PRODUCTION MODE: Verify session via httpOnly cookie
|
// Prod MODE: Verify session via httpOnly cookie
|
||||||
// The cookie is sent automatically with the request (withCredentials: true)
|
// The cookie is sent automatically with the request (withCredentials: true)
|
||||||
if (isProductionMode) {
|
if (isProductionMode) {
|
||||||
const storedUser = TokenManager.getUserData();
|
const storedUser = TokenManager.getUserData();
|
||||||
@ -369,7 +369,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEVELOPMENT MODE: Check local token
|
// Dev MODE: Check local token
|
||||||
const token = TokenManager.getAccessToken();
|
const token = TokenManager.getAccessToken();
|
||||||
const storedUser = TokenManager.getUserData();
|
const storedUser = TokenManager.getUserData();
|
||||||
|
|
||||||
@ -490,7 +490,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
try {
|
try {
|
||||||
// CRITICAL: Get id_token from TokenManager before clearing anything
|
//: Get id_token from TokenManager before clearing anything
|
||||||
// Needed for both Okta and Tanflow logout endpoints
|
// Needed for both Okta and Tanflow logout endpoints
|
||||||
const idToken = TokenManager.getIdToken();
|
const idToken = TokenManager.getIdToken();
|
||||||
|
|
||||||
@ -609,7 +609,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Development mode: tokens in localStorage
|
// Dev mode: tokens in localStorage
|
||||||
const token = TokenManager.getAccessToken();
|
const token = TokenManager.getAccessToken();
|
||||||
if (token && !isTokenExpired(token)) {
|
if (token && !isTokenExpired(token)) {
|
||||||
return token;
|
return token;
|
||||||
|
|||||||
@ -141,7 +141,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
// Create new approver only if it doesn't exist
|
// Create new approver only if it doesn't exist
|
||||||
if (step.isAuto) {
|
if (step.isAuto) {
|
||||||
// System steps
|
// System steps
|
||||||
const systemEmail = step.level === 8 ? 'finance@{{API_DOMAIN}}' : 'system@{{API_DOMAIN}}';
|
const systemEmail = step.level === 8 ? `finance@${process.env.VITE_APP_DOMAIN}` : `system@${process.env.VITE_APP_DOMAIN}`;
|
||||||
const systemName = step.level === 8 ? 'System/Finance' : 'System';
|
const systemName = step.level === 8 ? 'System/Finance' : 'System';
|
||||||
newApprovers.push({
|
newApprovers.push({
|
||||||
email: systemEmail,
|
email: systemEmail,
|
||||||
|
|||||||
@ -765,7 +765,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
// Note: Status normalization already handled in workflowSteps mapping above
|
// Note: Status normalization already handled in workflowSteps mapping above
|
||||||
// backendCurrentLevel is already calculated above before the map function
|
// backendCurrentLevel is already calculated above before the map function
|
||||||
|
|
||||||
// CRITICAL: If request is rejected or closed, no step should be active
|
//: If request is rejected or closed, no step should be active
|
||||||
let activeStep = null;
|
let activeStep = null;
|
||||||
let currentStep = 1;
|
let currentStep = 1;
|
||||||
|
|
||||||
@ -2519,7 +2519,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
stepNumber={selectedStepForEmail?.stepNumber || 4}
|
stepNumber={selectedStepForEmail?.stepNumber || 4}
|
||||||
stepName={selectedStepForEmail?.stepName || 'Activity Creation'}
|
stepName={selectedStepForEmail?.stepName || 'Activity Creation'}
|
||||||
requestNumber={request?.requestNumber || request?.id || request?.request_number}
|
requestNumber={request?.requestNumber || request?.id || request?.request_number}
|
||||||
recipientEmail="system@{{API_DOMAIN}}"
|
recipientEmail={`system@${window.location.hostname}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Additional Approver Review Modal */}
|
{/* Additional Approver Review Modal */}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export function EmailNotificationTemplateModal({
|
|||||||
stepNumber,
|
stepNumber,
|
||||||
stepName,
|
stepName,
|
||||||
requestNumber = 'RE-REQ-2024-CM-101',
|
requestNumber = 'RE-REQ-2024-CM-101',
|
||||||
recipientEmail = 'system@{{API_DOMAIN}}',
|
recipientEmail = `system@${window.location.hostname}`,
|
||||||
subject,
|
subject,
|
||||||
emailBody,
|
emailBody,
|
||||||
}: EmailNotificationTemplateModalProps) {
|
}: EmailNotificationTemplateModalProps) {
|
||||||
|
|||||||
@ -649,7 +649,7 @@ export function useRequestDetails(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed: Get final request object with fallback to static databases
|
* Computed: Get final request object with fallback to static databases
|
||||||
* Priority: API data → Custom DB → Claim DB → Dynamic props → null
|
* Priority: API data → Custom Database → Claim Database → Dynamic props → null
|
||||||
*/
|
*/
|
||||||
const request = useMemo(() => {
|
const request = useMemo(() => {
|
||||||
// Primary source: API data
|
// Primary source: API data
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import { useAppSelector } from '@/redux/hooks';
|
|||||||
import { Pagination } from '@/components/common/Pagination';
|
import { Pagination } from '@/components/common/Pagination';
|
||||||
import type { DateRange } from '@/services/dashboard.service';
|
import type { DateRange } from '@/services/dashboard.service';
|
||||||
import dashboardService from '@/services/dashboard.service';
|
import dashboardService from '@/services/dashboard.service';
|
||||||
import userApi from '@/services/userApi';
|
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { RequestsHeader } from './components/RequestsHeader';
|
import { RequestsHeader } from './components/RequestsHeader';
|
||||||
@ -70,7 +69,6 @@ export function Requests({ onViewRequest }: RequestsProps) {
|
|||||||
const [backendStats, setBackendStats] = useState<BackendStats | null>(null);
|
const [backendStats, setBackendStats] = useState<BackendStats | null>(null);
|
||||||
const [departments, setDepartments] = useState<string[]>([]);
|
const [departments, setDepartments] = useState<string[]>([]);
|
||||||
const [loadingDepartments, setLoadingDepartments] = useState(false);
|
const [loadingDepartments, setLoadingDepartments] = useState(false);
|
||||||
const [allUsers, setAllUsers] = useState<Array<{ userId: string; email: string; displayName?: string }>>([]);
|
|
||||||
|
|
||||||
// Pagination (currentPage now in Redux)
|
// Pagination (currentPage now in Redux)
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
@ -79,15 +77,15 @@ export function Requests({ onViewRequest }: RequestsProps) {
|
|||||||
|
|
||||||
// User search hooks
|
// User search hooks
|
||||||
const initiatorSearch = useUserSearch({
|
const initiatorSearch = useUserSearch({
|
||||||
allUsers,
|
|
||||||
filterValue: filters.initiatorFilter,
|
filterValue: filters.initiatorFilter,
|
||||||
onFilterChange: filters.setInitiatorFilter
|
onFilterChange: filters.setInitiatorFilter,
|
||||||
|
source: 'local'
|
||||||
});
|
});
|
||||||
|
|
||||||
const approverSearch = useUserSearch({
|
const approverSearch = useUserSearch({
|
||||||
allUsers,
|
|
||||||
filterValue: filters.approverFilter,
|
filterValue: filters.approverFilter,
|
||||||
onFilterChange: filters.setApproverFilter
|
onFilterChange: filters.setApproverFilter,
|
||||||
|
source: 'local'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch backend stats
|
// Fetch backend stats
|
||||||
@ -226,20 +224,6 @@ export function Requests({ onViewRequest }: RequestsProps) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Fetch users
|
|
||||||
const fetchUsers = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const usersData = await userApi.getAllUsers();
|
|
||||||
const usersList = usersData.map((user: any) => ({
|
|
||||||
userId: user.userId,
|
|
||||||
email: user.email,
|
|
||||||
displayName: user.displayName || user.email
|
|
||||||
}));
|
|
||||||
setAllUsers(usersList);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch users:', error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Use refs to store stable callbacks to prevent infinite loops
|
// Use refs to store stable callbacks to prevent infinite loops
|
||||||
const filtersRef = useRef(filters);
|
const filtersRef = useRef(filters);
|
||||||
@ -332,8 +316,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
|
|||||||
// Initial fetch
|
// Initial fetch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDepartments();
|
fetchDepartments();
|
||||||
fetchUsers();
|
}, [fetchDepartments]);
|
||||||
}, [fetchDepartments, fetchUsers]);
|
|
||||||
|
|
||||||
// Fetch backend stats when filters change (excluding status)
|
// Fetch backend stats when filters change (excluding status)
|
||||||
// Stats should reflect priority, department, initiator, approver, search, and date range filters
|
// Stats should reflect priority, department, initiator, approver, search, and date range filters
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import { useEffect, useState, useCallback, useMemo, useRef } from 'react';
|
|||||||
import { Pagination } from '@/components/common/Pagination';
|
import { Pagination } from '@/components/common/Pagination';
|
||||||
import dashboardService from '@/services/dashboard.service';
|
import dashboardService from '@/services/dashboard.service';
|
||||||
import type { DateRange } from '@/services/dashboard.service';
|
import type { DateRange } from '@/services/dashboard.service';
|
||||||
import userApi from '@/services/userApi';
|
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { RequestsHeader } from './components/RequestsHeader';
|
import { RequestsHeader } from './components/RequestsHeader';
|
||||||
@ -58,7 +57,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
|
|
||||||
// Determine once - use this throughout instead of checking repeatedly
|
// Determine once - use this throughout instead of checking repeatedly
|
||||||
const isDealer = userFilterType === 'DEALER';
|
const isDealer = userFilterType === 'DEALER';
|
||||||
|
|
||||||
// Helper to get filters for API - excludes dealer-restricted filters
|
// Helper to get filters for API - excludes dealer-restricted filters
|
||||||
// Since we know user type initially, this helper uses that knowledge
|
// Since we know user type initially, this helper uses that knowledge
|
||||||
const getFiltersForApi = useCallback(() => {
|
const getFiltersForApi = useCallback(() => {
|
||||||
@ -70,7 +69,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
}
|
}
|
||||||
return filterOptions;
|
return filterOptions;
|
||||||
}, [filters, isDealer]);
|
}, [filters, isDealer]);
|
||||||
|
|
||||||
// Helper to calculate active filters count based on user type
|
// Helper to calculate active filters count based on user type
|
||||||
const calculateActiveFiltersCount = useCallback(() => {
|
const calculateActiveFiltersCount = useCallback(() => {
|
||||||
if (isDealer) {
|
if (isDealer) {
|
||||||
@ -96,7 +95,6 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
const [backendStats, setBackendStats] = useState<BackendStats | null>(null); // Stats from backend API
|
const [backendStats, setBackendStats] = useState<BackendStats | null>(null); // Stats from backend API
|
||||||
const [departments, setDepartments] = useState<string[]>([]);
|
const [departments, setDepartments] = useState<string[]>([]);
|
||||||
const [loadingDepartments, setLoadingDepartments] = useState(false);
|
const [loadingDepartments, setLoadingDepartments] = useState(false);
|
||||||
const [allUsers, setAllUsers] = useState<Array<{ userId: string; email: string; displayName?: string }>>([]);
|
|
||||||
|
|
||||||
// Pagination (currentPage now in Redux)
|
// Pagination (currentPage now in Redux)
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
@ -105,31 +103,31 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
|
|
||||||
// User search hooks
|
// User search hooks
|
||||||
const initiatorSearch = useUserSearch({
|
const initiatorSearch = useUserSearch({
|
||||||
allUsers,
|
|
||||||
filterValue: filters.initiatorFilter,
|
filterValue: filters.initiatorFilter,
|
||||||
onFilterChange: filters.setInitiatorFilter
|
onFilterChange: filters.setInitiatorFilter,
|
||||||
|
source: 'local'
|
||||||
});
|
});
|
||||||
|
|
||||||
const approverSearch = useUserSearch({
|
const approverSearch = useUserSearch({
|
||||||
allUsers,
|
|
||||||
filterValue: filters.approverFilter,
|
filterValue: filters.approverFilter,
|
||||||
onFilterChange: filters.setApproverFilter
|
onFilterChange: filters.setApproverFilter,
|
||||||
|
source: 'local'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch backend stats using dashboard API
|
// Fetch backend stats using dashboard API
|
||||||
// OPTIMIZED: Uses backend stats API instead of fetching 100 records
|
// OPTIMIZED: Uses backend stats API instead of fetching 100 records
|
||||||
// Stats reflect all filters EXCEPT status - total stays stable when only status changes
|
// Stats reflect all filters EXCEPT status - total stays stable when only status changes
|
||||||
const fetchBackendStats = useCallback(async (
|
const fetchBackendStats = useCallback(async (
|
||||||
statsDateRange?: DateRange,
|
statsDateRange?: DateRange,
|
||||||
statsStartDate?: Date,
|
statsStartDate?: Date,
|
||||||
statsEndDate?: Date,
|
statsEndDate?: Date,
|
||||||
filtersWithoutStatus?: {
|
filtersWithoutStatus?: {
|
||||||
priority?: string;
|
priority?: string;
|
||||||
templateType?: string;
|
templateType?: string;
|
||||||
department?: string;
|
department?: string;
|
||||||
initiator?: string;
|
initiator?: string;
|
||||||
approver?: string;
|
approver?: string;
|
||||||
approverType?: 'current' | 'any';
|
approverType?: 'current' | 'any';
|
||||||
search?: string;
|
search?: string;
|
||||||
slaCompliance?: string;
|
slaCompliance?: string;
|
||||||
}
|
}
|
||||||
@ -180,26 +178,12 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Fetch users
|
|
||||||
const fetchUsers = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const usersData = await userApi.getAllUsers();
|
|
||||||
const usersList = usersData.map((user: any) => ({
|
|
||||||
userId: user.userId,
|
|
||||||
email: user.email,
|
|
||||||
displayName: user.displayName || user.email
|
|
||||||
}));
|
|
||||||
setAllUsers(usersList);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch users:', error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Use refs to store stable callbacks to prevent infinite loops
|
// Use refs to store stable callbacks to prevent infinite loops
|
||||||
const filtersRef = useRef(filters);
|
const filtersRef = useRef(filters);
|
||||||
const fetchBackendStatsRef = useRef(fetchBackendStats);
|
const fetchBackendStatsRef = useRef(fetchBackendStats);
|
||||||
const getFiltersForApiRef = useRef(getFiltersForApi);
|
const getFiltersForApiRef = useRef(getFiltersForApi);
|
||||||
|
|
||||||
// Update refs on each render
|
// Update refs on each render
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
filtersRef.current = filters;
|
filtersRef.current = filters;
|
||||||
@ -253,8 +237,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
// Initial fetch
|
// Initial fetch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDepartments();
|
fetchDepartments();
|
||||||
fetchUsers();
|
}, [fetchDepartments]);
|
||||||
}, [fetchDepartments, fetchUsers]);
|
|
||||||
|
|
||||||
// Fetch backend stats when filters change (except status filter)
|
// Fetch backend stats when filters change (except status filter)
|
||||||
// OPTIMIZED: Uses backend stats API instead of fetching 100 records
|
// OPTIMIZED: Uses backend stats API instead of fetching 100 records
|
||||||
@ -275,7 +258,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
approverType: filters.approverFilter !== 'all' ? filters.approverFilterType : undefined,
|
approverType: filters.approverFilter !== 'all' ? filters.approverFilterType : undefined,
|
||||||
search: filters.searchTerm || undefined,
|
search: filters.searchTerm || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only include priority, templateType, department, and slaCompliance if user is not a dealer
|
// Only include priority, templateType, department, and slaCompliance if user is not a dealer
|
||||||
if (!isDealer) {
|
if (!isDealer) {
|
||||||
if (filters.priorityFilter !== 'all') filtersWithoutStatus.priority = filters.priorityFilter;
|
if (filters.priorityFilter !== 'all') filtersWithoutStatus.priority = filters.priorityFilter;
|
||||||
@ -283,13 +266,13 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
if (filters.departmentFilter !== 'all') filtersWithoutStatus.department = filters.departmentFilter;
|
if (filters.departmentFilter !== 'all') filtersWithoutStatus.department = filters.departmentFilter;
|
||||||
if (filters.slaComplianceFilter !== 'all') filtersWithoutStatus.slaCompliance = filters.slaComplianceFilter;
|
if (filters.slaComplianceFilter !== 'all') filtersWithoutStatus.slaCompliance = filters.slaComplianceFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use 'all' if dateRange is 'all', otherwise use the selected dateRange or default to 'month'
|
// Use 'all' if dateRange is 'all', otherwise use the selected dateRange or default to 'month'
|
||||||
const statsDateRange = filters.dateRange === 'all' ? 'all' : (filters.dateRange || 'month');
|
const statsDateRange = filters.dateRange === 'all' ? 'all' : (filters.dateRange || 'month');
|
||||||
|
|
||||||
fetchBackendStatsRef.current(
|
fetchBackendStatsRef.current(
|
||||||
statsDateRange,
|
statsDateRange,
|
||||||
filters.customStartDate,
|
filters.customStartDate,
|
||||||
filters.customEndDate,
|
filters.customEndDate,
|
||||||
filtersWithoutStatus
|
filtersWithoutStatus
|
||||||
);
|
);
|
||||||
@ -329,7 +312,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
customEndDate: filters.customEndDate,
|
customEndDate: filters.customEndDate,
|
||||||
});
|
});
|
||||||
const hasInitialFetchRun = useRef(false);
|
const hasInitialFetchRun = useRef(false);
|
||||||
|
|
||||||
// Initial fetch on mount - use stored page from Redux
|
// Initial fetch on mount - use stored page from Redux
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedPage = filters.currentPage || 1;
|
const storedPage = filters.currentPage || 1;
|
||||||
@ -337,13 +320,13 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
hasInitialFetchRun.current = true;
|
hasInitialFetchRun.current = true;
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []); // Only on mount
|
}, []); // Only on mount
|
||||||
|
|
||||||
// Fetch when filters change
|
// Fetch when filters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasInitialFetchRun.current) return;
|
if (!hasInitialFetchRun.current) return;
|
||||||
|
|
||||||
const prev = prevFiltersRef.current;
|
const prev = prevFiltersRef.current;
|
||||||
const hasChanged =
|
const hasChanged =
|
||||||
prev.searchTerm !== filters.searchTerm ||
|
prev.searchTerm !== filters.searchTerm ||
|
||||||
prev.statusFilter !== filters.statusFilter ||
|
prev.statusFilter !== filters.statusFilter ||
|
||||||
prev.priorityFilter !== filters.priorityFilter ||
|
prev.priorityFilter !== filters.priorityFilter ||
|
||||||
@ -356,13 +339,13 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
prev.dateRange !== filters.dateRange ||
|
prev.dateRange !== filters.dateRange ||
|
||||||
prev.customStartDate !== filters.customStartDate ||
|
prev.customStartDate !== filters.customStartDate ||
|
||||||
prev.customEndDate !== filters.customEndDate;
|
prev.customEndDate !== filters.customEndDate;
|
||||||
|
|
||||||
if (!hasChanged) return;
|
if (!hasChanged) return;
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
filters.setCurrentPage(1);
|
filters.setCurrentPage(1);
|
||||||
fetchRequests(1);
|
fetchRequests(1);
|
||||||
|
|
||||||
prevFiltersRef.current = {
|
prevFiltersRef.current = {
|
||||||
searchTerm: filters.searchTerm,
|
searchTerm: filters.searchTerm,
|
||||||
statusFilter: filters.statusFilter,
|
statusFilter: filters.statusFilter,
|
||||||
@ -406,7 +389,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
|
|
||||||
// Transform requests
|
// Transform requests
|
||||||
const convertedRequests = useMemo(() => transformRequests(apiRequests), [apiRequests]);
|
const convertedRequests = useMemo(() => transformRequests(apiRequests), [apiRequests]);
|
||||||
|
|
||||||
// Calculate stats - Use backend stats API (OPTIMIZED)
|
// Calculate stats - Use backend stats API (OPTIMIZED)
|
||||||
const stats = useMemo(() => {
|
const stats = useMemo(() => {
|
||||||
// Use backend stats if available
|
// Use backend stats if available
|
||||||
@ -421,38 +404,38 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
closed: backendStats.closed || 0
|
closed: backendStats.closed || 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: calculate from current page (less accurate, but works during initial load)
|
// Fallback: calculate from current page (less accurate, but works during initial load)
|
||||||
const pending = convertedRequests.filter((r: any) => {
|
const pending = convertedRequests.filter((r: any) => {
|
||||||
const status = (r.status || '').toString().toLowerCase();
|
const status = (r.status || '').toString().toLowerCase();
|
||||||
return status === 'pending' || status === 'in-progress';
|
return status === 'pending' || status === 'in-progress';
|
||||||
}).length;
|
}).length;
|
||||||
const paused = convertedRequests.filter((r: any) => {
|
const paused = convertedRequests.filter((r: any) => {
|
||||||
const status = (r.status || '').toString().toLowerCase();
|
const status = (r.status || '').toString().toLowerCase();
|
||||||
return status === 'paused';
|
return status === 'paused';
|
||||||
}).length;
|
}).length;
|
||||||
const approved = convertedRequests.filter((r: any) => {
|
const approved = convertedRequests.filter((r: any) => {
|
||||||
const status = (r.status || '').toString().toLowerCase();
|
const status = (r.status || '').toString().toLowerCase();
|
||||||
return status === 'approved';
|
return status === 'approved';
|
||||||
}).length;
|
}).length;
|
||||||
const rejected = convertedRequests.filter((r: any) => {
|
const rejected = convertedRequests.filter((r: any) => {
|
||||||
const status = (r.status || '').toString().toLowerCase();
|
const status = (r.status || '').toString().toLowerCase();
|
||||||
return status === 'rejected';
|
return status === 'rejected';
|
||||||
}).length;
|
}).length;
|
||||||
const closed = convertedRequests.filter((r: any) => {
|
const closed = convertedRequests.filter((r: any) => {
|
||||||
const status = (r.status || '').toString().toLowerCase();
|
const status = (r.status || '').toString().toLowerCase();
|
||||||
return status === 'closed';
|
return status === 'closed';
|
||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: totalRecords > 0 ? totalRecords : convertedRequests.length,
|
total: totalRecords > 0 ? totalRecords : convertedRequests.length,
|
||||||
pending,
|
pending,
|
||||||
paused,
|
paused,
|
||||||
approved,
|
approved,
|
||||||
rejected,
|
rejected,
|
||||||
draft: 0,
|
draft: 0,
|
||||||
closed
|
closed
|
||||||
};
|
};
|
||||||
}, [backendStats, totalRecords, convertedRequests]);
|
}, [backendStats, totalRecords, convertedRequests]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -467,8 +450,8 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<RequestsStats
|
<RequestsStats
|
||||||
stats={stats}
|
stats={stats}
|
||||||
onStatusFilter={(status) => {
|
onStatusFilter={(status) => {
|
||||||
filters.setStatusFilter(status);
|
filters.setStatusFilter(status);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -4,30 +4,44 @@
|
|||||||
|
|
||||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
import type { User } from '../types/requests.types';
|
import type { User } from '../types/requests.types';
|
||||||
|
import { userApi } from '@/services/userApi';
|
||||||
|
|
||||||
interface UseUserSearchOptions {
|
interface UseUserSearchOptions {
|
||||||
allUsers: User[];
|
|
||||||
filterValue: string;
|
filterValue: string;
|
||||||
onFilterChange: (userId: string) => void;
|
onFilterChange: (userId: string) => void;
|
||||||
|
source?: 'local' | 'okta' | 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUserSearch({ allUsers, filterValue, onFilterChange }: UseUserSearchOptions) {
|
export function useUserSearch({ filterValue, onFilterChange, source = 'default' }: UseUserSearchOptions) {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [searchResults, setSearchResults] = useState<User[]>([]);
|
const [searchResults, setSearchResults] = useState<User[]>([]);
|
||||||
const [showResults, setShowResults] = useState(false);
|
const [showResults, setShowResults] = useState(false);
|
||||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||||
|
const [searching, setSearching] = useState(false);
|
||||||
const searchTimer = useRef<NodeJS.Timeout | null>(null);
|
const searchTimer = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// Initialize selected user from filter value
|
// Initialize selected user details if we only have the ID (filterValue)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filterValue !== 'all' && allUsers.length > 0) {
|
async function fetchUserDetail() {
|
||||||
const user = allUsers.find(u => u.userId === filterValue);
|
if (filterValue !== 'all' && !selectedUser) {
|
||||||
if (user) {
|
try {
|
||||||
setSelectedUser(user);
|
// Fetch specific user details by ID
|
||||||
setSearchQuery(user.displayName || user.email);
|
const user = await userApi.getUserById(filterValue);
|
||||||
|
if (user) {
|
||||||
|
setSelectedUser(user);
|
||||||
|
setSearchQuery(user.displayName || user.email);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch user detail for search:', err);
|
||||||
|
}
|
||||||
|
} else if (filterValue === 'all') {
|
||||||
|
setSelectedUser(null);
|
||||||
|
setSearchQuery('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [filterValue, allUsers]);
|
|
||||||
|
fetchUserDetail();
|
||||||
|
}, [filterValue]);
|
||||||
|
|
||||||
// Cleanup timer
|
// Cleanup timer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -51,17 +65,22 @@ export function useUserSearch({ allUsers, filterValue, onFilterChange }: UseUser
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
searchTimer.current = setTimeout(() => {
|
searchTimer.current = setTimeout(async () => {
|
||||||
const searchLower = query.toLowerCase().trim();
|
setSearching(true);
|
||||||
const filtered = allUsers.filter((user) => {
|
try {
|
||||||
const email = (user.email || '').toLowerCase();
|
const response = await userApi.searchUsers(query.trim(), 10, source);
|
||||||
const displayName = (user.displayName || '').toLowerCase();
|
const users = response.data?.data || [];
|
||||||
return email.includes(searchLower) || displayName.includes(searchLower);
|
setSearchResults(users);
|
||||||
});
|
setShowResults(users.length > 0);
|
||||||
setSearchResults(filtered.slice(0, 10));
|
} catch (err) {
|
||||||
setShowResults(filtered.length > 0);
|
console.error('Search API failed:', err);
|
||||||
}, 300);
|
setSearchResults([]);
|
||||||
}, [allUsers]);
|
setShowResults(false);
|
||||||
|
} finally {
|
||||||
|
setSearching(false);
|
||||||
|
}
|
||||||
|
}, 400); // Slightly longer debounce for API calls
|
||||||
|
}, [source]);
|
||||||
|
|
||||||
const handleSelect = useCallback((user: User) => {
|
const handleSelect = useCallback((user: User) => {
|
||||||
setSelectedUser(user);
|
setSelectedUser(user);
|
||||||
@ -84,6 +103,7 @@ export function useUserSearch({ allUsers, filterValue, onFilterChange }: UseUser
|
|||||||
searchResults,
|
searchResults,
|
||||||
showResults,
|
showResults,
|
||||||
selectedUser,
|
selectedUser,
|
||||||
|
searching,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
handleClear,
|
handleClear,
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
import { TokenManager } from '../utils/tokenManager';
|
import { TokenManager } from '../utils/tokenManager';
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1';
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
||||||
|
|
||||||
// Create axios instance with default config
|
// Create axios instance with default config
|
||||||
const apiClient: AxiosInstance = axios.create({
|
const apiClient: AxiosInstance = axios.create({
|
||||||
@ -25,16 +25,16 @@ apiClient.interceptors.request.use(
|
|||||||
// In production, cookies are sent automatically with withCredentials: true
|
// In production, cookies are sent automatically with withCredentials: true
|
||||||
// No need to set Authorization header
|
// No need to set Authorization header
|
||||||
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
||||||
|
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
// Development: Get token from localStorage and add to header
|
// Dev: Get token from localStorage and add to header
|
||||||
const token = TokenManager.getAccessToken();
|
const token = TokenManager.getAccessToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
// Prod: Cookies handle authentication automatically
|
||||||
// Production: Cookies handle authentication automatically
|
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@ -51,7 +51,7 @@ apiClient.interceptors.response.use(
|
|||||||
// Handle connection errors gracefully in development
|
// Handle connection errors gracefully in development
|
||||||
if (error.code === 'ERR_NETWORK' || error.code === 'ECONNREFUSED' || error.message?.includes('ERR_CONNECTION_REFUSED')) {
|
if (error.code === 'ERR_NETWORK' || error.code === 'ECONNREFUSED' || error.message?.includes('ERR_CONNECTION_REFUSED')) {
|
||||||
const isDevelopment = import.meta.env.DEV || import.meta.env.MODE === 'development';
|
const isDevelopment = import.meta.env.DEV || import.meta.env.MODE === 'development';
|
||||||
|
|
||||||
if (isDevelopment) {
|
if (isDevelopment) {
|
||||||
// In development, log a helpful message instead of spamming console
|
// In development, log a helpful message instead of spamming console
|
||||||
console.warn(`[API] Backend not reachable at ${API_BASE_URL}. Make sure the backend server is running on port 5000.`);
|
console.warn(`[API] Backend not reachable at ${API_BASE_URL}. Make sure the backend server is running on port 5000.`);
|
||||||
@ -67,7 +67,7 @@ apiClient.interceptors.response.use(
|
|||||||
// If error is 401 and we haven't retried yet
|
// If error is 401 and we haven't retried yet
|
||||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||||
originalRequest._retry = true;
|
originalRequest._retry = true;
|
||||||
|
|
||||||
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -75,7 +75,7 @@ apiClient.interceptors.response.use(
|
|||||||
// In production: Cookie is sent automatically via withCredentials
|
// In production: Cookie is sent automatically via withCredentials
|
||||||
// In development: Send refresh token from localStorage
|
// In development: Send refresh token from localStorage
|
||||||
const refreshToken = TokenManager.getRefreshToken();
|
const refreshToken = TokenManager.getRefreshToken();
|
||||||
|
|
||||||
// In production, refreshToken will be null but cookie will be sent
|
// In production, refreshToken will be null but cookie will be sent
|
||||||
// In development, we need the token in body
|
// In development, we need the token in body
|
||||||
if (!isProduction && !refreshToken) {
|
if (!isProduction && !refreshToken) {
|
||||||
@ -90,14 +90,14 @@ apiClient.interceptors.response.use(
|
|||||||
|
|
||||||
const responseData = response.data.data || response.data;
|
const responseData = response.data.data || response.data;
|
||||||
const accessToken = responseData.accessToken;
|
const accessToken = responseData.accessToken;
|
||||||
|
|
||||||
// In production: Backend sets new httpOnly cookie, no token in response
|
// In production: Backend sets new httpOnly cookie, no token in response
|
||||||
// In development: Token is in response, store it and add to header
|
// In development: Token is in response, store it and add to header
|
||||||
if (!isProduction && accessToken) {
|
if (!isProduction && accessToken) {
|
||||||
TokenManager.setAccessToken(accessToken);
|
TokenManager.setAccessToken(accessToken);
|
||||||
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
|
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry the original request
|
// Retry the original request
|
||||||
// In production: Cookie will be sent automatically
|
// In production: Cookie will be sent automatically
|
||||||
return apiClient(originalRequest);
|
return apiClient(originalRequest);
|
||||||
@ -156,7 +156,7 @@ export async function exchangeCodeForTokens(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if response is an array (buffer issue)
|
// Check if response is an array (buffer issue)
|
||||||
if (Array.isArray(response.data)) {
|
if (Array.isArray(response.data)) {
|
||||||
console.error('❌ Response is an array (buffer issue):', {
|
console.error('❌ Response is an array (buffer issue):', {
|
||||||
@ -166,28 +166,28 @@ export async function exchangeCodeForTokens(
|
|||||||
});
|
});
|
||||||
throw new Error('Invalid response format: received array instead of JSON. Check Content-Type header.');
|
throw new Error('Invalid response format: received array instead of JSON. Check Content-Type header.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = response.data as any;
|
const data = response.data as any;
|
||||||
const result = data.data || data;
|
const result = data.data || data;
|
||||||
|
|
||||||
// Store user data (always available)
|
// Store user data (always available)
|
||||||
if (result.user) {
|
if (result.user) {
|
||||||
TokenManager.setUserData(result.user);
|
TokenManager.setUserData(result.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store ID token if available (needed for Okta logout)
|
// Store ID token if available (needed for Okta logout)
|
||||||
if (result.idToken) {
|
if (result.idToken) {
|
||||||
TokenManager.setIdToken(result.idToken);
|
TokenManager.setIdToken(result.idToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SECURITY: In production, tokens are ONLY in httpOnly cookies (not in response body)
|
// SECURITY: In production, tokens are ONLY in httpOnly cookies (not in response body)
|
||||||
// In development, backend returns tokens for cross-port setup
|
// In development, backend returns tokens for cross-port setup
|
||||||
if (result.accessToken && result.refreshToken) {
|
if (result.accessToken && result.refreshToken) {
|
||||||
// Development mode: Backend returned tokens, store them
|
// Dev mode: Backend returned tokens, store them
|
||||||
TokenManager.setAccessToken(result.accessToken);
|
TokenManager.setAccessToken(result.accessToken);
|
||||||
TokenManager.setRefreshToken(result.refreshToken);
|
TokenManager.setRefreshToken(result.refreshToken);
|
||||||
}
|
}
|
||||||
// Production mode: No tokens in response - they're in httpOnly cookies
|
// Prod mode: No tokens in response - they're in httpOnly cookies
|
||||||
// TokenManager.setAccessToken/setRefreshToken are no-ops in production anyway
|
// TokenManager.setAccessToken/setRefreshToken are no-ops in production anyway
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -211,15 +211,15 @@ export async function exchangeCodeForTokens(
|
|||||||
*/
|
*/
|
||||||
export async function refreshAccessToken(): Promise<string> {
|
export async function refreshAccessToken(): Promise<string> {
|
||||||
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
||||||
|
|
||||||
// In development, check for refresh token in localStorage
|
// In development, check for refresh token in localStorage
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
const refreshToken = TokenManager.getRefreshToken();
|
const refreshToken = TokenManager.getRefreshToken();
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
throw new Error('No refresh token available');
|
throw new Error('No refresh token available');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// In production, httpOnly cookie with refresh token will be sent automatically
|
// In production, httpOnly cookie with refresh token will be sent automatically
|
||||||
// In development, we send the refresh token in the body
|
// In development, we send the refresh token in the body
|
||||||
const body = isProduction ? {} : { refreshToken: TokenManager.getRefreshToken() };
|
const body = isProduction ? {} : { refreshToken: TokenManager.getRefreshToken() };
|
||||||
@ -234,7 +234,7 @@ export async function refreshAccessToken(): Promise<string> {
|
|||||||
TokenManager.setAccessToken(accessToken);
|
TokenManager.setAccessToken(accessToken);
|
||||||
return accessToken;
|
return accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In production mode, token is set via httpOnly cookie by the backend
|
// In production mode, token is set via httpOnly cookie by the backend
|
||||||
// Return a placeholder to indicate success
|
// Return a placeholder to indicate success
|
||||||
if (isProduction && (data.success !== false)) {
|
if (isProduction && (data.success !== false)) {
|
||||||
@ -255,7 +255,7 @@ export async function getCurrentUser() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout user
|
* Logout user
|
||||||
* CRITICAL: This endpoint MUST clear httpOnly cookies set by backend
|
* IMPORTANT: This endpoint MUST clear httpOnly cookies set by backend
|
||||||
* Note: TokenManager.clearAll() is called in AuthContext.logout()
|
* Note: TokenManager.clearAll() is called in AuthContext.logout()
|
||||||
* We don't call it here to avoid double clearing
|
* We don't call it here to avoid double clearing
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -6,8 +6,8 @@
|
|||||||
import { TokenManager } from '../utils/tokenManager';
|
import { TokenManager } from '../utils/tokenManager';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const TANFLOW_BASE_URL = import.meta.env.VITE_TANFLOW_BASE_URL || '{{IDP_DOMAIN}}/realms/RE';
|
const TANFLOW_BASE_URL = import.meta.env.VITE_TANFLOW_BASE_URL || '';
|
||||||
const TANFLOW_CLIENT_ID = import.meta.env.VITE_TANFLOW_CLIENT_ID || 'REFLOW';
|
const TANFLOW_CLIENT_ID = import.meta.env.VITE_TANFLOW_CLIENT_ID || '';
|
||||||
const TANFLOW_REDIRECT_URI = `${window.location.origin}/login/callback`;
|
const TANFLOW_REDIRECT_URI = `${window.location.origin}/login/callback`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,7 +63,7 @@ export async function exchangeTanflowCodeForTokens(
|
|||||||
idToken: string;
|
idToken: string;
|
||||||
user: any;
|
user: any;
|
||||||
}> {
|
}> {
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1';
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
@ -112,7 +112,7 @@ export async function exchangeTanflowCodeForTokens(
|
|||||||
* Refresh access token using refresh token
|
* Refresh access token using refresh token
|
||||||
*/
|
*/
|
||||||
export async function refreshTanflowToken(): Promise<string> {
|
export async function refreshTanflowToken(): Promise<string> {
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1';
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
||||||
const refreshToken = TokenManager.getRefreshToken();
|
const refreshToken = TokenManager.getRefreshToken();
|
||||||
|
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
|
|||||||
@ -24,8 +24,8 @@ export interface UserSummary {
|
|||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchUsers(query: string, limit: number = 10) {
|
export async function searchUsers(query: string, limit: number = 10, source: 'local' | 'okta' | 'default' = 'default') {
|
||||||
const res = await apiClient.get('/users/search', { params: { q: query, limit } });
|
const res = await apiClient.get('/users/search', { params: { q: query, limit, source } });
|
||||||
// ResponseHandler.success returns { success: true, data: array }
|
// ResponseHandler.success returns { success: true, data: array }
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@ -66,11 +66,11 @@ export async function ensureUserExists(userData: {
|
|||||||
* @param role - Role to assign
|
* @param role - Role to assign
|
||||||
*/
|
*/
|
||||||
export async function assignRole(
|
export async function assignRole(
|
||||||
email: string,
|
email: string,
|
||||||
role: 'USER' | 'MANAGEMENT' | 'ADMIN'
|
role: 'USER' | 'MANAGEMENT' | 'ADMIN'
|
||||||
) {
|
) {
|
||||||
return await apiClient.post('/admin/users/assign-role', {
|
return await apiClient.post('/admin/users/assign-role', {
|
||||||
email,
|
email,
|
||||||
role
|
role
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -90,8 +90,8 @@ export async function getUsersByRole(
|
|||||||
page: number = 1,
|
page: number = 1,
|
||||||
limit: number = 10
|
limit: number = 10
|
||||||
) {
|
) {
|
||||||
return await apiClient.get('/admin/users/by-role', {
|
return await apiClient.get('/admin/users/by-role', {
|
||||||
params: { role: role || 'ELEVATED', page, limit }
|
params: { role: role || 'ELEVATED', page, limit }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +102,14 @@ export async function getRoleStatistics() {
|
|||||||
return await apiClient.get('/admin/users/role-statistics');
|
return await apiClient.get('/admin/users/role-statistics');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user by ID
|
||||||
|
*/
|
||||||
|
export async function getUserById(userId: string) {
|
||||||
|
const res = await apiClient.get(`/users/${userId}`);
|
||||||
|
return res.data?.data || res.data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all users from database (for filtering purposes)
|
* Get all users from database (for filtering purposes)
|
||||||
*/
|
*/
|
||||||
@ -111,8 +119,9 @@ export async function getAllUsers() {
|
|||||||
return res.data?.data?.users || [];
|
return res.data?.data?.users || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userApi = {
|
export const userApi = {
|
||||||
searchUsers,
|
searchUsers,
|
||||||
|
getUserById,
|
||||||
ensureUserExists,
|
ensureUserExists,
|
||||||
assignRole,
|
assignRole,
|
||||||
updateUserRole,
|
updateUserRole,
|
||||||
|
|||||||
@ -362,12 +362,12 @@ export async function getPauseDetails(requestId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkNoteAttachmentPreviewUrl(attachmentId: string): string {
|
export function getWorkNoteAttachmentPreviewUrl(attachmentId: string): string {
|
||||||
const baseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000';
|
const baseURL = import.meta.env.VITE_BASE_URL || '';
|
||||||
return `${baseURL}/api/v1/workflows/work-notes/attachments/${attachmentId}/preview`;
|
return `${baseURL}/api/v1/workflows/work-notes/attachments/${attachmentId}/preview`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDocumentPreviewUrl(documentId: string): string {
|
export function getDocumentPreviewUrl(documentId: string): string {
|
||||||
const baseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000';
|
const baseURL = import.meta.env.VITE_BASE_URL || '';
|
||||||
return `${baseURL}/api/v1/workflows/documents/${documentId}/preview`;
|
return `${baseURL}/api/v1/workflows/documents/${documentId}/preview`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,7 +404,7 @@ function extractFilenameFromContentDisposition(contentDisposition: string | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadDocument(documentId: string): Promise<void> {
|
export async function downloadDocument(documentId: string): Promise<void> {
|
||||||
const baseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000';
|
const baseURL = import.meta.env.VITE_BASE_URL || '';
|
||||||
const downloadUrl = `${baseURL}/api/v1/workflows/documents/${documentId}/download`;
|
const downloadUrl = `${baseURL}/api/v1/workflows/documents/${documentId}/download`;
|
||||||
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
||||||
|
|
||||||
@ -449,7 +449,7 @@ export async function downloadDocument(documentId: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadWorkNoteAttachment(attachmentId: string): Promise<void> {
|
export async function downloadWorkNoteAttachment(attachmentId: string): Promise<void> {
|
||||||
const downloadBaseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000';
|
const downloadBaseURL = import.meta.env.VITE_BASE_URL || '';
|
||||||
const downloadUrl = `${downloadBaseURL}/api/v1/workflows/work-notes/attachments/${attachmentId}/download`;
|
const downloadUrl = `${downloadBaseURL}/api/v1/workflows/work-notes/attachments/${attachmentId}/download`;
|
||||||
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
||||||
|
|
||||||
|
|||||||
@ -21,5 +21,8 @@ export function sanitizeHTML(html: string): string {
|
|||||||
// 5. Remove meta and link tags (except for purely visual ones if needed, but safer to remove)
|
// 5. Remove meta and link tags (except for purely visual ones if needed, but safer to remove)
|
||||||
sanitized = sanitized.replace(/<(meta|link|iframe|object|embed|applet)[^>]*>/gi, '');
|
sanitized = sanitized.replace(/<(meta|link|iframe|object|embed|applet)[^>]*>/gi, '');
|
||||||
|
|
||||||
|
// 6. Explicitly remove <a> tags to prevent HTML injection of links (VAPT compliance)
|
||||||
|
sanitized = sanitized.replace(/<a[^>]*>([\s\S]*?)<\/a>/gi, '$1');
|
||||||
|
|
||||||
return sanitized;
|
return sanitized;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,23 +12,22 @@ export function getSocketBaseUrl(): string {
|
|||||||
if (baseUrl) {
|
if (baseUrl) {
|
||||||
return baseUrl;
|
return baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: derive from VITE_API_BASE_URL by removing /api/v1
|
// Fallback: derive from VITE_API_BASE_URL by removing /api/v1
|
||||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
|
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
|
||||||
if (apiBaseUrl) {
|
if (apiBaseUrl) {
|
||||||
return apiBaseUrl.replace(/\/api\/v1\/?$/, '');
|
return apiBaseUrl.replace(/\/api\/v1\/?$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Development fallback
|
// Dev fallback
|
||||||
console.warn('[Socket] No VITE_BASE_URL or VITE_API_BASE_URL found, using localhost:5000');
|
return '';
|
||||||
return 'http://localhost:5000';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSocket(baseUrl?: string): Socket {
|
export function getSocket(baseUrl?: string): Socket {
|
||||||
// Use provided baseUrl or get from environment
|
// Use provided baseUrl or get from environment
|
||||||
const url = baseUrl || getSocketBaseUrl();
|
const url = baseUrl || getSocketBaseUrl();
|
||||||
if (socket) return socket;
|
if (socket) return socket;
|
||||||
|
|
||||||
socket = io(url, {
|
socket = io(url, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
transports: ['websocket', 'polling'],
|
transports: ['websocket', 'polling'],
|
||||||
@ -37,19 +36,19 @@ export function getSocket(baseUrl?: string): Socket {
|
|||||||
reconnectionDelay: 1000,
|
reconnectionDelay: 1000,
|
||||||
reconnectionAttempts: 5
|
reconnectionAttempts: 5
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
// Socket connected
|
// Socket connected
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('connect_error', (error) => {
|
socket.on('connect_error', (error) => {
|
||||||
console.error('[Socket] Connection error:', error.message);
|
console.error('[Socket] Connection error:', error.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', (_reason) => {
|
socket.on('disconnect', (_reason) => {
|
||||||
// Socket disconnected
|
// Socket disconnected
|
||||||
});
|
});
|
||||||
|
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export class TokenManager {
|
|||||||
return; // No-op - rely on httpOnly cookies
|
return; // No-op - rely on httpOnly cookies
|
||||||
}
|
}
|
||||||
|
|
||||||
// Development only: Store for debugging and cross-port requests
|
// Dev only: Store for debugging and cross-port requests
|
||||||
localStorage.setItem(ACCESS_TOKEN_KEY, token);
|
localStorage.setItem(ACCESS_TOKEN_KEY, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ export class TokenManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Development: Return from localStorage
|
// Dev: Return from localStorage
|
||||||
return localStorage.getItem(ACCESS_TOKEN_KEY);
|
return localStorage.getItem(ACCESS_TOKEN_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,7 +113,7 @@ export class TokenManager {
|
|||||||
return; // No-op - rely on httpOnly cookies
|
return; // No-op - rely on httpOnly cookies
|
||||||
}
|
}
|
||||||
|
|
||||||
// Development only
|
// Dev only
|
||||||
localStorage.setItem(REFRESH_TOKEN_KEY, token);
|
localStorage.setItem(REFRESH_TOKEN_KEY, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +163,7 @@ export class TokenManager {
|
|||||||
|
|
||||||
|
|
||||||
static clearAll(): void {
|
static clearAll(): void {
|
||||||
// CRITICAL: Set logout flag in sessionStorage FIRST (before clearing)
|
//: Set logout flag in sessionStorage FIRST (before clearing)
|
||||||
// This flag survives the redirect and prevents auto-authentication
|
// This flag survives the redirect and prevents auto-authentication
|
||||||
try {
|
try {
|
||||||
sessionStorage.setItem('__logout_in_progress__', 'true');
|
sessionStorage.setItem('__logout_in_progress__', 'true');
|
||||||
@ -193,7 +193,7 @@ export class TokenManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEVELOPMENT MODE: Clear everything
|
// Dev MODE: Clear everything
|
||||||
const authKeys = [
|
const authKeys = [
|
||||||
ACCESS_TOKEN_KEY,
|
ACCESS_TOKEN_KEY,
|
||||||
REFRESH_TOKEN_KEY,
|
REFRESH_TOKEN_KEY,
|
||||||
|
|||||||
2
src/vite-env.d.ts
vendored
2
src/vite-env.d.ts
vendored
@ -7,6 +7,8 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_APP_VERSION: string;
|
readonly VITE_APP_VERSION: string;
|
||||||
readonly VITE_ENABLE_ANALYTICS: string;
|
readonly VITE_ENABLE_ANALYTICS: string;
|
||||||
readonly VITE_ENABLE_DEBUG: string;
|
readonly VITE_ENABLE_DEBUG: string;
|
||||||
|
readonly VITE_TANFLOW_BASE_URL: string;
|
||||||
|
readonly VITE_TANFLOW_CLIENT_ID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
@ -60,9 +60,24 @@ const ensureChunkOrder = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Plugin to replace axios localhost fallback for VAPT compliance
|
||||||
|
const replaceAxiosLocalhost = () => {
|
||||||
|
return {
|
||||||
|
name: 'replace-axios-localhost',
|
||||||
|
transform(code: string, id: string) {
|
||||||
|
// Target the specific utils.js file in axios where the localhost string exists
|
||||||
|
if (id.includes('node_modules') && id.includes('axios') && id.includes('utils.js')) {
|
||||||
|
// Replace 'http://localhost' with empty string
|
||||||
|
return code.replace(/'http:\/\/localhost'/g, "''");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), suppressCssWarnings(), ensureChunkOrder()],
|
plugins: [react(), suppressCssWarnings(), ensureChunkOrder(), replaceAxiosLocalhost()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
@ -78,7 +93,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
sourcemap: true,
|
sourcemap: false,
|
||||||
// CSS minification warning is harmless - it's a known issue with certain Tailwind class combinations
|
// CSS minification warning is harmless - it's a known issue with certain Tailwind class combinations
|
||||||
// Re-enable minification with settings that preserve initialization order
|
// Re-enable minification with settings that preserve initialization order
|
||||||
// The "Cannot access 'React' before initialization" error is fixed by keeping React in main bundle
|
// The "Cannot access 'React' before initialization" error is fixed by keeping React in main bundle
|
||||||
@ -119,7 +134,7 @@ export default defineConfig({
|
|||||||
chunkFileNames: 'assets/[name]-[hash].js',
|
chunkFileNames: 'assets/[name]-[hash].js',
|
||||||
// Explicitly define chunk order - React must load before Radix UI
|
// Explicitly define chunk order - React must load before Radix UI
|
||||||
manualChunks(id) {
|
manualChunks(id) {
|
||||||
// CRITICAL FIX: Keep React in main bundle OR ensure it loads first
|
// IMPORTANT: Keep React in main bundle OR ensure it loads first
|
||||||
// The "Cannot access 'React' before initialization" error occurs when
|
// The "Cannot access 'React' before initialization" error occurs when
|
||||||
// Radix UI components try to access React before it's initialized
|
// Radix UI components try to access React before it's initialized
|
||||||
// Option 1: Don't split React - keep it in main bundle (most reliable)
|
// Option 1: Don't split React - keep it in main bundle (most reliable)
|
||||||
@ -128,7 +143,7 @@ export default defineConfig({
|
|||||||
// For now, let's keep React in main bundle to avoid initialization issues
|
// For now, let's keep React in main bundle to avoid initialization issues
|
||||||
// Only split other vendors
|
// Only split other vendors
|
||||||
|
|
||||||
// Radix UI - CRITICAL: ALL Radix packages MUST stay together in ONE chunk
|
// Radix UI - IMPORTANT: ALL Radix packages MUST stay together in ONE chunk
|
||||||
// This chunk will import React from the main bundle, avoiding initialization issues
|
// This chunk will import React from the main bundle, avoiding initialization issues
|
||||||
if (id.includes('node_modules/@radix-ui')) {
|
if (id.includes('node_modules/@radix-ui')) {
|
||||||
return 'radix-vendor';
|
return 'radix-vendor';
|
||||||
@ -172,7 +187,7 @@ export default defineConfig({
|
|||||||
chunkSizeWarningLimit: 1500, // Increased limit since we have manual chunks
|
chunkSizeWarningLimit: 1500, // Increased limit since we have manual chunks
|
||||||
},
|
},
|
||||||
esbuild: {
|
esbuild: {
|
||||||
// CRITICAL: Strip all legal comments to prevent "Suspicious Comments" alerts (e.g. from Redux docs)
|
//: Strip all legal comments to prevent "Suspicious Comments" alerts (e.g. from Redux docs)
|
||||||
legalComments: 'none',
|
legalComments: 'none',
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user