added csrf token
This commit is contained in:
parent
db816b4879
commit
e488062aed
@ -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}
|
||||
|
||||
@ -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 > {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>
|
||||
|
||||
@ -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]">
|
||||
|
||||
@ -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" : ""}>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user