fix: scope marks, labels, and subjects to the authenticated user
Marks (ratings/color labels) were stored without a user column — every user saw every other user's marks. Labels and subjects from PhotoPrism's global endpoints leaked across users because those endpoints ignore BasePath ACL. Sidecar: - Add UserName as composite primary key on Mark (photo_uid, user_name) - Replace validateSession with resolveSession that fetches the user identity from PhotoPrism's session endpoint - Filter all mark queries by user_name Frontend: - Filter listLabels/listSubjects through a BasePath-aware existence check — each label/subject is kept only if the user has at least one matching photo (single count=1 probe per item, batched at concurrency 8) - Skip filtering for admin users with empty BasePath (single-user compat) Also documents USER_BASEPATHS in .env.example — the env var that drives per-user library isolation via PhotoPrism's auth_users.base_path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -635,6 +635,35 @@ export async function aggregateKeywords(): Promise<AggregatedKeyword[]> {
|
||||
return Array.from(buckets.values()).sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
async function hasPhotosMatching(q: string): Promise<boolean> {
|
||||
const resp = await http.get<PpPhoto[]>('/photos', {
|
||||
params: { count: 1, offset: 0, q }
|
||||
});
|
||||
return Array.isArray(resp.data) && resp.data.length > 0;
|
||||
}
|
||||
|
||||
async function filterByUserPhotos<T>(
|
||||
items: T[],
|
||||
queryFor: (item: T) => string
|
||||
): Promise<T[]> {
|
||||
if (userBasePath() === '') return items;
|
||||
const CONCURRENCY = 8;
|
||||
const out: T[] = [];
|
||||
for (let i = 0; i < items.length; i += CONCURRENCY) {
|
||||
const batch = items.slice(i, i + CONCURRENCY);
|
||||
const checks = await Promise.all(
|
||||
batch.map(async (item) => ({
|
||||
item,
|
||||
has: await hasPhotosMatching(queryFor(item))
|
||||
}))
|
||||
);
|
||||
for (const { item, has } of checks) {
|
||||
if (has) out.push(item);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function listLabels(): Promise<PpLabel[]> {
|
||||
// `all=true` includes labels PhotoPrism has soft-deleted (auto-hidden
|
||||
// low-confidence classifier hits, manually-removed labels). They're
|
||||
@@ -646,7 +675,7 @@ export async function listLabels(): Promise<PpLabel[]> {
|
||||
const { data } = await http.get<PpLabel[]>('/labels', {
|
||||
params: { count: 1000, order: 'count', all: true }
|
||||
});
|
||||
return data;
|
||||
return filterByUserPhotos(data, (l) => `label:${l.Slug}`);
|
||||
}
|
||||
|
||||
// ── Subjects (people / face recognition) ────────────────────────────────────
|
||||
@@ -670,7 +699,7 @@ export async function listSubjects(): Promise<PpSubject[]> {
|
||||
const { data } = await http.get<PpSubject[]>('/subjects', {
|
||||
params: { count: 1000, order: 'count' }
|
||||
});
|
||||
return data ?? [];
|
||||
return filterByUserPhotos(data ?? [], (s) => `person:${s.Slug}`);
|
||||
}
|
||||
|
||||
export async function updateSubject(uid: string, patch: Partial<PpSubject>): Promise<PpSubject> {
|
||||
|
||||
Reference in New Issue
Block a user