vulnnearable comments removed and source exposing to frobrowser disabled worknote XSS fixed

This commit is contained in:
laxmanhalaki 2026-02-13 15:00:42 +05:30
parent 2fa52b90e3
commit a16346effd
23 changed files with 255 additions and 238 deletions

27
.env.local.backup Normal file
View 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

View File

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

@ -0,0 +1,4 @@
User-agent: *
Disallow: /api/
Sitemap: https://reflow.royalenfield.com/sitemap.xml

9
public/sitemap.xml Normal file
View 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>

View File

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

View File

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

View File

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

View File

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

View File

@ -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 */}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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