feat(server): adopt zod DTOs for the settings body contracts

SettingUpsertDto/SettingsBulkDto wrap the existing @trek/shared schemas; both
SettingsController entries drop off the body-contract allow-list (boot
ratchet). Per the todo precedent the dead bespoke 400 guards go: missing/empty
key and a non-record settings body now answer with the pipe envelope
({ error: 'key: …' } / { error: 'settings: …' }) instead of 'Key is required' /
'Settings object is required', and arrays for settings are rejected. The local
masked-value literal now imports MASKED_SETTING_VALUE from @trek/shared.
The settings e2e registers ZodValidationPipe and pins the DTO 400s plus the
bulk masked-sentinel skip.
This commit is contained in:
jubnl
2026-07-26 22:24:14 +02:00
parent cece6509b9
commit e0530680cb
5 changed files with 76 additions and 40 deletions
@@ -132,8 +132,6 @@ export const BODY_CONTRACT_ALLOW_LIST: string[] = [
'ReservationsController.update',
'ReservationsController.updatePositions',
'ReservationsController.updateTravelers',
'SettingsController.bulk',
'SettingsController.upsert',
'SynologyMemoriesController.putSettings',
'SynologyMemoriesController.search',
'SynologyMemoriesController.test',
+12 -17
View File
@@ -1,17 +1,18 @@
import { Body, Controller, Get, HttpCode, HttpException, Post, Put, UseGuards } from '@nestjs/common';
import { MASKED_SETTING_VALUE } from '@trek/shared';
import type { User } from '../../types';
import { SettingsService } from './settings.service';
import { SettingUpsertDto, SettingsBulkDto } from './settings.dto';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
const MASKED_VALUE = '••••••••';
/**
* /api/settings — per-user key/value preferences.
*
* Byte-identical to the legacy Express route (server/src/routes/settings.ts):
* get-all, single upsert (400 without a key, no-op on the masked sentinel), and
* bulk upsert (400 without an object, 500 on a write error). All answer 200.
* /api/settings — per-user key/value preferences: get-all, single upsert
* (no-op on the masked sentinel), and bulk upsert (500 on a write error). All
* answer 200. Bodies validate against the @trek/shared settings schemas via
* the DTO classes in settings.dto.ts + the global ZodValidationPipe (400 with
* the standard `{ error }` envelope on mismatch — this replaced the legacy
* bespoke 'Key is required' / 'Settings object is required' checks).
*/
@Controller('api/settings')
@UseGuards(JwtAuthGuard)
@@ -24,12 +25,9 @@ export class SettingsController {
}
@Put()
upsert(@CurrentUser() user: User, @Body() body: { key?: string; value?: unknown }) {
if (!body.key) {
throw new HttpException({ error: 'Key is required' }, 400);
}
upsert(@CurrentUser() user: User, @Body() body: SettingUpsertDto) {
// The client echoes a redacted secret back unchanged — treat as a no-op.
if (body.value === MASKED_VALUE) {
if (body.value === MASKED_SETTING_VALUE) {
return { success: true, key: body.key, unchanged: true };
}
this.settings.upsertSetting(user.id, body.key, body.value);
@@ -38,12 +36,9 @@ export class SettingsController {
@Post('bulk')
@HttpCode(200) // Express answers bulk with res.json (200), not the POST-default 201.
bulk(@CurrentUser() user: User, @Body() body: { settings?: unknown }) {
if (!body.settings || typeof body.settings !== 'object') {
throw new HttpException({ error: 'Settings object is required' }, 400);
}
bulk(@CurrentUser() user: User, @Body() body: SettingsBulkDto) {
try {
const updated = this.settings.bulkUpsertSettings(user.id, body.settings as Record<string, unknown>);
const updated = this.settings.bulkUpsertSettings(user.id, body.settings);
return { success: true, updated };
} catch (err) {
console.error('Error saving settings:', err);
+11
View File
@@ -0,0 +1,11 @@
import { createZodDto } from 'nestjs-zod';
import { settingUpsertRequestSchema, settingsBulkRequestSchema } from '@trek/shared';
/**
* Server-side createZodDto wrappers over the @trek/shared settings 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.
*/
export class SettingUpsertDto extends createZodDto(settingUpsertRequestSchema) {}
export class SettingsBulkDto extends createZodDto(settingsBulkRequestSchema) {}