AcadeDoc/apps/server/src/main.ts
Corentin 21ce2a94c7 feat(acadedoc): add OpenAPI Swagger documentation for /api/v1/* — R5.3
Install @nestjs/swagger@^11.4.2 + nestjs-zod@^5.3.0. Bootstrap SwaggerModule
in main.ts with cleanupOpenApiDoc (dev/staging only; SWAGGER_ENABLED=true for
prod). Add @ApiTags, @ApiBearerAuth, @ApiOperation, @ApiResponse, @ApiParam,
@ApiQuery, @ApiBody decorators to all 16 acadenice /v1/* controllers.
Add swagger.spec.ts (8 tests green). Add docs/api-docs.md (SDK gen guide).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 15:36:14 +02:00

169 lines
5.1 KiB
TypeScript

import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { Logger, NotFoundException, ValidationPipe } from '@nestjs/common';
import { Logger as PinoLogger } from 'nestjs-pino';
import { TransformHttpResponseInterceptor } from './common/interceptors/http-response.interceptor';
import { WsRedisIoAdapter } from './ws/adapter/ws-redis.adapter';
import fastifyMultipart from '@fastify/multipart';
import fastifyCookie from '@fastify/cookie';
import fastifyIp from 'fastify-ip';
import { InternalLogFilter } from './common/logger/internal-log-filter';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { cleanupOpenApiDoc } from 'nestjs-zod';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({
trustProxy: true,
routerOptions: {
maxParamLength: 1000,
ignoreTrailingSlash: true,
ignoreDuplicateSlashes: true,
},
}),
{
rawBody: true,
// captures NestJS internal errors
logger: new InternalLogFilter(),
// bufferLogs must be false else pino will fail
// to log OnApplicationBootstrap logs
bufferLogs: false,
},
);
app.useLogger(app.get(PinoLogger));
app.setGlobalPrefix('api', {
exclude: ['robots.txt', 'share/:shareId/p/:pageSlug', 'mcp'],
});
const reflector = app.get(Reflector);
const redisIoAdapter = new WsRedisIoAdapter(app);
await redisIoAdapter.connectToRedis();
app.useWebSocketAdapter(redisIoAdapter);
await app.register(fastifyIp);
await app.register(fastifyMultipart);
await app.register(fastifyCookie);
app
.getHttpAdapter()
.getInstance()
.addContentTypeParser(
'application/scim+json',
{ parseAs: 'string' },
(_, body, done) => {
try {
const json = JSON.parse(body.toString());
done(null, json);
} catch (err: any) {
done(err);
}
},
);
app
.getHttpAdapter()
.getInstance()
.decorateReply('setHeader', function (name: string, value: unknown) {
this.header(name, value);
})
.decorateReply('end', function () {
this.send('');
})
.addHook('preHandler', function (req, reply, done) {
// don't require workspaceId for the following paths
const excludedPaths = [
'/api/auth/setup',
'/api/health',
'/api/billing/stripe/webhook',
'/api/workspace/check-hostname',
'/api/sso/google',
'/api/workspace/create',
'/api/workspace/joined',
'/api/workspace/find-by-email',
];
if (
req.originalUrl.startsWith('/api') &&
!excludedPaths.some((path) => req.originalUrl.startsWith(path))
) {
if (!req.raw?.['workspaceId'] && req.originalUrl !== '/api') {
throw new NotFoundException('Workspace not found');
}
done();
} else {
done();
}
});
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
stopAtFirstError: true,
transform: true,
}),
);
app.enableCors();
app.useGlobalInterceptors(new TransformHttpResponseInterceptor(reflector));
app.enableShutdownHooks();
// Swagger UI — enabled in dev/staging by default; opt-in for production via env
const swaggerEnabled =
process.env.NODE_ENV !== 'production' ||
process.env.SWAGGER_ENABLED === 'true';
if (swaggerEnabled) {
const swaggerConfig = new DocumentBuilder()
.setTitle('AcadeDoc API')
.setDescription(
'API officielle AcadeDoc — endpoints v1 (acadenice fork extensions on Docmost upstream)',
)
.setVersion('1.0')
.addBearerAuth()
.addCookieAuth('authToken')
.addTag('templates', 'Templates de pages')
.addTag('sync-blocks', 'Sync blocks (cross-page content)')
.addTag('audit-log', 'Audit log (read-only)')
.addTag('api-keys', 'Personal API tokens')
.addTag('clipper', 'Web clipper')
.addTag('graph', 'Knowledge graph')
.addTag('rbac', 'RBAC permissions/roles')
.addTag('comments', 'Page + row comments')
.addTag('notifications', 'Notifications + preferences')
.addTag('security', 'OIDC / SSO config')
.addTag('backlinks', 'Bidirectional backlinks')
.addTag('slash-commands', 'Custom slash commands')
.build();
const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api/docs', app, cleanupOpenApiDoc(document));
}
const logger = new Logger('NestApplication');
process.on('unhandledRejection', (reason, promise) => {
logger.error(`UnhandledRejection, reason: ${reason}`, promise);
});
process.on('uncaughtException', (error) => {
logger.error('UncaughtException:', error);
});
const port = process.env.PORT || 3000;
const host = process.env.HOST || '0.0.0.0';
await app.listen(port, host, () => {
logger.log(
`Listening on http://127.0.0.1:${port} / ${process.env.APP_URL}`,
);
});
}
bootstrap();