feat: frontend MVP — auth, timeline, upload, albums, admin, image viewer
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
This commit is contained in:
62
k-photos-frontend/lib/api.ts
Normal file
62
k-photos-frontend/lib/api.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
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
|
||||
Reference in New Issue
Block a user