fix(security): add csrfFetch wrapper and apply to all client-side mutating requests
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m52s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m48s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m4s
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m52s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m48s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m4s
Introduces `csrfFetch` (= `makeCsrfFetch(fetch)`) in cookies.ts as a drop-in fetch replacement that auto-injects X-XSRF-TOKEN on POST/PUT/PATCH/DELETE. Previously 8 call sites sent mutating requests without the CSRF header — annotation resize, comment POST/PATCH/DELETE, Geschichte CRUD, Stammbaum relationship creation, bulk-edit PATCH, and file upload — all would fail with CSRF_TOKEN_MISSING if the backend's cookie-based protection triggered. All 14 client-side mutating fetches now use csrfFetch; withCsrf/makeCsrfFetch remain in the API for injectable-fetch use cases (e.g. useTranscriptionBlocks). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ import PdfViewer from '$lib/document/viewer/PdfViewer.svelte';
|
|||||||
import { bulkTitleFromFilename } from '$lib/document/filename';
|
import { bulkTitleFromFilename } from '$lib/document/filename';
|
||||||
import type { Tag } from '$lib/tag/TagInput.svelte';
|
import type { Tag } from '$lib/tag/TagInput.svelte';
|
||||||
import type { components } from '$lib/generated/api';
|
import type { components } from '$lib/generated/api';
|
||||||
import { withCsrf } from '$lib/shared/cookies';
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
|
|
||||||
type Person = components['schemas']['Person'];
|
type Person = components['schemas']['Person'];
|
||||||
|
|
||||||
@@ -184,10 +184,10 @@ async function saveUpload() {
|
|||||||
// FormData with per-chunk progress. Session cookie is sent automatically
|
// FormData with per-chunk progress. Session cookie is sent automatically
|
||||||
// by the browser for same-origin requests.
|
// by the browser for same-origin requests.
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await csrfFetch('/api/documents/quick-upload', {
|
||||||
'/api/documents/quick-upload',
|
method: 'POST',
|
||||||
withCsrf({ method: 'POST', body: formData })
|
body: formData
|
||||||
);
|
});
|
||||||
const body = await res.json().catch(() => ({ errors: [] }));
|
const body = await res.json().catch(() => ({ errors: [] }));
|
||||||
const errorFilenames = new Set<string>(
|
const errorFilenames = new Set<string>(
|
||||||
(body.errors ?? []).map((err: { filename: string }) => err.filename)
|
(body.errors ?? []).map((err: { filename: string }) => err.filename)
|
||||||
@@ -241,7 +241,7 @@ async function saveBulkEdit() {
|
|||||||
for (let i = 0; i < chunks.length; i++) {
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
const chunk = chunks[i];
|
const chunk = chunks[i];
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/documents/bulk', {
|
const res = await csrfFetch('/api/documents/bulk', {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ...dto, documentIds: chunk })
|
body: JSON.stringify({ ...dto, documentIds: chunk })
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import WhoWhenSection from '$lib/document/WhoWhenSection.svelte';
|
|||||||
import DescriptionSection from '$lib/document/DescriptionSection.svelte';
|
import DescriptionSection from '$lib/document/DescriptionSection.svelte';
|
||||||
import type { Tag } from '$lib/tag/TagInput.svelte';
|
import type { Tag } from '$lib/tag/TagInput.svelte';
|
||||||
import type { components } from '$lib/generated/api';
|
import type { components } from '$lib/generated/api';
|
||||||
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
import type { DatePrecision } from '$lib/shared/utils/documentDate';
|
import type { DatePrecision } from '$lib/shared/utils/documentDate';
|
||||||
|
|
||||||
type Person = components['schemas']['Person'];
|
type Person = components['schemas']['Person'];
|
||||||
@@ -86,7 +87,7 @@ async function handleFile(file: File) {
|
|||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
const res = await fetch(`/api/documents/${doc.id}/file`, {
|
const res = await csrfFetch(`/api/documents/${doc.id}/file`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
signal: controller.signal
|
signal: controller.signal
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import TranscribeCoachEmptyState from '$lib/shared/help/TranscribeCoachEmptyStat
|
|||||||
import type { PersonMention, TranscriptionBlockData } from '$lib/shared/types';
|
import type { PersonMention, TranscriptionBlockData } from '$lib/shared/types';
|
||||||
import { createBlockAutoSave } from '$lib/document/transcription/useBlockAutoSave.svelte';
|
import { createBlockAutoSave } from '$lib/document/transcription/useBlockAutoSave.svelte';
|
||||||
import { createBlockDragDrop } from '$lib/document/transcription/useBlockDragDrop.svelte';
|
import { createBlockDragDrop } from '$lib/document/transcription/useBlockDragDrop.svelte';
|
||||||
import { withCsrf } from '$lib/shared/cookies';
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
documentId: string;
|
documentId: string;
|
||||||
@@ -114,14 +114,11 @@ function handleDelete(blockId: string) {
|
|||||||
|
|
||||||
async function reorder(newOrder: string[]) {
|
async function reorder(newOrder: string[]) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await csrfFetch(`/api/documents/${documentId}/transcription-blocks/reorder`, {
|
||||||
`/api/documents/${documentId}/transcription-blocks/reorder`,
|
|
||||||
withCsrf({
|
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ blockIds: newOrder })
|
body: JSON.stringify({ blockIds: newOrder })
|
||||||
})
|
});
|
||||||
);
|
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const updated = await res.json();
|
const updated = await res.json();
|
||||||
for (const b of updated) {
|
for (const b of updated) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SvelteMap } from 'svelte/reactivity';
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
import type { PersonMention } from '$lib/shared/types';
|
import type { PersonMention } from '$lib/shared/types';
|
||||||
import { withCsrf } from '$lib/shared/cookies';
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
|
|
||||||
export type SaveState = 'idle' | 'saving' | 'saved' | 'fading' | 'error';
|
export type SaveState = 'idle' | 'saving' | 'saved' | 'fading' | 'error';
|
||||||
|
|
||||||
@@ -117,15 +117,12 @@ export function createBlockAutoSave({ saveFn, documentId }: Options) {
|
|||||||
for (const [blockId, text] of pendingTexts) {
|
for (const [blockId, text] of pendingTexts) {
|
||||||
const mentions = pendingMentions.get(blockId) ?? [];
|
const mentions = pendingMentions.get(blockId) ?? [];
|
||||||
clearDebounce(blockId);
|
clearDebounce(blockId);
|
||||||
void fetch(
|
void csrfFetch(`/api/documents/${documentId}/transcription-blocks/${blockId}`, {
|
||||||
`/api/documents/${documentId}/transcription-blocks/${blockId}`,
|
|
||||||
withCsrf({
|
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ text, mentionedPersons: mentions }),
|
body: JSON.stringify({ text, mentionedPersons: mentions }),
|
||||||
keepalive: true
|
keepalive: true
|
||||||
})
|
});
|
||||||
);
|
|
||||||
pendingTexts.delete(blockId);
|
pendingTexts.delete(blockId);
|
||||||
pendingMentions.delete(blockId);
|
pendingMentions.delete(blockId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import AnnotationLayer from '$lib/document/annotation/AnnotationLayer.svelte';
|
|||||||
import type { Annotation } from '$lib/shared/types';
|
import type { Annotation } from '$lib/shared/types';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import { parseBackendError, getErrorMessage } from '$lib/shared/errors';
|
import { parseBackendError, getErrorMessage } from '$lib/shared/errors';
|
||||||
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
|
|
||||||
type DrawRect = { x: number; y: number; width: number; height: number; pageNumber: number };
|
type DrawRect = { x: number; y: number; width: number; height: number; pageNumber: number };
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ async function updateAnnotation(
|
|||||||
coords: { x: number; y: number; width: number; height: number }
|
coords: { x: number; y: number; width: number; height: number }
|
||||||
) {
|
) {
|
||||||
if (!documentId) return;
|
if (!documentId) return;
|
||||||
const res = await fetch(`/api/documents/${documentId}/annotations/${annotationId}`, {
|
const res = await csrfFetch(`/api/documents/${documentId}/annotations/${annotationId}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(coords)
|
body: JSON.stringify(coords)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import TrainingHistory from './TrainingHistory.svelte';
|
import TrainingHistory from './TrainingHistory.svelte';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import type { TrainingRun } from '$lib/ocr/training.js';
|
import type { TrainingRun } from '$lib/ocr/training.js';
|
||||||
import { withCsrf } from '$lib/shared/cookies';
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
|
|
||||||
interface TrainingInfo {
|
interface TrainingInfo {
|
||||||
availableBlocks?: number;
|
availableBlocks?: number;
|
||||||
@@ -34,7 +34,7 @@ async function startTraining() {
|
|||||||
successMessage = null;
|
successMessage = null;
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/ocr/train', withCsrf({ method: 'POST' }));
|
const res = await csrfFetch('/api/ocr/train', { method: 'POST' });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
successMessage = m.training_success();
|
successMessage = m.training_success();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import TrainingHistory from './TrainingHistory.svelte';
|
import TrainingHistory from './TrainingHistory.svelte';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import type { TrainingRun } from '$lib/ocr/training.js';
|
import type { TrainingRun } from '$lib/ocr/training.js';
|
||||||
import { withCsrf } from '$lib/shared/cookies';
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
|
|
||||||
interface TrainingInfo {
|
interface TrainingInfo {
|
||||||
availableSegBlocks?: number;
|
availableSegBlocks?: number;
|
||||||
@@ -28,7 +28,7 @@ async function startTraining() {
|
|||||||
training = true;
|
training = true;
|
||||||
successMessage = null;
|
successMessage = null;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/ocr/segtrain', withCsrf({ method: 'POST' }));
|
const res = await csrfFetch('/api/ocr/segtrain', { method: 'POST' });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
successMessage = m.training_success();
|
successMessage = m.training_success();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { chipLabel, otherName, inferredRelationshipLabel } from '$lib/person/rel
|
|||||||
import AddRelationshipForm from '$lib/person/relationship/AddRelationshipForm.svelte';
|
import AddRelationshipForm from '$lib/person/relationship/AddRelationshipForm.svelte';
|
||||||
import type { RelFormData } from '$lib/person/relationship/AddRelationshipForm.svelte';
|
import type { RelFormData } from '$lib/person/relationship/AddRelationshipForm.svelte';
|
||||||
import type { components } from '$lib/generated/api';
|
import type { components } from '$lib/generated/api';
|
||||||
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
|
|
||||||
type PersonNodeDTO = components['schemas']['PersonNodeDTO'];
|
type PersonNodeDTO = components['schemas']['PersonNodeDTO'];
|
||||||
type RelationshipDTO = components['schemas']['RelationshipDTO'];
|
type RelationshipDTO = components['schemas']['RelationshipDTO'];
|
||||||
@@ -59,7 +60,7 @@ async function handleAddRelationship(data: RelFormData) {
|
|||||||
};
|
};
|
||||||
if (data.fromYear !== undefined) body.fromYear = data.fromYear;
|
if (data.fromYear !== undefined) body.fromYear = data.fromYear;
|
||||||
if (data.toYear !== undefined) body.toYear = data.toYear;
|
if (data.toYear !== undefined) body.toYear = data.toYear;
|
||||||
const res = await fetch(`/api/persons/${node.id}/relationships`, {
|
const res = await csrfFetch(`/api/persons/${node.id}/relationships`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ export function makeCsrfFetch(inner: typeof fetch): typeof fetch {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop-in replacement for fetch that automatically injects X-XSRF-TOKEN on
|
||||||
|
* all mutating requests (POST, PUT, PATCH, DELETE). Use this everywhere in
|
||||||
|
* client-side code instead of bare fetch + withCsrf().
|
||||||
|
*/
|
||||||
|
export const csrfFetch = makeCsrfFetch(fetch);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the fa_session cookie value from a list of Set-Cookie response headers.
|
* Extracts the fa_session cookie value from a list of Set-Cookie response headers.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { Comment, FlatMessage, MentionDTO } from '$lib/shared/types';
|
|||||||
import MentionEditor from '$lib/shared/discussion/MentionEditor.svelte';
|
import MentionEditor from '$lib/shared/discussion/MentionEditor.svelte';
|
||||||
import CommentMessage from '$lib/shared/discussion/CommentMessage.svelte';
|
import CommentMessage from '$lib/shared/discussion/CommentMessage.svelte';
|
||||||
import { extractContent } from '$lib/shared/discussion/mention';
|
import { extractContent } from '$lib/shared/discussion/mention';
|
||||||
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
type Props = {
|
type Props = {
|
||||||
documentId: string;
|
documentId: string;
|
||||||
annotationId?: string | null;
|
annotationId?: string | null;
|
||||||
@@ -79,7 +80,7 @@ async function postComment() {
|
|||||||
posting = true;
|
posting = true;
|
||||||
try {
|
try {
|
||||||
const { content, mentionedUserIds } = extractContent(text, newMentionCandidates);
|
const { content, mentionedUserIds } = extractContent(text, newMentionCandidates);
|
||||||
const res = await fetch(commentsBase, {
|
const res = await csrfFetch(commentsBase, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ content, mentionedUserIds })
|
body: JSON.stringify({ content, mentionedUserIds })
|
||||||
@@ -104,7 +105,7 @@ async function saveEdit(commentId: string) {
|
|||||||
if (!text || posting) return;
|
if (!text || posting) return;
|
||||||
posting = true;
|
posting = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/documents/${documentId}/comments/${commentId}`, {
|
const res = await csrfFetch(`/api/documents/${documentId}/comments/${commentId}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ content: text })
|
body: JSON.stringify({ content: text })
|
||||||
@@ -138,7 +139,7 @@ async function deleteComment(commentId: string) {
|
|||||||
if (posting) return;
|
if (posting) return;
|
||||||
posting = true;
|
posting = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/documents/${documentId}/comments/${commentId}`, {
|
const res = await csrfFetch(`/api/documents/${documentId}/comments/${commentId}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { onDestroy } from 'svelte';
|
|||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import ImportStatusCard from './ImportStatusCard.svelte';
|
import ImportStatusCard from './ImportStatusCard.svelte';
|
||||||
import type { ImportStatus } from './types.js';
|
import type { ImportStatus } from './types.js';
|
||||||
import { withCsrf } from '$lib/shared/cookies';
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
|
|
||||||
let backfillResult: number | null = $state(null);
|
let backfillResult: number | null = $state(null);
|
||||||
let backfillLoading = $state(false);
|
let backfillLoading = $state(false);
|
||||||
@@ -62,7 +62,7 @@ async function fetchImportStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function triggerImport() {
|
async function triggerImport() {
|
||||||
const res = await fetch('/api/admin/trigger-import', withCsrf({ method: 'POST' }));
|
const res = await csrfFetch('/api/admin/trigger-import', { method: 'POST' });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
importStatus = await res.json();
|
importStatus = await res.json();
|
||||||
if (importStatus!.state === 'RUNNING') {
|
if (importStatus!.state === 'RUNNING') {
|
||||||
@@ -84,7 +84,7 @@ async function fetchThumbnailStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function triggerThumbnails() {
|
async function triggerThumbnails() {
|
||||||
const res = await fetch('/api/admin/generate-thumbnails', withCsrf({ method: 'POST' }));
|
const res = await csrfFetch('/api/admin/generate-thumbnails', { method: 'POST' });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
thumbnailStatus = await res.json();
|
thumbnailStatus = await res.json();
|
||||||
if (thumbnailStatus!.state === 'RUNNING') {
|
if (thumbnailStatus!.state === 'RUNNING') {
|
||||||
@@ -107,7 +107,7 @@ async function backfillVersions() {
|
|||||||
backfillLoading = true;
|
backfillLoading = true;
|
||||||
backfillResult = null;
|
backfillResult = null;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/admin/backfill-versions', withCsrf({ method: 'POST' }));
|
const res = await csrfFetch('/api/admin/backfill-versions', { method: 'POST' });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
backfillResult = data.count;
|
backfillResult = data.count;
|
||||||
@@ -121,7 +121,7 @@ async function backfillFileHashes() {
|
|||||||
backfillHashesLoading = true;
|
backfillHashesLoading = true;
|
||||||
backfillHashesResult = null;
|
backfillHashesResult = null;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/admin/backfill-file-hashes', withCsrf({ method: 'POST' }));
|
const res = await csrfFetch('/api/admin/backfill-file-hashes', { method: 'POST' });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
backfillHashesResult = data.count;
|
backfillHashesResult = data.count;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import BulkDocumentEditLayout, {
|
|||||||
} from '$lib/document/BulkDocumentEditLayout.svelte';
|
} from '$lib/document/BulkDocumentEditLayout.svelte';
|
||||||
import { getErrorMessage, parseBackendError } from '$lib/shared/errors';
|
import { getErrorMessage, parseBackendError } from '$lib/shared/errors';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
|
|
||||||
let entries = $state<BulkEditEntry[]>([]);
|
let entries = $state<BulkEditEntry[]>([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
@@ -22,7 +23,7 @@ onMount(async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/documents/batch-metadata', {
|
const res = await csrfFetch('/api/documents/batch-metadata', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ids })
|
body: JSON.stringify({ ids })
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { safeHtml } from '$lib/shared/utils/sanitize';
|
|||||||
import { formatDate } from '$lib/shared/utils/date';
|
import { formatDate } from '$lib/shared/utils/date';
|
||||||
import { getConfirmService } from '$lib/shared/services/confirm.svelte';
|
import { getConfirmService } from '$lib/shared/services/confirm.svelte';
|
||||||
import BackButton from '$lib/shared/primitives/BackButton.svelte';
|
import BackButton from '$lib/shared/primitives/BackButton.svelte';
|
||||||
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
@@ -35,7 +36,7 @@ async function handleDelete() {
|
|||||||
destructive: true
|
destructive: true
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
const res = await fetch(`/api/geschichten/${g.id}`, { method: 'DELETE' });
|
const res = await csrfFetch(`/api/geschichten/${g.id}`, { method: 'DELETE' });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
goto('/geschichten');
|
goto('/geschichten');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { m } from '$lib/paraglide/messages.js';
|
|||||||
import GeschichteEditor from '$lib/geschichte/GeschichteEditor.svelte';
|
import GeschichteEditor from '$lib/geschichte/GeschichteEditor.svelte';
|
||||||
import BackButton from '$lib/shared/primitives/BackButton.svelte';
|
import BackButton from '$lib/shared/primitives/BackButton.svelte';
|
||||||
import { getErrorMessage } from '$lib/shared/errors';
|
import { getErrorMessage } from '$lib/shared/errors';
|
||||||
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
@@ -21,7 +22,7 @@ async function handleSubmit(payload: {
|
|||||||
submitting = true;
|
submitting = true;
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/geschichten/${data.geschichte.id}`, {
|
const res = await csrfFetch(`/api/geschichten/${data.geschichte.id}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { m } from '$lib/paraglide/messages.js';
|
|||||||
import GeschichteEditor from '$lib/geschichte/GeschichteEditor.svelte';
|
import GeschichteEditor from '$lib/geschichte/GeschichteEditor.svelte';
|
||||||
import BackButton from '$lib/shared/primitives/BackButton.svelte';
|
import BackButton from '$lib/shared/primitives/BackButton.svelte';
|
||||||
import { getErrorMessage } from '$lib/shared/errors';
|
import { getErrorMessage } from '$lib/shared/errors';
|
||||||
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
@@ -21,7 +22,7 @@ async function handleSubmit(payload: {
|
|||||||
submitting = true;
|
submitting = true;
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/geschichten', {
|
const res = await csrfFetch('/api/geschichten', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
|
|||||||
Reference in New Issue
Block a user