dorfteich/apps/api/src/public/feed-tokens.controller.ts
Claude Fable 5 afef45732a
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m52s
CI / Build container images (pull_request) Successful in 3m55s
CI / Auth e2e pack (pull_request) Successful in 7m52s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 16s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m55s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m34s
CI / Import/export fidelity gate (push) Successful in 58s
#191: feeds.enabled instance switch, feed-token log masking
Chosen path: an instance master switch following the api.enabled/
mcp.enabled pattern — while off, both feed routes AND the feed-token
management answer 404 (existence hidden). Default ON: feeds predate the
switch, existing instances and their subscribed readers keep working;
the VS-NfD reference configuration (#227) turns it off. Admin UI gets
the toggle next to the API/MCP switches (i18n de+en).

Moving the token out of the query string is documented as rejected: a
path segment lands in the same proxy and request logs, and feed readers
cannot send headers — that is why the credential is in the URL at all.
What DID leak was our own request log (pino logs req.url): the req
serializer now masks ?token= values (common/mask-token-param.ts), so no
code path logs the credential.

Refs #191

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 11:34:43 +02:00

64 lines
1.8 KiB
TypeScript

import {
Body,
Controller,
Delete,
Get,
HttpCode,
NotFoundException,
Param,
Post,
Req,
} from '@nestjs/common';
import {
createFeedTokenInputSchema,
type CreateFeedTokenInput,
type FeedTokenCreatedView,
type FeedTokenView,
} from '@dorfteich/shared';
import { AuthedRequest } from '../auth/auth.guard';
import { ZodValidationPipe } from '../common/zod-validation.pipe';
import { AuthenticatedOnly } from '../permissions/permission.decorators';
import { InstanceSettingsService } from '../settings/instance-settings.service';
import { FeedTokensService } from './feed-tokens.service';
/**
* Feed-token lifecycle for the settings UI (issue #149) —
* session-authenticated and owner-scoped, like the API-token controller.
*/
@Controller('users/me/feed-tokens')
@AuthenticatedOnly()
export class FeedTokensController {
constructor(
private readonly tokens: FeedTokensService,
private readonly settings: InstanceSettingsService,
) {}
/** Feed master switch (issue #191): disabled ⇒ the surface answers 404. */
private async assertFeedsEnabled(): Promise<void> {
if (!(await this.settings.get('feeds.enabled'))) throw new NotFoundException();
}
@Get()
async list(@Req() request: AuthedRequest): Promise<FeedTokenView[]> {
await this.assertFeedsEnabled();
return this.tokens.list(request.user!);
}
@Post()
async create(
@Body(new ZodValidationPipe(createFeedTokenInputSchema)) input: CreateFeedTokenInput,
@Req() request: AuthedRequest,
): Promise<FeedTokenCreatedView> {
await this.assertFeedsEnabled();
return this.tokens.create(request.user!, input);
}
@Delete(':id')
@HttpCode(204)
async remove(@Param('id') id: string, @Req() request: AuthedRequest): Promise<void> {
await this.assertFeedsEnabled();
await this.tokens.remove(request.user!, id);
}
}