Spaces:
Sleeping
Sleeping
| import { Body, Controller, Get, Headers, HttpCode, Param, Post, Put, Req, Res, UseGuards } from '@nestjs/common'; | |
| import type { Request, Response } from 'express'; | |
| import type { User } from '../../types'; | |
| import { MemoriesService } from './memories.service'; | |
| import { JwtAuthGuard } from '../auth/jwt-auth.guard'; | |
| import { CurrentUser } from '../auth/current-user.decorator'; | |
| import { getClientIp } from '../../services/auditLog'; | |
| /** | |
| * /api/integrations/memories/immich — Immich connection, browse/search, asset | |
| * proxy and album linking. | |
| * | |
| * Byte-identical to the legacy Express router (server/src/routes/memories/immich.ts): | |
| * `/status` and `/test` answer 200 even on connection failure (the service shapes | |
| * `{ connected: false, ... }`); `/settings` PUT validates with a 400; the asset | |
| * routes do the 400 invalid-id guard then the canAccessUserPhoto 403 ('Forbidden') | |
| * before streaming or returning info; the album sync answers 200 then broadcasts. | |
| * The legacy `canAccessTrip` import there is dead code — intentionally not ported. | |
| */ | |
| ('api/integrations/memories/immich') | |
| (JwtAuthGuard) | |
| export class ImmichMemoriesController { | |
| constructor(private readonly memories: MemoriesService) {} | |
| ('settings') | |
| getSettings(() user: User) { | |
| return this.memories.immichGetConnectionSettings(user.id); | |
| } | |
| ('settings') | |
| async putSettings( | |
| () user: User, | |
| () body: { immich_url?: string; immich_api_key?: string; auto_upload?: unknown }, | |
| () req: Request, | |
| () res: Response, | |
| ): Promise<void> { | |
| const { immich_url, immich_api_key, auto_upload } = body; | |
| const result = await this.memories.immichSaveSettings(user.id, immich_url, immich_api_key, getClientIp(req)); | |
| if (!result.success) { | |
| res.status(400).json({ error: result.error }); | |
| return; | |
| } | |
| if (typeof auto_upload === 'boolean') { | |
| this.memories.immichSetAutoUpload(user.id, auto_upload); | |
| } | |
| if (result.warning) { | |
| res.json({ success: true, warning: result.warning }); | |
| return; | |
| } | |
| res.json({ success: true }); | |
| } | |
| ('status') | |
| async getStatus(() user: User) { | |
| return this.memories.immichGetConnectionStatus(user.id); | |
| } | |
| ('test') | |
| (200) | |
| async test(() body: { immich_url?: string; immich_api_key?: string }) { | |
| const { immich_url, immich_api_key } = body; | |
| if (!immich_url || !immich_api_key) { | |
| return { connected: false, error: 'URL and API key required' }; | |
| } | |
| return this.memories.immichTestConnection(immich_url, immich_api_key); | |
| } | |
| ('browse') | |
| async browse(() user: User, () res: Response): Promise<void> { | |
| const result = await this.memories.immichBrowseTimeline(user.id); | |
| if (result.error) { | |
| res.status(result.status!).json({ error: result.error }); | |
| return; | |
| } | |
| res.json({ buckets: result.buckets }); | |
| } | |
| ('search') | |
| (200) | |
| async search(() user: User, () body: Record<string, unknown>, () res: Response): Promise<void> { | |
| const { from, to, size, page } = body as { from?: string; to?: string; size?: unknown; page?: unknown }; | |
| const pageNum = Math.max(1, Number(page) || 1); | |
| const pageSize = Math.min(Number(size) || 50, 200); | |
| const result = await this.memories.immichSearchPhotos(user.id, from, to, pageNum, pageSize); | |
| if (result.error) { | |
| res.status(result.status!).json({ error: result.error }); | |
| return; | |
| } | |
| res.json({ assets: result.assets || [], hasMore: !!result.hasMore }); | |
| } | |
| ('assets/:tripId/:assetId/:ownerId/info') | |
| async assetInfo( | |
| () user: User, | |
| ('tripId') tripId: string, | |
| ('assetId') assetId: string, | |
| ('ownerId') ownerId: string, | |
| () res: Response, | |
| ): Promise<void> { | |
| if (!this.memories.immichIsValidAssetId(assetId)) { | |
| res.status(400).json({ error: 'Invalid asset ID' }); | |
| return; | |
| } | |
| if (!this.memories.canAccessUserPhoto(user.id, Number(ownerId), tripId, assetId, 'immich')) { | |
| res.status(403).json({ error: 'Forbidden' }); | |
| return; | |
| } | |
| const result = await this.memories.immichGetAssetInfo(user.id, assetId, Number(ownerId)); | |
| if (result.error) { | |
| res.status(result.status!).json({ error: result.error }); | |
| return; | |
| } | |
| res.json(result.data); | |
| } | |
| ('assets/:tripId/:assetId/:ownerId/thumbnail') | |
| async assetThumbnail( | |
| () user: User, | |
| ('tripId') tripId: string, | |
| ('assetId') assetId: string, | |
| ('ownerId') ownerId: string, | |
| () res: Response, | |
| ): Promise<void> { | |
| if (!this.memories.immichIsValidAssetId(assetId)) { | |
| res.status(400).json({ error: 'Invalid asset ID' }); | |
| return; | |
| } | |
| if (!this.memories.canAccessUserPhoto(user.id, Number(ownerId), tripId, assetId, 'immich')) { | |
| res.status(403).json({ error: 'Forbidden' }); | |
| return; | |
| } | |
| await this.memories.immichStreamAsset(res, user.id, assetId, 'thumbnail', Number(ownerId)); | |
| } | |
| ('assets/:tripId/:assetId/:ownerId/original') | |
| async assetOriginal( | |
| () user: User, | |
| ('tripId') tripId: string, | |
| ('assetId') assetId: string, | |
| ('ownerId') ownerId: string, | |
| () res: Response, | |
| ): Promise<void> { | |
| if (!this.memories.immichIsValidAssetId(assetId)) { | |
| res.status(400).json({ error: 'Invalid asset ID' }); | |
| return; | |
| } | |
| if (!this.memories.canAccessUserPhoto(user.id, Number(ownerId), tripId, assetId, 'immich')) { | |
| res.status(403).json({ error: 'Forbidden' }); | |
| return; | |
| } | |
| await this.memories.immichStreamAsset(res, user.id, assetId, 'original', Number(ownerId)); | |
| } | |
| ('albums') | |
| async albums(() user: User, () res: Response): Promise<void> { | |
| const result = await this.memories.immichListAlbums(user.id); | |
| if (result.error) { | |
| res.status(result.status!).json({ error: result.error }); | |
| return; | |
| } | |
| res.json({ albums: result.albums }); | |
| } | |
| ('albums/:albumId/photos') | |
| async albumPhotos(() user: User, ('albumId') albumId: string, () res: Response): Promise<void> { | |
| const result = await this.memories.immichGetAlbumPhotos(user.id, albumId); | |
| if (result.error) { | |
| res.status(result.status!).json({ error: result.error }); | |
| return; | |
| } | |
| res.json({ assets: result.assets }); | |
| } | |
| ('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> { | |
| const result = await this.memories.immichSyncAlbumAssets(tripId, linkId, user.id, sid); | |
| if (result.error) { | |
| res.status(result.status!).json({ error: result.error }); | |
| return; | |
| } | |
| res.json({ success: true, added: result.added, total: result.total }); | |
| if (result.added! > 0) { | |
| this.memories.broadcast(tripId, 'memories:updated', { userId: user.id }, sid); | |
| } | |
| } | |
| } | |