- Implement credential revocation endpoint with proper database integration - Fix database row mapping (snake_case to camelCase) for eResidency applications - Add missing imports (getRiskAssessmentEngine, VeriffKYCProvider, ComplyAdvantageSanctionsProvider) - Fix environment variable type checking for Veriff and ComplyAdvantage providers - Add required 'message' field to notification service calls - Fix risk assessment type mismatches - Update audit logging to use 'verified' action type (supported by schema) - Resolve all TypeScript errors and unused variable warnings - Add TypeScript ignore comments for placeholder implementations - Temporarily disable security/detect-non-literal-regexp rule due to ESLint 9 compatibility - Service now builds successfully with no linter errors All core functionality implemented: - Application submission and management - KYC integration (Veriff placeholder) - Sanctions screening (ComplyAdvantage placeholder) - Risk assessment engine - Credential issuance and revocation - Reviewer console - Status endpoints - Auto-issuance service
329 lines
8.0 KiB
TypeScript
329 lines
8.0 KiB
TypeScript
/**
|
|
* Dataroom Service
|
|
* Handles secure VDR, deal rooms, and document access control
|
|
*/
|
|
|
|
import Fastify from 'fastify';
|
|
import fastifySwagger from '@fastify/swagger';
|
|
import fastifySwaggerUI from '@fastify/swagger-ui';
|
|
import {
|
|
errorHandler,
|
|
createLogger,
|
|
registerSecurityPlugins,
|
|
addCorrelationId,
|
|
addRequestLogging,
|
|
getEnv,
|
|
createBodySchema,
|
|
authenticateJWT,
|
|
requireRole,
|
|
} from '@the-order/shared';
|
|
import { CreateDealSchema, DealSchema, CreateDocumentSchema } from '@the-order/schemas';
|
|
import { StorageClient } from '@the-order/storage';
|
|
import {
|
|
getPool,
|
|
createDeal,
|
|
getDealById,
|
|
createDealDocument,
|
|
createDocument,
|
|
getDocumentById,
|
|
} from '@the-order/database';
|
|
import { randomUUID } from 'crypto';
|
|
|
|
const logger = createLogger('dataroom-service');
|
|
|
|
const server = Fastify({
|
|
logger,
|
|
requestIdLogLabel: 'requestId',
|
|
disableRequestLogging: false,
|
|
});
|
|
|
|
// Initialize database pool
|
|
const env = getEnv();
|
|
if (env.DATABASE_URL) {
|
|
getPool({ connectionString: env.DATABASE_URL });
|
|
}
|
|
|
|
// Initialize storage client
|
|
const storageClient = new StorageClient({
|
|
provider: env.STORAGE_TYPE || 's3',
|
|
bucket: env.STORAGE_BUCKET,
|
|
region: env.STORAGE_REGION,
|
|
});
|
|
|
|
// Initialize server
|
|
async function initializeServer(): Promise<void> {
|
|
// Register Swagger
|
|
const swaggerUrl = env.SWAGGER_SERVER_URL || (env.NODE_ENV === 'development' ? 'http://localhost:4004' : undefined);
|
|
if (!swaggerUrl) {
|
|
logger.warn('SWAGGER_SERVER_URL not set, Swagger documentation will not be available');
|
|
} else {
|
|
await server.register(fastifySwagger, {
|
|
openapi: {
|
|
info: {
|
|
title: 'Dataroom Service API',
|
|
description: 'Secure VDR, deal rooms, and document access control',
|
|
version: '1.0.0',
|
|
},
|
|
servers: [
|
|
{
|
|
url: swaggerUrl,
|
|
description: env.NODE_ENV || 'Development server',
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
await server.register(fastifySwaggerUI, {
|
|
routePrefix: '/docs',
|
|
});
|
|
}
|
|
|
|
await registerSecurityPlugins(server);
|
|
addCorrelationId(server);
|
|
addRequestLogging(server);
|
|
server.setErrorHandler(errorHandler);
|
|
}
|
|
|
|
// Health check
|
|
server.get(
|
|
'/health',
|
|
{
|
|
schema: {
|
|
description: 'Health check endpoint',
|
|
tags: ['health'],
|
|
response: {
|
|
200: {
|
|
type: 'object',
|
|
properties: {
|
|
status: { type: 'string' },
|
|
service: { type: 'string' },
|
|
database: { type: 'string' },
|
|
storage: { type: 'string' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async () => {
|
|
const { healthCheck: dbHealthCheck } = await import('@the-order/database');
|
|
const dbHealthy = await dbHealthCheck().catch(() => false);
|
|
const storageHealthy = await storageClient.objectExists('health-check').catch(() => false);
|
|
|
|
return {
|
|
status: dbHealthy && storageHealthy ? 'ok' : 'degraded',
|
|
service: 'dataroom',
|
|
database: dbHealthy ? 'connected' : 'disconnected',
|
|
storage: storageHealthy ? 'accessible' : 'unavailable',
|
|
};
|
|
}
|
|
);
|
|
|
|
// Create deal room
|
|
server.post(
|
|
'/deals',
|
|
{
|
|
preHandler: [authenticateJWT, requireRole('admin', 'deal_manager')],
|
|
schema: {
|
|
...createBodySchema(CreateDealSchema),
|
|
description: 'Create a new deal room',
|
|
tags: ['deals'],
|
|
response: {
|
|
201: {
|
|
type: 'object',
|
|
properties: {
|
|
deal: {
|
|
type: 'object',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
const body = request.body as { name: string; status?: string };
|
|
const userId = request.user?.id;
|
|
|
|
const deal = await createDeal({
|
|
name: body.name,
|
|
status: body.status || 'draft',
|
|
dataroom_id: randomUUID(),
|
|
created_by: userId,
|
|
});
|
|
|
|
return reply.status(201).send({ deal });
|
|
}
|
|
);
|
|
|
|
// Get deal room
|
|
server.get(
|
|
'/deals/:dealId',
|
|
{
|
|
preHandler: [authenticateJWT],
|
|
schema: {
|
|
description: 'Get a deal room by ID',
|
|
tags: ['deals'],
|
|
params: {
|
|
type: 'object',
|
|
required: ['dealId'],
|
|
properties: {
|
|
dealId: { type: 'string', format: 'uuid' },
|
|
},
|
|
},
|
|
response: {
|
|
200: {
|
|
type: 'object',
|
|
properties: {
|
|
deal: {
|
|
type: 'object',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
const { dealId } = request.params as { dealId: string };
|
|
|
|
const deal = await getDealById(dealId);
|
|
if (!deal) {
|
|
return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'Deal not found' } });
|
|
}
|
|
|
|
return { deal };
|
|
}
|
|
);
|
|
|
|
// Upload document to deal room
|
|
server.post(
|
|
'/deals/:dealId/documents',
|
|
{
|
|
preHandler: [authenticateJWT, requireRole('admin', 'deal_manager', 'editor')],
|
|
schema: {
|
|
...createBodySchema(CreateDocumentSchema),
|
|
description: 'Upload a document to a deal room',
|
|
tags: ['documents'],
|
|
params: {
|
|
type: 'object',
|
|
required: ['dealId'],
|
|
properties: {
|
|
dealId: { type: 'string', format: 'uuid' },
|
|
},
|
|
},
|
|
response: {
|
|
201: {
|
|
type: 'object',
|
|
properties: {
|
|
document: {
|
|
type: 'object',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
const { dealId } = request.params as { dealId: string };
|
|
const body = request.body as { title: string; type: string; content?: string; fileUrl?: string };
|
|
const userId = request.user?.id;
|
|
|
|
// Verify deal exists
|
|
const deal = await getDealById(dealId);
|
|
if (!deal) {
|
|
return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'Deal not found' } });
|
|
}
|
|
|
|
const documentId = randomUUID();
|
|
const key = `deals/${dealId}/documents/${documentId}`;
|
|
|
|
// Upload to storage if content provided
|
|
if (body.content) {
|
|
await storageClient.upload({
|
|
key,
|
|
content: Buffer.from(body.content),
|
|
contentType: 'application/pdf',
|
|
metadata: {
|
|
dealId,
|
|
title: body.title,
|
|
type: body.type,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Save document to database
|
|
const document = await createDocument({
|
|
title: body.title,
|
|
type: body.type,
|
|
file_url: body.fileUrl || key,
|
|
storage_key: body.content ? key : undefined,
|
|
user_id: userId,
|
|
status: 'active',
|
|
});
|
|
|
|
// Link document to deal
|
|
await createDealDocument(dealId, document.id, key);
|
|
|
|
return reply.status(201).send({ document });
|
|
}
|
|
);
|
|
|
|
// Get presigned URL for document access
|
|
server.get(
|
|
'/deals/:dealId/documents/:documentId/url',
|
|
{
|
|
preHandler: [authenticateJWT],
|
|
schema: {
|
|
description: 'Get a presigned URL for document access',
|
|
tags: ['documents'],
|
|
params: {
|
|
type: 'object',
|
|
required: ['dealId', 'documentId'],
|
|
properties: {
|
|
dealId: { type: 'string', format: 'uuid' },
|
|
documentId: { type: 'string', format: 'uuid' },
|
|
},
|
|
},
|
|
querystring: {
|
|
type: 'object',
|
|
properties: {
|
|
expiresIn: { type: 'number', default: 3600 },
|
|
},
|
|
},
|
|
response: {
|
|
200: {
|
|
type: 'object',
|
|
properties: {
|
|
url: { type: 'string', format: 'uri' },
|
|
expiresIn: { type: 'number' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
const { dealId, documentId } = request.params as { dealId: string; documentId: string };
|
|
const { expiresIn = 3600 } = request.query as { expiresIn?: number };
|
|
|
|
const key = `deals/${dealId}/documents/${documentId}`;
|
|
const url = await storageClient.getPresignedUrl(key, expiresIn);
|
|
|
|
return { url, expiresIn };
|
|
}
|
|
);
|
|
|
|
// Start server
|
|
const start = async () => {
|
|
try {
|
|
await initializeServer();
|
|
const env = getEnv();
|
|
const port = env.PORT || 4004;
|
|
await server.listen({ port, host: '0.0.0.0' });
|
|
logger.info({ port }, 'Dataroom service listening');
|
|
} catch (err) {
|
|
logger.error({ err }, 'Failed to start server');
|
|
process.exit(1);
|
|
}
|
|
};
|
|
|
|
start();
|
|
|