mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-30 18:46:00 +00:00
changing routes and hierarchy of files for memories
This commit is contained in:
@@ -89,7 +89,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
|
|
||||||
const loadAlbumLinks = async () => {
|
const loadAlbumLinks = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(`/integrations/memories/trips/${tripId}/album-links`)
|
const res = await apiClient.get(`/integrations/memories/unified/trips/${tripId}/album-links`)
|
||||||
setAlbumLinks(res.data.links || [])
|
setAlbumLinks(res.data.links || [])
|
||||||
} catch { setAlbumLinks([]) }
|
} catch { setAlbumLinks([]) }
|
||||||
}
|
}
|
||||||
@@ -98,7 +98,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
if (!provider) return
|
if (!provider) return
|
||||||
setAlbumsLoading(true)
|
setAlbumsLoading(true)
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(`/integrations/${provider}/albums`)
|
const res = await apiClient.get(`/integrations/memories/${provider}/albums`)
|
||||||
setAlbums(res.data.albums || [])
|
setAlbums(res.data.albums || [])
|
||||||
} catch {
|
} catch {
|
||||||
setAlbums([])
|
setAlbums([])
|
||||||
@@ -120,7 +120,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.post(`/integrations/memories/trips/${tripId}/album-links`, {
|
await apiClient.post(`/integrations/memories/unified/trips/${tripId}/album-links`, {
|
||||||
album_id: albumId,
|
album_id: albumId,
|
||||||
album_name: albumName,
|
album_name: albumName,
|
||||||
provider: selectedProvider,
|
provider: selectedProvider,
|
||||||
@@ -128,7 +128,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
setShowAlbumPicker(false)
|
setShowAlbumPicker(false)
|
||||||
await loadAlbumLinks()
|
await loadAlbumLinks()
|
||||||
// Auto-sync after linking
|
// Auto-sync after linking
|
||||||
const linksRes = await apiClient.get(`/integrations/memories/trips/${tripId}/album-links`)
|
const linksRes = await apiClient.get(`/integrations/memories/unified/trips/${tripId}/album-links`)
|
||||||
const newLink = (linksRes.data.links || []).find((l: any) => l.album_id === albumId && l.provider === selectedProvider)
|
const newLink = (linksRes.data.links || []).find((l: any) => l.album_id === albumId && l.provider === selectedProvider)
|
||||||
if (newLink) await syncAlbum(newLink.id)
|
if (newLink) await syncAlbum(newLink.id)
|
||||||
} catch { toast.error(t('memories.error.linkAlbum')) }
|
} catch { toast.error(t('memories.error.linkAlbum')) }
|
||||||
@@ -136,7 +136,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
|
|
||||||
const unlinkAlbum = async (linkId: number) => {
|
const unlinkAlbum = async (linkId: number) => {
|
||||||
try {
|
try {
|
||||||
await apiClient.delete(`/integrations/memories/trips/${tripId}/album-links/${linkId}`)
|
await apiClient.delete(`/integrations/memories/unified/trips/${tripId}/album-links/${linkId}`)
|
||||||
loadAlbumLinks()
|
loadAlbumLinks()
|
||||||
} catch { toast.error(t('memories.error.unlinkAlbum')) }
|
} catch { toast.error(t('memories.error.unlinkAlbum')) }
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
if (!targetProvider) return
|
if (!targetProvider) return
|
||||||
setSyncing(linkId)
|
setSyncing(linkId)
|
||||||
try {
|
try {
|
||||||
await apiClient.post(`/integrations/${targetProvider}/trips/${tripId}/album-links/${linkId}/sync`)
|
await apiClient.post(`/integrations/memories/${targetProvider}/trips/${tripId}/album-links/${linkId}/sync`)
|
||||||
await loadAlbumLinks()
|
await loadAlbumLinks()
|
||||||
await loadPhotos()
|
await loadPhotos()
|
||||||
} catch { toast.error(t('memories.error.syncAlbum')) }
|
} catch { toast.error(t('memories.error.syncAlbum')) }
|
||||||
@@ -175,7 +175,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
|
|
||||||
const loadPhotos = async () => {
|
const loadPhotos = async () => {
|
||||||
try {
|
try {
|
||||||
const photosRes = await apiClient.get(`/integrations/memories/trips/${tripId}/photos`)
|
const photosRes = await apiClient.get(`/integrations/memories/unified/trips/${tripId}/photos`)
|
||||||
setTripPhotos(photosRes.data.photos || [])
|
setTripPhotos(photosRes.data.photos || [])
|
||||||
} catch {
|
} catch {
|
||||||
setTripPhotos([])
|
setTripPhotos([])
|
||||||
@@ -257,7 +257,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
setPickerPhotos([])
|
setPickerPhotos([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const res = await apiClient.post(`/integrations/${provider.id}/search`, {
|
const res = await apiClient.post(`/integrations/memories/${provider.id}/search`, {
|
||||||
from: useDate && startDate ? startDate : undefined,
|
from: useDate && startDate ? startDate : undefined,
|
||||||
to: useDate && endDate ? endDate : undefined,
|
to: useDate && endDate ? endDate : undefined,
|
||||||
})
|
})
|
||||||
@@ -296,7 +296,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
groupedByProvider.set(provider, list)
|
groupedByProvider.set(provider, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
await apiClient.post(`/integrations/memories/trips/${tripId}/photos`, {
|
await apiClient.post(`/integrations/memories/unified/trips/${tripId}/photos`, {
|
||||||
selections: [...groupedByProvider.entries()].map(([provider, asset_ids]) => ({ provider, asset_ids })),
|
selections: [...groupedByProvider.entries()].map(([provider, asset_ids]) => ({ provider, asset_ids })),
|
||||||
shared: true,
|
shared: true,
|
||||||
})
|
})
|
||||||
@@ -310,7 +310,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
|
|
||||||
const removePhoto = async (photo: TripPhoto) => {
|
const removePhoto = async (photo: TripPhoto) => {
|
||||||
try {
|
try {
|
||||||
await apiClient.delete(`/integrations/memories/trips/${tripId}/photos`, {
|
await apiClient.delete(`/integrations/memories/unified/trips/${tripId}/photos`, {
|
||||||
data: {
|
data: {
|
||||||
asset_id: photo.asset_id,
|
asset_id: photo.asset_id,
|
||||||
provider: photo.provider,
|
provider: photo.provider,
|
||||||
@@ -324,7 +324,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
|
|
||||||
const toggleSharing = async (photo: TripPhoto, shared: boolean) => {
|
const toggleSharing = async (photo: TripPhoto, shared: boolean) => {
|
||||||
try {
|
try {
|
||||||
await apiClient.put(`/integrations/memories/trips/${tripId}/photos/sharing`, {
|
await apiClient.put(`/integrations/memories/unified/trips/${tripId}/photos/sharing`, {
|
||||||
shared,
|
shared,
|
||||||
asset_id: photo.asset_id,
|
asset_id: photo.asset_id,
|
||||||
provider: photo.provider,
|
provider: photo.provider,
|
||||||
@@ -338,7 +338,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const thumbnailBaseUrl = (photo: TripPhoto) =>
|
const thumbnailBaseUrl = (photo: TripPhoto) =>
|
||||||
`/api/integrations/${photo.provider}/assets/${tripId}/${photo.asset_id}/${photo.user_id}/thumbnail`
|
`/api/integrations/memories/${photo.provider}/assets/${tripId}/${photo.asset_id}/${photo.user_id}/thumbnail`
|
||||||
|
|
||||||
const makePickerKey = (provider: string, assetId: string): string => `${provider}::${assetId}`
|
const makePickerKey = (provider: string, assetId: string): string => `${provider}::${assetId}`
|
||||||
|
|
||||||
@@ -598,7 +598,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
outline: isSelected ? '3px solid var(--text-primary)' : 'none',
|
outline: isSelected ? '3px solid var(--text-primary)' : 'none',
|
||||||
outlineOffset: -3,
|
outlineOffset: -3,
|
||||||
}}>
|
}}>
|
||||||
<ProviderImg baseUrl={`/api/integrations/${asset.provider}/assets//${tripId}/${asset.id}/${currentUser!.id}/thumbnail`} provider={asset.provider} loading="lazy"
|
<ProviderImg baseUrl={`/api/integrations/memories/${asset.provider}/assets/${tripId}/${asset.id}/${currentUser!.id}/thumbnail`} provider={asset.provider} loading="lazy"
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -778,9 +778,9 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
setLightboxId(photo.asset_id); setLightboxUserId(photo.user_id); setLightboxInfo(null)
|
setLightboxId(photo.asset_id); setLightboxUserId(photo.user_id); setLightboxInfo(null)
|
||||||
if (lightboxOriginalSrc) URL.revokeObjectURL(lightboxOriginalSrc)
|
if (lightboxOriginalSrc) URL.revokeObjectURL(lightboxOriginalSrc)
|
||||||
setLightboxOriginalSrc('')
|
setLightboxOriginalSrc('')
|
||||||
fetchImageAsBlob(`/api/integrations/${photo.provider}/assets/${tripId}/${photo.asset_id}/${photo.user_id}/original`).then(setLightboxOriginalSrc)
|
fetchImageAsBlob(`/api/integrations/memories/${photo.provider}/assets/${tripId}/${photo.asset_id}/${photo.user_id}/original`).then(setLightboxOriginalSrc)
|
||||||
setLightboxInfoLoading(true)
|
setLightboxInfoLoading(true)
|
||||||
apiClient.get(`/integrations/${photo.provider}/assets/${tripId}/${photo.asset_id}/${photo.user_id}/info`)
|
apiClient.get(`/integrations/memories/${photo.provider}/assets/${tripId}/${photo.asset_id}/${photo.user_id}/info`)
|
||||||
.then(r => setLightboxInfo(r.data)).catch(() => {}).finally(() => setLightboxInfoLoading(false))
|
.then(r => setLightboxInfo(r.data)).catch(() => {}).finally(() => setLightboxInfoLoading(false))
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
|
|||||||
+1
-5
@@ -33,9 +33,7 @@ import backupRoutes from './routes/backup';
|
|||||||
import oidcRoutes from './routes/oidc';
|
import oidcRoutes from './routes/oidc';
|
||||||
import vacayRoutes from './routes/vacay';
|
import vacayRoutes from './routes/vacay';
|
||||||
import atlasRoutes from './routes/atlas';
|
import atlasRoutes from './routes/atlas';
|
||||||
import immichRoutes from './routes/immich';
|
import memoriesRoutes from './routes/memories/unified';
|
||||||
import synologyRoutes from './routes/synology';
|
|
||||||
import memoriesRoutes from './routes/memories';
|
|
||||||
import notificationRoutes from './routes/notifications';
|
import notificationRoutes from './routes/notifications';
|
||||||
import shareRoutes from './routes/share';
|
import shareRoutes from './routes/share';
|
||||||
import { mcpHandler } from './mcp';
|
import { mcpHandler } from './mcp';
|
||||||
@@ -255,8 +253,6 @@ export function createApp(): express.Application {
|
|||||||
app.use('/api/addons/vacay', vacayRoutes);
|
app.use('/api/addons/vacay', vacayRoutes);
|
||||||
app.use('/api/addons/atlas', atlasRoutes);
|
app.use('/api/addons/atlas', atlasRoutes);
|
||||||
app.use('/api/integrations/memories', memoriesRoutes);
|
app.use('/api/integrations/memories', memoriesRoutes);
|
||||||
app.use('/api/integrations/immich', immichRoutes);
|
|
||||||
app.use('/api/integrations/synologyphotos', synologyRoutes);
|
|
||||||
app.use('/api/maps', mapsRoutes);
|
app.use('/api/maps', mapsRoutes);
|
||||||
app.use('/api/weather', weatherRoutes);
|
app.use('/api/weather', weatherRoutes);
|
||||||
app.use('/api/settings', settingsRoutes);
|
app.use('/api/settings', settingsRoutes);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import express, { Request, Response, NextFunction } from 'express';
|
import express, { Request, Response, NextFunction } from 'express';
|
||||||
import { db, canAccessTrip } from '../db/database';
|
import { db, canAccessTrip } from '../../db/database';
|
||||||
import { authenticate } from '../middleware/auth';
|
import { authenticate } from '../../middleware/auth';
|
||||||
import { broadcast } from '../websocket';
|
import { broadcast } from '../../websocket';
|
||||||
import { AuthRequest } from '../types';
|
import { AuthRequest } from '../../types';
|
||||||
import { consumeEphemeralToken } from '../services/ephemeralTokens';
|
import { consumeEphemeralToken } from '../../services/ephemeralTokens';
|
||||||
import { getClientIp } from '../services/auditLog';
|
import { getClientIp } from '../../services/auditLog';
|
||||||
import {
|
import {
|
||||||
getConnectionSettings,
|
getConnectionSettings,
|
||||||
saveImmichSettings,
|
saveImmichSettings,
|
||||||
@@ -14,12 +14,11 @@ import {
|
|||||||
searchPhotos,
|
searchPhotos,
|
||||||
proxyThumbnail,
|
proxyThumbnail,
|
||||||
proxyOriginal,
|
proxyOriginal,
|
||||||
isValidAssetId,
|
|
||||||
listAlbums,
|
listAlbums,
|
||||||
syncAlbumAssets,
|
syncAlbumAssets,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
} from '../services/immichService';
|
} from '../../services/memories/immichService';
|
||||||
import { canAccessUserPhoto } from '../services/memoriesService';
|
import { canAccessUserPhoto } from '../../services/memories/helpersService';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import express, { Request, Response } from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
import { authenticate } from '../middleware/auth';
|
import { authenticate } from '../../middleware/auth';
|
||||||
import { broadcast } from '../websocket';
|
import { broadcast } from '../../websocket';
|
||||||
import { AuthRequest } from '../types';
|
import { AuthRequest } from '../../types';
|
||||||
import {
|
import {
|
||||||
getSynologySettings,
|
getSynologySettings,
|
||||||
updateSynologySettings,
|
updateSynologySettings,
|
||||||
@@ -15,8 +15,8 @@ import {
|
|||||||
streamSynologyAsset,
|
streamSynologyAsset,
|
||||||
handleSynologyError,
|
handleSynologyError,
|
||||||
SynologyServiceError,
|
SynologyServiceError,
|
||||||
} from '../services/synologyService';
|
} from '../../services/memories/synologyService';
|
||||||
import { canAccessUserPhoto } from '../services/memoriesService';
|
import { canAccessUserPhoto } from '../../services/memories/helpersService';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import express, { Request, Response } from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
import { authenticate } from '../middleware/auth';
|
import { authenticate } from '../../middleware/auth';
|
||||||
import { AuthRequest } from '../types';
|
import { AuthRequest } from '../../types';
|
||||||
import {
|
import {
|
||||||
listTripPhotos,
|
listTripPhotos,
|
||||||
listTripAlbumLinks,
|
listTripAlbumLinks,
|
||||||
@@ -9,14 +9,19 @@ import {
|
|||||||
addTripPhotos,
|
addTripPhotos,
|
||||||
removeTripPhoto,
|
removeTripPhoto,
|
||||||
setTripPhotoSharing,
|
setTripPhotoSharing,
|
||||||
} from '../services/memoriesService';
|
} from '../../services/memories/unifiedService';
|
||||||
|
import immichRouter from './immich';
|
||||||
|
import synologyRouter from './synology';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.use('/immich', immichRouter);
|
||||||
|
router.use('/synologyphotos', synologyRouter);
|
||||||
|
|
||||||
//------------------------------------------------
|
//------------------------------------------------
|
||||||
// routes for managing photos linked to trip
|
// routes for managing photos linked to trip
|
||||||
|
|
||||||
router.get('/trips/:tripId/photos', authenticate, (req: Request, res: Response) => {
|
router.get('/unified/trips/:tripId/photos', authenticate, (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { tripId } = req.params;
|
const { tripId } = req.params;
|
||||||
const result = listTripPhotos(tripId, authReq.user.id);
|
const result = listTripPhotos(tripId, authReq.user.id);
|
||||||
@@ -24,7 +29,7 @@ router.get('/trips/:tripId/photos', authenticate, (req: Request, res: Response)
|
|||||||
res.json({ photos: result.data });
|
res.json({ photos: result.data });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/trips/:tripId/photos', authenticate, async (req: Request, res: Response) => {
|
router.post('/unified/trips/:tripId/photos', authenticate, async (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { tripId } = req.params;
|
const { tripId } = req.params;
|
||||||
const sid = req.headers['x-socket-id'] as string;
|
const sid = req.headers['x-socket-id'] as string;
|
||||||
@@ -42,7 +47,7 @@ router.post('/trips/:tripId/photos', authenticate, async (req: Request, res: Res
|
|||||||
res.json({ success: true, added: result.data.added });
|
res.json({ success: true, added: result.data.added });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put('/trips/:tripId/photos/sharing', authenticate, async (req: Request, res: Response) => {
|
router.put('/unified/trips/:tripId/photos/sharing', authenticate, async (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { tripId } = req.params;
|
const { tripId } = req.params;
|
||||||
const result = await setTripPhotoSharing(
|
const result = await setTripPhotoSharing(
|
||||||
@@ -56,7 +61,7 @@ router.put('/trips/:tripId/photos/sharing', authenticate, async (req: Request, r
|
|||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete('/trips/:tripId/photos', authenticate, async (req: Request, res: Response) => {
|
router.delete('/unified/trips/:tripId/photos', authenticate, async (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { tripId } = req.params;
|
const { tripId } = req.params;
|
||||||
const result = await removeTripPhoto(tripId, authReq.user.id, req.body?.provider, req.body?.asset_id);
|
const result = await removeTripPhoto(tripId, authReq.user.id, req.body?.provider, req.body?.asset_id);
|
||||||
@@ -67,7 +72,7 @@ router.delete('/trips/:tripId/photos', authenticate, async (req: Request, res: R
|
|||||||
//------------------------------
|
//------------------------------
|
||||||
// routes for managing album links
|
// routes for managing album links
|
||||||
|
|
||||||
router.get('/trips/:tripId/album-links', authenticate, (req: Request, res: Response) => {
|
router.get('/unified/trips/:tripId/album-links', authenticate, (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { tripId } = req.params;
|
const { tripId } = req.params;
|
||||||
const result = listTripAlbumLinks(tripId, authReq.user.id);
|
const result = listTripAlbumLinks(tripId, authReq.user.id);
|
||||||
@@ -75,7 +80,7 @@ router.get('/trips/:tripId/album-links', authenticate, (req: Request, res: Respo
|
|||||||
res.json({ links: result.data });
|
res.json({ links: result.data });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/trips/:tripId/album-links', authenticate, async (req: Request, res: Response) => {
|
router.post('/unified/trips/:tripId/album-links', authenticate, async (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { tripId } = req.params;
|
const { tripId } = req.params;
|
||||||
const result = createTripAlbumLink(tripId, authReq.user.id, req.body?.provider, req.body?.album_id, req.body?.album_name);
|
const result = createTripAlbumLink(tripId, authReq.user.id, req.body?.provider, req.body?.album_id, req.body?.album_name);
|
||||||
@@ -83,7 +88,7 @@ router.post('/trips/:tripId/album-links', authenticate, async (req: Request, res
|
|||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete('/trips/:tripId/album-links/:linkId', authenticate, async (req: Request, res: Response) => {
|
router.delete('/unified/trips/:tripId/album-links/:linkId', authenticate, async (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { tripId, linkId } = req.params;
|
const { tripId, linkId } = req.params;
|
||||||
const result = removeAlbumLink(tripId, linkId, authReq.user.id);
|
const result = removeAlbumLink(tripId, linkId, authReq.user.id);
|
||||||
@@ -91,4 +96,7 @@ router.delete('/trips/:tripId/album-links/:linkId', authenticate, async (req: Re
|
|||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { canAccessTrip, db } from "../../db/database";
|
||||||
|
|
||||||
|
// helpers for handling return types
|
||||||
|
|
||||||
|
type ServiceError = { success: false; error: { message: string; status: number } };
|
||||||
|
export type ServiceResult<T> = { success: true; data: T } | ServiceError;
|
||||||
|
|
||||||
|
|
||||||
|
export function fail(error: string, status: number): ServiceError {
|
||||||
|
return { success: false, error: { message: error, status } };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function success<T>(data: T): ServiceResult<T> {
|
||||||
|
return { success: true, data: data };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function mapDbError(error: unknown, fallbackMessage: string): ServiceError {
|
||||||
|
if (error instanceof Error && /unique|constraint/i.test(error.message)) {
|
||||||
|
return fail('Resource already exists', 409);
|
||||||
|
}
|
||||||
|
return fail(fallbackMessage, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
// types used across memories services
|
||||||
|
export type Selection = {
|
||||||
|
provider: string;
|
||||||
|
asset_ids: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//-----------------------------------------------
|
||||||
|
//access check helper
|
||||||
|
|
||||||
|
export function canAccessUserPhoto(requestingUserId: number, ownerUserId: number, tripId: string, assetId: string, provider: string): boolean {
|
||||||
|
if (requestingUserId === ownerUserId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const sharedAsset = db.prepare(`
|
||||||
|
SELECT 1
|
||||||
|
FROM trip_photos
|
||||||
|
WHERE user_id = ?
|
||||||
|
AND asset_id = ?
|
||||||
|
AND provider = ?
|
||||||
|
AND trip_id = ?
|
||||||
|
AND shared = 1
|
||||||
|
LIMIT 1
|
||||||
|
`).get(ownerUserId, assetId, provider, tripId);
|
||||||
|
|
||||||
|
if (!sharedAsset) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !!canAccessTrip(tripId, requestingUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
//helpers for album link syncing
|
||||||
|
|
||||||
|
export function getAlbumIdFromLink(tripId: string, linkId: string, userId: number): ServiceResult<string> {
|
||||||
|
const access = canAccessTrip(tripId, userId);
|
||||||
|
if (!access) return fail('Trip not found or access denied', 404);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const row = db.prepare('SELECT album_id FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?')
|
||||||
|
.get(linkId, tripId, userId) as { album_id: string } | null;
|
||||||
|
|
||||||
|
return row ? success(row.album_id) : fail('Album link not found', 404);
|
||||||
|
} catch {
|
||||||
|
return fail('Failed to retrieve album link', 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSyncTimeForAlbumLink(linkId: string): void {
|
||||||
|
db.prepare('UPDATE trip_album_links SET last_synced_at = CURRENT_TIMESTAMP WHERE id = ?').run(linkId);
|
||||||
|
}
|
||||||
+6
-6
@@ -1,9 +1,9 @@
|
|||||||
import { db } from '../db/database';
|
import { db } from '../../db/database';
|
||||||
import { maybe_encrypt_api_key, decrypt_api_key } from './apiKeyCrypto';
|
import { maybe_encrypt_api_key, decrypt_api_key } from '../apiKeyCrypto';
|
||||||
import { checkSsrf } from '../utils/ssrfGuard';
|
import { checkSsrf } from '../../utils/ssrfGuard';
|
||||||
import { writeAudit } from './auditLog';
|
import { writeAudit } from '../auditLog';
|
||||||
import { addTripPhotos, getAlbumIdFromLink, Selection, updateSyncTimeForAlbumLink } from './memoriesService';
|
import { addTripPhotos} from './unifiedService';
|
||||||
import { error } from 'node:console';
|
import { getAlbumIdFromLink, updateSyncTimeForAlbumLink, Selection } from './helpersService';
|
||||||
|
|
||||||
// ── Credentials ────────────────────────────────────────────────────────────
|
// ── Credentials ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
+5
-6
@@ -1,12 +1,11 @@
|
|||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
import { pipeline } from 'node:stream/promises';
|
import { pipeline } from 'node:stream/promises';
|
||||||
import { Response as ExpressResponse } from 'express';
|
import { Response as ExpressResponse } from 'express';
|
||||||
import { db } from '../db/database';
|
import { db } from '../../db/database';
|
||||||
import { decrypt_api_key, maybe_encrypt_api_key } from './apiKeyCrypto';
|
import { decrypt_api_key, maybe_encrypt_api_key } from '../apiKeyCrypto';
|
||||||
import { checkSsrf } from '../utils/ssrfGuard';
|
import { checkSsrf } from '../../utils/ssrfGuard';
|
||||||
import { addTripPhotos, getAlbumIdFromLink, Selection, updateSyncTimeForAlbumLink } from './memoriesService';
|
import { addTripPhotos} from './unifiedService';
|
||||||
import { error } from 'node:console';
|
import { getAlbumIdFromLink, updateSyncTimeForAlbumLink, Selection } from './helpersService';
|
||||||
import { th } from 'zod/locales';
|
|
||||||
|
|
||||||
const SYNOLOGY_API_TIMEOUT_MS = 30000;
|
const SYNOLOGY_API_TIMEOUT_MS = 30000;
|
||||||
const SYNOLOGY_PROVIDER = 'synologyphotos';
|
const SYNOLOGY_PROVIDER = 'synologyphotos';
|
||||||
+10
-71
@@ -1,49 +1,15 @@
|
|||||||
import { db, canAccessTrip } from '../db/database';
|
import { db, canAccessTrip } from '../../db/database';
|
||||||
import { notifyTripMembers } from './notifications';
|
import { notifyTripMembers } from '../notifications';
|
||||||
import { broadcast } from '../websocket';
|
import { broadcast } from '../../websocket';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ServiceResult,
|
||||||
|
fail,
|
||||||
|
success,
|
||||||
|
mapDbError,
|
||||||
|
Selection,
|
||||||
|
} from './helpersService';
|
||||||
|
|
||||||
type ServiceError = { success: false; error: { message: string; status: number } };
|
|
||||||
type ServiceResult<T> = { success: true; data: T } | ServiceError;
|
|
||||||
|
|
||||||
function fail(error: string, status: number): ServiceError {
|
|
||||||
return { success: false, error: { message: error, status }};
|
|
||||||
}
|
|
||||||
|
|
||||||
function success<T>(data: T): ServiceResult<T> {
|
|
||||||
return { success: true, data: data };
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDbError(error: unknown, fallbackMessage: string): ServiceError {
|
|
||||||
if (error instanceof Error && /unique|constraint/i.test(error.message)) {
|
|
||||||
return fail('Resource already exists', 409);
|
|
||||||
}
|
|
||||||
return fail(fallbackMessage, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
//-----------------------------------------------
|
|
||||||
//access check helper
|
|
||||||
|
|
||||||
export function canAccessUserPhoto(requestingUserId: number, ownerUserId: number, tripId: string, assetId: string, provider: string): boolean {
|
|
||||||
if (requestingUserId === ownerUserId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const sharedAsset = db.prepare(`
|
|
||||||
SELECT 1
|
|
||||||
FROM trip_photos
|
|
||||||
WHERE user_id = ?
|
|
||||||
AND asset_id = ?
|
|
||||||
AND provider = ?
|
|
||||||
AND trip_id = ?
|
|
||||||
AND shared = 1
|
|
||||||
LIMIT 1
|
|
||||||
`).get(ownerUserId, assetId, provider, tripId);
|
|
||||||
|
|
||||||
if (!sharedAsset) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !!canAccessTrip(tripId, requestingUserId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listTripPhotos(tripId: string, userId: number): ServiceResult<any[]> {
|
export function listTripPhotos(tripId: string, userId: number): ServiceResult<any[]> {
|
||||||
const access = canAccessTrip(tripId, userId);
|
const access = canAccessTrip(tripId, userId);
|
||||||
@@ -108,11 +74,6 @@ function _addTripPhoto(tripId: string, userId: number, provider: string, assetId
|
|||||||
return result.changes > 0;
|
return result.changes > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Selection = {
|
|
||||||
provider: string;
|
|
||||||
asset_ids: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function addTripPhotos(
|
export async function addTripPhotos(
|
||||||
tripId: string,
|
tripId: string,
|
||||||
userId: number,
|
userId: number,
|
||||||
@@ -181,7 +142,6 @@ export async function setTripPhotoSharing(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function removeTripPhoto(
|
export function removeTripPhoto(
|
||||||
tripId: string,
|
tripId: string,
|
||||||
userId: number,
|
userId: number,
|
||||||
@@ -262,25 +222,6 @@ export function removeAlbumLink(tripId: string, linkId: string, userId: number):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//helpers for album link syncing
|
|
||||||
|
|
||||||
export function getAlbumIdFromLink(tripId: string, linkId: string, userId: number): ServiceResult<string> {
|
|
||||||
const access = canAccessTrip(tripId, userId);
|
|
||||||
if (!access) return fail('Trip not found or access denied', 404);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const row = db.prepare('SELECT album_id FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?')
|
|
||||||
.get(linkId, tripId, userId) as { album_id: string } | null;
|
|
||||||
|
|
||||||
return row ? success(row.album_id) : fail('Album link not found', 404);
|
|
||||||
} catch {
|
|
||||||
return fail('Failed to retrieve album link', 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateSyncTimeForAlbumLink(linkId: string): void {
|
|
||||||
db.prepare('UPDATE trip_album_links SET last_synced_at = CURRENT_TIMESTAMP WHERE id = ?').run(linkId);
|
|
||||||
}
|
|
||||||
|
|
||||||
//-----------------------------------------------
|
//-----------------------------------------------
|
||||||
// notifications helper
|
// notifications helper
|
||||||
@@ -306,5 +247,3 @@ async function _notifySharedTripPhotos(
|
|||||||
return fail('Failed to send notifications', 500);
|
return fail('Failed to send notifications', 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user