diff --git a/server/src/routes/memories/unified.ts b/server/src/routes/memories/unified.ts index 3e4561d..569bb2b 100644 --- a/server/src/routes/memories/unified.ts +++ b/server/src/routes/memories/unified.ts @@ -12,6 +12,7 @@ import { } from '../../services/memories/unifiedService'; import immichRouter from './immich'; import synologyRouter from './synology'; +import { Selection } from '../../services/memories/helpersService'; const router = express.Router(); @@ -33,13 +34,14 @@ router.post('/unified/trips/:tripId/photos', authenticate, async (req: Request, const authReq = req as AuthRequest; const { tripId } = req.params; const sid = req.headers['x-socket-id'] as string; + const selections: Selection[] = Array.isArray(req.body?.selections) ? req.body.selections : []; const shared = req.body?.shared === undefined ? true : !!req.body?.shared; const result = await addTripPhotos( tripId, authReq.user.id, shared, - req.body?.selections || [], + selections, sid, ); if ('error' in result) return res.status(result.error.status).json({ error: result.error.message }); diff --git a/server/src/services/memories/helpersService.ts b/server/src/services/memories/helpersService.ts index 161d7bd..2ef7cf4 100644 --- a/server/src/services/memories/helpersService.ts +++ b/server/src/services/memories/helpersService.ts @@ -2,6 +2,7 @@ import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import { Response } from 'express'; import { canAccessTrip, db } from "../../db/database"; +import { checkSsrf } from '../../utils/ssrfGuard'; // helpers for handling return types @@ -163,6 +164,13 @@ export function updateSyncTimeForAlbumLink(linkId: string): void { export async function pipeAsset(url: string, response: Response): Promise { try{ + + const SsrfResult = await checkSsrf(url); + if (!SsrfResult.allowed) { + response.status(400).json({ error: SsrfResult.error }); + response.end(); + return; + } const resp = await fetch(url); response.status(resp.status); diff --git a/server/src/services/memories/synologyService.ts b/server/src/services/memories/synologyService.ts index b9bdff2..c754ea9 100644 --- a/server/src/services/memories/synologyService.ts +++ b/server/src/services/memories/synologyService.ts @@ -1,7 +1,7 @@ import { Response } from 'express'; import { db } from '../../db/database'; -import { decrypt_api_key, maybe_encrypt_api_key } from '../apiKeyCrypto'; +import { decrypt_api_key, encrypt_api_key, maybe_encrypt_api_key } from '../apiKeyCrypto'; import { checkSsrf } from '../../utils/ssrfGuard'; import { addTripPhotos } from './unifiedService'; import { @@ -136,6 +136,10 @@ function _buildSynologyFormBody(params: ApiCallParams): URLSearchParams { async function _fetchSynologyJson(url: string, body: URLSearchParams): Promise> { const endpoint = _buildSynologyEndpoint(url); + const SsrfResult = await checkSsrf(endpoint); + if (!SsrfResult.allowed) { + return fail(SsrfResult.error, 400); + } const resp = await fetch(endpoint, { method: 'POST', headers: { @@ -226,9 +230,6 @@ function _normalizeSynologyPhotoInfo(item: SynologyPhotoItem): AssetInfo { }; } -function _cacheSynologySID(userId: number, sid: string): void { - db.prepare('UPDATE users SET synology_sid = ? WHERE id = ?').run(sid, userId); -} function _clearSynologySID(userId: number): void { db.prepare('UPDATE users SET synology_sid = NULL WHERE id = ?').run(userId); @@ -239,11 +240,11 @@ function _splitPackedSynologyId(rawId: string): { id: string; cacheKey: string; return { id, cacheKey: rawId, assetId: rawId }; } - async function _getSynologySession(userId: number): Promise> { const cachedSid = _readSynologyUser(userId, ['synology_sid']); if (cachedSid.success && cachedSid.data?.synology_sid) { - return success(cachedSid.data.synology_sid); + const decryptedSid = decrypt_api_key(cachedSid.data.synology_sid); + return success(decryptedSid); } const creds = _getSynologyCredentials(userId); @@ -257,7 +258,8 @@ async function _getSynologySession(userId: number): Promise; } - _cacheSynologySID(userId, resp.data); + const encrypted = encrypt_api_key(resp.data); + db.prepare('UPDATE users SET synology_sid = ? WHERE id = ?').run(encrypted, userId); return success(resp.data); } diff --git a/server/src/services/memories/unifiedService.ts b/server/src/services/memories/unifiedService.ts index b3918b8..0885f30 100644 --- a/server/src/services/memories/unifiedService.ts +++ b/server/src/services/memories/unifiedService.ts @@ -9,8 +9,16 @@ import { mapDbError, Selection, } from './helpersService'; +import { ca } from 'zod/locales'; + + +function _validProvider(provider: string): boolean { + const validProviders = ['immich', 'synologyphotos']; + return validProviders.includes(provider.toLowerCase()); +} + export function listTripPhotos(tripId: string, userId: number): ServiceResult { const access = canAccessTrip(tripId, userId); if (!access) { @@ -67,11 +75,19 @@ export function listTripAlbumLinks(tripId: string, userId: number): ServiceResul //----------------------------------------------- // managing photos in trip -function _addTripPhoto(tripId: string, userId: number, provider: string, assetId: string, shared: boolean, albumLinkId?: string): boolean { - const result = db.prepare( - 'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, asset_id, provider, shared, album_link_id) VALUES (?, ?, ?, ?, ?, ?)' - ).run(tripId, userId, assetId, provider, shared ? 1 : 0, albumLinkId || null); - return result.changes > 0; +function _addTripPhoto(tripId: string, userId: number, provider: string, assetId: string, shared: boolean, albumLinkId?: string): ServiceResult { + if (!_validProvider(provider)) { + return fail(`Provider: "${provider}" is not supported`, 400); + } + try { + const result = db.prepare( + 'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, asset_id, provider, shared, album_link_id) VALUES (?, ?, ?, ?, ?, ?)' + ).run(tripId, userId, assetId, provider, shared ? 1 : 0, albumLinkId || null); + return success(result.changes > 0); + } + catch (error) { + return mapDbError(error, 'Failed to add photo to trip'); + } } export async function addTripPhotos( @@ -91,24 +107,27 @@ export async function addTripPhotos( return fail('No photos selected', 400); } - try { - let added = 0; - for (const selection of selections) { - for (const raw of selection.asset_ids) { - const assetId = String(raw || '').trim(); - if (!assetId) continue; - if (_addTripPhoto(tripId, userId, selection.provider, assetId, shared, albumLinkId)) { - added++; - } + let added = 0; + for (const selection of selections) { + if (!_validProvider(selection.provider)) { + return fail(`Provider: "${selection.provider}" is not supported`, 400); + } + for (const raw of selection.asset_ids) { + const assetId = String(raw || '').trim(); + if (!assetId) continue; + const result = _addTripPhoto(tripId, userId, selection.provider, assetId, shared, albumLinkId); + if (!result.success) { + return result as ServiceResult<{ added: number; shared: boolean }>; + } + if (result.data) { + added++; } } - - await _notifySharedTripPhotos(tripId, userId, added); - broadcast(tripId, 'memories:updated', { userId }, sid); - return success({ added, shared }); - } catch (error) { - return mapDbError(error, 'Failed to add trip photos'); } + + await _notifySharedTripPhotos(tripId, userId, added); + broadcast(tripId, 'memories:updated', { userId }, sid); + return success({ added, shared }); } @@ -163,7 +182,7 @@ export function removeTripPhoto( AND asset_id = ? AND provider = ? `).run(tripId, userId, assetId, provider); - + broadcast(tripId, 'memories:updated', { userId }, sid); return success(true); @@ -192,6 +211,11 @@ export function createTripAlbumLink(tripId: string, userId: number, providerRaw: return fail('album_id required', 400); } + + if (!_validProvider(provider)) { + return fail(`Provider: "${provider}" is not supported`, 400); + } + try { const result = db.prepare( 'INSERT OR IGNORE INTO trip_album_links (trip_id, user_id, provider, album_id, album_name) VALUES (?, ?, ?, ?, ?)' @@ -214,10 +238,12 @@ export function removeAlbumLink(tripId: string, linkId: string, userId: number): } try { - db.prepare('DELETE FROM trip_photos WHERE trip_id = ? AND album_link_id = ?') - .run(tripId, linkId); - db.prepare('DELETE FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?') - .run(linkId, tripId, userId); + db.transaction(() => { + db.prepare('DELETE FROM trip_photos WHERE trip_id = ? AND album_link_id = ?') + .run(tripId, linkId); + db.prepare('DELETE FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?') + .run(linkId, tripId, userId); + }); return success(true); } catch (error) { return mapDbError(error, 'Failed to remove album link');