Spaces:
Sleeping
Sleeping
| 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. | |
| */ | |
| ('api/integrations/memories/synologyphotos') | |
| (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); | |
| } | |
| } | |
| ('settings') | |
| async getSettings(() user: User, () res: Response): Promise<void> { | |
| this.handle(res, await this.memories.synologyGetSettings(user.id)); | |
| } | |
| ('settings') | |
| async putSettings(() user: User, () body: Record<string, unknown>, () 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)); | |
| } | |
| } | |
| ('status') | |
| async getStatus(() user: User, () res: Response): Promise<void> { | |
| this.handle(res, await this.memories.synologyGetStatus(user.id)); | |
| } | |
| ('test') | |
| (200) | |
| async test(() user: User, () body: Record<string, unknown>, () 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)); | |
| } | |
| } | |
| ('albums') | |
| async albums(() user: User, () res: Response): Promise<void> { | |
| this.handle(res, await this.memories.synologyListAlbums(user.id)); | |
| } | |
| ('albums/:albumId/photos') | |
| async albumPhotos(() user: User, ('albumId') albumId: string, ('passphrase') passphraseRaw: string | undefined, () res: Response): Promise<void> { | |
| const passphrase = passphraseRaw ? String(passphraseRaw) : undefined; | |
| this.handle(res, await this.memories.synologyGetAlbumPhotos(user.id, albumId, passphrase)); | |
| } | |
| ('trips/:tripId/album-links/:linkId/sync') | |
| (200) | |
| async sync( | |
| () user: User, | |
| ('tripId') tripId: string, | |
| ('linkId') linkId: string, | |
| ('x-socket-id') sid: string, | |
| () res: Response, | |
| ): Promise<void> { | |
| this.handle(res, await this.memories.synologySyncAlbumLink(user.id, tripId, linkId, sid)); | |
| } | |
| ('search') | |
| (200) | |
| async search(() user: User, () body: Record<string, unknown>, () 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)); | |
| } | |
| ('assets/:tripId/:photoId/:ownerId/info') | |
| async assetInfo( | |
| () user: User, | |
| ('tripId') tripId: string, | |
| ('photoId') photoId: string, | |
| ('ownerId') ownerId: string, | |
| ('passphrase') passphraseRaw: string | undefined, | |
| () 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)); | |
| } | |
| } | |
| ('assets/:tripId/:photoId/:ownerId/:kind') | |
| async asset( | |
| () user: User, | |
| ('tripId') tripId: string, | |
| ('photoId') photoId: string, | |
| ('ownerId') ownerId: string, | |
| ('kind') kind: string, | |
| ('size') sizeRaw: string | undefined, | |
| ('passphrase') passphraseRaw: string | undefined, | |
| () 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); | |
| } | |
| } | |
| } | |