mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 06:11:45 +00:00
Merge branch 'test' into dev
This commit is contained in:
@@ -317,9 +317,9 @@ router.post('/ws-token', authenticate, (req: Request, res: Response) => {
|
||||
// Short-lived single-use token for direct resource URLs
|
||||
router.post('/resource-token', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const result = createResourceToken(authReq.user.id, req.body.purpose);
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
res.json({ token: result.token });
|
||||
const token = createResourceToken(authReq.user.id, req.body.purpose);
|
||||
if (!token) return res.status(503).json({ error: 'Service unavailable' });
|
||||
res.json(token);
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
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 {
|
||||
getConnectionSettings,
|
||||
saveImmichSettings,
|
||||
testConnection,
|
||||
getConnectionStatus,
|
||||
browseTimeline,
|
||||
searchPhotos,
|
||||
listTripPhotos,
|
||||
addTripPhotos,
|
||||
removeTripPhoto,
|
||||
togglePhotoSharing,
|
||||
getAssetInfo,
|
||||
proxyThumbnail,
|
||||
proxyOriginal,
|
||||
isValidAssetId,
|
||||
canAccessUserPhoto,
|
||||
listAlbums,
|
||||
listAlbumLinks,
|
||||
createAlbumLink,
|
||||
deleteAlbumLink,
|
||||
syncAlbumAssets,
|
||||
} from '../services/immichService';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// ── Dual auth middleware (JWT or ephemeral token for <img> src) ─────────────
|
||||
|
||||
function authFromQuery(req: Request, res: Response, next: NextFunction) {
|
||||
const queryToken = req.query.token as string | undefined;
|
||||
if (queryToken) {
|
||||
const userId = consumeEphemeralToken(queryToken, 'immich');
|
||||
if (!userId) return res.status(401).send('Invalid or expired token');
|
||||
const user = db.prepare('SELECT id, username, email, role, mfa_enabled FROM users WHERE id = ?').get(userId) as any;
|
||||
if (!user) return res.status(401).send('User not found');
|
||||
(req as AuthRequest).user = user;
|
||||
return next();
|
||||
}
|
||||
return (authenticate as any)(req, res, next);
|
||||
}
|
||||
|
||||
// ── Immich Connection Settings ─────────────────────────────────────────────
|
||||
|
||||
router.get('/settings', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
res.json(getConnectionSettings(authReq.user.id));
|
||||
});
|
||||
|
||||
router.put('/settings', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { immich_url, immich_api_key } = req.body;
|
||||
const result = await saveImmichSettings(authReq.user.id, immich_url, immich_api_key, getClientIp(req));
|
||||
if (!result.success) return res.status(400).json({ error: result.error });
|
||||
if (result.warning) return res.json({ success: true, warning: result.warning });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
router.get('/status', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
res.json(await getConnectionStatus(authReq.user.id));
|
||||
});
|
||||
|
||||
router.post('/test', authenticate, async (req: Request, res: Response) => {
|
||||
const { immich_url, immich_api_key } = req.body;
|
||||
if (!immich_url || !immich_api_key) return res.json({ connected: false, error: 'URL and API key required' });
|
||||
res.json(await testConnection(immich_url, immich_api_key));
|
||||
});
|
||||
|
||||
// ── Browse Immich Library (for photo picker) ───────────────────────────────
|
||||
|
||||
router.get('/browse', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const result = await browseTimeline(authReq.user.id);
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
res.json({ buckets: result.buckets });
|
||||
});
|
||||
|
||||
router.post('/search', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { from, to } = req.body;
|
||||
const result = await searchPhotos(authReq.user.id, from, to);
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
res.json({ assets: result.assets });
|
||||
});
|
||||
|
||||
// ── Trip Photos (selected by user) ────────────────────────────────────────
|
||||
|
||||
router.get('/trips/:tripId/photos', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
if (!canAccessTrip(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' });
|
||||
res.json({ photos: listTripPhotos(tripId, authReq.user.id) });
|
||||
});
|
||||
|
||||
router.post('/trips/:tripId/photos', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
if (!canAccessTrip(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' });
|
||||
const { asset_ids, shared = true } = req.body;
|
||||
|
||||
if (!Array.isArray(asset_ids) || asset_ids.length === 0) {
|
||||
return res.status(400).json({ error: 'asset_ids required' });
|
||||
}
|
||||
|
||||
const added = addTripPhotos(tripId, authReq.user.id, asset_ids, shared);
|
||||
res.json({ success: true, added });
|
||||
broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string);
|
||||
|
||||
// Notify trip members about shared photos
|
||||
if (shared && added > 0) {
|
||||
import('../services/notifications').then(({ notifyTripMembers }) => {
|
||||
const tripInfo = db.prepare('SELECT title FROM trips WHERE id = ?').get(tripId) as { title: string } | undefined;
|
||||
notifyTripMembers(Number(tripId), authReq.user.id, 'photos_shared', { trip: tripInfo?.title || 'Untitled', actor: authReq.user.email, count: String(added) }).catch(() => {});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/trips/:tripId/photos/:assetId', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
if (!canAccessTrip(req.params.tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' });
|
||||
removeTripPhoto(req.params.tripId, authReq.user.id, req.params.assetId);
|
||||
res.json({ success: true });
|
||||
broadcast(req.params.tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
|
||||
router.put('/trips/:tripId/photos/:assetId/sharing', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
if (!canAccessTrip(req.params.tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' });
|
||||
const { shared } = req.body;
|
||||
togglePhotoSharing(req.params.tripId, authReq.user.id, req.params.assetId, shared);
|
||||
res.json({ success: true });
|
||||
broadcast(req.params.tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
|
||||
// ── Asset Details ──────────────────────────────────────────────────────────
|
||||
|
||||
router.get('/assets/:assetId/info', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { assetId } = req.params;
|
||||
if (!isValidAssetId(assetId)) return res.status(400).json({ error: 'Invalid asset ID' });
|
||||
const queryUserId = req.query.userId ? Number(req.query.userId) : undefined;
|
||||
const ownerUserId = queryUserId && queryUserId !== authReq.user.id ? queryUserId : undefined;
|
||||
if (ownerUserId && !canAccessUserPhoto(authReq.user.id, ownerUserId, assetId)) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
const result = await getAssetInfo(authReq.user.id, assetId, ownerUserId);
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
res.json(result.data);
|
||||
});
|
||||
|
||||
// ── Proxy Immich Assets ────────────────────────────────────────────────────
|
||||
|
||||
router.get('/assets/:assetId/thumbnail', authFromQuery, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { assetId } = req.params;
|
||||
if (!isValidAssetId(assetId)) return res.status(400).send('Invalid asset ID');
|
||||
const queryUserId = req.query.userId ? Number(req.query.userId) : undefined;
|
||||
const ownerUserId = queryUserId && queryUserId !== authReq.user.id ? queryUserId : undefined;
|
||||
if (ownerUserId && !canAccessUserPhoto(authReq.user.id, ownerUserId, assetId)) {
|
||||
return res.status(403).send('Forbidden');
|
||||
}
|
||||
const result = await proxyThumbnail(authReq.user.id, assetId, ownerUserId);
|
||||
if (result.error) return res.status(result.status!).send(result.error);
|
||||
res.set('Content-Type', result.contentType!);
|
||||
res.set('Cache-Control', 'public, max-age=86400');
|
||||
res.send(result.buffer);
|
||||
});
|
||||
|
||||
router.get('/assets/:assetId/original', authFromQuery, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { assetId } = req.params;
|
||||
if (!isValidAssetId(assetId)) return res.status(400).send('Invalid asset ID');
|
||||
const queryUserId = req.query.userId ? Number(req.query.userId) : undefined;
|
||||
const ownerUserId = queryUserId && queryUserId !== authReq.user.id ? queryUserId : undefined;
|
||||
if (ownerUserId && !canAccessUserPhoto(authReq.user.id, ownerUserId, assetId)) {
|
||||
return res.status(403).send('Forbidden');
|
||||
}
|
||||
const result = await proxyOriginal(authReq.user.id, assetId, ownerUserId);
|
||||
if (result.error) return res.status(result.status!).send(result.error);
|
||||
res.set('Content-Type', result.contentType!);
|
||||
res.set('Cache-Control', 'public, max-age=86400');
|
||||
res.send(result.buffer);
|
||||
});
|
||||
|
||||
// ── Album Linking ──────────────────────────────────────────────────────────
|
||||
|
||||
router.get('/albums', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const result = await listAlbums(authReq.user.id);
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
res.json({ albums: result.albums });
|
||||
});
|
||||
|
||||
router.get('/trips/:tripId/album-links', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
if (!canAccessTrip(req.params.tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' });
|
||||
res.json({ links: listAlbumLinks(req.params.tripId) });
|
||||
});
|
||||
|
||||
router.post('/trips/:tripId/album-links', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
if (!canAccessTrip(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' });
|
||||
const { album_id, album_name } = req.body;
|
||||
if (!album_id) return res.status(400).json({ error: 'album_id required' });
|
||||
const result = createAlbumLink(tripId, authReq.user.id, album_id, album_name);
|
||||
if (!result.success) return res.status(400).json({ error: result.error });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
router.delete('/trips/:tripId/album-links/:linkId', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
deleteAlbumLink(req.params.linkId, req.params.tripId, authReq.user.id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
router.post('/trips/:tripId/album-links/:linkId/sync', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, linkId } = req.params;
|
||||
const result = await syncAlbumAssets(tripId, linkId, authReq.user.id);
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
res.json({ success: true, added: result.added, total: result.total });
|
||||
if (result.added! > 0) {
|
||||
broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,147 @@
|
||||
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 {
|
||||
getConnectionSettings,
|
||||
saveImmichSettings,
|
||||
testConnection,
|
||||
getConnectionStatus,
|
||||
browseTimeline,
|
||||
searchPhotos,
|
||||
proxyThumbnail,
|
||||
proxyOriginal,
|
||||
listAlbums,
|
||||
syncAlbumAssets,
|
||||
getAssetInfo,
|
||||
} from '../../services/memories/immichService';
|
||||
import { canAccessUserPhoto } from '../../services/memories/helpersService';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// ── Dual auth middleware (JWT or ephemeral token for <img> src) ─────────────
|
||||
function authFromQuery(req: Request, res: Response, next: NextFunction) {
|
||||
const queryToken = req.query.token as string | undefined;
|
||||
if (queryToken) {
|
||||
const userId = consumeEphemeralToken(queryToken, 'immich');
|
||||
if (!userId) return res.status(401).send('Invalid or expired token');
|
||||
const user = db.prepare('SELECT id, username, email, role, mfa_enabled FROM users WHERE id = ?').get(userId) as any;
|
||||
if (!user) return res.status(401).send('User not found');
|
||||
(req as AuthRequest).user = user;
|
||||
return next();
|
||||
}
|
||||
return (authenticate as any)(req, res, next);
|
||||
}
|
||||
|
||||
// ── Immich Connection Settings ─────────────────────────────────────────────
|
||||
|
||||
router.get('/settings', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
res.json(getConnectionSettings(authReq.user.id));
|
||||
});
|
||||
|
||||
router.put('/settings', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { immich_url, immich_api_key } = req.body;
|
||||
const result = await saveImmichSettings(authReq.user.id, immich_url, immich_api_key, getClientIp(req));
|
||||
if (!result.success) return res.status(400).json({ error: result.error });
|
||||
if (result.warning) return res.json({ success: true, warning: result.warning });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
router.get('/status', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
res.json(await getConnectionStatus(authReq.user.id));
|
||||
});
|
||||
|
||||
router.post('/test', authenticate, async (req: Request, res: Response) => {
|
||||
const { immich_url, immich_api_key } = req.body;
|
||||
if (!immich_url || !immich_api_key) return res.json({ connected: false, error: 'URL and API key required' });
|
||||
res.json(await testConnection(immich_url, immich_api_key));
|
||||
});
|
||||
|
||||
// ── Browse Immich Library (for photo picker) ───────────────────────────────
|
||||
|
||||
router.get('/browse', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const result = await browseTimeline(authReq.user.id);
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
res.json({ buckets: result.buckets });
|
||||
});
|
||||
|
||||
router.post('/search', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { from, to } = req.body;
|
||||
const result = await searchPhotos(authReq.user.id, from, to);
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
res.json({ assets: result.assets });
|
||||
});
|
||||
|
||||
// ── Asset Details ──────────────────────────────────────────────────────────
|
||||
|
||||
router.get('/assets/:tripId/:assetId/:ownerId/info', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, assetId, ownerId } = req.params;
|
||||
|
||||
if (!canAccessUserPhoto(authReq.user.id, Number(ownerId), tripId, assetId, 'immich')) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
const result = await getAssetInfo(authReq.user.id, assetId, Number(ownerId));
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
res.json(result.data);
|
||||
});
|
||||
|
||||
// ── Proxy Immich Assets ────────────────────────────────────────────────────
|
||||
|
||||
router.get('/assets/:tripId/:assetId/:ownerId/thumbnail', authFromQuery, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, assetId, ownerId } = req.params;
|
||||
|
||||
if (!canAccessUserPhoto(authReq.user.id, Number(ownerId), tripId, assetId, 'immich')) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
const result = await proxyThumbnail(authReq.user.id, assetId, Number(ownerId));
|
||||
if (result.error) return res.status(result.status!).send(result.error);
|
||||
res.set('Content-Type', result.contentType!);
|
||||
res.set('Cache-Control', 'public, max-age=86400');
|
||||
res.send(result.buffer);
|
||||
});
|
||||
|
||||
router.get('/assets/:tripId/:assetId/:ownerId/original', authFromQuery, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, assetId, ownerId } = req.params;
|
||||
|
||||
if (!canAccessUserPhoto(authReq.user.id, Number(ownerId), tripId, assetId, 'immich')) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
const result = await proxyOriginal(authReq.user.id, assetId, Number(ownerId));
|
||||
if (result.error) return res.status(result.status!).send(result.error);
|
||||
res.set('Content-Type', result.contentType!);
|
||||
res.set('Cache-Control', 'public, max-age=86400');
|
||||
res.send(result.buffer);
|
||||
});
|
||||
|
||||
// ── Album Linking ──────────────────────────────────────────────────────────
|
||||
|
||||
router.get('/albums', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const result = await listAlbums(authReq.user.id);
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
res.json({ albums: result.albums });
|
||||
});
|
||||
|
||||
router.post('/trips/:tripId/album-links/:linkId/sync', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, linkId } = req.params;
|
||||
const result = await syncAlbumAssets(tripId, linkId, authReq.user.id);
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
res.json({ success: true, added: result.added, total: result.total });
|
||||
if (result.added! > 0) {
|
||||
broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,126 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { authenticate } from '../../middleware/auth';
|
||||
import { AuthRequest } from '../../types';
|
||||
import {
|
||||
getSynologySettings,
|
||||
updateSynologySettings,
|
||||
getSynologyStatus,
|
||||
testSynologyConnection,
|
||||
listSynologyAlbums,
|
||||
syncSynologyAlbumLink,
|
||||
searchSynologyPhotos,
|
||||
getSynologyAssetInfo,
|
||||
streamSynologyAsset,
|
||||
} from '../../services/memories/synologyService';
|
||||
import { canAccessUserPhoto, handleServiceResult, fail } from '../../services/memories/helpersService';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function _parseStringBodyField(value: unknown): string {
|
||||
return String(value ?? '').trim();
|
||||
}
|
||||
|
||||
function _parseNumberBodyField(value: unknown, fallback: number): number {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
router.get('/settings', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
handleServiceResult(res, await getSynologySettings(authReq.user.id));
|
||||
});
|
||||
|
||||
router.put('/settings', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const body = req.body as Record<string, unknown>;
|
||||
const synology_url = _parseStringBodyField(body.synology_url);
|
||||
const synology_username = _parseStringBodyField(body.synology_username);
|
||||
const synology_password = _parseStringBodyField(body.synology_password);
|
||||
|
||||
if (!synology_url || !synology_username) {
|
||||
handleServiceResult(res, fail('URL and username are required', 400));
|
||||
}
|
||||
else {
|
||||
handleServiceResult(res, await updateSynologySettings(authReq.user.id, synology_url, synology_username, synology_password));
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/status', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
handleServiceResult(res, await getSynologyStatus(authReq.user.id));
|
||||
});
|
||||
|
||||
router.post('/test', authenticate, async (req: Request, res: Response) => {
|
||||
const body = req.body as Record<string, unknown>;
|
||||
const synology_url = _parseStringBodyField(body.synology_url);
|
||||
const synology_username = _parseStringBodyField(body.synology_username);
|
||||
const synology_password = _parseStringBodyField(body.synology_password);
|
||||
|
||||
if (!synology_url || !synology_username || !synology_password) {
|
||||
handleServiceResult(res, fail('URL, username, and password are required', 400));
|
||||
}
|
||||
else{
|
||||
handleServiceResult(res, await testSynologyConnection(synology_url, synology_username, synology_password));
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/albums', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
handleServiceResult(res, await listSynologyAlbums(authReq.user.id));
|
||||
});
|
||||
|
||||
router.post('/trips/:tripId/album-links/:linkId/sync', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, linkId } = req.params;
|
||||
|
||||
handleServiceResult(res, await syncSynologyAlbumLink(authReq.user.id, tripId, linkId));
|
||||
});
|
||||
|
||||
router.post('/search', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const body = req.body as Record<string, unknown>;
|
||||
const from = _parseStringBodyField(body.from);
|
||||
const to = _parseStringBodyField(body.to);
|
||||
const offset = _parseNumberBodyField(body.offset, 0);
|
||||
const limit = _parseNumberBodyField(body.limit, 100);
|
||||
|
||||
handleServiceResult(res, await searchSynologyPhotos(
|
||||
authReq.user.id,
|
||||
from || undefined,
|
||||
to || undefined,
|
||||
offset,
|
||||
limit,
|
||||
));
|
||||
});
|
||||
|
||||
router.get('/assets/:tripId/:photoId/:ownerId/info', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, photoId, ownerId } = req.params;
|
||||
|
||||
if (!canAccessUserPhoto(authReq.user.id, Number(ownerId), tripId, photoId, 'synologyphotos')) {
|
||||
handleServiceResult(res, fail('You don\'t have access to this photo', 403));
|
||||
}
|
||||
else {
|
||||
handleServiceResult(res, await getSynologyAssetInfo(authReq.user.id, photoId, Number(ownerId)));
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/assets/:tripId/:photoId/:ownerId/:kind', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, photoId, ownerId, kind } = req.params;
|
||||
const { size = "sm" } = req.query;
|
||||
|
||||
if (kind !== 'thumbnail' && kind !== 'original') {
|
||||
handleServiceResult(res, fail('Invalid asset kind', 400));
|
||||
}
|
||||
|
||||
if (!canAccessUserPhoto(authReq.user.id, Number(ownerId), tripId, photoId, 'synologyphotos')) {
|
||||
handleServiceResult(res, fail('You don\'t have access to this photo', 403));
|
||||
}
|
||||
else{
|
||||
await streamSynologyAsset(res, authReq.user.id, Number(ownerId), photoId, kind as 'thumbnail' | 'original', String(size));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,102 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { authenticate } from '../../middleware/auth';
|
||||
import { AuthRequest } from '../../types';
|
||||
import {
|
||||
listTripPhotos,
|
||||
listTripAlbumLinks,
|
||||
createTripAlbumLink,
|
||||
removeAlbumLink,
|
||||
addTripPhotos,
|
||||
removeTripPhoto,
|
||||
setTripPhotoSharing,
|
||||
} 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('/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);
|
||||
if ('error' in result) return res.status(result.error.status).json({ error: result.error.message });
|
||||
res.json({ photos: result.data });
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
const shared = req.body?.shared === undefined ? true : !!req.body?.shared;
|
||||
const result = await addTripPhotos(
|
||||
tripId,
|
||||
authReq.user.id,
|
||||
shared,
|
||||
req.body?.selections || [],
|
||||
sid,
|
||||
);
|
||||
if ('error' in result) return res.status(result.error.status).json({ error: result.error.message });
|
||||
|
||||
res.json({ success: true, added: result.data.added });
|
||||
});
|
||||
|
||||
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(
|
||||
tripId,
|
||||
authReq.user.id,
|
||||
req.body?.provider,
|
||||
req.body?.asset_id,
|
||||
req.body?.shared,
|
||||
);
|
||||
if ('error' in result) return res.status(result.error.status).json({ error: result.error.message });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
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);
|
||||
if ('error' in result) return res.status(result.error.status).json({ error: result.error.message });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
//------------------------------
|
||||
// routes for managing album links
|
||||
|
||||
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);
|
||||
if ('error' in result) return res.status(result.error.status).json({ error: result.error.message });
|
||||
res.json({ links: result.data });
|
||||
});
|
||||
|
||||
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);
|
||||
if ('error' in result) return res.status(result.error.status).json({ error: result.error.message });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
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);
|
||||
if ('error' in result) return res.status(result.error.status).json({ error: result.error.message });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user