このコミットが含まれているのは:
+42
-10
@@ -63,7 +63,22 @@ const withSession = (opt?: Opt): AxiosRequestConfig => {
|
||||
|
||||
|
||||
const withCSRF = async (opt?: Opt): Promise<AxiosRequestConfig> => {
|
||||
const token = await csrfToken ()
|
||||
const signal = opt?.signal
|
||||
if (signal?.aborted) throw new DOMException ('Aborted', 'AbortError')
|
||||
const pending = csrfToken ()
|
||||
// A shared CSRF lookup must not make an individual aborted write wait forever.
|
||||
const token = signal == null ? await pending : await new Promise<string> ((resolve, reject) => {
|
||||
const abort = () => reject (new DOMException ('Aborted', 'AbortError'))
|
||||
signal.addEventListener ('abort', abort, { once: true })
|
||||
if (signal.aborted) abort ()
|
||||
void pending.then (value => {
|
||||
signal.removeEventListener ('abort', abort)
|
||||
resolve (value)
|
||||
}, error => {
|
||||
signal.removeEventListener ('abort', abort)
|
||||
reject (error)
|
||||
})
|
||||
})
|
||||
const config = withSession (opt)
|
||||
return { ...config, headers: { ...config.headers, 'X-CSRF-Token': token } }
|
||||
}
|
||||
@@ -73,7 +88,8 @@ const csrfToken = async (): Promise<string> => {
|
||||
const state = csrfState ()
|
||||
if (state.token != null)
|
||||
return state.token
|
||||
state.pending ??= client.get<{ token: string }> ('/users/csrf', withSession ())
|
||||
state.pending ??= client.get<{ token: string }> (
|
||||
'/users/csrf', { ...withSession (), timeout: 15_000 })
|
||||
.then (response => {
|
||||
if (csrfState () !== state)
|
||||
return csrfToken ()
|
||||
@@ -223,19 +239,35 @@ export const startAPIDownload = (
|
||||
): (() => void) => {
|
||||
const frame = document.createElement ('iframe')
|
||||
frame.hidden = true
|
||||
let timer: ReturnType<typeof window.setTimeout> | null = null
|
||||
let closed = false
|
||||
const release = () => {
|
||||
if (closed) return
|
||||
closed = true
|
||||
if (timer != null) window.clearTimeout (timer)
|
||||
frame.remove ()
|
||||
}
|
||||
const failure = () => {
|
||||
if (closed) return
|
||||
release ()
|
||||
onError?.()
|
||||
}
|
||||
frame.addEventListener ('load', () => {
|
||||
const document = frame.contentDocument
|
||||
if (document != null && document.URL !== 'about:blank')
|
||||
try
|
||||
{
|
||||
onError?.()
|
||||
frame.remove ()
|
||||
const content = frame.contentDocument
|
||||
// Attachment navigations normally have no document. An error document is failure.
|
||||
if (content != null && content.URL !== 'about:blank') failure ()
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Cross-origin navigation cannot be inspected; the authenticated ACK decides.
|
||||
}
|
||||
})
|
||||
frame.addEventListener ('error', () => { onError?.(); frame.remove () })
|
||||
frame.addEventListener ('error', failure)
|
||||
const suffix = query == null ? '' : `?${query.toString ()}`
|
||||
frame.src = `${API_BASE_URL}${path}/files/${encodeURIComponent (token)}${suffix}`
|
||||
document.body.append (frame)
|
||||
// Attachments do not reliably fire load; keep this isolated from page navigation.
|
||||
const timer = window.setTimeout (() => frame.remove (), 10 * 60_000)
|
||||
return () => { window.clearTimeout (timer); frame.remove () }
|
||||
timer = window.setTimeout (release, 10 * 60_000)
|
||||
return release
|
||||
}
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする