refactor(js): extract shared getCsrfToken util (#475)
## What Same XSRF-TOKEN cookie-parsing snippet was duplicated inline in **12 files** (some as local `getCsrfToken()` copies, some inline `decodeURIComponent(...)` blocks). Extracted to single util: `resources/js/lib/csrf.ts`, with vitest coverage. ## Why Part of duplication-removal series — PRs that mostly delete code. ## Stats - App code: **-89 / +23 lines** - 12 files now import one shared util - New: `lib/csrf.ts` (8 lines) + `lib/csrf.test.ts` ## Checks - `bun run test` — 154 pass (3 new) - `bun run lint` — clean (1 pre-existing warning in chart.tsx) - `bun run format` — applied - `tsc --noEmit` — 59 pre-existing errors on main, same 59 on branch (none introduced)
This commit is contained in:
parent
e178f1b1bd
commit
aa3b811027
|
|
@ -34,6 +34,7 @@ import {
|
|||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { useLocale } from '@/hooks/use-locale';
|
||||
import { getCsrfToken } from '@/lib/csrf';
|
||||
import type { SharedData } from '@/types';
|
||||
import type { Account, AccountBalance } from '@/types/account';
|
||||
import {
|
||||
|
|
@ -158,12 +159,7 @@ export function BalancesModal({
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-XSRF-TOKEN': decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
),
|
||||
'X-XSRF-TOKEN': getCsrfToken(),
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
|
|
@ -202,12 +198,7 @@ export function BalancesModal({
|
|||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
),
|
||||
'X-XSRF-TOKEN': getCsrfToken(),
|
||||
Accept: 'application/json',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { getCsrfToken } from '@/lib/csrf';
|
||||
import { SharedData } from '@/types';
|
||||
import { Account } from '@/types/account';
|
||||
import { __ } from '@/utils/i18n';
|
||||
|
|
@ -87,12 +88,7 @@ export function CreateAccountDialog({
|
|||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
),
|
||||
'X-XSRF-TOKEN': getCsrfToken(),
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { decrypt, importKey } from '@/lib/crypto';
|
||||
import { getCsrfToken } from '@/lib/csrf';
|
||||
import { getStoredKey } from '@/lib/key-storage';
|
||||
import type { Account, LoanDetail, RealEstateDetail } from '@/types/account';
|
||||
import { __ } from '@/utils/i18n';
|
||||
|
|
@ -165,12 +166,7 @@ export function EditAccountDialog({
|
|||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
),
|
||||
'X-XSRF-TOKEN': getCsrfToken(),
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
loadBalanceImportConfig,
|
||||
saveBalanceImportConfig,
|
||||
} from '@/lib/balance-import-config-storage';
|
||||
import { getCsrfToken } from '@/lib/csrf';
|
||||
import {
|
||||
autoDetectDateFormat,
|
||||
parseAmount,
|
||||
|
|
@ -425,12 +426,7 @@ export function ImportBalancesDrawer({
|
|||
const BATCH_SIZE = 50;
|
||||
let processedCount = 0;
|
||||
|
||||
const xsrfToken = decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
);
|
||||
const xsrfToken = getCsrfToken();
|
||||
|
||||
for (let i = 0; i < state.balances.length; i += BATCH_SIZE) {
|
||||
const batch = state.balances.slice(i, i + BATCH_SIZE);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { getCsrfToken } from '@/lib/csrf';
|
||||
import type { SharedData } from '@/types';
|
||||
import {
|
||||
type Account,
|
||||
|
|
@ -131,12 +132,7 @@ export function UpdateBalanceDialog({
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-XSRF-TOKEN': decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
),
|
||||
'X-XSRF-TOKEN': getCsrfToken(),
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { StepHeader } from '@/components/onboarding/step-header';
|
|||
import { ConnectAccountInline } from '@/components/open-banking/connect-account-inline';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CreatedAccount } from '@/hooks/use-onboarding-state';
|
||||
import { getCsrfToken } from '@/lib/csrf';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type SharedData } from '@/types';
|
||||
import { formatCurrency } from '@/utils/currency';
|
||||
|
|
@ -128,12 +129,7 @@ export function StepCreateAccount({
|
|||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
),
|
||||
'X-XSRF-TOKEN': getCsrfToken(),
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
|
@ -262,12 +258,7 @@ export function StepCreateAccount({
|
|||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-XSRF-TOKEN': decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
),
|
||||
'X-XSRF-TOKEN': getCsrfToken(),
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { getCsrfToken } from '@/lib/csrf';
|
||||
import type {
|
||||
BankingConnection,
|
||||
EnableBankingInstitution,
|
||||
|
|
@ -81,15 +82,6 @@ interface ConnectAccountDialogProps {
|
|||
|
||||
type Step = 'country' | 'bank' | 'confirm';
|
||||
|
||||
function getCsrfToken(): string {
|
||||
return decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
);
|
||||
}
|
||||
|
||||
export function ConnectAccountDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useWebHaptics } from '@/hooks/use-web-haptics';
|
||||
import { getCsrfToken } from '@/lib/csrf';
|
||||
import type {
|
||||
BankingConnection,
|
||||
EnableBankingInstitution,
|
||||
|
|
@ -70,15 +71,6 @@ const COINBASE_INSTITUTION: EnableBankingInstitution = {
|
|||
|
||||
type Step = 'country' | 'bank' | 'confirm';
|
||||
|
||||
function getCsrfToken(): string {
|
||||
return decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
);
|
||||
}
|
||||
|
||||
interface ConnectAccountInlineProps {
|
||||
onBack: () => void;
|
||||
connections?: BankingConnection[];
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { getCsrfToken } from '@/lib/csrf';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getLabelColorClasses, LABEL_COLORS, type Label } from '@/types/label';
|
||||
import { __ } from '@/utils/i18n';
|
||||
|
|
@ -101,12 +102,7 @@ export function LabelCombobox({
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-XSRF-TOKEN': decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
),
|
||||
'X-XSRF-TOKEN': getCsrfToken(),
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { Textarea } from '@/components/ui/textarea';
|
|||
import { useSyncContext } from '@/contexts/sync-context';
|
||||
import { useLocale } from '@/hooks/use-locale';
|
||||
import { decrypt, importKey } from '@/lib/crypto';
|
||||
import { getCsrfToken } from '@/lib/csrf';
|
||||
import { getStoredKey } from '@/lib/key-storage';
|
||||
import { evaluateRulesForNewTransaction } from '@/lib/rule-engine';
|
||||
import { appendNoteIfNotPresent } from '@/lib/utils';
|
||||
|
|
@ -281,12 +282,7 @@ export function EditTransactionDialog({
|
|||
transactionDateStr: string,
|
||||
transactionAmount: number,
|
||||
) {
|
||||
const xsrfToken = decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
);
|
||||
const xsrfToken = getCsrfToken();
|
||||
|
||||
try {
|
||||
// Fetch balances from backend
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from '@/components/ui/drawer';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { importKey } from '@/lib/crypto';
|
||||
import { getCsrfToken } from '@/lib/csrf';
|
||||
import {
|
||||
autoDetectColumns,
|
||||
calculateBalancesFromTransactions,
|
||||
|
|
@ -633,12 +634,7 @@ export function ImportTransactionsDrawer({
|
|||
|
||||
if (balancesToImport.size > 0) {
|
||||
try {
|
||||
const xsrfToken = decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
);
|
||||
const xsrfToken = getCsrfToken();
|
||||
|
||||
const balanceRecords = Array.from(balancesToImport.entries());
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { getCsrfToken } from './csrf';
|
||||
|
||||
describe('getCsrfToken', () => {
|
||||
afterEach(() => {
|
||||
document.cookie = 'XSRF-TOKEN=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
||||
document.cookie = 'other=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
||||
});
|
||||
|
||||
it('returns the decoded XSRF-TOKEN cookie value', () => {
|
||||
document.cookie = 'XSRF-TOKEN=abc%3D%3D123';
|
||||
|
||||
expect(getCsrfToken()).toBe('abc==123');
|
||||
});
|
||||
|
||||
it('finds the token among other cookies', () => {
|
||||
document.cookie = 'other=value';
|
||||
document.cookie = 'XSRF-TOKEN=token';
|
||||
|
||||
expect(getCsrfToken()).toBe('token');
|
||||
});
|
||||
|
||||
it('returns an empty string when the cookie is missing', () => {
|
||||
expect(getCsrfToken()).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export function getCsrfToken(): string {
|
||||
return decodeURIComponent(
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1] || '',
|
||||
);
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
import { Spinner } from '@/components/ui/spinner';
|
||||
import AppLayout from '@/layouts/app-layout';
|
||||
import SettingsLayout from '@/layouts/settings/layout';
|
||||
import { getCsrfToken } from '@/lib/csrf';
|
||||
import type { SharedData } from '@/types';
|
||||
import type { BankingConnection } from '@/types/banking';
|
||||
import { __ } from '@/utils/i18n';
|
||||
|
|
@ -37,11 +38,6 @@ import {
|
|||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
function getCsrfToken(): string {
|
||||
const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);
|
||||
return match ? decodeURIComponent(match[1]) : '';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
connections: BankingConnection[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue