From a16346effd711d856614a69a754447589cfab0d9 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Fri, 13 Feb 2026 15:00:42 +0530 Subject: [PATCH] vulnnearable comments removed and source exposing to frobrowser disabled worknote XSS fixed --- .env.local.backup | 27 ++++ index.html | 2 +- public/robots.txt | 4 + public/sitemap.xml | 9 ++ src/App.tsx | 41 +----- .../CreateRequest/ApprovalWorkflowStep.tsx | 10 +- src/contexts/AuthContext.tsx | 14 +- .../ClaimApproverSelectionStep.tsx | 2 +- .../components/request-detail/WorkflowTab.tsx | 4 +- .../modals/EmailNotificationTemplateModal.tsx | 2 +- src/hooks/useRequestDetails.ts | 2 +- src/pages/Requests/Requests.tsx | 27 +--- src/pages/Requests/UserAllRequests.tsx | 129 ++++++++---------- src/pages/Requests/hooks/useUserSearch.ts | 60 +++++--- src/services/authApi.ts | 58 ++++---- src/services/tanflowAuth.ts | 8 +- src/services/userApi.ts | 27 ++-- src/services/workflowApi.ts | 8 +- src/utils/sanitizer.ts | 3 + src/utils/socket.ts | 19 ++- src/utils/tokenManager.ts | 10 +- src/vite-env.d.ts | 2 + vite.config.ts | 25 +++- 23 files changed, 255 insertions(+), 238 deletions(-) create mode 100644 .env.local.backup create mode 100644 public/robots.txt create mode 100644 public/sitemap.xml diff --git a/.env.local.backup b/.env.local.backup new file mode 100644 index 0000000..bd7e054 --- /dev/null +++ b/.env.local.backup @@ -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 \ No newline at end of file diff --git a/index.html b/index.html index 8100c49..4cc8cde 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@ Royal Enfield | Approval Portal - + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..9b9571e --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Disallow: /api/ + +Sitemap: https://reflow.royalenfield.com/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..667956f --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,9 @@ + + + + https://reflow.royalenfield.com + 2024-03-20T12:00:00+00:00 + daily + 1.0 + + diff --git a/src/App.tsx b/src/App.tsx index a8dfa86..92fcbb4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,7 +20,7 @@ import { Profile } from '@/pages/Profile'; import { Settings } from '@/pages/Settings'; import { Notifications } from '@/pages/Notifications'; import { DetailedReports } from '@/pages/DetailedReports'; -import { Admin } from '@/pages/Admin'; + import { AdminTemplatesList } from '@/pages/Admin/Templates/AdminTemplatesList'; import { CreateTemplate } from '@/pages/Admin/Templates/CreateTemplate'; import { CreateAdminRequest } from '@/pages/CreateAdminRequest/CreateAdminRequest'; @@ -216,7 +216,7 @@ function AppRoutes({ onLogout }: AppProps) { name: 'Current User', role: requestData.initiatorRole || 'Employee', department: requestData.department || 'General', - email: 'current.user@{{API_DOMAIN}}', + email: 'current.user@royalenfield.com', phone: '+91 98765 43290', avatar: 'CU' }, @@ -462,44 +462,7 @@ function AppRoutes({ onLogout }: AppProps) { } /> - - - - } - /> - {/* Admin Routes Group with Shared Layout */} - - - - } - > - } /> - } /> - } /> - - - {/* Create Request from Admin Template (Dedicated Flow) */} - - } - /> - - - - - } - /> {/* Open Requests */}
{level}
@@ -334,7 +334,7 @@ export function ApprovalWorkflowStep({ handleApproverEmailChange(index, e.target.value)} className="h-10 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full" diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 11445a5..c5f0458 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -129,7 +129,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { } // 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 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 @@ -149,14 +149,14 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { // In production: Always verify with server (cookies are sent automatically) // In development: Check local auth data first if (isProductionMode) { - // Production: Verify session with server via httpOnly cookie + // Prod: Verify session with server via httpOnly cookie if (!isLoggingOut) { checkAuthStatus(); } else { setIsLoading(false); } } else { - // Development: If no auth data exists, user is not authenticated + // Dev: If no auth data exists, user is not authenticated if (!hasAuthData) { setIsAuthenticated(false); setUser(null); @@ -323,7 +323,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { try { 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) if (isProductionMode) { const storedUser = TokenManager.getUserData(); @@ -369,7 +369,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { return; } - // DEVELOPMENT MODE: Check local token + // Dev MODE: Check local token const token = TokenManager.getAccessToken(); const storedUser = TokenManager.getUserData(); @@ -490,7 +490,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { const logout = async () => { 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 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(); if (token && !isTokenExpired(token)) { return token; diff --git a/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx b/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx index 4424758..945498f 100644 --- a/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx +++ b/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx @@ -141,7 +141,7 @@ export function ClaimApproverSelectionStep({ // Create new approver only if it doesn't exist if (step.isAuto) { // 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'; newApprovers.push({ email: systemEmail, diff --git a/src/dealer-claim/components/request-detail/WorkflowTab.tsx b/src/dealer-claim/components/request-detail/WorkflowTab.tsx index 5bc5d9a..d1a6aff 100644 --- a/src/dealer-claim/components/request-detail/WorkflowTab.tsx +++ b/src/dealer-claim/components/request-detail/WorkflowTab.tsx @@ -765,7 +765,7 @@ export function DealerClaimWorkflowTab({ // Note: Status normalization already handled in workflowSteps mapping above // 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 currentStep = 1; @@ -2519,7 +2519,7 @@ export function DealerClaimWorkflowTab({ stepNumber={selectedStepForEmail?.stepNumber || 4} stepName={selectedStepForEmail?.stepName || 'Activity Creation'} requestNumber={request?.requestNumber || request?.id || request?.request_number} - recipientEmail="system@{{API_DOMAIN}}" + recipientEmail={`system@${window.location.hostname}`} /> {/* Additional Approver Review Modal */} diff --git a/src/dealer-claim/components/request-detail/modals/EmailNotificationTemplateModal.tsx b/src/dealer-claim/components/request-detail/modals/EmailNotificationTemplateModal.tsx index 84e12d4..3a3668f 100644 --- a/src/dealer-claim/components/request-detail/modals/EmailNotificationTemplateModal.tsx +++ b/src/dealer-claim/components/request-detail/modals/EmailNotificationTemplateModal.tsx @@ -32,7 +32,7 @@ export function EmailNotificationTemplateModal({ stepNumber, stepName, requestNumber = 'RE-REQ-2024-CM-101', - recipientEmail = 'system@{{API_DOMAIN}}', + recipientEmail = `system@${window.location.hostname}`, subject, emailBody, }: EmailNotificationTemplateModalProps) { diff --git a/src/hooks/useRequestDetails.ts b/src/hooks/useRequestDetails.ts index 9b9c005..958e2aa 100644 --- a/src/hooks/useRequestDetails.ts +++ b/src/hooks/useRequestDetails.ts @@ -649,7 +649,7 @@ export function useRequestDetails( /** * 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(() => { // Primary source: API data diff --git a/src/pages/Requests/Requests.tsx b/src/pages/Requests/Requests.tsx index 54d2510..0758be1 100644 --- a/src/pages/Requests/Requests.tsx +++ b/src/pages/Requests/Requests.tsx @@ -11,7 +11,6 @@ import { useAppSelector } from '@/redux/hooks'; import { Pagination } from '@/components/common/Pagination'; import type { DateRange } from '@/services/dashboard.service'; import dashboardService from '@/services/dashboard.service'; -import userApi from '@/services/userApi'; // Components import { RequestsHeader } from './components/RequestsHeader'; @@ -70,7 +69,6 @@ export function Requests({ onViewRequest }: RequestsProps) { const [backendStats, setBackendStats] = useState(null); const [departments, setDepartments] = useState([]); const [loadingDepartments, setLoadingDepartments] = useState(false); - const [allUsers, setAllUsers] = useState>([]); // Pagination (currentPage now in Redux) const [totalPages, setTotalPages] = useState(1); @@ -79,15 +77,15 @@ export function Requests({ onViewRequest }: RequestsProps) { // User search hooks const initiatorSearch = useUserSearch({ - allUsers, filterValue: filters.initiatorFilter, - onFilterChange: filters.setInitiatorFilter + onFilterChange: filters.setInitiatorFilter, + source: 'local' }); const approverSearch = useUserSearch({ - allUsers, filterValue: filters.approverFilter, - onFilterChange: filters.setApproverFilter + onFilterChange: filters.setApproverFilter, + source: 'local' }); // 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 const filtersRef = useRef(filters); @@ -332,8 +316,7 @@ export function Requests({ onViewRequest }: RequestsProps) { // Initial fetch useEffect(() => { fetchDepartments(); - fetchUsers(); - }, [fetchDepartments, fetchUsers]); + }, [fetchDepartments]); // Fetch backend stats when filters change (excluding status) // Stats should reflect priority, department, initiator, approver, search, and date range filters diff --git a/src/pages/Requests/UserAllRequests.tsx b/src/pages/Requests/UserAllRequests.tsx index 3f1cfe0..bec12bf 100644 --- a/src/pages/Requests/UserAllRequests.tsx +++ b/src/pages/Requests/UserAllRequests.tsx @@ -11,7 +11,6 @@ import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { Pagination } from '@/components/common/Pagination'; import dashboardService from '@/services/dashboard.service'; import type { DateRange } from '@/services/dashboard.service'; -import userApi from '@/services/userApi'; // Components import { RequestsHeader } from './components/RequestsHeader'; @@ -58,7 +57,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { // Determine once - use this throughout instead of checking repeatedly const isDealer = userFilterType === 'DEALER'; - + // Helper to get filters for API - excludes dealer-restricted filters // Since we know user type initially, this helper uses that knowledge const getFiltersForApi = useCallback(() => { @@ -70,7 +69,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { } return filterOptions; }, [filters, isDealer]); - + // Helper to calculate active filters count based on user type const calculateActiveFiltersCount = useCallback(() => { if (isDealer) { @@ -96,7 +95,6 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { const [backendStats, setBackendStats] = useState(null); // Stats from backend API const [departments, setDepartments] = useState([]); const [loadingDepartments, setLoadingDepartments] = useState(false); - const [allUsers, setAllUsers] = useState>([]); // Pagination (currentPage now in Redux) const [totalPages, setTotalPages] = useState(1); @@ -105,31 +103,31 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { // User search hooks const initiatorSearch = useUserSearch({ - allUsers, filterValue: filters.initiatorFilter, - onFilterChange: filters.setInitiatorFilter + onFilterChange: filters.setInitiatorFilter, + source: 'local' }); const approverSearch = useUserSearch({ - allUsers, filterValue: filters.approverFilter, - onFilterChange: filters.setApproverFilter + onFilterChange: filters.setApproverFilter, + source: 'local' }); // Fetch backend stats using dashboard API // OPTIMIZED: Uses backend stats API instead of fetching 100 records // Stats reflect all filters EXCEPT status - total stays stable when only status changes const fetchBackendStats = useCallback(async ( - statsDateRange?: DateRange, - statsStartDate?: Date, + statsDateRange?: DateRange, + statsStartDate?: Date, statsEndDate?: Date, - filtersWithoutStatus?: { - priority?: string; + filtersWithoutStatus?: { + priority?: string; templateType?: string; - department?: string; - initiator?: string; - approver?: string; - approverType?: 'current' | 'any'; + department?: string; + initiator?: string; + approver?: string; + approverType?: 'current' | 'any'; search?: 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 const filtersRef = useRef(filters); const fetchBackendStatsRef = useRef(fetchBackendStats); const getFiltersForApiRef = useRef(getFiltersForApi); - + // Update refs on each render useEffect(() => { filtersRef.current = filters; @@ -253,8 +237,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { // Initial fetch useEffect(() => { fetchDepartments(); - fetchUsers(); - }, [fetchDepartments, fetchUsers]); + }, [fetchDepartments]); // Fetch backend stats when filters change (except status filter) // 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, search: filters.searchTerm || undefined, }; - + // Only include priority, templateType, department, and slaCompliance if user is not a dealer if (!isDealer) { 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.slaComplianceFilter !== 'all') filtersWithoutStatus.slaCompliance = filters.slaComplianceFilter; } - + // Use 'all' if dateRange is 'all', otherwise use the selected dateRange or default to 'month' const statsDateRange = filters.dateRange === 'all' ? 'all' : (filters.dateRange || 'month'); - + fetchBackendStatsRef.current( - statsDateRange, - filters.customStartDate, + statsDateRange, + filters.customStartDate, filters.customEndDate, filtersWithoutStatus ); @@ -329,7 +312,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { customEndDate: filters.customEndDate, }); const hasInitialFetchRun = useRef(false); - + // Initial fetch on mount - use stored page from Redux useEffect(() => { const storedPage = filters.currentPage || 1; @@ -337,13 +320,13 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { hasInitialFetchRun.current = true; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Only on mount - + // Fetch when filters change useEffect(() => { if (!hasInitialFetchRun.current) return; - + const prev = prevFiltersRef.current; - const hasChanged = + const hasChanged = prev.searchTerm !== filters.searchTerm || prev.statusFilter !== filters.statusFilter || prev.priorityFilter !== filters.priorityFilter || @@ -356,13 +339,13 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { prev.dateRange !== filters.dateRange || prev.customStartDate !== filters.customStartDate || prev.customEndDate !== filters.customEndDate; - + if (!hasChanged) return; - + const timeoutId = setTimeout(() => { filters.setCurrentPage(1); fetchRequests(1); - + prevFiltersRef.current = { searchTerm: filters.searchTerm, statusFilter: filters.statusFilter, @@ -406,7 +389,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { // Transform requests const convertedRequests = useMemo(() => transformRequests(apiRequests), [apiRequests]); - + // Calculate stats - Use backend stats API (OPTIMIZED) const stats = useMemo(() => { // Use backend stats if available @@ -421,38 +404,38 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { closed: backendStats.closed || 0 }; } - + // Fallback: calculate from current page (less accurate, but works during initial load) - const pending = convertedRequests.filter((r: any) => { - const status = (r.status || '').toString().toLowerCase(); - return status === 'pending' || status === 'in-progress'; - }).length; + const pending = convertedRequests.filter((r: any) => { + const status = (r.status || '').toString().toLowerCase(); + return status === 'pending' || status === 'in-progress'; + }).length; const paused = convertedRequests.filter((r: any) => { const status = (r.status || '').toString().toLowerCase(); return status === 'paused'; }).length; - const approved = convertedRequests.filter((r: any) => { - const status = (r.status || '').toString().toLowerCase(); - return status === 'approved'; - }).length; - const rejected = convertedRequests.filter((r: any) => { - const status = (r.status || '').toString().toLowerCase(); - return status === 'rejected'; - }).length; - const closed = convertedRequests.filter((r: any) => { - const status = (r.status || '').toString().toLowerCase(); - return status === 'closed'; - }).length; - - return { + const approved = convertedRequests.filter((r: any) => { + const status = (r.status || '').toString().toLowerCase(); + return status === 'approved'; + }).length; + const rejected = convertedRequests.filter((r: any) => { + const status = (r.status || '').toString().toLowerCase(); + return status === 'rejected'; + }).length; + const closed = convertedRequests.filter((r: any) => { + const status = (r.status || '').toString().toLowerCase(); + return status === 'closed'; + }).length; + + return { total: totalRecords > 0 ? totalRecords : convertedRequests.length, - pending, + pending, paused, - approved, - rejected, - draft: 0, - closed - }; + approved, + rejected, + draft: 0, + closed + }; }, [backendStats, totalRecords, convertedRequests]); return ( @@ -467,8 +450,8 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { /> {/* Stats */} - { filters.setStatusFilter(status); }} diff --git a/src/pages/Requests/hooks/useUserSearch.ts b/src/pages/Requests/hooks/useUserSearch.ts index 78a66d4..dc473a6 100644 --- a/src/pages/Requests/hooks/useUserSearch.ts +++ b/src/pages/Requests/hooks/useUserSearch.ts @@ -4,30 +4,44 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import type { User } from '../types/requests.types'; +import { userApi } from '@/services/userApi'; interface UseUserSearchOptions { - allUsers: User[]; filterValue: string; 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 [searchResults, setSearchResults] = useState([]); const [showResults, setShowResults] = useState(false); const [selectedUser, setSelectedUser] = useState(null); + const [searching, setSearching] = useState(false); const searchTimer = useRef(null); - // Initialize selected user from filter value + // Initialize selected user details if we only have the ID (filterValue) useEffect(() => { - if (filterValue !== 'all' && allUsers.length > 0) { - const user = allUsers.find(u => u.userId === filterValue); - if (user) { - setSelectedUser(user); - setSearchQuery(user.displayName || user.email); + async function fetchUserDetail() { + if (filterValue !== 'all' && !selectedUser) { + try { + // Fetch specific user details by ID + 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 useEffect(() => { @@ -51,17 +65,22 @@ export function useUserSearch({ allUsers, filterValue, onFilterChange }: UseUser return; } - searchTimer.current = setTimeout(() => { - const searchLower = query.toLowerCase().trim(); - const filtered = allUsers.filter((user) => { - const email = (user.email || '').toLowerCase(); - const displayName = (user.displayName || '').toLowerCase(); - return email.includes(searchLower) || displayName.includes(searchLower); - }); - setSearchResults(filtered.slice(0, 10)); - setShowResults(filtered.length > 0); - }, 300); - }, [allUsers]); + searchTimer.current = setTimeout(async () => { + setSearching(true); + try { + const response = await userApi.searchUsers(query.trim(), 10, source); + const users = response.data?.data || []; + setSearchResults(users); + setShowResults(users.length > 0); + } catch (err) { + console.error('Search API failed:', err); + setSearchResults([]); + setShowResults(false); + } finally { + setSearching(false); + } + }, 400); // Slightly longer debounce for API calls + }, [source]); const handleSelect = useCallback((user: User) => { setSelectedUser(user); @@ -84,6 +103,7 @@ export function useUserSearch({ allUsers, filterValue, onFilterChange }: UseUser searchResults, showResults, selectedUser, + searching, handleSearch, handleSelect, handleClear, diff --git a/src/services/authApi.ts b/src/services/authApi.ts index 85ed810..66a07e7 100644 --- a/src/services/authApi.ts +++ b/src/services/authApi.ts @@ -6,7 +6,7 @@ import axios, { AxiosInstance } from 'axios'; 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 const apiClient: AxiosInstance = axios.create({ @@ -25,16 +25,16 @@ apiClient.interceptors.request.use( // In production, cookies are sent automatically with withCredentials: true // No need to set Authorization header const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production'; - + if (!isProduction) { - // Development: Get token from localStorage and add to header - const token = TokenManager.getAccessToken(); - if (token) { - config.headers.Authorization = `Bearer ${token}`; + // Dev: Get token from localStorage and add to header + const token = TokenManager.getAccessToken(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } } - } - // Production: Cookies handle authentication automatically - + // Prod: Cookies handle authentication automatically + return config; }, (error) => { @@ -51,7 +51,7 @@ apiClient.interceptors.response.use( // Handle connection errors gracefully in development 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'; - + if (isDevelopment) { // 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.`); @@ -67,7 +67,7 @@ apiClient.interceptors.response.use( // If error is 401 and we haven't retried yet if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; - + const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production'; try { @@ -75,7 +75,7 @@ apiClient.interceptors.response.use( // In production: Cookie is sent automatically via withCredentials // In development: Send refresh token from localStorage const refreshToken = TokenManager.getRefreshToken(); - + // In production, refreshToken will be null but cookie will be sent // In development, we need the token in body if (!isProduction && !refreshToken) { @@ -90,14 +90,14 @@ apiClient.interceptors.response.use( const responseData = response.data.data || response.data; const accessToken = responseData.accessToken; - + // In production: Backend sets new httpOnly cookie, no token in response // In development: Token is in response, store it and add to header if (!isProduction && accessToken) { TokenManager.setAccessToken(accessToken); originalRequest.headers.Authorization = `Bearer ${accessToken}`; } - + // Retry the original request // In production: Cookie will be sent automatically return apiClient(originalRequest); @@ -156,7 +156,7 @@ export async function exchangeCodeForTokens( }, } ); - + // Check if response is an array (buffer issue) if (Array.isArray(response.data)) { 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.'); } - + const data = response.data as any; const result = data.data || data; - + // Store user data (always available) if (result.user) { TokenManager.setUserData(result.user); } - + // Store ID token if available (needed for Okta logout) if (result.idToken) { TokenManager.setIdToken(result.idToken); } - + // SECURITY: In production, tokens are ONLY in httpOnly cookies (not in response body) // In development, backend returns tokens for cross-port setup if (result.accessToken && result.refreshToken) { - // Development mode: Backend returned tokens, store them + // Dev mode: Backend returned tokens, store them TokenManager.setAccessToken(result.accessToken); 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 return result; @@ -211,15 +211,15 @@ export async function exchangeCodeForTokens( */ export async function refreshAccessToken(): Promise { const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production'; - + // In development, check for refresh token in localStorage if (!isProduction) { - const refreshToken = TokenManager.getRefreshToken(); - if (!refreshToken) { - throw new Error('No refresh token available'); + const refreshToken = TokenManager.getRefreshToken(); + if (!refreshToken) { + throw new Error('No refresh token available'); + } } - } - + // In production, httpOnly cookie with refresh token will be sent automatically // In development, we send the refresh token in the body const body = isProduction ? {} : { refreshToken: TokenManager.getRefreshToken() }; @@ -234,7 +234,7 @@ export async function refreshAccessToken(): Promise { TokenManager.setAccessToken(accessToken); return accessToken; } - + // In production mode, token is set via httpOnly cookie by the backend // Return a placeholder to indicate success if (isProduction && (data.success !== false)) { @@ -255,7 +255,7 @@ export async function getCurrentUser() { /** * 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() * We don't call it here to avoid double clearing */ diff --git a/src/services/tanflowAuth.ts b/src/services/tanflowAuth.ts index 7c3b08c..0b3c5fc 100644 --- a/src/services/tanflowAuth.ts +++ b/src/services/tanflowAuth.ts @@ -6,8 +6,8 @@ import { TokenManager } from '../utils/tokenManager'; import axios from 'axios'; -const TANFLOW_BASE_URL = import.meta.env.VITE_TANFLOW_BASE_URL || '{{IDP_DOMAIN}}/realms/RE'; -const TANFLOW_CLIENT_ID = import.meta.env.VITE_TANFLOW_CLIENT_ID || 'REFLOW'; +const TANFLOW_BASE_URL = import.meta.env.VITE_TANFLOW_BASE_URL || ''; +const TANFLOW_CLIENT_ID = import.meta.env.VITE_TANFLOW_CLIENT_ID || ''; const TANFLOW_REDIRECT_URI = `${window.location.origin}/login/callback`; /** @@ -63,7 +63,7 @@ export async function exchangeTanflowCodeForTokens( idToken: string; 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 { const response = await axios.post( @@ -112,7 +112,7 @@ export async function exchangeTanflowCodeForTokens( * Refresh access token using refresh token */ export async function refreshTanflowToken(): Promise { - 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(); if (!refreshToken) { diff --git a/src/services/userApi.ts b/src/services/userApi.ts index f867c69..705e987 100644 --- a/src/services/userApi.ts +++ b/src/services/userApi.ts @@ -24,8 +24,8 @@ export interface UserSummary { isActive?: boolean; } -export async function searchUsers(query: string, limit: number = 10) { - const res = await apiClient.get('/users/search', { params: { q: query, limit } }); +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, source } }); // ResponseHandler.success returns { success: true, data: array } return res; } @@ -66,11 +66,11 @@ export async function ensureUserExists(userData: { * @param role - Role to assign */ export async function assignRole( - email: string, + email: string, role: 'USER' | 'MANAGEMENT' | 'ADMIN' ) { - return await apiClient.post('/admin/users/assign-role', { - email, + return await apiClient.post('/admin/users/assign-role', { + email, role }); } @@ -90,8 +90,8 @@ export async function getUsersByRole( page: number = 1, limit: number = 10 ) { - return await apiClient.get('/admin/users/by-role', { - params: { role: role || 'ELEVATED', page, limit } + return await apiClient.get('/admin/users/by-role', { + params: { role: role || 'ELEVATED', page, limit } }); } @@ -102,6 +102,14 @@ export async function getRoleStatistics() { 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) */ @@ -111,8 +119,9 @@ export async function getAllUsers() { return res.data?.data?.users || []; } -export const userApi = { - searchUsers, +export const userApi = { + searchUsers, + getUserById, ensureUserExists, assignRole, updateUserRole, diff --git a/src/services/workflowApi.ts b/src/services/workflowApi.ts index 27e7c16..8f2d140 100644 --- a/src/services/workflowApi.ts +++ b/src/services/workflowApi.ts @@ -362,12 +362,12 @@ export async function getPauseDetails(requestId: 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`; } 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`; } @@ -404,7 +404,7 @@ function extractFilenameFromContentDisposition(contentDisposition: string | null } export async function downloadDocument(documentId: string): Promise { - 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 isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production'; @@ -449,7 +449,7 @@ export async function downloadDocument(documentId: string): Promise { } export async function downloadWorkNoteAttachment(attachmentId: string): Promise { - 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 isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production'; diff --git a/src/utils/sanitizer.ts b/src/utils/sanitizer.ts index 4b39c7e..2019035 100644 --- a/src/utils/sanitizer.ts +++ b/src/utils/sanitizer.ts @@ -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) sanitized = sanitized.replace(/<(meta|link|iframe|object|embed|applet)[^>]*>/gi, ''); + // 6. Explicitly remove tags to prevent HTML injection of links (VAPT compliance) + sanitized = sanitized.replace(/]*>([\s\S]*?)<\/a>/gi, '$1'); + return sanitized; } diff --git a/src/utils/socket.ts b/src/utils/socket.ts index 643d468..18323fe 100644 --- a/src/utils/socket.ts +++ b/src/utils/socket.ts @@ -12,23 +12,22 @@ export function getSocketBaseUrl(): string { if (baseUrl) { return baseUrl; } - + // Fallback: derive from VITE_API_BASE_URL by removing /api/v1 const apiBaseUrl = import.meta.env.VITE_API_BASE_URL; if (apiBaseUrl) { return apiBaseUrl.replace(/\/api\/v1\/?$/, ''); } - - // Development fallback - console.warn('[Socket] No VITE_BASE_URL or VITE_API_BASE_URL found, using localhost:5000'); - return 'http://localhost:5000'; + + // Dev fallback + return ''; } export function getSocket(baseUrl?: string): Socket { // Use provided baseUrl or get from environment const url = baseUrl || getSocketBaseUrl(); if (socket) return socket; - + socket = io(url, { withCredentials: true, transports: ['websocket', 'polling'], @@ -37,19 +36,19 @@ export function getSocket(baseUrl?: string): Socket { reconnectionDelay: 1000, reconnectionAttempts: 5 }); - + socket.on('connect', () => { // Socket connected }); - + socket.on('connect_error', (error) => { console.error('[Socket] Connection error:', error.message); }); - + socket.on('disconnect', (_reason) => { // Socket disconnected }); - + return socket; } diff --git a/src/utils/tokenManager.ts b/src/utils/tokenManager.ts index df04962..8e175c7 100644 --- a/src/utils/tokenManager.ts +++ b/src/utils/tokenManager.ts @@ -86,7 +86,7 @@ export class TokenManager { 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); } @@ -100,7 +100,7 @@ export class TokenManager { return null; } - // Development: Return from localStorage + // Dev: Return from localStorage return localStorage.getItem(ACCESS_TOKEN_KEY); } @@ -113,7 +113,7 @@ export class TokenManager { return; // No-op - rely on httpOnly cookies } - // Development only + // Dev only localStorage.setItem(REFRESH_TOKEN_KEY, token); } @@ -163,7 +163,7 @@ export class TokenManager { 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 try { sessionStorage.setItem('__logout_in_progress__', 'true'); @@ -193,7 +193,7 @@ export class TokenManager { return; } - // DEVELOPMENT MODE: Clear everything + // Dev MODE: Clear everything const authKeys = [ ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index d8abad8..ce2f39b 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -7,6 +7,8 @@ interface ImportMetaEnv { readonly VITE_APP_VERSION: string; readonly VITE_ENABLE_ANALYTICS: string; readonly VITE_ENABLE_DEBUG: string; + readonly VITE_TANFLOW_BASE_URL: string; + readonly VITE_TANFLOW_CLIENT_ID: string; } interface ImportMeta { diff --git a/vite.config.ts b/vite.config.ts index 85e509e..436204e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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/ export default defineConfig({ - plugins: [react(), suppressCssWarnings(), ensureChunkOrder()], + plugins: [react(), suppressCssWarnings(), ensureChunkOrder(), replaceAxiosLocalhost()], resolve: { alias: { '@': path.resolve(__dirname, './src'), @@ -78,7 +93,7 @@ export default defineConfig({ }, build: { outDir: 'dist', - sourcemap: true, + sourcemap: false, // CSS minification warning is harmless - it's a known issue with certain Tailwind class combinations // Re-enable minification with settings that preserve initialization order // 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', // Explicitly define chunk order - React must load before Radix UI 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 // Radix UI components try to access React before it's initialized // 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 // 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 if (id.includes('node_modules/@radix-ui')) { return 'radix-vendor'; @@ -172,7 +187,7 @@ export default defineConfig({ chunkSizeWarningLimit: 1500, // Increased limit since we have manual chunks }, 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', }, optimizeDeps: {