mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 06:41:46 +00:00
fix(immich): proxy shared photos using owner's Immich credentials
Trip members viewing another member's shared photo were getting a 404 because the proxy endpoints always used the requesting user's Immich credentials instead of the photo owner's. The ?userId= query param the client already sent was silently ignored. - Add canAccessUserPhoto() to verify the asset is shared and the requesting user is a trip member before allowing cross-user proxying - Pass optional ownerUserId through proxyThumbnail, proxyOriginal, and getAssetInfo so credentials are fetched for the correct user - Enforce shared=1 check so unshared photos remain inaccessible
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
|||||||
proxyThumbnail,
|
proxyThumbnail,
|
||||||
proxyOriginal,
|
proxyOriginal,
|
||||||
isValidAssetId,
|
isValidAssetId,
|
||||||
|
canAccessUserPhoto,
|
||||||
listAlbums,
|
listAlbums,
|
||||||
listAlbumLinks,
|
listAlbumLinks,
|
||||||
createAlbumLink,
|
createAlbumLink,
|
||||||
@@ -143,7 +144,12 @@ router.get('/assets/:assetId/info', authenticate, async (req: Request, res: Resp
|
|||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { assetId } = req.params;
|
const { assetId } = req.params;
|
||||||
if (!isValidAssetId(assetId)) return res.status(400).json({ error: 'Invalid asset ID' });
|
if (!isValidAssetId(assetId)) return res.status(400).json({ error: 'Invalid asset ID' });
|
||||||
const result = await getAssetInfo(authReq.user.id, assetId);
|
const queryUserId = req.query.userId ? Number(req.query.userId) : undefined;
|
||||||
|
const ownerUserId = queryUserId && queryUserId !== authReq.user.id ? queryUserId : undefined;
|
||||||
|
if (ownerUserId && !canAccessUserPhoto(authReq.user.id, ownerUserId, assetId)) {
|
||||||
|
return res.status(403).json({ error: 'Forbidden' });
|
||||||
|
}
|
||||||
|
const result = await getAssetInfo(authReq.user.id, assetId, ownerUserId);
|
||||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||||
res.json(result.data);
|
res.json(result.data);
|
||||||
});
|
});
|
||||||
@@ -154,7 +160,12 @@ router.get('/assets/:assetId/thumbnail', authFromQuery, async (req: Request, res
|
|||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { assetId } = req.params;
|
const { assetId } = req.params;
|
||||||
if (!isValidAssetId(assetId)) return res.status(400).send('Invalid asset ID');
|
if (!isValidAssetId(assetId)) return res.status(400).send('Invalid asset ID');
|
||||||
const result = await proxyThumbnail(authReq.user.id, assetId);
|
const queryUserId = req.query.userId ? Number(req.query.userId) : undefined;
|
||||||
|
const ownerUserId = queryUserId && queryUserId !== authReq.user.id ? queryUserId : undefined;
|
||||||
|
if (ownerUserId && !canAccessUserPhoto(authReq.user.id, ownerUserId, assetId)) {
|
||||||
|
return res.status(403).send('Forbidden');
|
||||||
|
}
|
||||||
|
const result = await proxyThumbnail(authReq.user.id, assetId, ownerUserId);
|
||||||
if (result.error) return res.status(result.status!).send(result.error);
|
if (result.error) return res.status(result.status!).send(result.error);
|
||||||
res.set('Content-Type', result.contentType!);
|
res.set('Content-Type', result.contentType!);
|
||||||
res.set('Cache-Control', 'public, max-age=86400');
|
res.set('Cache-Control', 'public, max-age=86400');
|
||||||
@@ -165,7 +176,12 @@ router.get('/assets/:assetId/original', authFromQuery, async (req: Request, res:
|
|||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { assetId } = req.params;
|
const { assetId } = req.params;
|
||||||
if (!isValidAssetId(assetId)) return res.status(400).send('Invalid asset ID');
|
if (!isValidAssetId(assetId)) return res.status(400).send('Invalid asset ID');
|
||||||
const result = await proxyOriginal(authReq.user.id, assetId);
|
const queryUserId = req.query.userId ? Number(req.query.userId) : undefined;
|
||||||
|
const ownerUserId = queryUserId && queryUserId !== authReq.user.id ? queryUserId : undefined;
|
||||||
|
if (ownerUserId && !canAccessUserPhoto(authReq.user.id, ownerUserId, assetId)) {
|
||||||
|
return res.status(403).send('Forbidden');
|
||||||
|
}
|
||||||
|
const result = await proxyOriginal(authReq.user.id, assetId, ownerUserId);
|
||||||
if (result.error) return res.status(result.status!).send(result.error);
|
if (result.error) return res.status(result.status!).send(result.error);
|
||||||
res.set('Content-Type', result.contentType!);
|
res.set('Content-Type', result.contentType!);
|
||||||
res.set('Cache-Control', 'public, max-age=86400');
|
res.set('Cache-Control', 'public, max-age=86400');
|
||||||
|
|||||||
@@ -230,11 +230,27 @@ export function togglePhotoSharing(tripId: string, userId: number, assetId: stri
|
|||||||
|
|
||||||
// ── Asset Info / Proxy ─────────────────────────────────────────────────────
|
// ── Asset Info / Proxy ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that requestingUserId can access a shared photo belonging to ownerUserId.
|
||||||
|
* The asset must be shared (shared=1) and the requesting user must be a member of
|
||||||
|
* the same trip that contains the photo.
|
||||||
|
*/
|
||||||
|
export function canAccessUserPhoto(requestingUserId: number, ownerUserId: number, assetId: string): boolean {
|
||||||
|
const row = db.prepare(`
|
||||||
|
SELECT tp.trip_id FROM trip_photos tp
|
||||||
|
WHERE tp.immich_asset_id = ? AND tp.user_id = ? AND tp.shared = 1
|
||||||
|
`).get(assetId, ownerUserId) as { trip_id: number } | undefined;
|
||||||
|
if (!row) return false;
|
||||||
|
return !!canAccessTrip(String(row.trip_id), requestingUserId);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAssetInfo(
|
export async function getAssetInfo(
|
||||||
userId: number,
|
userId: number,
|
||||||
assetId: string
|
assetId: string,
|
||||||
|
ownerUserId?: number
|
||||||
): Promise<{ data?: any; error?: string; status?: number }> {
|
): Promise<{ data?: any; error?: string; status?: number }> {
|
||||||
const creds = getImmichCredentials(userId);
|
const effectiveUserId = ownerUserId ?? userId;
|
||||||
|
const creds = getImmichCredentials(effectiveUserId);
|
||||||
if (!creds) return { error: 'Not found', status: 404 };
|
if (!creds) return { error: 'Not found', status: 404 };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -272,9 +288,11 @@ export async function getAssetInfo(
|
|||||||
|
|
||||||
export async function proxyThumbnail(
|
export async function proxyThumbnail(
|
||||||
userId: number,
|
userId: number,
|
||||||
assetId: string
|
assetId: string,
|
||||||
|
ownerUserId?: number
|
||||||
): Promise<{ buffer?: Buffer; contentType?: string; error?: string; status?: number }> {
|
): Promise<{ buffer?: Buffer; contentType?: string; error?: string; status?: number }> {
|
||||||
const creds = getImmichCredentials(userId);
|
const effectiveUserId = ownerUserId ?? userId;
|
||||||
|
const creds = getImmichCredentials(effectiveUserId);
|
||||||
if (!creds) return { error: 'Not found', status: 404 };
|
if (!creds) return { error: 'Not found', status: 404 };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -293,9 +311,11 @@ export async function proxyThumbnail(
|
|||||||
|
|
||||||
export async function proxyOriginal(
|
export async function proxyOriginal(
|
||||||
userId: number,
|
userId: number,
|
||||||
assetId: string
|
assetId: string,
|
||||||
|
ownerUserId?: number
|
||||||
): Promise<{ buffer?: Buffer; contentType?: string; error?: string; status?: number }> {
|
): Promise<{ buffer?: Buffer; contentType?: string; error?: string; status?: number }> {
|
||||||
const creds = getImmichCredentials(userId);
|
const effectiveUserId = ownerUserId ?? userId;
|
||||||
|
const creds = getImmichCredentials(effectiveUserId);
|
||||||
if (!creds) return { error: 'Not found', status: 404 };
|
if (!creds) return { error: 'Not found', status: 404 };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user