mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
feat(server): give the memories endpoints their Zod contracts
The last nine memories entries in the body-contract allow list are gone: three each on the immich, synology and unified controllers. They read @Body() as Record<string, unknown> and coerced by hand. The schemas are deliberately permissive and loose, and that is the point rather than a shortcut. Paging arrives as `2` or `'2'` because the controller does `Number(page) || 1`; synology_skip_ssl arrives as a boolean or the string 'true'; auto_upload is accepted in any shape because only a real boolean is acted on. A schema that described the ideal body instead of the real one would turn working clients into 400s, and parity outranks tightening. Narrowing them is a separate, client-visible change. Loose objects for the same reason: a strict object strips unknown keys, and `selections[]` goes straight to the services as `Selection[]` - stripping there would be a silent data loss the day a provider adds a field. This unblocks be-memories-fold, which could not move a controller whose body had no contract to move with it.
This commit is contained in:
@@ -12,9 +12,6 @@ export const BODY_CONTRACT_ALLOW_LIST: string[] = [
|
||||
'BookingImportController.confirm',
|
||||
'BookingImportController.preview',
|
||||
'BookingImportController.previewAsync',
|
||||
'ImmichMemoriesController.putSettings',
|
||||
'ImmichMemoriesController.search',
|
||||
'ImmichMemoriesController.test',
|
||||
'JourneyController.addContributor',
|
||||
'JourneyController.addTrip',
|
||||
'JourneyController.create',
|
||||
@@ -43,12 +40,6 @@ export const BODY_CONTRACT_ALLOW_LIST: string[] = [
|
||||
'PluginsController.setEgressHosts',
|
||||
'PluginsController.uninstall',
|
||||
'PluginsController.updateConfig',
|
||||
'SynologyMemoriesController.putSettings',
|
||||
'SynologyMemoriesController.search',
|
||||
'SynologyMemoriesController.test',
|
||||
'TagsController.create',
|
||||
'TagsController.update',
|
||||
'UnifiedMemoriesController.addPhotos',
|
||||
'UnifiedMemoriesController.createAlbumLink',
|
||||
'UnifiedMemoriesController.setSharing',
|
||||
];
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MemoriesService } from './memories.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { getClientIp } from '../audit/client-ip';
|
||||
import { ImmichSearchDto, ImmichSettingsDto, ImmichTestDto } from './memories.dto';
|
||||
|
||||
/**
|
||||
* /api/integrations/memories/immich — Immich connection, browse/search, asset
|
||||
@@ -30,7 +31,7 @@ export class ImmichMemoriesController {
|
||||
@Put('settings')
|
||||
async putSettings(
|
||||
@CurrentUser() user: User,
|
||||
@Body() body: { immich_url?: string; immich_api_key?: string; auto_upload?: unknown },
|
||||
@Body() body: ImmichSettingsDto,
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
@@ -57,7 +58,7 @@ export class ImmichMemoriesController {
|
||||
|
||||
@Post('test')
|
||||
@HttpCode(200)
|
||||
async test(@Body() body: { immich_url?: string; immich_api_key?: string }) {
|
||||
async test(@Body() body: ImmichTestDto) {
|
||||
const { immich_url, immich_api_key } = body;
|
||||
if (!immich_url || !immich_api_key) {
|
||||
return { connected: false, error: 'URL and API key required' };
|
||||
@@ -77,7 +78,7 @@ export class ImmichMemoriesController {
|
||||
|
||||
@Post('search')
|
||||
@HttpCode(200)
|
||||
async search(@CurrentUser() user: User, @Body() body: Record<string, unknown>, @Res() res: Response): Promise<void> {
|
||||
async search(@CurrentUser() user: User, @Body() body: ImmichSearchDto, @Res() 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);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import {
|
||||
addTripPhotosSchema,
|
||||
createAlbumLinkSchema,
|
||||
immichSearchSchema,
|
||||
immichSettingsSchema,
|
||||
immichTestSchema,
|
||||
setTripPhotoSharingSchema,
|
||||
synologySearchSchema,
|
||||
synologySettingsSchema,
|
||||
synologyTestSchema,
|
||||
} from '@trek/shared';
|
||||
|
||||
/**
|
||||
* Server-side createZodDto wrappers over the @trek/shared memories contracts.
|
||||
* The global ZodValidationPipe (APP_PIPE in app.module.ts) validates any @Body()
|
||||
* parameter typed with one of these classes by metatype — the Zod schemas in
|
||||
* shared/ remain the single source of truth for the wire contract.
|
||||
*
|
||||
* These nine routes were the last memories entries in
|
||||
* common/body-contract-allow-list.ts; the boot gate throws on stale entries, so
|
||||
* they were removed in the same change.
|
||||
*/
|
||||
export class ImmichSettingsDto extends createZodDto(immichSettingsSchema) {}
|
||||
export class ImmichTestDto extends createZodDto(immichTestSchema) {}
|
||||
export class ImmichSearchDto extends createZodDto(immichSearchSchema) {}
|
||||
|
||||
export class SynologySettingsDto extends createZodDto(synologySettingsSchema) {}
|
||||
export class SynologyTestDto extends createZodDto(synologyTestSchema) {}
|
||||
export class SynologySearchDto extends createZodDto(synologySearchSchema) {}
|
||||
|
||||
export class AddTripPhotosDto extends createZodDto(addTripPhotosSchema) {}
|
||||
export class SetTripPhotoSharingDto extends createZodDto(setTripPhotoSharingSchema) {}
|
||||
export class CreateAlbumLinkDto extends createZodDto(createAlbumLinkSchema) {}
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
import { SynologySearchDto, SynologySettingsDto, SynologyTestDto } from './memories.dto';
|
||||
|
||||
function _parseStringBodyField(value: unknown): string {
|
||||
return String(value ?? '').trim();
|
||||
@@ -47,7 +48,7 @@ export class SynologyMemoriesController {
|
||||
}
|
||||
|
||||
@Put('settings')
|
||||
async putSettings(@CurrentUser() user: User, @Body() body: Record<string, unknown>, @Res() res: Response): Promise<void> {
|
||||
async putSettings(@CurrentUser() user: User, @Body() body: SynologySettingsDto, @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);
|
||||
@@ -67,7 +68,7 @@ export class SynologyMemoriesController {
|
||||
|
||||
@Post('test')
|
||||
@HttpCode(200)
|
||||
async test(@CurrentUser() user: User, @Body() body: Record<string, unknown>, @Res() res: Response): Promise<void> {
|
||||
async test(@CurrentUser() user: User, @Body() body: SynologyTestDto, @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);
|
||||
@@ -110,7 +111,7 @@ export class SynologyMemoriesController {
|
||||
|
||||
@Post('search')
|
||||
@HttpCode(200)
|
||||
async search(@CurrentUser() user: User, @Body() body: Record<string, unknown>, @Res() res: Response): Promise<void> {
|
||||
async search(@CurrentUser() user: User, @Body() body: SynologySearchDto, @Res() res: Response): Promise<void> {
|
||||
const from = _parseStringBodyField(body.from);
|
||||
const to = _parseStringBodyField(body.to);
|
||||
let offset = _parseNumberBodyField(body.offset, 0);
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Selection } from '../../services/memories/helpersService';
|
||||
import { MemoriesService } from './memories.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { AddTripPhotosDto, CreateAlbumLinkDto, SetTripPhotoSharingDto } from './memories.dto';
|
||||
|
||||
/**
|
||||
* /api/integrations/memories/unified — provider-agnostic trip photo + album-link
|
||||
@@ -35,7 +36,7 @@ export class UnifiedMemoriesController {
|
||||
async addPhotos(
|
||||
@CurrentUser() user: User,
|
||||
@Param('tripId') tripId: string,
|
||||
@Body() body: Record<string, unknown>,
|
||||
@Body() body: AddTripPhotosDto,
|
||||
@Headers('x-socket-id') sid: string,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
@@ -53,7 +54,7 @@ export class UnifiedMemoriesController {
|
||||
async setSharing(
|
||||
@CurrentUser() user: User,
|
||||
@Param('tripId') tripId: string,
|
||||
@Body() body: Record<string, unknown>,
|
||||
@Body() body: SetTripPhotoSharingDto,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const result = await this.memories.setTripPhotoSharing(tripId, user.id, Number(body?.photo_id), body?.shared as boolean);
|
||||
@@ -94,7 +95,7 @@ export class UnifiedMemoriesController {
|
||||
createAlbumLink(
|
||||
@CurrentUser() user: User,
|
||||
@Param('tripId') tripId: string,
|
||||
@Body() body: Record<string, unknown>,
|
||||
@Body() body: CreateAlbumLinkDto,
|
||||
@Res() res: Response,
|
||||
): void {
|
||||
const passphrase = body?.passphrase ? String(body.passphrase) : undefined;
|
||||
|
||||
Reference in New Issue
Block a user