Files
TREK/server/src/routes/journeyPublic.ts
T
Maurice c0c59b6d80 feat: unified photo provider abstraction layer (#584)
Introduce trek_photos as central photo registry. Frontend uses
/api/photos/:id/:kind instead of provider-specific URLs. Adding
a new photo provider is now backend-only work.

- New trek_photos table (migration 98) with photo_id FK in
  trip_photos and journey_photos
- Unified /api/photos/:id/thumbnail|original|info endpoint
- photoResolverService for central resolution and streaming
- ProviderPicker: add "All Photos" tab, rename tabs, fix i18n
- Localize all hardcoded strings in JourneyDetailPage (14 langs)
- Fix date formatting to use browser locale instead of hardcoded 'en'
- Journey stats as styled tile cards
2026-04-13 20:08:31 +02:00

57 lines
2.4 KiB
TypeScript

import express, { Request, Response } from 'express';
import { getPublicJourney, validateShareTokenForAsset, validateShareTokenForPhoto } from '../services/journeyShareService';
import { streamPhoto } from '../services/memories/photoResolverService';
import { streamImmichAsset } from '../services/memories/immichService';
import path from 'node:path';
import fs from 'node:fs';
const router = express.Router();
router.get('/:token', (req: Request, res: Response) => {
const data = getPublicJourney(req.params.token);
if (!data) return res.status(404).json({ error: 'Not found' });
res.json(data);
});
// Unified public photo proxy — uses trek_photo_id
router.get('/:token/photos/:photoId/:kind', async (req: Request, res: Response) => {
const { token, photoId, kind } = req.params;
const valid = validateShareTokenForPhoto(token, Number(photoId));
if (!valid) return res.status(404).json({ error: 'Not found' });
await streamPhoto(res, valid.ownerId, Number(photoId), kind === 'thumbnail' ? 'thumbnail' : 'original');
});
// Legacy public photo proxy — validates share token instead of auth
router.get('/:token/photo/:provider/:assetId/:ownerId/:kind', async (req: Request, res: Response) => {
const { token, provider, assetId, ownerId, kind } = req.params;
const valid = validateShareTokenForAsset(token, assetId);
if (!valid) return res.status(404).json({ error: 'Not found' });
if (provider === 'local') {
const filePath = path.join(__dirname, '../../uploads/journey', assetId);
const resolved = path.resolve(filePath);
const uploadsDir = path.resolve(__dirname, '../../uploads');
if (!resolved.startsWith(uploadsDir) || !fs.existsSync(resolved)) {
return res.status(404).json({ error: 'Not found' });
}
res.set('Cache-Control', 'public, max-age=86400');
return res.sendFile(resolved);
}
const effectiveOwnerId = valid.ownerId || Number(ownerId);
if (provider === 'immich') {
await streamImmichAsset(res, effectiveOwnerId, assetId, kind === 'thumbnail' ? 'thumbnail' : 'original', effectiveOwnerId);
} else {
try {
const { streamSynologyAsset } = await import('../services/memories/synologyService');
await streamSynologyAsset(res, effectiveOwnerId, effectiveOwnerId, assetId, kind === 'thumbnail' ? 'thumbnail' : 'original');
} catch {
res.status(404).json({ error: 'Provider not supported' });
}
}
});
export default router;