Spaces:
Sleeping
Sleeping
File size: 7,591 Bytes
57a889c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | import { Body, Controller, Get, Headers, HttpCode, Param, Post, Put, Query, Res, UseGuards } from '@nestjs/common';
import type { Response } from 'express';
import type { User } from '../../types';
import type { ServiceResult } from '../../services/memories/helpersService';
import { fail, success } from '../../services/memories/helpersService';
import { MemoriesService } from './memories.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
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;
}
/**
* /api/integrations/memories/synologyphotos — Synology Photos connection,
* search, albums and asset proxy.
*
* Byte-identical to the legacy Express router (server/src/routes/memories/synology.ts):
* every response goes through the service `ServiceResult` envelope (success →
* `res.json(data)` at 200, error → status + `{ error }`); `/status` and `/test`
* always answer 200 (the service shapes `{ connected: false, error }` on
* failure); the asset routes use the distinct 403 string "You don't have access
* to this photo"; `/info` is declared before the catch-all `/:kind` so the
* literal route wins as Express ordered it; lenient hand-rolled coercion is kept.
*/
@Controller('api/integrations/memories/synologyphotos')
@UseGuards(JwtAuthGuard)
export class SynologyMemoriesController {
constructor(private readonly memories: MemoriesService) {}
private handle<T>(res: Response, result: ServiceResult<T>): void {
if ('error' in result) {
res.status(result.error.status).json({ error: result.error.message });
} else {
res.json(result.data);
}
}
@Get('settings')
async getSettings(@CurrentUser() user: User, @Res() res: Response): Promise<void> {
this.handle(res, await this.memories.synologyGetSettings(user.id));
}
@Put('settings')
async putSettings(@CurrentUser() user: User, @Body() body: Record<string, unknown>, @Res() res: Response): Promise<void> {
const synology_url = _parseStringBodyField(body.synology_url);
const synology_username = _parseStringBodyField(body.synology_username);
const synology_password = _parseStringBodyField(body.synology_password);
const synology_skip_ssl = body.synology_skip_ssl === true || body.synology_skip_ssl === 'true';
if (!synology_url || !synology_username) {
this.handle(res, fail('URL and username are required', 400));
} else {
this.handle(res, await this.memories.synologyUpdateSettings(user.id, synology_url, synology_username, synology_password, synology_skip_ssl));
}
}
@Get('status')
async getStatus(@CurrentUser() user: User, @Res() res: Response): Promise<void> {
this.handle(res, await this.memories.synologyGetStatus(user.id));
}
@Post('test')
@HttpCode(200)
async test(@CurrentUser() user: User, @Body() body: Record<string, unknown>, @Res() res: Response): Promise<void> {
const synology_url = _parseStringBodyField(body.synology_url);
const synology_username = _parseStringBodyField(body.synology_username);
const synology_password = _parseStringBodyField(body.synology_password);
const synology_otp = _parseStringBodyField(body.synology_otp);
const synology_skip_ssl = body.synology_skip_ssl === true || body.synology_skip_ssl === 'true';
if (!synology_url || !synology_username || !synology_password) {
const missingFields: string[] = [];
if (!synology_url) missingFields.push('URL');
if (!synology_username) missingFields.push('Username');
if (!synology_password) missingFields.push('Password');
this.handle(res, success({ connected: false, error: `${missingFields.join(', ')} ${missingFields.length > 1 ? 'are' : 'is'} required` }));
} else {
this.handle(res, await this.memories.synologyTestConnection(user.id, synology_url, synology_username, synology_password, synology_otp, synology_skip_ssl));
}
}
@Get('albums')
async albums(@CurrentUser() user: User, @Res() res: Response): Promise<void> {
this.handle(res, await this.memories.synologyListAlbums(user.id));
}
@Get('albums/:albumId/photos')
async albumPhotos(@CurrentUser() user: User, @Param('albumId') albumId: string, @Query('passphrase') passphraseRaw: string | undefined, @Res() res: Response): Promise<void> {
const passphrase = passphraseRaw ? String(passphraseRaw) : undefined;
this.handle(res, await this.memories.synologyGetAlbumPhotos(user.id, albumId, passphrase));
}
@Post('trips/:tripId/album-links/:linkId/sync')
@HttpCode(200)
async sync(
@CurrentUser() user: User,
@Param('tripId') tripId: string,
@Param('linkId') linkId: string,
@Headers('x-socket-id') sid: string,
@Res() res: Response,
): Promise<void> {
this.handle(res, await this.memories.synologySyncAlbumLink(user.id, tripId, linkId, sid));
}
@Post('search')
@HttpCode(200)
async search(@CurrentUser() user: User, @Body() body: Record<string, unknown>, @Res() res: Response): Promise<void> {
const from = _parseStringBodyField(body.from);
const to = _parseStringBodyField(body.to);
let offset = _parseNumberBodyField(body.offset, 0);
const page = _parseNumberBodyField(body.page, 1) - 1;
let limit = _parseNumberBodyField(body.limit, 100);
const size = _parseNumberBodyField(body.size, 0);
if (size > 0) limit = size;
if (page > 0) offset = page * limit;
this.handle(res, await this.memories.synologySearchPhotos(user.id, from || undefined, to || undefined, offset, limit));
}
@Get('assets/:tripId/:photoId/:ownerId/info')
async assetInfo(
@CurrentUser() user: User,
@Param('tripId') tripId: string,
@Param('photoId') photoId: string,
@Param('ownerId') ownerId: string,
@Query('passphrase') passphraseRaw: string | undefined,
@Res() res: Response,
): Promise<void> {
const passphrase = passphraseRaw ? String(passphraseRaw) : undefined;
if (!this.memories.canAccessUserPhoto(user.id, Number(ownerId), tripId, photoId, 'synologyphotos')) {
this.handle(res, fail("You don't have access to this photo", 403));
} else {
this.handle(res, await this.memories.synologyGetAssetInfo(user.id, photoId, Number(ownerId), passphrase));
}
}
@Get('assets/:tripId/:photoId/:ownerId/:kind')
async asset(
@CurrentUser() user: User,
@Param('tripId') tripId: string,
@Param('photoId') photoId: string,
@Param('ownerId') ownerId: string,
@Param('kind') kind: string,
@Query('size') sizeRaw: string | undefined,
@Query('passphrase') passphraseRaw: string | undefined,
@Res() res: Response,
): Promise<void> {
const VALID_SIZES = ['sm', 'm', 'xl'] as const;
const rawSize = String(sizeRaw ?? 'sm');
const size = (VALID_SIZES as readonly string[]).includes(rawSize) ? rawSize : 'sm';
const passphrase = passphraseRaw ? String(passphraseRaw) : undefined;
if (kind !== 'thumbnail' && kind !== 'original') {
this.handle(res, fail('Invalid asset kind', 400));
return;
}
if (!this.memories.canAccessUserPhoto(user.id, Number(ownerId), tripId, photoId, 'synologyphotos')) {
this.handle(res, fail("You don't have access to this photo", 403));
} else {
await this.memories.synologyStreamAsset(res, user.id, Number(ownerId), photoId, kind, String(size), passphrase);
}
}
}
|