POST /voice/extract behind JwtAuthGuard + ClinicOrgGuard, plus GET /voice/availability so the frontend can decide whether to render the microphone — it cannot learn that from NEXT_PUBLIC_*, which are baked in at build time. Audio is held in memory for the request only: never written to disk, never a Prisma row. The transcript goes back to the client and is not persisted. What is logged is structured and patient-free — clip length, which fields resolved, unresolved count, vendor cost, outcome — with log lines as the interim sink until this repo has metrics infrastructure. On extraction failure the transcript still travels back in the error details, so the words the clinician already paid for can be salvaged into a note. v1 ships ungated beyond a configured locale profile; the Plan.features design is deferred, not dropped. From review of this commit, four of which were load-bearing: - Express's 100 kb default body limit rejected any recording past ~20 seconds, making the endpoint unusable at its own 2-minute cap. Body parsers are now registered explicitly with a 10 MB limit scoped to the voice route only. Verified empirically: 600 KB reaches /api/voice/extract, while /api/auth/login still 413s. - ThrottlerGuard keys on req.ip, so behind nginx the whole deployment would share one bucket and an abuser rotating IPs would bypass it. VoiceThrottlerGuard keys on the user id instead — with no plan gate, this is the only control on metered vendor spend. - ThrottlerException had no 429 fallback and surfaced as INTERNAL_ERROR; the guard now throws VOICE_RATE_LIMITED directly. - durationMs was optional, so omitting it bypassed VOICE_MAX_RECORDING_MS entirely. It is required. - VOICE_UNSUPPORTED_FORMAT was dead code — the DTO's @IsIn already rejects unknown containers — so it is gone rather than left unreachable. ThrottlerModule is deliberately not bound as a global APP_GUARD: a global ThrottlerGuard rate-limits every route against every named throttler, which would have capped the whole API at the voice limit. All seven remaining VOICE_* codes have errors.* keys in en, fa and nl. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
// backend/src/main.ts
|
|
import { NestFactory } from '@nestjs/core';
|
|
import { json, urlencoded } from 'express';
|
|
import { AppModule } from './app.module';
|
|
import { ValidationPipe } from '@nestjs/common';
|
|
import cookieParser from 'cookie-parser'; // 👈 Change this line!
|
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
import {
|
|
HttpExceptionFilter,
|
|
validationExceptionFactory,
|
|
} from './common/errors';
|
|
|
|
// At the VERY TOP of main.ts, before anything else
|
|
const originalConsoleLog = console.log;
|
|
console.log = (...args) => {
|
|
// Check if this is the massive Prisma dump (contains _clientVersion)
|
|
if (args.some(arg => arg && typeof arg === 'object' && arg._clientVersion)) {
|
|
console.error = originalConsoleLog; // Temporarily restore for this message
|
|
originalConsoleLog('🔍🔍🔍 PRISMA CLIENT DUMP DETECTED 🔍🔍🔍');
|
|
originalConsoleLog('Stack trace:', new Error().stack);
|
|
return; // Don't print the actual object
|
|
}
|
|
originalConsoleLog.apply(console, args);
|
|
};
|
|
|
|
async function bootstrap() {
|
|
// bodyParser is disabled here so the JSON parsers can be registered in an explicit
|
|
// order below; Nest's built-in one is installed during create() and would otherwise
|
|
// reject a voice recording at its 100 kb default before any later middleware ran.
|
|
const app = await NestFactory.create(AppModule, { bodyParser: false });
|
|
|
|
// Voice recordings are base64 JSON and pass 100 kb at roughly 20 seconds of audio.
|
|
// Registered first and scoped to the one route: body-parser marks the request handled,
|
|
// so the default-limit parser below skips it and every other endpoint keeps the
|
|
// standard limit.
|
|
app.use('/api/voice/extract', json({ limit: '10mb' }));
|
|
app.use(json());
|
|
app.use(urlencoded({ extended: true }));
|
|
|
|
app.useGlobalFilters(new HttpExceptionFilter());
|
|
|
|
// Global pipes
|
|
app.useGlobalPipes(new ValidationPipe({
|
|
whitelist: true,
|
|
forbidNonWhitelisted: true,
|
|
transform: true,
|
|
exceptionFactory: validationExceptionFactory,
|
|
}));
|
|
|
|
// Cookie parser - this is correct for Express
|
|
app.use(cookieParser());
|
|
|
|
// CORS
|
|
app.enableCors({
|
|
origin: process.env.FRONTEND_URL || 'http://localhost:3001',
|
|
credentials: true,
|
|
});
|
|
|
|
// Global prefix
|
|
app.setGlobalPrefix('api');
|
|
|
|
// Swagger configuration
|
|
const swaggerConfig = new DocumentBuilder()
|
|
.setTitle('Dyolink API')
|
|
.setDescription('Dental Clinic & Lab Communication Hub API')
|
|
.setVersion('1.0')
|
|
.addTag('auth', 'Authentication endpoints')
|
|
.addBearerAuth(
|
|
{
|
|
type: 'http',
|
|
scheme: 'bearer',
|
|
bearerFormat: 'JWT',
|
|
name: 'JWT',
|
|
description: 'Enter JWT token',
|
|
in: 'header',
|
|
},
|
|
'JWT-auth',
|
|
)
|
|
.build();
|
|
|
|
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
|
|
|
SwaggerModule.setup('api/docs', app, document, {
|
|
swaggerOptions: {
|
|
persistAuthorization: true,
|
|
tagsSorter: 'alpha',
|
|
operationsSorter: 'alpha',
|
|
},
|
|
customSiteTitle: 'Dyolink API Documentation',
|
|
});
|
|
|
|
const port = parseInt(process.env.PORT || '', 10) || 3000;
|
|
await app.listen(port);
|
|
|
|
console.log(`🚀 Application is running on: http://localhost:${port}/api`);
|
|
console.log(`📚 Swagger documentation: http://localhost:${port}/api/docs`);
|
|
console.log(`📚 AdminJS Panel: http://localhost:${port}/admin`);
|
|
}
|
|
|
|
bootstrap(); |