Backend: - user roles (DB + JWT + first-user-is-admin) - volume-aware file resolver (multi-volume asset serving) - directory scanner uses volume URI directly - date-summary endpoint (capture date from EXIF) - timeline ordered by capture date - list endpoints: volumes, plugins, pipelines, library paths - delete endpoints: volumes, library paths - configurable upload body limit (MAX_UPLOAD_BYTES) Frontend: - auth: login/register, token refresh, role-based admin gate - timeline: date-grouped grid, infinite scroll, date scrubber - image viewer: fullscreen zoom/pan/pinch, metadata sidebar - upload: drag-drop, sequential upload, progress tracking - albums: create, add/remove photos, asset picker dialog - admin: storage (import library), jobs (pagination, error details), plugins (list + toggle), pipelines, sidecars, duplicates - multi-select mode with add-to-album action - TanStack Query for all data fetching
63 lines
1.4 KiB
TypeScript
63 lines
1.4 KiB
TypeScript
import axios from "axios"
|
|
import { getTokens, setTokens, clearTokens } from "./auth"
|
|
import type { AuthResponse } from "./types"
|
|
|
|
const api = axios.create({
|
|
baseURL: "/api/v1",
|
|
})
|
|
|
|
api.interceptors.request.use((config) => {
|
|
const { access } = getTokens()
|
|
if (access) {
|
|
config.headers.Authorization = `Bearer ${access}`
|
|
}
|
|
return config
|
|
})
|
|
|
|
let refreshPromise: Promise<string> | null = null
|
|
|
|
api.interceptors.response.use(
|
|
(res) => res,
|
|
async (error) => {
|
|
const original = error.config
|
|
if (error.response?.status !== 401 || original._retry) {
|
|
return Promise.reject(error)
|
|
}
|
|
|
|
original._retry = true
|
|
const { refresh } = getTokens()
|
|
if (!refresh) {
|
|
clearTokens()
|
|
window.location.href = "/login"
|
|
return Promise.reject(error)
|
|
}
|
|
|
|
if (!refreshPromise) {
|
|
refreshPromise = axios
|
|
.post<AuthResponse>("/api/v1/auth/refresh", {
|
|
refresh_token: refresh,
|
|
})
|
|
.then((res) => {
|
|
setTokens(res.data.token, res.data.refresh_token)
|
|
return res.data.token
|
|
})
|
|
.catch(() => {
|
|
clearTokens()
|
|
window.location.href = "/login"
|
|
return ""
|
|
})
|
|
.finally(() => {
|
|
refreshPromise = null
|
|
})
|
|
}
|
|
|
|
const newToken = await refreshPromise
|
|
if (!newToken) return Promise.reject(error)
|
|
|
|
original.headers.Authorization = `Bearer ${newToken}`
|
|
return api(original)
|
|
},
|
|
)
|
|
|
|
export default api
|