fix: No token refresh or revocation mechanism~

This commit is contained in:
voltsrage
2026-06-25 14:14:20 +08:00
parent a8964381a2
commit fdcc646fae
26 changed files with 3453 additions and 55 deletions
+96 -1
View File
@@ -1,23 +1,90 @@
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:5270'
let authToken = null
let refreshToken = null
let refreshPromise = null
let onSessionExpired = null
export function setAuthToken(token) {
authToken = token
}
export function setRefreshToken(token) {
refreshToken = token
}
export function onSessionExpiredCallback(callback) {
onSessionExpired = callback
}
function authHeaders(extra = {}) {
const headers = { 'Content-Type': 'application/json', ...extra }
if (authToken) headers['Authorization'] = `Bearer ${authToken}`
return headers
}
async function attemptRefresh() {
if (!refreshToken) return false
if (refreshPromise) return refreshPromise
refreshPromise = (async () => {
try {
const res = await fetch(`${BASE_URL}/api/v1/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
})
if (!res.ok) return false
const envelope = await res.json()
if (!envelope.success) return false
authToken = envelope.data.accessToken
refreshToken = envelope.data.refreshToken
if (onSessionExpired) {
onSessionExpired({
type: 'refreshed',
accessToken: envelope.data.accessToken,
refreshToken: envelope.data.refreshToken,
expiresAt: envelope.data.expiresAt,
})
}
return true
} catch {
return false
} finally {
refreshPromise = null
}
})()
return refreshPromise
}
function handleSessionExpired() {
if (onSessionExpired) {
onSessionExpired({ type: 'expired' })
}
}
async function request(path, options = {}) {
const res = await fetch(`${BASE_URL}${path}`, {
headers: authHeaders(options.headers),
...options,
})
if (res.status === 401) {
const refreshed = await attemptRefresh()
if (refreshed) {
const retry = await fetch(`${BASE_URL}${path}`, {
headers: authHeaders(options.headers),
...options,
})
const retryEnvelope = await retry.json()
if (!retry.ok || !retryEnvelope.success) {
const msg = retryEnvelope.error?.message ?? `API ${retry.status}: ${path}`
throw new Error(msg)
}
return retryEnvelope.data
}
handleSessionExpired()
throw new Error('Session expired — please log in again.')
}
const envelope = await res.json()
@@ -28,13 +95,26 @@ async function request(path, options = {}) {
return envelope.data
}
/** Returns null when the resource does not exist yet (HTTP 404 or empty optional payload). */
async function requestOptional(path) {
const res = await fetch(`${BASE_URL}${path}`, {
headers: authHeaders(),
})
if (res.status === 404) return null
if (res.status === 401) {
const refreshed = await attemptRefresh()
if (refreshed) {
const retry = await fetch(`${BASE_URL}${path}`, {
headers: authHeaders(),
})
if (retry.status === 404) return null
const retryEnvelope = await retry.json()
if (!retry.ok || !retryEnvelope.success) {
const msg = retryEnvelope.error?.message ?? `API ${retry.status}: ${path}`
throw new Error(msg)
}
return retryEnvelope.data ?? null
}
handleSessionExpired()
throw new Error('Session expired — please log in again.')
}
const envelope = await res.json()
@@ -66,6 +146,21 @@ export const api = {
headers: authHeaders(),
})
if (res.status === 401) {
const refreshed = await attemptRefresh()
if (refreshed) {
const retry = await fetch(`${BASE_URL}${path}`, {
method: 'DELETE',
headers: authHeaders(),
})
if (retry.status === 204) return null
const retryEnvelope = await retry.json()
if (!retry.ok || !retryEnvelope.success) {
const msg = retryEnvelope.error?.message ?? `API ${retry.status}: ${path}`
throw new Error(msg)
}
return retryEnvelope.data ?? null
}
handleSessionExpired()
throw new Error('Session expired — please log in again.')
}
if (res.status === 204) return null