fix: sidecar response interceptor + folder proxy
- Added 401-handling response interceptor to sidecar axios instance (matches existing http instance) so expired/invalid tokens redirect to login instead of showing raw 404/401 errors - Added GET /api/sidecar/folders — proxies PhotoPrism's /api/v1/folders/originals with BasePath post-filter - Updated listFolders() frontend to call sidecar proxy - Updated plan with remaining fixes
This commit is contained in:
@@ -28,6 +28,14 @@ const http: AxiosInstance = axios.create({
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
/** Axios instance for sidecar endpoints — no baseURL prefix so paths
|
||||
* like `/api/sidecar/timeline` resolve directly through Caddy's
|
||||
* `/api/sidecar/*` rule instead of becoming `/api/v1/api/sidecar/*`. */
|
||||
const sidecar: AxiosInstance = axios.create({
|
||||
baseURL: '',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
http.interceptors.request.use((config) => {
|
||||
if (session.accessToken) {
|
||||
config.headers = config.headers ?? {};
|
||||
@@ -36,6 +44,14 @@ http.interceptors.request.use((config) => {
|
||||
return config;
|
||||
});
|
||||
|
||||
sidecar.interceptors.request.use((config) => {
|
||||
if (session.accessToken) {
|
||||
config.headers = config.headers ?? {};
|
||||
(config.headers as Record<string, string>)['X-Auth-Token'] = session.accessToken;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
http.interceptors.response.use(
|
||||
(r) => r,
|
||||
(err: AxiosError) => {
|
||||
@@ -51,6 +67,20 @@ http.interceptors.response.use(
|
||||
}
|
||||
);
|
||||
|
||||
sidecar.interceptors.response.use(
|
||||
(r) => r,
|
||||
(err: AxiosError) => {
|
||||
if (err.response?.status === 401 && browser) {
|
||||
clearSession();
|
||||
const url = err.config?.url ?? '';
|
||||
if (!url.endsWith('/session')) {
|
||||
void goto('/login', { replaceState: true });
|
||||
}
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
// ── Auth ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function login(username: string, password: string): Promise<PpSessionResponse> {
|
||||
@@ -144,7 +174,7 @@ export interface ListPhotosParams {
|
||||
}
|
||||
|
||||
export async function listPhotos(params: ListPhotosParams = {}): Promise<PpPhoto[]> {
|
||||
const { data } = await http.get<PpPhoto[]>('/api/sidecar/timeline', {
|
||||
const { data } = await sidecar.get<PpPhoto[]>('/api/sidecar/timeline', {
|
||||
params: {
|
||||
count: 60,
|
||||
offset: 0,
|
||||
@@ -260,7 +290,7 @@ export async function listPhotosAround(p: AroundParams): Promise<PpPhoto[]> {
|
||||
*/
|
||||
export async function countPhotos(q: string, opts: { merged?: boolean } = {}): Promise<number> {
|
||||
const merged = opts.merged ?? false;
|
||||
const resp = await http.get<PpPhoto[]>('/api/sidecar/timeline', {
|
||||
const resp = await sidecar.get<PpPhoto[]>('/api/sidecar/timeline', {
|
||||
params: { count: 10000, offset: 0, merged, q }
|
||||
});
|
||||
// PhotoPrism's `X-Count` header counts SQL file rows (one row per
|
||||
@@ -454,11 +484,14 @@ export interface PpFolder {
|
||||
* BasePath is empty (today's admin default) this is a no-op.
|
||||
*/
|
||||
export async function listFolders(): Promise<PpFolder[]> {
|
||||
const { data } = await http.get<{ folders?: PpFolder[] }>(
|
||||
'/folders/originals',
|
||||
const { data } = await sidecar.get<{ folders?: PpFolder[] }>(
|
||||
'/api/sidecar/folders',
|
||||
{ params: { recursive: true, uncached: true, files: false } }
|
||||
);
|
||||
const bp = userBasePath();
|
||||
// Sidecar already filters by BasePath; the frontend still applies the
|
||||
// filter + path rewrite as a safety net for admin (bp="") and for any
|
||||
// folders that might have slipped through.
|
||||
const folders = data.folders ?? [];
|
||||
if (bp === '') return folders;
|
||||
return folders
|
||||
@@ -636,7 +669,7 @@ export async function aggregateKeywords(): Promise<AggregatedKeyword[]> {
|
||||
}
|
||||
|
||||
async function hasPhotosMatching(q: string): Promise<boolean> {
|
||||
const resp = await http.get<PpPhoto[]>('/api/sidecar/timeline', {
|
||||
const resp = await sidecar.get<PpPhoto[]>('/api/sidecar/timeline', {
|
||||
params: { count: 1, offset: 0, q }
|
||||
});
|
||||
return Array.isArray(resp.data) && resp.data.length > 0;
|
||||
@@ -677,7 +710,7 @@ export async function listLabels(): Promise<PpLabel[]> {
|
||||
// /api/v1/labels so PhotoCount reflects only photos under the user's
|
||||
// BasePath. The sidecar proxies the request through to PP then
|
||||
// post-filters each label's count.
|
||||
const { data } = await http.get<PpLabel[]>('/api/sidecar/labels', {
|
||||
const { data } = await sidecar.get<PpLabel[]>('/api/sidecar/labels', {
|
||||
params: { count: 1000, order: 'count', all: true, perPage: 1000 }
|
||||
});
|
||||
return filterByUserPhotos(data, (l) => `label:${l.Slug}`);
|
||||
|
||||
Reference in New Issue
Block a user