mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
fix(synology): wire shared-album passphrase through journey-entry add flow
Thread selectedAlbumPassphrase from ProviderPicker through onAdd → journeyApi.addProviderPhotos → POST /entries/:entryId/provider-photos → addProviderPhoto service → getOrCreateTrekPhoto so shared-album photos have their passphrase encrypted and persisted on trek_photos at add-time, enabling streamPhoto to forward it to Synology correctly (#689).
This commit is contained in:
@@ -115,13 +115,14 @@ router.post('/entries/:entryId/photos', authenticate, upload.array('photos', 10)
|
||||
|
||||
router.post('/entries/:entryId/provider-photos', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { provider, asset_id, asset_ids, caption } = req.body || {};
|
||||
const { provider, asset_id, asset_ids, caption, passphrase } = req.body || {};
|
||||
const pp = passphrase && typeof passphrase === 'string' ? passphrase : undefined;
|
||||
|
||||
// Batch mode: { provider, asset_ids: string[] }
|
||||
if (Array.isArray(asset_ids) && provider) {
|
||||
const added: any[] = [];
|
||||
for (const id of asset_ids) {
|
||||
const photo = svc.addProviderPhoto(Number(req.params.entryId), authReq.user.id, provider, String(id), caption);
|
||||
const photo = svc.addProviderPhoto(Number(req.params.entryId), authReq.user.id, provider, String(id), caption, pp);
|
||||
if (photo) added.push(photo);
|
||||
}
|
||||
return res.status(201).json({ photos: added, added: added.length });
|
||||
@@ -129,7 +130,7 @@ router.post('/entries/:entryId/provider-photos', authenticate, (req: Request, re
|
||||
|
||||
// Single mode (backward compat)
|
||||
if (!provider || !asset_id) return res.status(400).json({ error: 'provider and asset_id required' });
|
||||
const photo = svc.addProviderPhoto(Number(req.params.entryId), authReq.user.id, provider, asset_id, caption);
|
||||
const photo = svc.addProviderPhoto(Number(req.params.entryId), authReq.user.id, provider, asset_id, caption, pp);
|
||||
if (!photo) return res.status(403).json({ error: 'Not allowed or duplicate' });
|
||||
res.status(201).json(photo);
|
||||
});
|
||||
|
||||
@@ -628,12 +628,12 @@ export function addPhoto(entryId: number, userId: number, filePath: string, thum
|
||||
return db.prepare(`SELECT ${JP_SELECT} FROM ${JP_JOIN} WHERE jp.id = ?`).get(Number(res.lastInsertRowid)) as JourneyPhoto;
|
||||
}
|
||||
|
||||
export function addProviderPhoto(entryId: number, userId: number, provider: string, assetId: string, caption?: string): JourneyPhoto | null {
|
||||
export function addProviderPhoto(entryId: number, userId: number, provider: string, assetId: string, caption?: string, passphrase?: string): JourneyPhoto | null {
|
||||
const entry = db.prepare('SELECT * FROM journey_entries WHERE id = ?').get(entryId) as JourneyEntry | undefined;
|
||||
if (!entry) return null;
|
||||
if (!canEdit(entry.journey_id, userId)) return null;
|
||||
|
||||
const trekPhotoId = getOrCreateTrekPhoto(provider, assetId, userId);
|
||||
const trekPhotoId = getOrCreateTrekPhoto(provider, assetId, userId, passphrase);
|
||||
|
||||
// skip if already added
|
||||
const exists = db.prepare('SELECT 1 FROM journey_photos WHERE entry_id = ? AND photo_id = ?').get(entryId, trekPhotoId);
|
||||
|
||||
@@ -936,6 +936,50 @@ describe('Share link update', () => {
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Provider photos passphrase (JOURNEY-INT-046, 047)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Provider photos — passphrase persistence', () => {
|
||||
it('JOURNEY-INT-046 — single mode with passphrase persists encrypted passphrase on trek_photos', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const journey = createJourney(testDb, user.id);
|
||||
const entry = createJourneyEntry(testDb, journey.id, user.id, { entry_date: '2026-04-01' });
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/journeys/entries/${entry.id}/provider-photos`)
|
||||
.set('Cookie', authCookie(user.id))
|
||||
.send({ provider: 'synologyphotos', asset_id: 'shared-asset-1', passphrase: 'pp-test' });
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
|
||||
const row = testDb.prepare('SELECT passphrase FROM trek_photos WHERE provider = ? AND asset_id = ? AND owner_id = ?')
|
||||
.get('synologyphotos', 'shared-asset-1', user.id) as { passphrase: string | null } | undefined;
|
||||
expect(row?.passphrase).not.toBeNull();
|
||||
expect(typeof row?.passphrase).toBe('string');
|
||||
});
|
||||
|
||||
it('JOURNEY-INT-047 — batch mode with passphrase persists encrypted passphrase on all trek_photos rows', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const journey = createJourney(testDb, user.id);
|
||||
const entry = createJourneyEntry(testDb, journey.id, user.id, { entry_date: '2026-04-02' });
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/journeys/entries/${entry.id}/provider-photos`)
|
||||
.set('Cookie', authCookie(user.id))
|
||||
.send({ provider: 'synologyphotos', asset_ids: ['batch-asset-1', 'batch-asset-2'], passphrase: 'pp-batch' });
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.added).toBe(2);
|
||||
|
||||
for (const assetId of ['batch-asset-1', 'batch-asset-2']) {
|
||||
const row = testDb.prepare('SELECT passphrase FROM trek_photos WHERE provider = ? AND asset_id = ? AND owner_id = ?')
|
||||
.get('synologyphotos', assetId, user.id) as { passphrase: string | null } | undefined;
|
||||
expect(row?.passphrase).not.toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Photo upload without files (JOURNEY-INT-045)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1412,3 +1412,24 @@ describe('Edge cases', () => {
|
||||
expect(filledRow.source_place_id).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// -- Passphrase on addProviderPhoto -------------------------------------------
|
||||
|
||||
describe('addProviderPhoto — passphrase', () => {
|
||||
it('JOURNEY-SVC-088: addProviderPhoto with passphrase stores encrypted value on trek_photos', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const journey = createJourney(testDb, user.id);
|
||||
const entry = createJourneyEntry(testDb, journey.id, user.id, { entry_date: '2026-03-15' });
|
||||
|
||||
const photo = addProviderPhoto(entry.id, user.id, 'synologyphotos', 'pp-asset-1', undefined, 'secret-pp');
|
||||
|
||||
expect(photo).not.toBeNull();
|
||||
|
||||
const row = testDb.prepare('SELECT passphrase FROM trek_photos WHERE provider = ? AND asset_id = ? AND owner_id = ?')
|
||||
.get('synologyphotos', 'pp-asset-1', user.id) as { passphrase: string | null } | undefined;
|
||||
expect(row?.passphrase).not.toBeNull();
|
||||
expect(typeof row?.passphrase).toBe('string');
|
||||
// stored value must be encrypted (not plaintext)
|
||||
expect(row?.passphrase).not.toBe('secret-pp');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user