mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
e606ebd9b9
* chore(nest): register LlmParseModule in AppModule
The module was only in the container because BookingImportModule and
PluginsModule happen to import it, so /api/admin/llm/local depended on those two
keeping that import. Register it where every other module is registered.
* refactor(nest): give the rate limiter one home and one instance
RateLimitService keeps its counters in a Map on the instance, but it was listed
in the providers of auth, oauth, transit and trip-invite, so each of them got its
own. Nothing is broken today — the bucket names in use (login/mfa/forgot/reset,
oauth_*, transit_*, trip_invite) don't overlap — but two modules picking the same
name would have quietly allowed twice the configured attempts.
Move it to nest/common, where it belongs now that four domains use it, and hand
it out through a small module they import. Not @Global: the e2e suites build a
container around one domain module, so a global AppModule never pulled in isn't
there at all.
transit.mcp.ts keeps its own instance on purpose — it keys by userId under
mcp_transit_* while the REST path keys by IP.
* test(ws): pin the wire envelope before touching the transport
Every frame on the socket is flat: {type, tripId, ...payload} out, {type, tripId}
in. Nest's WsAdapter dispatches on message.event and answers {event, data}, so
moving this onto a gateway without a custom serialiser would leave join silently
unhandled — no welcome.socketId to send back as X-Socket-Id, and every client
would then receive its own writes.
Nothing catches that today: the 100+ tests that touch realtime mock src/websocket
and never look at the bytes. WS-032 sends the exact shape the stock adapter would
and asserts the server ignores it.
* docs(nest): drop the parts of the README that no longer exist
strangler.ts, DEFAULT_NEST_PREFIXES/NEST_PREFIXES and tests/parity/ are all gone —
every request goes through Nest now — but the README still walks a new contributor
through adding a prefix and writing a parity test against the Express route.
Also spell out that the 80% gate is an average over src/nest/**, not a floor per
file, so a large untested module can sit behind the well-covered small ones.
71 lines
2.4 KiB
TypeScript
71 lines
2.4 KiB
TypeScript
import { Controller, Get, HttpException, Query, Req, UseGuards } from '@nestjs/common';
|
|
import type { Request } from 'express';
|
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
|
import { RateLimitService } from '../common/rate-limit.service';
|
|
import { TransitService } from './transit.service';
|
|
|
|
const RL_WINDOW = 15 * 60 * 1000;
|
|
|
|
/**
|
|
* /api/transit — public transit routing (#1065) proxied through Transitous
|
|
* (or a self-hosted MOTIS via TRANSIT_API_URL). JWT-guarded and rate-limited:
|
|
* the Transitous usage policy asks integrators to keep expensive routing
|
|
* traffic reasonable, so planning gets a tighter bucket than geocoding.
|
|
*/
|
|
@Controller('api/transit')
|
|
@UseGuards(JwtAuthGuard)
|
|
export class TransitController {
|
|
constructor(
|
|
private readonly rl: RateLimitService,
|
|
private readonly transit: TransitService,
|
|
) {}
|
|
|
|
private limit(bucket: string, req: Request, max: number): void {
|
|
if (!this.rl.check(bucket, req.ip || 'unknown', max, RL_WINDOW, Date.now())) {
|
|
throw new HttpException({ error: 'Too many requests. Please try again later.' }, 429);
|
|
}
|
|
}
|
|
|
|
private rethrow(err: unknown): never {
|
|
const status = (err as { status?: number }).status || 502;
|
|
const message = err instanceof Error ? err.message : 'Transit provider error';
|
|
throw new HttpException({ error: message }, status);
|
|
}
|
|
|
|
@Get('geocode')
|
|
async geocode(
|
|
@Query('q') q: string | undefined,
|
|
@Query('lang') lang: string | undefined,
|
|
@Query('near') near: string | undefined,
|
|
@Req() req: Request,
|
|
) {
|
|
this.limit('transit_geocode', req, 300);
|
|
try {
|
|
return await this.transit.geocode(q || '', lang, near);
|
|
} catch (err) { this.rethrow(err); }
|
|
}
|
|
|
|
@Get('plan')
|
|
async plan(
|
|
@Query('from') from: string | undefined,
|
|
@Query('to') to: string | undefined,
|
|
@Query('time') time: string | undefined,
|
|
@Query('arriveBy') arriveBy: string | undefined,
|
|
@Query('modes') modes: string | undefined,
|
|
@Query('maxTransfers') maxTransfers: string | undefined,
|
|
@Req() req: Request,
|
|
) {
|
|
this.limit('transit_plan', req, 60);
|
|
try {
|
|
return await this.transit.plan({
|
|
from: from || '',
|
|
to: to || '',
|
|
time,
|
|
arriveBy: arriveBy === 'true' || arriveBy === '1',
|
|
modes,
|
|
maxTransfers: maxTransfers !== undefined && maxTransfers !== '' ? Number(maxTransfers) : undefined,
|
|
});
|
|
} catch (err) { this.rethrow(err); }
|
|
}
|
|
}
|