This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
Settings, Trash2, X, CalendarRange, Repeat2,
|
||||
} from 'lucide-vue-next'
|
||||
import { filterTasks, fromDateTimeLocal, groupTaskTree, renderMarkdown, toDateTimeLocal } from './lib/task-utils'
|
||||
import { csrfHeader } from './lib/csrf'
|
||||
import MvpPanel from './MvpPanel.vue'
|
||||
|
||||
type FolderItem = { id: string; name: string }
|
||||
@@ -77,9 +78,15 @@ watch(showCompleted, () => {
|
||||
})
|
||||
|
||||
async function api(path: string, options: RequestInit = {}) {
|
||||
const headers = new Headers(options.headers || {})
|
||||
if (!headers.has('Content-Type') && options.body && !(options.body instanceof FormData)) {
|
||||
headers.set('Content-Type', 'application/json')
|
||||
}
|
||||
const csrf = csrfHeader(options.method)
|
||||
if (csrf['x-csrf-token']) headers.set('x-csrf-token', csrf['x-csrf-token'])
|
||||
const response = await fetch('/api/v1' + path, {
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
|
||||
headers,
|
||||
...options,
|
||||
})
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import interactionPlugin from '@fullcalendar/interaction'
|
||||
import type { CalendarOptions, EventDropArg } from '@fullcalendar/core'
|
||||
import { Activity, ArchiveRestore, Download, FileJson, LogOut, Plus, RefreshCw, Trash2, Upload } from 'lucide-vue-next'
|
||||
import { dateKey, habitWeek, mergePage, moveDueDate } from './lib/mvp-utils'
|
||||
import { csrfHeader } from './lib/csrf'
|
||||
|
||||
type View = 'calendar'|'habits'|'settings'
|
||||
type Task = { id:string; title:string; due_at:string|null; version:number }
|
||||
@@ -21,6 +22,8 @@ const week = computed(() => habitWeek())
|
||||
async function request(path:string, options:RequestInit={}) {
|
||||
const headers:Record<string,string> = { ...(options.headers as Record<string,string> || {}) }
|
||||
if (options.body && !(options.body instanceof FormData)) headers['Content-Type']='application/json'
|
||||
const csrf = csrfHeader(options.method)
|
||||
if (csrf['x-csrf-token']) headers['x-csrf-token'] = csrf['x-csrf-token']
|
||||
const response = await fetch('/api/v1'+path,{ credentials:'include',...options,headers })
|
||||
if (!response.ok) throw new Error((await response.json().catch(()=>({}))).detail || `请求失败 (${response.status})`)
|
||||
const type=response.headers.get('content-type')||''
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it, beforeEach, afterEach } from 'vitest'
|
||||
import { csrfHeader } from './csrf'
|
||||
|
||||
let originalCookie = ''
|
||||
|
||||
describe('csrf utilities', () => {
|
||||
beforeEach(() => {
|
||||
originalCookie = document.cookie
|
||||
document.cookie = 'dodo_csrf=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.cookie = originalCookie
|
||||
})
|
||||
|
||||
it('adds the x-csrf-token header for write methods', () => {
|
||||
document.cookie = 'dodo_csrf=token123; path=/'
|
||||
expect(csrfHeader('POST')).toEqual({ 'x-csrf-token': 'token123' })
|
||||
expect(csrfHeader('PATCH')).toEqual({ 'x-csrf-token': 'token123' })
|
||||
expect(csrfHeader('DELETE')).toEqual({ 'x-csrf-token': 'token123' })
|
||||
})
|
||||
|
||||
it('leaves read methods untouched', () => {
|
||||
document.cookie = 'dodo_csrf=token123; path=/'
|
||||
expect(csrfHeader('GET')).toEqual({})
|
||||
expect(csrfHeader(undefined)).toEqual({})
|
||||
})
|
||||
|
||||
it('does not add a header when the cookie is missing', () => {
|
||||
expect(csrfHeader('POST')).toEqual({})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
export function csrfHeader(method?: string) {
|
||||
if (method && !['GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase())) {
|
||||
const token = getCookie('dodo_csrf')
|
||||
if (token) return { 'x-csrf-token': token }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
export function getCookie(name: string) {
|
||||
if (typeof document === 'undefined') return ''
|
||||
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`))
|
||||
return match ? decodeURIComponent(match[1]) : ''
|
||||
}
|
||||
Reference in New Issue
Block a user