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 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
label="Total Establishments"
value={loading ? '--' : summary.total_establishments}

View File

@ -590,108 +590,81 @@ const ValidationReview = () => {
const checked = e.target.checked;
setShowPreviousNext(checked);
if (checked) {
try {
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
// Uncheck reset
if (!checked) {
setNextQuarterData({});
setAdditionalForecastQuarter("");
setAdditionalForecastYear("");
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
const getCurrentYearForecastData = (productId) => {
if (!showPreviousNext || !productId) {
@ -920,7 +893,7 @@ const getQuarterYearLabel = (quarter, year) => {
const previousYear = quarterPeriods?.previous_year || 2025;
const currentQuarter = quarterPeriods?.current_quarter || 'Q4';
const currentYear = quarterPeriods?.current_year || 2025;
const forecastQuarter = quarterPeriods?.forecast_quarter || 'Q1';
const forecastQuarter = quarterPeriods?.forecast_quarter || '-';
const forecastYear = quarterPeriods?.forecast_year || 2026;
// Get dynamic month information from API response
@ -962,8 +935,6 @@ const additionalForecastLabel =
? `${additionalForecastQuarter} ${additionalForecastYear}`
: '';
const establishmentDetails = [
{
label: 'Establishment Name',
@ -1140,7 +1111,7 @@ const additionalForecastLabel =
<select
value={threshold}
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="10">10%</option>
@ -1154,6 +1125,7 @@ const additionalForecastLabel =
<option value="80">80%</option>
<option value="90">90%</option>
<option value="100">100%</option>
<option value="101">Above 100%</option>
</select>
</div>
)}
@ -1175,11 +1147,14 @@ const additionalForecastLabel =
</label>
</div>
</div>
{varianceHighlighting && (
<div className="text-xs text-[#6B7280]">
Cells with variance &gt; {threshold}% will be highlighted
</div>
)}
{varianceHighlighting && (
<div className="text-xs text-[#6B7280]">
Cells with variance{" "}
{threshold <= 100 ? `${threshold}%` : "> 100%"} will be highlighted
</div>
)}
</div>
<table className="min-w-full text-sm text-left border-collapse">
<thead>

View File

@ -444,7 +444,7 @@ React.useEffect(() => {
<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">
<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>
<div className="flex flex-wrap gap-3">
<div className="relative w-[320px]">

View File

@ -30,26 +30,27 @@ const ToggleableInput = ({
error ? "border-red-500" : "border-[#92722A]"
} focus:border-[#92722A] focus:ring-0 px-4 pr-11 text-sm bg-white`}
/>
<button
type="button"
className="absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer"
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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z" />
<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"
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" d="m3 3 18 18" />
</svg>
)}
</button>
{type === 'password' && (
<button
type="button"
className="absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer"
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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z" />
<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"
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" d="m3 3 18 18" />
</svg>
)}
</button>
)}
</div>
{error && <p className="text-xs text-red-600 mt-1">{error}</p>}
</div>
@ -220,49 +221,50 @@ if (newPassword !== confirmPassword) {
</div>
<ToggleableInput
label="Verification Code"
required
value={otp}
onChange={(e) => setOtp(e.target.value)}
show={showOtp}
toggleShow={() => setShowOtp((v) => !v)}
placeholder="Enter 6-digit code"
error={otpError}
autoComplete="one-time-code"
/>
label="Verification Code"
required
value={otp}
onChange={(e) => setOtp(e.target.value)}
placeholder="Enter 6-digit code"
error={otpError}
autoComplete="one-time-code"
type="text"
/>
<ToggleableInput
label="New Password"
required
value={newPassword}
onChange={(e) => {
const value = e.target.value;
setNewPassword(value);
const passwordError = validatePassword(value);
setNewPasswordError(passwordError);
}}
autoComplete="new-password"
show={showNewPassword}
toggleShow={() => setShowNewPassword((v) => !v)}
placeholder="Enter new password"
error={newPasswordError}
/>
<ToggleableInput
label="New Password"
required
value={newPassword}
onChange={(e) => {
const value = e.target.value;
setNewPassword(value);
const passwordError = validatePassword(value);
setNewPasswordError(passwordError);
}}
type="password"
autoComplete="new-password"
show={showNewPassword}
toggleShow={() => setShowNewPassword((v) => !v)}
placeholder="Enter new password"
error={newPasswordError}
/>
<ToggleableInput
label="Confirm New Password"
required
value={confirmPassword}
onChange={(e) => {
const value = e.target.value;
setConfirmPassword(value);
if (newPassword === value) setConfirmPasswordError("");
}}
autoComplete="new-password"
show={showConfirmPassword}
toggleShow={() => setShowConfirmPassword((v) => !v)}
placeholder="Re-enter new password"
error={confirmPasswordError}
/>
<ToggleableInput
label="Confirm New Password"
required
value={confirmPassword}
onChange={(e) => {
const value = e.target.value;
setConfirmPassword(value);
if (newPassword === value) setConfirmPasswordError("");
}}
type="password"
autoComplete="new-password"
show={showConfirmPassword}
toggleShow={() => setShowConfirmPassword((v) => !v)}
placeholder="Re-enter new password"
error={confirmPasswordError}
/>
{newPassword && (
<ul className="text-xs text-gray-600 list-disc pl-5 space-y-1">
<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 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 {
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 fallbackToken = import.meta.env.VITE_API_TOKEN;
// Store CSRF token in memory
let csrfToken = null;
const apiClient = axios.create({
baseURL,
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) => {
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) {
return config;
}
@ -34,6 +63,7 @@ apiClient.interceptors.request.use((config) => {
// config.headers.Authorization = `Bearer ${token}`;
// }
// }
return config;
});