mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
Finish the contract ratchet, the upload bridge and the client leftovers
BODY_CONTRACT_ALLOW_LIST is empty. The last seventeen handlers type their
body with a createZodDto class, so the fail-closed boot gate has nothing left
to forgive; it still refuses a boot for a new handler that reads @Body()
without one.
Several of those contracts describe rather than constrain, and that is the
whole care in this change. The pipe runs before the handler, so a schema
stricter than the handler moves a user-visible answer:
- TagsController.create answers { error: 'Tag name is required' }, pinned
byte-for-byte in two tests, and the pipe formats as `field: message`. The
DTO leaves name optional and the handler keeps the check that owns the
message. Same shape for the booking-import confirm body, which additionally
gains per-item validation the handler never did.
- PluginsController.activate and install answer 503 when plugins are disabled,
and check that BEFORE looking at the body. A schema that rejected first
would turn that into a 400 for an operator whose server has plugins off.
- setEgressHosts reads a non-array `hosts` as "clear the list", which is the
documented reset. Typing it as string[] would have removed a working admin
action rather than tightened anything.
- PluginRoutesController.route answers { route: null } at 200 for everything
it dislikes, so the client falls back to straight lines. A 400 there would
turn a graceful degradation into a visible error.
files.bridge is gone. The multer options are built by
MulterModule.registerAsync from an AllowedFileTypesService leaf, so the
fileFilter reads the operator's extension list from the container instead of
from a second FilesService built outside it. Nest injects
MULTER_MODULE_OPTIONS with @Optional(), so a token that fails to resolve is
not a boot error: it silently falls back to defaults, with no filter and no
size cap. FILE-020 already proved that for trip files by uploading an .svg
over HTTP and expecting 400; journey uploads had no such case, and now do.
VIDEO_UPLOAD is untouched, including the filename callback that forces .jpg
regardless of what the client sends, which guards the public share proxy.
PluginFrame gets an onError. A frame whose document fails to load left a
blank element at full height, which reads as a hung app rather than as a
plugin that is gone.
typecheck:tests runs in CI now, with the test tree at zero errors. tz-lookup
is left as it is: the build puts it in a lazy helpers chunk, not the entry
chunk, and the remaining split would trade a synchronous render for an async
one to move 172 kB that is already off the critical path.
This commit is contained in:
@@ -11,6 +11,7 @@ import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { getClientIp } from '../audit/client-ip';
|
||||
import { pluginsEnabled } from './kill-switch';
|
||||
import { devLinkEnabled } from './dev-link';
|
||||
import { PluginActivateDto, PluginConfigDto, PluginEgressHostsDto, PluginInstallDto, PluginLinkDto, PluginRetrustDto, PluginUninstallDto } from './plugins.dto';
|
||||
|
||||
/**
|
||||
* Flatten a registry/install failure into the error envelope — CARRYING THE CODE.
|
||||
@@ -70,7 +71,7 @@ export class PluginsController {
|
||||
|
||||
@Post('install')
|
||||
@HttpCode(200)
|
||||
async install(@Body() body: { id?: string; version?: string; constraint?: string; withDependencies?: boolean }) {
|
||||
async install(@Body() body: PluginInstallDto) {
|
||||
if (!pluginsEnabled()) throw new HttpException({ error: 'Plugins are disabled by server configuration' }, 503);
|
||||
if (!body?.id) throw new HttpException({ error: 'id is required' }, 400);
|
||||
try {
|
||||
@@ -104,7 +105,7 @@ export class PluginsController {
|
||||
*/
|
||||
@Post('link')
|
||||
@HttpCode(200)
|
||||
async link(@Body() body: { path?: string }) {
|
||||
async link(@Body() body: PluginLinkDto) {
|
||||
if (!pluginsEnabled()) throw new HttpException({ error: 'Plugins are disabled by server configuration' }, 503);
|
||||
if (!devLinkEnabled()) throw new HttpException({ error: 'Dev-link is disabled (set TREK_PLUGINS_DEV_LINK=1)' }, 403);
|
||||
const dir = body?.path?.trim();
|
||||
@@ -122,7 +123,7 @@ export class PluginsController {
|
||||
}
|
||||
|
||||
@Put(':id/config')
|
||||
updateConfig(@Param('id') id: string, @Body() body: Record<string, unknown>) {
|
||||
updateConfig(@Param('id') id: string, @Body() body: PluginConfigDto) {
|
||||
return { config: this.plugins.updateInstanceConfig(id, body || {}) };
|
||||
}
|
||||
|
||||
@@ -138,7 +139,7 @@ export class PluginsController {
|
||||
}
|
||||
|
||||
@Put(':id/egress-hosts')
|
||||
async setEgressHosts(@Param('id') id: string, @Body() body: { hosts?: unknown } = {}) {
|
||||
async setEgressHosts(@Param('id') id: string, @Body() body: PluginEgressHostsDto) {
|
||||
if (!pluginsEnabled()) throw new HttpException({ error: 'Plugins are disabled by server configuration' }, 503);
|
||||
const hosts = Array.isArray(body.hosts) ? body.hosts.map(String) : [];
|
||||
try {
|
||||
@@ -150,7 +151,7 @@ export class PluginsController {
|
||||
|
||||
@Post(':id/activate')
|
||||
@HttpCode(200)
|
||||
async activate(@Param('id') id: string, @Body() body: { consent?: boolean } = {}) {
|
||||
async activate(@Param('id') id: string, @Body() body: PluginActivateDto) {
|
||||
if (!pluginsEnabled()) throw new HttpException({ error: 'Plugins are disabled by server configuration' }, 503);
|
||||
try {
|
||||
await this.runtime.activate(id, !!body?.consent);
|
||||
@@ -231,7 +232,7 @@ export class PluginsController {
|
||||
@HttpCode(200)
|
||||
async retrust(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { version?: string; publicKey?: string },
|
||||
@Body() body: PluginRetrustDto,
|
||||
@CurrentUser() user: { id: number },
|
||||
@Req() req: Request,
|
||||
) {
|
||||
@@ -247,7 +248,7 @@ export class PluginsController {
|
||||
|
||||
@Post(':id/uninstall')
|
||||
@HttpCode(200)
|
||||
async uninstall(@Param('id') id: string, @Body() body: { deleteData?: boolean }) {
|
||||
async uninstall(@Param('id') id: string, @Body() body: PluginUninstallDto) {
|
||||
await this.runtime.uninstall(id, !!body?.deleteData);
|
||||
return { status: 'uninstalled' };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user