added csrf token

This commit is contained in:
Malini 2025-12-19 12:22:03 +05:30
parent db816b4879
commit e488062aed
6 changed files with 194 additions and 170 deletions

View File

@ -143,7 +143,7 @@ const AdminDashboard = () => {
</div> </div>
<div className="w-full"> <div className="w-full">
<div className="mx-auto flex w-full max-w-[1280px] items-center gap-4"> <div className="cursor-pointer mx-auto flex w-full max-w-[1280px] items-center gap-4">
<StatCard <StatCard
label="Total Establishments" label="Total Establishments"
value={loading ? '--' : summary.total_establishments} value={loading ? '--' : summary.total_establishments}

View File

@ -590,108 +590,81 @@ const ValidationReview = () => {
const checked = e.target.checked; const checked = e.target.checked;
setShowPreviousNext(checked); setShowPreviousNext(checked);
if (checked) { // Uncheck reset
try { if (!checked) {
setFetchingNextQuarter(true);
const currentQuarter = quarterPeriods?.current_quarter || "Q4";
const currentYear = quarterPeriods?.current_year || 2025;
const establishmentId = submissionData?.establishment?.id;
const productId = submissionData?.products?.[0]?.product?.id;
if (!establishmentId || !currentQuarter || !currentYear || !productId) {
console.error("Missing required parameters:", {
establishmentId,
currentQuarter,
currentYear,
productId,
});
return;
}
// API call
const response = await getBeforePreviousData(
establishmentId,
currentQuarter,
currentYear,
productId
);
let mappedDataMap = {};
if (response?.data) {
submissionData.products.forEach((product) => {
const pid = product?.product?.id;
if (pid) {
mappedDataMap[pid] = {
forecast_quantity_period_one:
response.data.current_quantity_period_one ?? 0,
forecast_quantity_period_two:
response.data.current_quantity_period_two ?? 0,
forecast_quantity_period_three:
response.data.current_quantity_period_three ?? 0,
forecast_cost_period_one:
response.data.current_cost_period_one ?? 0,
forecast_cost_period_two:
response.data.current_cost_period_two ?? 0,
forecast_cost_period_three:
response.data.current_cost_period_three ?? 0,
};
}
});
} else {
// if no backend data
submissionData.products.forEach((product) => {
const pid = product?.product?.id;
if (pid) {
mappedDataMap[pid] = {
forecast_quantity_period_one: 0,
forecast_quantity_period_two: 0,
forecast_quantity_period_three: 0,
forecast_cost_period_one: 0,
forecast_cost_period_two: 0,
forecast_cost_period_three: 0,
};
}
});
}
setNextQuarterData(mappedDataMap);
// Calculate previous-previous quarter UI labels
const q = Number(currentQuarter.replace("Q", ""));
let historicalQuarter = `Q${q - 2}`;
let historicalYear = currentYear;
if (q - 2 <= 0) {
historicalQuarter = `Q${4 + (q - 2)}`;
historicalYear = currentYear - 1;
}
setAdditionalForecastQuarter(historicalQuarter);
setAdditionalForecastYear(historicalYear);
setAdditionalForecastMonths(
getMonthsForQuarter(historicalQuarter, historicalYear)
);
} catch (error) {
console.error("Error fetching before-previous data:", error);
} finally {
setFetchingNextQuarter(false);
}
} else {
// On uncheck, clear values
setNextQuarterData({}); setNextQuarterData({});
setAdditionalForecastQuarter(""); setAdditionalForecastQuarter("");
setAdditionalForecastYear(""); setAdditionalForecastYear("");
setAdditionalForecastMonths(["", "", ""]); setAdditionalForecastMonths(["", "", ""]);
return;
}
try {
setFetchingNextQuarter(true);
const establishmentId = submissionData?.establishment?.id;
const productId = submissionData?.products?.[0]?.product?.id;
if (!establishmentId || !productId) {
console.error("Missing required params");
return;
}
// API Call
const response = await getBeforePreviousData(
establishmentId,
quarterPeriods?.current_quarter,
quarterPeriods?.current_year,
productId
);
let mappedDataMap = {};
submissionData?.products?.forEach((product) => {
const pid = product?.product?.id;
if (!pid) return;
mappedDataMap[pid] = {
forecast_quantity_period_one:
response?.data?.forecast_quantity_period_one ?? 0,
forecast_quantity_period_two:
response?.data?.forecast_quantity_period_two ?? 0,
forecast_quantity_period_three:
response?.data?.forecast_quantity_period_three ?? 0,
forecast_cost_period_one:
response?.data?.forecast_cost_period_one ?? 0,
forecast_cost_period_two:
response?.data?.forecast_cost_period_two ?? 0,
forecast_cost_period_three:
response?.data?.forecast_cost_period_three ?? 0,
};
});
setNextQuarterData(mappedDataMap);
// DIRECTLY FROM API NO CALCULATION
const qp = response?.data?.quarter_periods;
setAdditionalForecastQuarter(qp?.forecast_quarter);
setAdditionalForecastYear(qp?.forecast_year);
setAdditionalForecastMonths([
qp?.forecast_month?.forecast_period_one,
qp?.forecast_month?.forecast_period_two,
qp?.forecast_month?.forecast_period_three,
]);
} catch (error) {
console.error("Error fetching forecast data:", error);
} finally {
setFetchingNextQuarter(false);
} }
}; };
// Helper to get forecast data for a specific product // Helper to get forecast data for a specific product
const getCurrentYearForecastData = (productId) => { const getCurrentYearForecastData = (productId) => {
if (!showPreviousNext || !productId) { if (!showPreviousNext || !productId) {
@ -920,7 +893,7 @@ const getQuarterYearLabel = (quarter, year) => {
const previousYear = quarterPeriods?.previous_year || 2025; const previousYear = quarterPeriods?.previous_year || 2025;
const currentQuarter = quarterPeriods?.current_quarter || 'Q4'; const currentQuarter = quarterPeriods?.current_quarter || 'Q4';
const currentYear = quarterPeriods?.current_year || 2025; const currentYear = quarterPeriods?.current_year || 2025;
const forecastQuarter = quarterPeriods?.forecast_quarter || 'Q1'; const forecastQuarter = quarterPeriods?.forecast_quarter || '-';
const forecastYear = quarterPeriods?.forecast_year || 2026; const forecastYear = quarterPeriods?.forecast_year || 2026;
// Get dynamic month information from API response // Get dynamic month information from API response
@ -962,8 +935,6 @@ const additionalForecastLabel =
? `${additionalForecastQuarter} ${additionalForecastYear}` ? `${additionalForecastQuarter} ${additionalForecastYear}`
: ''; : '';
const establishmentDetails = [ const establishmentDetails = [
{ {
label: 'Establishment Name', label: 'Establishment Name',
@ -1140,7 +1111,7 @@ const additionalForecastLabel =
<select <select
value={threshold} value={threshold}
onChange={(e) => setThreshold(Number(e.target.value))} onChange={(e) => setThreshold(Number(e.target.value))}
className="border border-[#D1D5DB] rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-[#92722A]" className="border border-[#D1D5DB] rounded-md px-3 py-1 text-sm "
> >
<option value="5">5%</option> <option value="5">5%</option>
<option value="10">10%</option> <option value="10">10%</option>
@ -1154,6 +1125,7 @@ const additionalForecastLabel =
<option value="80">80%</option> <option value="80">80%</option>
<option value="90">90%</option> <option value="90">90%</option>
<option value="100">100%</option> <option value="100">100%</option>
<option value="101">Above 100%</option>
</select> </select>
</div> </div>
)} )}
@ -1175,11 +1147,14 @@ const additionalForecastLabel =
</label> </label>
</div> </div>
</div> </div>
{varianceHighlighting && ( {varianceHighlighting && (
<div className="text-xs text-[#6B7280]"> <div className="text-xs text-[#6B7280]">
Cells with variance &gt; {threshold}% will be highlighted Cells with variance{" "}
</div> {threshold <= 100 ? `${threshold}%` : "> 100%"} will be highlighted
)} </div>
)}
</div> </div>
<table className="min-w-full text-sm text-left border-collapse"> <table className="min-w-full text-sm text-left border-collapse">
<thead> <thead>

View File

@ -444,7 +444,7 @@ React.useEffect(() => {
<div className="px-6 pt-6 pb-5 space-y-4"> <div className="px-6 pt-6 pb-5 space-y-4">
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<h2 className="text-[18px] font-medium text-[#232528]"> <h2 className="text-[18px] font-medium text-[#232528]">
Manage Submissions <span className="text-[#7A7F87]">({filtered.length})</span> Manage Submissions <span className="text-[#232528]">({filtered.length})</span>
</h2> </h2>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<div className="relative w-[320px]"> <div className="relative w-[320px]">

View File

@ -30,26 +30,27 @@ const ToggleableInput = ({
error ? "border-red-500" : "border-[#92722A]" error ? "border-red-500" : "border-[#92722A]"
} focus:border-[#92722A] focus:ring-0 px-4 pr-11 text-sm bg-white`} } focus:border-[#92722A] focus:ring-0 px-4 pr-11 text-sm bg-white`}
/> />
<button {type === 'password' && (
type="button" <button
className="absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer" type="button"
onClick={toggleShow} className="absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer"
aria-label="Toggle visibility" onClick={toggleShow}
> aria-label="Toggle visibility"
{show ? ( >
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-[#92722A]" fill="none" viewBox="0 0 24 24" stroke="currentColor"> {show ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z" /> <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-[#92722A]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<circle cx="12" cy="12" r="3" strokeWidth="2" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z" />
</svg> <circle cx="12" cy="12" r="3" strokeWidth="2" />
) : ( </svg>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-[#92722A]" fill="none" viewBox="0 0 24 24" stroke="currentColor"> ) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-[#92722A]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
d="M13.875 18.825A10.05 10.05 0 0 1 12 19c-5.523 0-10-4.477-10-10a9.96 9.96 0 0 1 2.122-6.21m3.086-1.955A9.953 9.953 0 0 1 12 3c5.523 0 10 4.477 10 10 0 1.591-.372 3.093-1.034 4.432M9.88 9.88a3 3 0 1 0 4.24 4.24" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m3 3 18 18" /> d="M13.875 18.825A10.05 10.05 0 0 1 12 19c-5.523 0-10-4.477-10-10a9.96 9.96 0 0 1 2.122-6.21m3.086-1.955A9.953 9.953 0 0 1 12 3c5.523 0 10 4.477 10 10 0 1.591-.372 3.093-1.034 4.432M9.88 9.88a3 3 0 1 0 4.24 4.24" />
</svg> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m3 3 18 18" />
</svg>
)} )}
</button> </button>
)}
</div> </div>
{error && <p className="text-xs text-red-600 mt-1">{error}</p>} {error && <p className="text-xs text-red-600 mt-1">{error}</p>}
</div> </div>
@ -220,49 +221,50 @@ if (newPassword !== confirmPassword) {
</div> </div>
<ToggleableInput <ToggleableInput
label="Verification Code" label="Verification Code"
required required
value={otp} value={otp}
onChange={(e) => setOtp(e.target.value)} onChange={(e) => setOtp(e.target.value)}
show={showOtp} placeholder="Enter 6-digit code"
toggleShow={() => setShowOtp((v) => !v)} error={otpError}
placeholder="Enter 6-digit code" autoComplete="one-time-code"
error={otpError} type="text"
autoComplete="one-time-code" />
/>
<ToggleableInput <ToggleableInput
label="New Password" label="New Password"
required required
value={newPassword} value={newPassword}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
setNewPassword(value); setNewPassword(value);
const passwordError = validatePassword(value); const passwordError = validatePassword(value);
setNewPasswordError(passwordError); setNewPasswordError(passwordError);
}} }}
autoComplete="new-password" type="password"
show={showNewPassword} autoComplete="new-password"
toggleShow={() => setShowNewPassword((v) => !v)} show={showNewPassword}
placeholder="Enter new password" toggleShow={() => setShowNewPassword((v) => !v)}
error={newPasswordError} placeholder="Enter new password"
/> error={newPasswordError}
/>
<ToggleableInput <ToggleableInput
label="Confirm New Password" label="Confirm New Password"
required required
value={confirmPassword} value={confirmPassword}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
setConfirmPassword(value); setConfirmPassword(value);
if (newPassword === value) setConfirmPasswordError(""); if (newPassword === value) setConfirmPasswordError("");
}} }}
autoComplete="new-password" type="password"
show={showConfirmPassword} autoComplete="new-password"
toggleShow={() => setShowConfirmPassword((v) => !v)} show={showConfirmPassword}
placeholder="Re-enter new password" toggleShow={() => setShowConfirmPassword((v) => !v)}
error={confirmPasswordError} placeholder="Re-enter new password"
/> error={confirmPasswordError}
/>
{newPassword && ( {newPassword && (
<ul className="text-xs text-gray-600 list-disc pl-5 space-y-1"> <ul className="text-xs text-gray-600 list-disc pl-5 space-y-1">
<li className={/.{8,}/.test(newPassword) ? "text-green-600" : ""}> <li className={/.{8,}/.test(newPassword) ? "text-green-600" : ""}>

View File

@ -1,12 +1,29 @@
import apiClient from '@/services/api/apiClient'; import apiClient, { fetchCsrfToken } from './apiClient';
const withCsrf = async (requestFn, ...args) => {
try {
await fetchCsrfToken();
return await requestFn(...args);
} catch (error) {
if (error.response?.status === 403 && error.response?.data?.message?.includes('CSRF')) {
// If we get a CSRF error, try to refresh the token and retry once
await fetchCsrfToken();
return await requestFn(...args);
}
throw error;
}
};
export const getRequest = (url, config = {}) => apiClient.get(url, config); export const getRequest = (url, config = {}) => apiClient.get(url, config);
export const postRequest = (url, data, config = {}) => apiClient.post(url, data, config); export const postRequest = (url, data, config = {}) =>
withCsrf(apiClient.post.bind(apiClient), url, data, config);
export const putRequest = (url, data, config = {}) => apiClient.put(url, data, config); export const putRequest = (url, data, config = {}) =>
withCsrf(apiClient.put.bind(apiClient), url, data, config);
export const deleteRequest = (url, config = {}) => apiClient.delete(url, config); export const deleteRequest = (url, config = {}) =>
withCsrf(apiClient.delete.bind(apiClient), url, config);
export default { export default {
getRequest, getRequest,

View File

@ -4,6 +4,9 @@ const baseURL = import.meta.env.VITE_API_BASE_URL;
const appSignature = import.meta.env.VITE_API_APP_SIGNATURE; const appSignature = import.meta.env.VITE_API_APP_SIGNATURE;
const fallbackToken = import.meta.env.VITE_API_TOKEN; const fallbackToken = import.meta.env.VITE_API_TOKEN;
// Store CSRF token in memory
let csrfToken = null;
const apiClient = axios.create({ const apiClient = axios.create({
baseURL, baseURL,
withCredentials: true, withCredentials: true,
@ -14,8 +17,34 @@ const apiClient = axios.create({
}, },
}); });
/**
* Fetches and stores a new CSRF token from the server
* @returns {Promise<string>} The CSRF token
*/
export const fetchCsrfToken = async () => {
try {
const response = await apiClient.get('/csrf-token');
csrfToken = response.data.csrfToken;
return csrfToken;
} catch (error) {
console.error('Failed to fetch CSRF token:', error);
throw error;
}
};
apiClient.interceptors.request.use((config) => { apiClient.interceptors.request.use((config) => {
config.headers = config.headers ?? {}; config.headers = config.headers ?? {};
// Skip auth for CSRF token endpoint
if (config.url === '/csrf-token') {
return config;
}
// Add CSRF token for write operations
if (csrfToken && ['post', 'put', 'patch', 'delete'].includes(config.method?.toLowerCase())) {
config.headers['X-CSRF-Token'] = csrfToken;
}
if (config.skipAuth) { if (config.skipAuth) {
return config; return config;
} }
@ -34,6 +63,7 @@ apiClient.interceptors.request.use((config) => {
// config.headers.Authorization = `Bearer ${token}`; // config.headers.Authorization = `Bearer ${token}`;
// } // }
// } // }
return config; return config;
}); });