diff --git a/README.md b/README.md index 8248ee6..0e122ad 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ services: # - COOKIE_SECURE=false # Uncomment if accessing over plain HTTP (no HTTPS). Not recommended for production. - TRUST_PROXY=1 # Number of trusted proxies for X-Forwarded-For # - ALLOW_INTERNAL_NETWORK=true # Uncomment if Immich or other services are on your local network (RFC-1918 IPs) - - APP_URL=${APP_URL:-} # Base URL of this instance — required when OIDC is enabled; must match the redirect URI registered with your IdP + - APP_URL=${APP_URL:-} # Base URL of this instance — required when OIDC is enabled; must match the redirect URI registered with your IdP; Also used as the base URL for email notifications and other external links # - OIDC_ISSUER=https://auth.example.com # OpenID Connect provider URL # - OIDC_CLIENT_ID=trek # OpenID Connect client ID # - OIDC_CLIENT_SECRET=supersecret # OpenID Connect client secret @@ -285,6 +285,7 @@ trek.yourdomain.com { | `COOKIE_SECURE` | Set to `false` to allow session cookies over plain HTTP (e.g. accessing via IP without HTTPS). Defaults to `true` in production. **Not recommended to disable in production.** | `true` | | `TRUST_PROXY` | Number of trusted reverse proxies for `X-Forwarded-For` | `1` | | `ALLOW_INTERNAL_NETWORK` | Allow outbound requests to private/RFC-1918 IP addresses. Set to `true` if Immich or other integrated services are hosted on your local network. Loopback (`127.x`) and link-local/metadata addresses (`169.254.x`) are always blocked regardless of this setting. | `false` | +| `APP_URL` | Public base URL of this instance (e.g. `https://trek.example.com`). Required when OIDC is enabled — must match the redirect URI registered with your IdP. Also used as the base URL for external links in email notifications. | — | | **OIDC / SSO** | | | | `OIDC_ISSUER` | OpenID Connect provider URL | — | | `OIDC_CLIENT_ID` | OIDC client ID | — | diff --git a/chart/templates/configmap.yaml b/chart/templates/configmap.yaml index a7a4eb7..7322505 100644 --- a/chart/templates/configmap.yaml +++ b/chart/templates/configmap.yaml @@ -10,6 +10,9 @@ data: {{- if .Values.env.ALLOWED_ORIGINS }} ALLOWED_ORIGINS: {{ .Values.env.ALLOWED_ORIGINS | quote }} {{- end }} + {{- if .Values.env.APP_URL }} + APP_URL: {{ .Values.env.APP_URL | quote }} + {{- end }} {{- if .Values.env.ALLOW_INTERNAL_NETWORK }} ALLOW_INTERNAL_NETWORK: {{ .Values.env.ALLOW_INTERNAL_NETWORK | quote }} {{- end }} diff --git a/chart/values.yaml b/chart/values.yaml index 471dafa..9501c60 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -17,6 +17,9 @@ env: PORT: 3000 # ALLOWED_ORIGINS: "" # NOTE: If using ingress, ensure env.ALLOWED_ORIGINS matches the domains in ingress.hosts for proper CORS configuration. + # APP_URL: "https://trek.example.com" + # Public base URL of this instance. Required when OIDC is enabled — must match the redirect URI registered with your IdP. + # Also used as the base URL for links in email notifications and other external links. # ALLOW_INTERNAL_NETWORK: "false" # Set to "true" if Immich or other integrated services are hosted on a private/RFC-1918 network address. # Loopback (127.x) and link-local/metadata addresses (169.254.x) are always blocked. diff --git a/client/src/components/Vacay/VacayMonthCard.tsx b/client/src/components/Vacay/VacayMonthCard.tsx index cc9a77f..1e63958 100644 --- a/client/src/components/Vacay/VacayMonthCard.tsx +++ b/client/src/components/Vacay/VacayMonthCard.tsx @@ -81,6 +81,7 @@ export default function VacayMonthCard({ return (
= { 'dashboard.sharedBy': 'شاركها {name}', 'dashboard.days': 'الأيام', 'dashboard.places': 'الأماكن', + 'dashboard.members': 'ال חברים', 'dashboard.archive': 'أرشفة', 'dashboard.restore': 'استعادة', 'dashboard.archived': 'مؤرشفة', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index b22072d..762b6ee 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -80,6 +80,7 @@ const br: Record = { 'dashboard.sharedBy': 'Compartilhada por {name}', 'dashboard.days': 'Dias', 'dashboard.places': 'Lugares', + 'dashboard.members': 'Parceiros de viagem', 'dashboard.archive': 'Arquivar', 'dashboard.restore': 'Restaurar', 'dashboard.archived': 'Arquivada', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 559e547..77b1736 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -81,6 +81,7 @@ const cs: Record = { 'dashboard.sharedBy': 'Sdílí {name}', 'dashboard.days': 'Dní', 'dashboard.places': 'Míst', + 'dashboard.members': 'Cestovní parťáci', 'dashboard.archive': 'Archivovat', 'dashboard.restore': 'Obnovit', 'dashboard.archived': 'Archivováno', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index c118948..85c4df7 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -80,6 +80,7 @@ const de: Record = { 'dashboard.sharedBy': 'Geteilt von {name}', 'dashboard.days': 'Tage', 'dashboard.places': 'Orte', + 'dashboard.members': 'Reise-Buddies', 'dashboard.archive': 'Archivieren', 'dashboard.restore': 'Wiederherstellen', 'dashboard.archived': 'Archiviert', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 5be3f50..86bd28a 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -80,6 +80,7 @@ const en: Record = { 'dashboard.sharedBy': 'Shared by {name}', 'dashboard.days': 'Days', 'dashboard.places': 'Places', + 'dashboard.members': 'Buddies', 'dashboard.archive': 'Archive', 'dashboard.restore': 'Restore', 'dashboard.archived': 'Archived', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 381ab28..a72bfe0 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -81,6 +81,7 @@ const es: Record = { 'dashboard.sharedBy': 'Compartido por {name}', 'dashboard.days': 'Días', 'dashboard.places': 'Lugares', + 'dashboard.members': 'Compañeros de viaje', 'dashboard.archive': 'Archivar', 'dashboard.restore': 'Restaurar', 'dashboard.archived': 'Archivado', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index da25c20..c2b53c3 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -80,6 +80,7 @@ const fr: Record = { 'dashboard.sharedBy': 'Partagé par {name}', 'dashboard.days': 'Jours', 'dashboard.places': 'Lieux', + 'dashboard.members': 'Compagnons de voyage', 'dashboard.archive': 'Archiver', 'dashboard.restore': 'Restaurer', 'dashboard.archived': 'Archivé', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index a567d1c..98d71d0 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -80,6 +80,7 @@ const hu: Record = { 'dashboard.sharedBy': 'Megosztotta: {name}', 'dashboard.days': 'nap', 'dashboard.places': 'hely', + 'dashboard.members': 'Útitársak', 'dashboard.archive': 'Archiválás', 'dashboard.restore': 'Visszaállítás', 'dashboard.archived': 'Archivált', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index fac9285..b6edd83 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -80,6 +80,7 @@ const it: Record = { 'dashboard.sharedBy': 'Condiviso da {name}', 'dashboard.days': 'Giorni', 'dashboard.places': 'Luoghi', + 'dashboard.members': 'Compagni di viaggio', 'dashboard.archive': 'Archivia', 'dashboard.restore': 'Ripristina', 'dashboard.archived': 'Archiviati', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 6c88ef6..4c97a6c 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -80,6 +80,7 @@ const nl: Record = { 'dashboard.sharedBy': 'Gedeeld door {name}', 'dashboard.days': 'Dagen', 'dashboard.places': 'Plaatsen', + 'dashboard.members': 'Reisgenoten', 'dashboard.archive': 'Archiveren', 'dashboard.restore': 'Herstellen', 'dashboard.archived': 'Gearchiveerd', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 9e1bb55..096a7b0 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -80,6 +80,7 @@ const ru: Record = { 'dashboard.sharedBy': 'Поделился {name}', 'dashboard.days': 'Дни', 'dashboard.places': 'Места', + 'dashboard.members': 'Попутчики', 'dashboard.archive': 'Архивировать', 'dashboard.restore': 'Восстановить', 'dashboard.archived': 'В архиве', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 2f6eade..4cc8cb8 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -80,6 +80,7 @@ const zh: Record = { 'dashboard.sharedBy': '由 {name} 分享', 'dashboard.days': '天', 'dashboard.places': '地点', + 'dashboard.members': '旅伴', 'dashboard.archive': '归档', 'dashboard.restore': '恢复', 'dashboard.archived': '已归档', diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index 6272fe8..c2db567 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -14,7 +14,7 @@ import ConfirmDialog from '../components/shared/ConfirmDialog' import { useToast } from '../components/shared/Toast' import { Plus, Calendar, Trash2, Edit2, Map, ChevronDown, ChevronUp, - Archive, ArchiveRestore, Clock, MapPin, Settings, X, ArrowRightLeft, + Archive, ArchiveRestore, Clock, MapPin, Settings, X, ArrowRightLeft, Users, LayoutGrid, List, } from 'lucide-react' import { useCanDo } from '../store/permissionsStore' @@ -31,6 +31,7 @@ interface DashboardTrip { owner_username?: string day_count?: number place_count?: number + shared_count?: number [key: string]: string | number | boolean | null | undefined } @@ -224,6 +225,9 @@ function SpotlightCard({ trip, onEdit, onDelete, onArchive, onClick, t, locale,
{trip.place_count || 0} {t('dashboard.places')}
+
+ {trip.shared_count+1 || 0} {t('dashboard.members')} +
@@ -307,6 +311,7 @@ function TripCard({ trip, onEdit, onDelete, onArchive, onClick, t, locale }: Omi
+
{(onEdit || onArchive || onDelete) && ( @@ -406,6 +411,9 @@ function TripListItem({ trip, onEdit, onDelete, onArchive, onClick, t, locale }:
{trip.place_count || 0}
+
+ {trip.shared_count+1 || 0} +
{/* Actions */} diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index fd48341..b092c9b 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -50,7 +50,6 @@ export default function LoginPage(): React.ReactElement { setError('Invalid or expired invite link') }) window.history.replaceState({}, '', window.location.pathname) - return } if (oidcCode) { @@ -87,7 +86,7 @@ export default function LoginPage(): React.ReactElement { if (config) { setAppConfig(config) if (!config.has_users) setMode('register') - if (config.oidc_only_mode && config.oidc_configured && config.has_users) { + if (config.oidc_only_mode && config.oidc_configured && config.has_users && !invite) { window.location.href = '/api/auth/oidc/login' } } diff --git a/docker-compose.yml b/docker-compose.yml index 768f73f..4645f2d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,7 @@ services: # - COOKIE_SECURE=false # Uncomment if accessing over plain HTTP (no HTTPS). Not recommended for production. - TRUST_PROXY=1 # Number of trusted proxies (for X-Forwarded-For / real client IP) - ALLOW_INTERNAL_NETWORK=false # Set to true if Immich or other services are hosted on your local network (RFC-1918 IPs). Loopback and link-local addresses remain blocked regardless. +# - APP_URL=https://trek.example.com # Public base URL — required when OIDC is enabled (must match the redirect URI registered with your IdP); also used as base URL for links in email notifications # - OIDC_ISSUER=https://auth.example.com # OpenID Connect provider URL # - OIDC_CLIENT_ID=trek # OpenID Connect client ID # - OIDC_CLIENT_SECRET=supersecret # OpenID Connect client secret diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts index 8770f79..8558fe6 100644 --- a/server/src/routes/admin.ts +++ b/server/src/routes/admin.ts @@ -379,12 +379,9 @@ router.post('/rotate-jwt-secret', (req: Request, res: Response) => { if (result.error) return res.status(result.status!).json({ error: result.error }); const authReq = req as AuthRequest; writeAudit({ - user_id: authReq.user?.id ?? null, - username: authReq.user?.username ?? 'unknown', + userId: authReq.user?.id ?? null, action: 'admin.rotate_jwt_secret', - target_type: 'system', - target_id: null, - details: null, + resource: 'system', ip: getClientIp(req), }); res.json({ success: true }); diff --git a/server/src/routes/immich.ts b/server/src/routes/immich.ts index dd02468..6c4363a 100644 --- a/server/src/routes/immich.ts +++ b/server/src/routes/immich.ts @@ -30,7 +30,6 @@ import { const router = express.Router(); // ── Dual auth middleware (JWT or ephemeral token for src) ───────────── - function authFromQuery(req: Request, res: Response, next: NextFunction) { const queryToken = req.query.token as string | undefined; if (queryToken) { @@ -178,7 +177,6 @@ router.get('/albums', authenticate, async (req: Request, res: Response) => { res.status(502).json({ error: 'Could not reach Immich' }); } }); - router.post('/trips/:tripId/album-links', authenticate, async (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; diff --git a/server/src/services/adminService.ts b/server/src/services/adminService.ts index ae42c92..f7df1f4 100644 --- a/server/src/services/adminService.ts +++ b/server/src/services/adminService.ts @@ -465,17 +465,92 @@ export function deleteTemplateItem(itemId: string) { export function listAddons() { const addons = db.prepare('SELECT * FROM addons ORDER BY sort_order, id').all() as Addon[]; - return addons.map(a => ({ ...a, enabled: !!a.enabled, config: JSON.parse(a.config || '{}') })); + const providers = db.prepare(` + SELECT id, name, description, icon, enabled, config, sort_order + FROM photo_providers + ORDER BY sort_order, id + `).all() as Array<{ id: string; name: string; description?: string | null; icon: string; enabled: number; config: string; sort_order: number }>; + const fields = db.prepare(` + SELECT provider_id, field_key, label, input_type, placeholder, required, secret, settings_key, payload_key, sort_order + FROM photo_provider_fields + ORDER BY sort_order, id + `).all() as Array<{ + provider_id: string; + field_key: string; + label: string; + input_type: string; + placeholder?: string | null; + required: number; + secret: number; + settings_key?: string | null; + payload_key?: string | null; + sort_order: number; + }>; + const fieldsByProvider = new Map(); + for (const field of fields) { + const arr = fieldsByProvider.get(field.provider_id) || []; + arr.push(field); + fieldsByProvider.set(field.provider_id, arr); + } + + return [ + ...addons.map(a => ({ ...a, enabled: !!a.enabled, config: JSON.parse(a.config || '{}') })), + ...providers.map(p => ({ + id: p.id, + name: p.name, + description: p.description, + type: 'photo_provider', + icon: p.icon, + enabled: !!p.enabled, + config: JSON.parse(p.config || '{}'), + fields: (fieldsByProvider.get(p.id) || []).map(f => ({ + key: f.field_key, + label: f.label, + input_type: f.input_type, + placeholder: f.placeholder || '', + required: !!f.required, + secret: !!f.secret, + settings_key: f.settings_key || null, + payload_key: f.payload_key || null, + sort_order: f.sort_order, + })), + sort_order: p.sort_order, + })), + ]; } export function updateAddon(id: string, data: { enabled?: boolean; config?: Record }) { - const addon = db.prepare('SELECT * FROM addons WHERE id = ?').get(id); - if (!addon) return { error: 'Addon not found', status: 404 }; - if (data.enabled !== undefined) db.prepare('UPDATE addons SET enabled = ? WHERE id = ?').run(data.enabled ? 1 : 0, id); - if (data.config !== undefined) db.prepare('UPDATE addons SET config = ? WHERE id = ?').run(JSON.stringify(data.config), id); - const updated = db.prepare('SELECT * FROM addons WHERE id = ?').get(id) as Addon; + const addon = db.prepare('SELECT * FROM addons WHERE id = ?').get(id) as Addon | undefined; + const provider = db.prepare('SELECT * FROM photo_providers WHERE id = ?').get(id) as { id: string; name: string; description?: string | null; icon: string; enabled: number; config: string; sort_order: number } | undefined; + if (!addon && !provider) return { error: 'Addon not found', status: 404 }; + + if (addon) { + if (data.enabled !== undefined) db.prepare('UPDATE addons SET enabled = ? WHERE id = ?').run(data.enabled ? 1 : 0, id); + if (data.config !== undefined) db.prepare('UPDATE addons SET config = ? WHERE id = ?').run(JSON.stringify(data.config), id); + } else { + if (data.enabled !== undefined) db.prepare('UPDATE photo_providers SET enabled = ? WHERE id = ?').run(data.enabled ? 1 : 0, id); + if (data.config !== undefined) db.prepare('UPDATE photo_providers SET config = ? WHERE id = ?').run(JSON.stringify(data.config), id); + } + + const updatedAddon = db.prepare('SELECT * FROM addons WHERE id = ?').get(id) as Addon | undefined; + const updatedProvider = db.prepare('SELECT * FROM photo_providers WHERE id = ?').get(id) as { id: string; name: string; description?: string | null; icon: string; enabled: number; config: string; sort_order: number } | undefined; + const updated = updatedAddon + ? { ...updatedAddon, enabled: !!updatedAddon.enabled, config: JSON.parse(updatedAddon.config || '{}') } + : updatedProvider + ? { + id: updatedProvider.id, + name: updatedProvider.name, + description: updatedProvider.description, + type: 'photo_provider', + icon: updatedProvider.icon, + enabled: !!updatedProvider.enabled, + config: JSON.parse(updatedProvider.config || '{}'), + sort_order: updatedProvider.sort_order, + } + : null; + return { - addon: { ...updated, enabled: !!updated.enabled, config: JSON.parse(updated.config || '{}') }, + addon: updated, auditDetails: { enabled: data.enabled !== undefined ? !!data.enabled : undefined, config_changed: data.config !== undefined }, }; } diff --git a/server/src/services/authService.ts b/server/src/services/authService.ts index c069645..d743519 100644 --- a/server/src/services/authService.ts +++ b/server/src/services/authService.ts @@ -981,7 +981,7 @@ export function createWsToken(userId: number): { error?: string; status?: number } export function createResourceToken(userId: number, purpose?: string): { error?: string; status?: number; token?: string } { - if (purpose !== 'download' && purpose !== 'immich') { + if (purpose !== 'download' && purpose !== 'immich' && purpose !== 'synologyphotos') { return { error: 'Invalid purpose', status: 400 }; } const token = createEphemeralToken(userId, purpose); diff --git a/server/src/services/immichService.ts b/server/src/services/immichService.ts index 9dd3de5..4a3169f 100644 --- a/server/src/services/immichService.ts +++ b/server/src/services/immichService.ts @@ -175,11 +175,12 @@ export async function searchPhotos( export function listTripPhotos(tripId: string, userId: number) { return db.prepare(` - SELECT tp.immich_asset_id, tp.user_id, tp.shared, tp.added_at, + SELECT tp.asset_id AS immich_asset_id, tp.user_id, tp.shared, tp.added_at, u.username, u.avatar, u.immich_url FROM trip_photos tp JOIN users u ON tp.user_id = u.id WHERE tp.trip_id = ? + AND tp.provider = 'immich' AND (tp.user_id = ? OR tp.shared = 1) ORDER BY tp.added_at ASC `).all(tripId, userId); @@ -191,25 +192,23 @@ export function addTripPhotos( assetIds: string[], shared: boolean ): number { - const insert = db.prepare( - 'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, immich_asset_id, shared) VALUES (?, ?, ?, ?)' - ); + const insert = db.prepare('INSERT OR IGNORE INTO trip_photos (trip_id, user_id, asset_id, provider, shared) VALUES (?, ?, ?, ?, ?)'); let added = 0; for (const assetId of assetIds) { - const result = insert.run(tripId, userId, assetId, shared ? 1 : 0); + const result = insert.run(tripId, userId, assetId, 'immich', shared ? 1 : 0); if (result.changes > 0) added++; } return added; } export function removeTripPhoto(tripId: string, userId: number, assetId: string) { - db.prepare('DELETE FROM trip_photos WHERE trip_id = ? AND user_id = ? AND immich_asset_id = ?') - .run(tripId, userId, assetId); + db.prepare('DELETE FROM trip_photos WHERE trip_id = ? AND user_id = ? AND asset_id = ? AND provider = ?') + .run(tripId, userId, assetId, 'immich'); } export function togglePhotoSharing(tripId: string, userId: number, assetId: string, shared: boolean) { - db.prepare('UPDATE trip_photos SET shared = ? WHERE trip_id = ? AND user_id = ? AND immich_asset_id = ?') - .run(shared ? 1 : 0, tripId, userId, assetId); + db.prepare('UPDATE trip_photos SET shared = ? WHERE trip_id = ? AND user_id = ? AND asset_id = ? AND provider = ?') + .run(shared ? 1 : 0, tripId, userId, assetId, 'immich'); } // ── Asset Info / Proxy ───────────────────────────────────────────────────── @@ -329,7 +328,7 @@ export function listAlbumLinks(tripId: string) { SELECT tal.*, u.username FROM trip_album_links tal JOIN users u ON tal.user_id = u.id - WHERE tal.trip_id = ? + WHERE tal.trip_id = ? AND tal.provider = 'immich' ORDER BY tal.created_at ASC `).all(tripId); } @@ -342,8 +341,8 @@ export function createAlbumLink( ): { success: boolean; error?: string } { try { db.prepare( - 'INSERT OR IGNORE INTO trip_album_links (trip_id, user_id, immich_album_id, album_name) VALUES (?, ?, ?, ?)' - ).run(tripId, userId, albumId, albumName || ''); + 'INSERT OR IGNORE INTO trip_album_links (trip_id, user_id, provider, album_id, album_name) VALUES (?, ?, ?, ?, ?)' + ).run(tripId, userId, 'immich', albumId, albumName || ''); return { success: true }; } catch { return { success: false, error: 'Album already linked' }; @@ -360,15 +359,15 @@ export async function syncAlbumAssets( linkId: string, userId: number ): Promise<{ success?: boolean; added?: number; total?: number; error?: string; status?: number }> { - const link = db.prepare('SELECT * FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?') - .get(linkId, tripId, userId) as any; + const link = db.prepare('SELECT * FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ? AND provider = ?') + .get(linkId, tripId, userId, 'immich') as any; if (!link) return { error: 'Album link not found', status: 404 }; const creds = getImmichCredentials(userId); if (!creds) return { error: 'Immich not configured', status: 400 }; try { - const resp = await fetch(`${creds.immich_url}/api/albums/${link.immich_album_id}`, { + const resp = await fetch(`${creds.immich_url}/api/albums/${link.album_id}`, { headers: { 'x-api-key': creds.immich_api_key, 'Accept': 'application/json' }, signal: AbortSignal.timeout(15000), }); @@ -376,9 +375,7 @@ export async function syncAlbumAssets( const albumData = await resp.json() as { assets?: any[] }; const assets = (albumData.assets || []).filter((a: any) => a.type === 'IMAGE'); - const insert = db.prepare( - 'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, immich_asset_id, shared) VALUES (?, ?, ?, 1)' - ); + const insert = db.prepare("INSERT OR IGNORE INTO trip_photos (trip_id, user_id, asset_id, provider, shared) VALUES (?, ?, ?, 'immich', 1)"); let added = 0; for (const asset of assets) { const r = insert.run(tripId, userId, asset.id); diff --git a/server/src/services/notifications.ts b/server/src/services/notifications.ts index 06f5f76..b25e187 100644 --- a/server/src/services/notifications.ts +++ b/server/src/services/notifications.ts @@ -44,6 +44,7 @@ function getWebhookUrl(): string | null { } function getAppUrl(): string { + if (process.env.APP_URL) return process.env.APP_URL; const origins = process.env.ALLOWED_ORIGINS; if (origins) { const first = origins.split(',')[0]?.trim(); diff --git a/server/src/services/shareService.ts b/server/src/services/shareService.ts index 157e9cc..ca7eb26 100644 --- a/server/src/services/shareService.ts +++ b/server/src/services/shareService.ts @@ -175,7 +175,7 @@ export function getSharedTripData(token: string): Record | null { // Collab messages (only if owner chose to share) const collabMessages = permissions.share_collab - ? db.prepare('SELECT m.*, u.username, u.avatar FROM collab_messages m JOIN users u ON m.user_id = u.id WHERE m.trip_id = ? ORDER BY m.created_at ASC').all(tripId) + ? db.prepare('SELECT m.*, u.username, u.avatar FROM collab_messages m JOIN users u ON m.user_id = u.id WHERE m.trip_id = ? AND m.deleted = 0 ORDER BY m.created_at').all(tripId) : []; return {