mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 22:01:45 +00:00
Merge branch 'test' into dev
This commit is contained in:
+88
-11
@@ -1,6 +1,7 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { authenticate, adminOnly } from '../middleware/auth';
|
||||
import { AuthRequest } from '../types';
|
||||
import { db } from '../db/database';
|
||||
import { AuthRequest, Addon } from '../types';
|
||||
import { writeAudit, getClientIp, logInfo } from '../services/auditLog';
|
||||
import * as svc from '../services/adminService';
|
||||
|
||||
@@ -264,21 +265,100 @@ router.delete('/packing-templates/:templateId/items/:itemId', (req: Request, res
|
||||
// ── Addons ─────────────────────────────────────────────────────────────────
|
||||
|
||||
router.get('/addons', (_req: Request, res: Response) => {
|
||||
res.json({ addons: svc.listAddons() });
|
||||
const addons = db.prepare('SELECT * FROM addons ORDER BY sort_order, id').all() as Addon[];
|
||||
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<string, typeof fields>();
|
||||
for (const field of fields) {
|
||||
const arr = fieldsByProvider.get(field.provider_id) || [];
|
||||
arr.push(field);
|
||||
fieldsByProvider.set(field.provider_id, arr);
|
||||
}
|
||||
|
||||
res.json({
|
||||
addons: [
|
||||
...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,
|
||||
})),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
router.put('/addons/:id', (req: Request, res: Response) => {
|
||||
const result = svc.updateAddon(req.params.id, req.body);
|
||||
if ('error' in result) return res.status(result.status!).json({ error: result.error });
|
||||
const addon = db.prepare('SELECT * FROM addons WHERE id = ?').get(req.params.id) as Addon | undefined;
|
||||
const provider = db.prepare('SELECT * FROM photo_providers WHERE id = ?').get(req.params.id) as { id: string; name: string; description?: string | null; icon: string; enabled: number; config: string; sort_order: number } | undefined;
|
||||
if (!addon && !provider) return res.status(404).json({ error: 'Addon not found' });
|
||||
const { enabled, config } = req.body;
|
||||
if (addon) {
|
||||
if (enabled !== undefined) db.prepare('UPDATE addons SET enabled = ? WHERE id = ?').run(enabled ? 1 : 0, req.params.id);
|
||||
if (config !== undefined) db.prepare('UPDATE addons SET config = ? WHERE id = ?').run(JSON.stringify(config), req.params.id);
|
||||
} else {
|
||||
if (enabled !== undefined) db.prepare('UPDATE photo_providers SET enabled = ? WHERE id = ?').run(enabled ? 1 : 0, req.params.id);
|
||||
if (config !== undefined) db.prepare('UPDATE photo_providers SET config = ? WHERE id = ?').run(JSON.stringify(config), req.params.id);
|
||||
}
|
||||
const updatedAddon = db.prepare('SELECT * FROM addons WHERE id = ?').get(req.params.id) as Addon | undefined;
|
||||
const updatedProvider = db.prepare('SELECT * FROM photo_providers WHERE id = ?').get(req.params.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;
|
||||
const authReq = req as AuthRequest;
|
||||
writeAudit({
|
||||
userId: authReq.user.id,
|
||||
action: 'admin.addon_update',
|
||||
resource: String(req.params.id),
|
||||
ip: getClientIp(req),
|
||||
details: result.auditDetails,
|
||||
details: { enabled: req.body.enabled, config: req.body.config },
|
||||
});
|
||||
res.json({ addon: result.addon });
|
||||
res.json({ addon: updated });
|
||||
});
|
||||
|
||||
// ── MCP Tokens ─────────────────────────────────────────────────────────────
|
||||
@@ -300,12 +380,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 });
|
||||
|
||||
@@ -317,9 +317,13 @@ 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 { purpose } = req.body as { purpose?: string };
|
||||
if (purpose !== 'download' && purpose !== 'immich' && purpose !== 'synologyphotos') {
|
||||
return res.status(400).json({ error: 'Invalid purpose' });
|
||||
}
|
||||
const token = createResourceToken(authReq.user.id, purpose);
|
||||
if (!token) return res.status(503).json({ error: 'Service unavailable' });
|
||||
res.json(token);
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
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) {
|
||||
@@ -88,7 +87,7 @@ router.post('/search', authenticate, async (req: Request, res: Response) => {
|
||||
res.json({ assets: result.assets });
|
||||
});
|
||||
|
||||
// ── Trip Photos (selected by user) ────────────────────────────────────────
|
||||
// ── Asset Details ──────────────────────────────────────────────────────────
|
||||
|
||||
router.get('/trips/:tripId/photos', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
@@ -97,57 +96,6 @@ router.get('/trips/:tripId/photos', authenticate, (req: Request, res: Response)
|
||||
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 result = await getAssetInfo(authReq.user.id, assetId);
|
||||
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) => {
|
||||
@@ -181,12 +129,6 @@ router.get('/albums', authenticate, async (req: Request, res: Response) => {
|
||||
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;
|
||||
@@ -198,12 +140,6 @@ router.post('/trips/:tripId/album-links', authenticate, async (req: Request, res
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { db, canAccessTrip } from '../db/database';
|
||||
import { authenticate } from '../middleware/auth';
|
||||
import { broadcast } from '../websocket';
|
||||
import { AuthRequest } from '../types';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
||||
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' });
|
||||
}
|
||||
|
||||
const photos = db.prepare(`
|
||||
SELECT tp.asset_id, tp.provider, tp.user_id, tp.shared, tp.added_at,
|
||||
u.username, u.avatar
|
||||
FROM trip_photos tp
|
||||
JOIN users u ON tp.user_id = u.id
|
||||
WHERE tp.trip_id = ?
|
||||
AND (tp.user_id = ? OR tp.shared = 1)
|
||||
ORDER BY tp.added_at ASC
|
||||
`).all(tripId, authReq.user.id) as any[];
|
||||
|
||||
res.json({ photos });
|
||||
});
|
||||
|
||||
router.get('/trips/:tripId/album-links', 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 links = db.prepare(`
|
||||
SELECT tal.id,
|
||||
tal.trip_id,
|
||||
tal.user_id,
|
||||
tal.provider,
|
||||
tal.album_id,
|
||||
tal.album_name,
|
||||
tal.sync_enabled,
|
||||
tal.last_synced_at,
|
||||
tal.created_at,
|
||||
u.username
|
||||
FROM trip_album_links tal
|
||||
JOIN users u ON tal.user_id = u.id
|
||||
WHERE tal.trip_id = ?
|
||||
ORDER BY tal.created_at ASC
|
||||
`).all(tripId);
|
||||
|
||||
res.json({ links });
|
||||
});
|
||||
|
||||
router.delete('/trips/:tripId/album-links/:linkId', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, linkId } = req.params;
|
||||
|
||||
if (!canAccessTrip(tripId, authReq.user.id)) {
|
||||
return res.status(404).json({ error: 'Trip not found' });
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?')
|
||||
.run(linkId, tripId, authReq.user.id);
|
||||
|
||||
res.json({ success: true });
|
||||
broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
|
||||
router.post('/trips/:tripId/photos', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
const { shared = true } = req.body;
|
||||
const selectionsRaw = Array.isArray(req.body?.selections) ? req.body.selections : null;
|
||||
const provider = String(req.body?.provider || '').toLowerCase();
|
||||
const assetIdsRaw = req.body?.asset_ids;
|
||||
|
||||
if (!canAccessTrip(tripId, authReq.user.id)) {
|
||||
return res.status(404).json({ error: 'Trip not found' });
|
||||
}
|
||||
|
||||
const selections = selectionsRaw && selectionsRaw.length > 0
|
||||
? selectionsRaw
|
||||
.map((selection: any) => ({
|
||||
provider: String(selection?.provider || '').toLowerCase(),
|
||||
asset_ids: Array.isArray(selection?.asset_ids) ? selection.asset_ids : [],
|
||||
}))
|
||||
.filter((selection: { provider: string; asset_ids: unknown[] }) => selection.provider && selection.asset_ids.length > 0)
|
||||
: (provider && Array.isArray(assetIdsRaw) && assetIdsRaw.length > 0
|
||||
? [{ provider, asset_ids: assetIdsRaw }]
|
||||
: []);
|
||||
|
||||
if (selections.length === 0) {
|
||||
return res.status(400).json({ error: 'selections required' });
|
||||
}
|
||||
|
||||
const insert = db.prepare(
|
||||
'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, asset_id, provider, shared) VALUES (?, ?, ?, ?, ?)'
|
||||
);
|
||||
|
||||
let added = 0;
|
||||
for (const selection of selections) {
|
||||
for (const raw of selection.asset_ids) {
|
||||
const assetId = String(raw || '').trim();
|
||||
if (!assetId) continue;
|
||||
const result = insert.run(tripId, authReq.user.id, assetId, selection.provider, shared ? 1 : 0);
|
||||
if (result.changes > 0) added++;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, added });
|
||||
broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string);
|
||||
|
||||
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.username || authReq.user.email,
|
||||
count: String(added),
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/trips/:tripId/photos', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
const provider = String(req.body?.provider || '').toLowerCase();
|
||||
const assetId = String(req.body?.asset_id || '');
|
||||
|
||||
if (!assetId) {
|
||||
return res.status(400).json({ error: 'asset_id is required' });
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
return res.status(400).json({ error: 'provider is required' });
|
||||
}
|
||||
|
||||
if (!canAccessTrip(tripId, authReq.user.id)) {
|
||||
return res.status(404).json({ error: 'Trip not found' });
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
DELETE FROM trip_photos
|
||||
WHERE trip_id = ?
|
||||
AND user_id = ?
|
||||
AND asset_id = ?
|
||||
AND provider = ?
|
||||
`).run(tripId, authReq.user.id, assetId, provider);
|
||||
|
||||
res.json({ success: true });
|
||||
broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
|
||||
router.put('/trips/:tripId/photos/sharing', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
const provider = String(req.body?.provider || '').toLowerCase();
|
||||
const assetId = String(req.body?.asset_id || '');
|
||||
const { shared } = req.body;
|
||||
|
||||
if (!assetId) {
|
||||
return res.status(400).json({ error: 'asset_id is required' });
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
return res.status(400).json({ error: 'provider is required' });
|
||||
}
|
||||
|
||||
if (!canAccessTrip(tripId, authReq.user.id)) {
|
||||
return res.status(404).json({ error: 'Trip not found' });
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
UPDATE trip_photos
|
||||
SET shared = ?
|
||||
WHERE trip_id = ?
|
||||
AND user_id = ?
|
||||
AND asset_id = ?
|
||||
AND provider = ?
|
||||
`).run(shared ? 1 : 0, tripId, authReq.user.id, assetId, provider);
|
||||
|
||||
res.json({ success: true });
|
||||
broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,183 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { authenticate } from '../middleware/auth';
|
||||
import { broadcast } from '../websocket';
|
||||
import { AuthRequest } from '../types';
|
||||
import {
|
||||
getSynologySettings,
|
||||
updateSynologySettings,
|
||||
getSynologyStatus,
|
||||
testSynologyConnection,
|
||||
listSynologyAlbums,
|
||||
linkSynologyAlbum,
|
||||
syncSynologyAlbumLink,
|
||||
searchSynologyPhotos,
|
||||
getSynologyAssetInfo,
|
||||
pipeSynologyProxy,
|
||||
synologyAuthFromQuery,
|
||||
getSynologyTargetUserId,
|
||||
streamSynologyAsset,
|
||||
handleSynologyError,
|
||||
SynologyServiceError,
|
||||
} from '../services/synologyService';
|
||||
|
||||
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, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
res.json(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) {
|
||||
return handleSynologyError(res, new SynologyServiceError(400, 'URL and username are required'), 'Missing required fields');
|
||||
}
|
||||
|
||||
try {
|
||||
await updateSynologySettings(authReq.user.id, synology_url, synology_username, synology_password);
|
||||
res.json({ success: true });
|
||||
} catch (err: unknown) {
|
||||
handleSynologyError(res, err, 'Failed to save settings');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/status', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
res.json(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) {
|
||||
return handleSynologyError(res, new SynologyServiceError(400, 'URL, username and password are required'), 'Missing required fields');
|
||||
}
|
||||
|
||||
res.json(await testSynologyConnection(synology_url, synology_username, synology_password));
|
||||
});
|
||||
|
||||
router.get('/albums', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
try {
|
||||
res.json(await listSynologyAlbums(authReq.user.id));
|
||||
} catch (err: unknown) {
|
||||
handleSynologyError(res, err, 'Could not reach Synology');
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/trips/:tripId/album-links', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
const body = req.body as Record<string, unknown>;
|
||||
const albumId = parseStringBodyField(body.album_id);
|
||||
const albumName = parseStringBodyField(body.album_name);
|
||||
|
||||
if (!albumId) {
|
||||
return handleSynologyError(res, new SynologyServiceError(400, 'Album ID is required'), 'Missing required fields');
|
||||
}
|
||||
|
||||
try {
|
||||
linkSynologyAlbum(authReq.user.id, tripId, albumId, albumName || undefined);
|
||||
res.json({ success: true });
|
||||
} catch (err: unknown) {
|
||||
handleSynologyError(res, err, 'Failed to link album');
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/trips/:tripId/album-links/:linkId/sync', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, linkId } = req.params;
|
||||
|
||||
try {
|
||||
const result = await syncSynologyAlbumLink(authReq.user.id, tripId, linkId);
|
||||
res.json({ success: true, ...result });
|
||||
if (result.added > 0) {
|
||||
broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
handleSynologyError(res, err, 'Could not reach Synology');
|
||||
}
|
||||
});
|
||||
|
||||
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, 300);
|
||||
|
||||
try {
|
||||
const result = await searchSynologyPhotos(
|
||||
authReq.user.id,
|
||||
from || undefined,
|
||||
to || undefined,
|
||||
offset,
|
||||
limit,
|
||||
);
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
handleSynologyError(res, err, 'Could not reach Synology');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/assets/:photoId/info', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { photoId } = req.params;
|
||||
|
||||
try {
|
||||
res.json(await getSynologyAssetInfo(authReq.user.id, photoId, getSynologyTargetUserId(req)));
|
||||
} catch (err: unknown) {
|
||||
handleSynologyError(res, err, 'Could not reach Synology');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/assets/:photoId/thumbnail', synologyAuthFromQuery, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { photoId } = req.params;
|
||||
const { size = 'sm' } = req.query;
|
||||
|
||||
try {
|
||||
const proxy = await streamSynologyAsset(authReq.user.id, getSynologyTargetUserId(req), photoId, 'thumbnail', String(size));
|
||||
await pipeSynologyProxy(res, proxy);
|
||||
} catch (err: unknown) {
|
||||
if (res.headersSent) {
|
||||
return;
|
||||
}
|
||||
handleSynologyError(res, err, 'Proxy error');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/assets/:photoId/original', synologyAuthFromQuery, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { photoId } = req.params;
|
||||
|
||||
try {
|
||||
const proxy = await streamSynologyAsset(authReq.user.id, getSynologyTargetUserId(req), photoId, 'original');
|
||||
await pipeSynologyProxy(res, proxy);
|
||||
} catch (err: unknown) {
|
||||
if (res.headersSent) {
|
||||
return;
|
||||
}
|
||||
handleSynologyError(res, err, 'Proxy error');
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user