mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 23:01:48 +00:00
feat: Journey addon — travel journal with entries, photos, public sharing & PDF export
- 5-table schema (journeys, entries, photos, trips, contributors) with migrations 87-91 - Trip-to-Journey sync engine with skeleton entries and photo sync - Full CRUD API for journeys, entries, photos with Immich/Synology integration - Timeline, Gallery and Map views with entry editor (markdown, mood, weather, pros/cons) - Journey frontpage with hero card, stats and trip suggestions - Public share links with token-based access and photo proxy - PDF photo book export (Polarsteps-inspired) - Dashboard redesign: mobile greeting, live trip hero, quick actions, unified card design - BottomNav profile sheet with settings/admin/logout - DayPlan mobile inline place picker - TripFormModal members management - Vacay calendar trip date indicator dots - Fix contributor photo access (403) for journey Immich/Synology photos - Trip deletion cleanup for journey skeleton entries - i18n: 231 new keys across all 14 languages (native translations, no fallbacks)
This commit is contained in:
@@ -123,6 +123,31 @@ export function canAccessUserPhoto(requestingUserId: number, ownerUserId: number
|
||||
if (requestingUserId === ownerUserId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Journey photos use tripId=0 — check journey_photos + journey_contributors
|
||||
if (tripId === '0') {
|
||||
const journeyPhoto = db.prepare(`
|
||||
SELECT jp.entry_id, je.journey_id
|
||||
FROM journey_photos jp
|
||||
JOIN journey_entries je ON je.id = jp.entry_id
|
||||
WHERE jp.asset_id = ?
|
||||
AND jp.provider = ?
|
||||
AND jp.owner_id = ?
|
||||
LIMIT 1
|
||||
`).get(assetId, provider, ownerUserId) as { entry_id: number; journey_id: number } | undefined;
|
||||
if (!journeyPhoto) return false;
|
||||
|
||||
// Check if requesting user is the journey owner or a contributor
|
||||
const access = db.prepare(`
|
||||
SELECT 1 FROM journeys WHERE id = ? AND user_id = ?
|
||||
UNION ALL
|
||||
SELECT 1 FROM journey_contributors WHERE journey_id = ? AND user_id = ?
|
||||
LIMIT 1
|
||||
`).get(journeyPhoto.journey_id, requestingUserId, journeyPhoto.journey_id, requestingUserId);
|
||||
return !!access;
|
||||
}
|
||||
|
||||
// Regular trip photos
|
||||
const sharedAsset = db.prepare(`
|
||||
SELECT 1
|
||||
FROM trip_photos
|
||||
|
||||
@@ -357,3 +357,63 @@ export async function syncAlbumAssets(
|
||||
return { error: 'Could not reach Immich', status: 502 };
|
||||
}
|
||||
}
|
||||
|
||||
// ── Upload to Immich ──────────────────────────────────────────────────────
|
||||
|
||||
export async function uploadToImmich(userId: number, filePath: string, fileName: string): Promise<string | null> {
|
||||
const creds = getImmichCredentials(userId);
|
||||
if (!creds) return null;
|
||||
|
||||
const fs = await import('node:fs');
|
||||
const path = await import('node:path');
|
||||
|
||||
const fullPath = path.join(__dirname, '../../../uploads', filePath);
|
||||
if (!fs.existsSync(fullPath)) return null;
|
||||
|
||||
try {
|
||||
const fileBuffer = fs.readFileSync(fullPath);
|
||||
const boundary = '----ImmichUpload' + Date.now();
|
||||
const ext = path.extname(fileName).toLowerCase();
|
||||
const mimeTypes: Record<string, string> = {
|
||||
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
|
||||
'.gif': 'image/gif', '.webp': 'image/webp', '.heic': 'image/heic',
|
||||
};
|
||||
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const parts: Buffer[] = [];
|
||||
const addField = (name: string, value: string) => {
|
||||
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`));
|
||||
};
|
||||
addField('deviceAssetId', `trek-${Date.now()}`);
|
||||
addField('deviceId', 'TREK');
|
||||
addField('fileCreatedAt', now);
|
||||
addField('fileModifiedAt', now);
|
||||
|
||||
parts.push(Buffer.from(
|
||||
`--${boundary}\r\nContent-Disposition: form-data; name="assetData"; filename="${fileName}"\r\nContent-Type: ${contentType}\r\n\r\n`
|
||||
));
|
||||
parts.push(fileBuffer);
|
||||
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
|
||||
|
||||
const body = Buffer.concat(parts);
|
||||
|
||||
const res = await safeFetch(`${creds.immich_url}/api/assets`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-api-key': creds.immich_api_key,
|
||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||
'Content-Length': String(body.length),
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json() as { id?: string };
|
||||
return data.id || null;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user