- Update active state styles for better visibility in the mock server. - BE updates catered to improving content type handling in the mock server. - Introduced a `disableMockServerInPersonalWorkspace` platform-level feature flag. - Remove inactive keyboard shorthand nudges from the Mock server dashboard context menu. --- Co-authored-by: mirarifhasan <arif.ishan05@gmail.com> Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
139 lines
4.5 KiB
TypeScript
139 lines
4.5 KiB
TypeScript
import {
|
|
Controller,
|
|
All,
|
|
Req,
|
|
Res,
|
|
HttpStatus,
|
|
UseGuards,
|
|
UseInterceptors,
|
|
} from '@nestjs/common';
|
|
import { Request, Response } from 'express';
|
|
import { MockServerService } from './mock-server.service';
|
|
import { MockServerLoggingInterceptor } from './mock-server-logging.interceptor';
|
|
import * as E from 'fp-ts/Either';
|
|
import { MockRequestGuard } from './mock-request.guard';
|
|
import { MockServer } from '@prisma/client';
|
|
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
|
|
|
/**
|
|
* Mock server controller with dual routing support:
|
|
* 1. Subdomain pattern: mock-server-id.mock.hopp.io/product
|
|
* 2. Route pattern: backend.hopp.io/mock/mock-server-id/product
|
|
*
|
|
* The MockRequestGuard handles extraction of mock server ID from both patterns
|
|
* The MockServerLoggingInterceptor handles logging of all requests
|
|
*/
|
|
@UseGuards(ThrottlerBehindProxyGuard)
|
|
@Controller({ path: 'mock' })
|
|
export class MockServerController {
|
|
constructor(private readonly mockServerService: MockServerService) {}
|
|
|
|
@All('*path')
|
|
@UseGuards(MockRequestGuard)
|
|
@UseInterceptors(MockServerLoggingInterceptor)
|
|
async handleMockRequest(@Req() req: Request, @Res() res: Response) {
|
|
// Mock server ID and info are attached by the guard
|
|
const mockServerId = (req as any).mockServerId as string;
|
|
const mockServer = (req as any).mockServer as MockServer;
|
|
|
|
if (!mockServerId) {
|
|
return res.status(HttpStatus.NOT_FOUND).json({
|
|
error: 'Not found',
|
|
message: 'Mock server ID not found',
|
|
});
|
|
}
|
|
|
|
const method = req.method;
|
|
// Get clean path (removes /mock/mock-server-id prefix for route-based pattern)
|
|
const path = MockRequestGuard.getCleanPath(
|
|
req.path || '/',
|
|
mockServer.subdomain,
|
|
);
|
|
|
|
// Extract query parameters
|
|
const queryParams = req.query as Record<string, string>;
|
|
|
|
// Extract request headers (convert to lowercase for case-insensitive matching)
|
|
const requestHeaders: Record<string, string> = {};
|
|
Object.keys(req.headers).forEach((key) => {
|
|
const value = req.headers[key];
|
|
if (typeof value === 'string') {
|
|
requestHeaders[key.toLowerCase()] = value;
|
|
} else if (Array.isArray(value)) {
|
|
requestHeaders[key.toLowerCase()] = value[0];
|
|
}
|
|
});
|
|
|
|
try {
|
|
const result = await this.mockServerService.handleMockRequest(
|
|
mockServer,
|
|
path,
|
|
method,
|
|
queryParams,
|
|
requestHeaders,
|
|
);
|
|
|
|
if (E.isLeft(result)) {
|
|
return res.status(HttpStatus.NOT_FOUND).json({
|
|
error: 'Endpoint not found',
|
|
message: result.left,
|
|
});
|
|
}
|
|
|
|
const mockResponse = result.right;
|
|
|
|
// Set response headers if any
|
|
if (mockResponse.headers) {
|
|
try {
|
|
const headers = JSON.parse(mockResponse.headers);
|
|
Object.keys(headers).forEach((key) => {
|
|
console.log('Setting header:', key, headers[key]);
|
|
res.setHeader(key, headers[key]);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error parsing response headers:', error);
|
|
}
|
|
}
|
|
|
|
// Add delay if specified
|
|
if (mockServer.delayInMs && mockServer.delayInMs > 0) {
|
|
await new Promise((resolve) =>
|
|
setTimeout(resolve, mockServer.delayInMs),
|
|
);
|
|
}
|
|
|
|
// Only set Content-Type if not already set
|
|
if (!res.getHeader('Content-Type')) {
|
|
let defaultContentType = 'text/plain';
|
|
|
|
// Check if body is a string and try to parse it to determine content type
|
|
if (typeof mockResponse.body === 'string') {
|
|
try {
|
|
JSON.parse(mockResponse.body);
|
|
// If parsing succeeds, it's JSON
|
|
defaultContentType = 'application/json';
|
|
} catch {
|
|
// If parsing fails, it's plain text
|
|
defaultContentType = 'text/plain';
|
|
}
|
|
} else if (typeof mockResponse.body === 'object') {
|
|
// If it's already an object, it's JSON
|
|
defaultContentType = 'application/json';
|
|
}
|
|
|
|
res.setHeader('Content-Type', defaultContentType);
|
|
}
|
|
// Security headers
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
|
|
// Send response
|
|
return res.status(mockResponse.statusCode).send(mockResponse.body);
|
|
} catch (error) {
|
|
console.error('Error handling mock request:', error);
|
|
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
|
error: 'Internal server error',
|
|
message: 'Failed to process mock request',
|
|
});
|
|
}
|
|
}
|
|
}
|