changing routes and hierarchy of files for memories

This commit is contained in:
Marek Maslowski
2026-04-04 14:01:51 +02:00
parent 504713d920
commit bca82b3f8c
9 changed files with 147 additions and 127 deletions

View File

@@ -33,9 +33,7 @@ import backupRoutes from './routes/backup';
import oidcRoutes from './routes/oidc';
import vacayRoutes from './routes/vacay';
import atlasRoutes from './routes/atlas';
import immichRoutes from './routes/immich';
import synologyRoutes from './routes/synology';
import memoriesRoutes from './routes/memories';
import memoriesRoutes from './routes/memories/unified';
import notificationRoutes from './routes/notifications';
import shareRoutes from './routes/share';
import { mcpHandler } from './mcp';
@@ -255,8 +253,6 @@ export function createApp(): express.Application {
app.use('/api/addons/vacay', vacayRoutes);
app.use('/api/addons/atlas', atlasRoutes);
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/weather', weatherRoutes);
app.use('/api/settings', settingsRoutes);

View File

@@ -1,10 +1,10 @@
import express, { Request, Response, NextFunction } from 'express';
import { db, canAccessTrip } from '../db/database';
import { authenticate } from '../middleware/auth';
import { broadcast } from '../websocket';
import { AuthRequest } from '../types';
import { consumeEphemeralToken } from '../services/ephemeralTokens';
import { getClientIp } from '../services/auditLog';
import { db, canAccessTrip } from '../../db/database';
import { authenticate } from '../../middleware/auth';
import { broadcast } from '../../websocket';
import { AuthRequest } from '../../types';
import { consumeEphemeralToken } from '../../services/ephemeralTokens';
import { getClientIp } from '../../services/auditLog';
import {
getConnectionSettings,
saveImmichSettings,
@@ -14,12 +14,11 @@ import {
searchPhotos,
proxyThumbnail,
proxyOriginal,
isValidAssetId,
listAlbums,
syncAlbumAssets,
getAssetInfo,
} from '../services/immichService';
import { canAccessUserPhoto } from '../services/memoriesService';
} from '../../services/memories/immichService';
import { canAccessUserPhoto } from '../../services/memories/helpersService';
const router = express.Router();

View File

@@ -1,7 +1,7 @@
import express, { Request, Response } from 'express';
import { authenticate } from '../middleware/auth';
import { broadcast } from '../websocket';
import { AuthRequest } from '../types';
import { authenticate } from '../../middleware/auth';
import { broadcast } from '../../websocket';
import { AuthRequest } from '../../types';
import {
getSynologySettings,
updateSynologySettings,
@@ -15,8 +15,8 @@ import {
streamSynologyAsset,
handleSynologyError,
SynologyServiceError,
} from '../services/synologyService';
import { canAccessUserPhoto } from '../services/memoriesService';
} from '../../services/memories/synologyService';
import { canAccessUserPhoto } from '../../services/memories/helpersService';
const router = express.Router();

View File

@@ -1,6 +1,6 @@
import express, { Request, Response } from 'express';
import { authenticate } from '../middleware/auth';
import { AuthRequest } from '../types';
import { authenticate } from '../../middleware/auth';
import { AuthRequest } from '../../types';
import {
listTripPhotos,
listTripAlbumLinks,
@@ -9,14 +9,19 @@ import {
addTripPhotos,
removeTripPhoto,
setTripPhotoSharing,
} from '../services/memoriesService';
} from '../../services/memories/unifiedService';
import immichRouter from './immich';
import synologyRouter from './synology';
const router = express.Router();
router.use('/immich', immichRouter);
router.use('/synologyphotos', synologyRouter);
//------------------------------------------------
// 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 { tripId } = req.params;
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 });
});
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 { tripId } = req.params;
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 });
});
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 { tripId } = req.params;
const result = await setTripPhotoSharing(
@@ -56,7 +61,7 @@ router.put('/trips/:tripId/photos/sharing', authenticate, async (req: Request, r
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 { tripId } = req.params;
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
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 { tripId } = req.params;
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 });
});
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 { tripId } = req.params;
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 });
});
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 { tripId, linkId } = req.params;
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 });
});
export default router;

View File

@@ -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);
}

View File

@@ -1,9 +1,9 @@
import { db } from '../db/database';
import { maybe_encrypt_api_key, decrypt_api_key } from './apiKeyCrypto';
import { checkSsrf } from '../utils/ssrfGuard';
import { writeAudit } from './auditLog';
import { addTripPhotos, getAlbumIdFromLink, Selection, updateSyncTimeForAlbumLink } from './memoriesService';
import { error } from 'node:console';
import { db } from '../../db/database';
import { maybe_encrypt_api_key, decrypt_api_key } from '../apiKeyCrypto';
import { checkSsrf } from '../../utils/ssrfGuard';
import { writeAudit } from '../auditLog';
import { addTripPhotos} from './unifiedService';
import { getAlbumIdFromLink, updateSyncTimeForAlbumLink, Selection } from './helpersService';
// ── Credentials ────────────────────────────────────────────────────────────

View File

@@ -1,12 +1,11 @@
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { Response as ExpressResponse } from 'express';
import { db } from '../db/database';
import { decrypt_api_key, maybe_encrypt_api_key } from './apiKeyCrypto';
import { checkSsrf } from '../utils/ssrfGuard';
import { addTripPhotos, getAlbumIdFromLink, Selection, updateSyncTimeForAlbumLink } from './memoriesService';
import { error } from 'node:console';
import { th } from 'zod/locales';
import { db } from '../../db/database';
import { decrypt_api_key, maybe_encrypt_api_key } from '../apiKeyCrypto';
import { checkSsrf } from '../../utils/ssrfGuard';
import { addTripPhotos} from './unifiedService';
import { getAlbumIdFromLink, updateSyncTimeForAlbumLink, Selection } from './helpersService';
const SYNOLOGY_API_TIMEOUT_MS = 30000;
const SYNOLOGY_PROVIDER = 'synologyphotos';

View File

@@ -1,49 +1,15 @@
import { db, canAccessTrip } from '../db/database';
import { notifyTripMembers } from './notifications';
import { broadcast } from '../websocket';
import { db, canAccessTrip } from '../../db/database';
import { notifyTripMembers } from '../notifications';
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[]> {
const access = canAccessTrip(tripId, userId);
@@ -108,11 +74,6 @@ function _addTripPhoto(tripId: string, userId: number, provider: string, assetId
return result.changes > 0;
}
export type Selection = {
provider: string;
asset_ids: string[];
};
export async function addTripPhotos(
tripId: string,
userId: number,
@@ -181,7 +142,6 @@ export async function setTripPhotoSharing(
}
}
export function removeTripPhoto(
tripId: string,
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
@@ -306,5 +247,3 @@ async function _notifySharedTripPhotos(
return fail('Failed to send notifications', 500);
}
}