Files
syndarix/frontend/scripts/generate-msw-handlers.ts
Felipe Cardoso 5b0ae54365 Remove MSW handlers and update demo credentials for improved standardization
- Deleted `admin.ts`, `auth.ts`, and `users.ts` MSW handler files to streamline demo mode setup.
- Updated demo credentials logic in `DemoCredentialsModal` and `DemoModeBanner` for stronger password requirements (≥12 characters).
- Refined documentation in `CLAUDE.md` to align with new credential standards and auto-generated MSW workflows.
2025-11-24 19:20:28 +01:00

370 lines
10 KiB
JavaScript

#!/usr/bin/env node
/**
* MSW Handler Generator
*
* Automatically generates MSW request handlers from OpenAPI specification.
* This keeps mock API in sync with real backend automatically.
*
* Usage: node scripts/generate-msw-handlers.ts /tmp/openapi.json
*/
import fs from 'fs';
import path from 'path';
interface OpenAPISpec {
paths: {
[path: string]: {
[method: string]: {
operationId?: string;
summary?: string;
responses: {
[status: string]: {
description: string;
content?: {
'application/json'?: {
schema?: unknown;
};
};
};
};
parameters?: Array<{
name: string;
in: string;
required?: boolean;
schema?: { type: string };
}>;
requestBody?: {
content?: {
'application/json'?: {
schema?: unknown;
};
};
};
};
};
};
}
function parseOpenAPISpec(specPath: string): OpenAPISpec {
const spec = JSON.parse(fs.readFileSync(specPath, 'utf-8'));
return spec;
}
function getMethodName(method: string): string {
const methodMap: Record<string, string> = {
get: 'get',
post: 'post',
put: 'put',
patch: 'patch',
delete: 'delete',
};
return methodMap[method.toLowerCase()] || method;
}
function convertPathToMSWPattern(path: string): string {
// Convert OpenAPI path params {id} to MSW params :id
return path.replace(/\{([^}]+)\}/g, ':$1');
}
function shouldSkipEndpoint(path: string, method: string): boolean {
// Skip health check and root endpoints
if (path === '/' || path === '/health') return true;
// Skip OAuth endpoints (handled by regular login)
if (path.includes('/oauth')) return true;
return false;
}
function getHandlerCategory(path: string): 'auth' | 'users' | 'admin' | 'organizations' {
if (path.startsWith('/api/v1/auth')) return 'auth';
if (path.startsWith('/api/v1/admin')) return 'admin';
if (path.startsWith('/api/v1/organizations')) return 'organizations';
return 'users';
}
function generateMockResponse(path: string, method: string, operation: any): string {
const category = getHandlerCategory(path);
// Auth endpoints
if (category === 'auth') {
if (path.includes('/login') && method === 'post') {
return `
const body = (await request.json()) as any;
const user = validateCredentials(body.email, body.password);
if (!user) {
return HttpResponse.json(
{ detail: 'Incorrect email or password' },
{ status: 401 }
);
}
const accessToken = \`demo-access-\${user.id}-\${Date.now()}\`;
const refreshToken = \`demo-refresh-\${user.id}-\${Date.now()}\`;
setCurrentUser(user);
return HttpResponse.json({
access_token: accessToken,
refresh_token: refreshToken,
token_type: 'bearer',
expires_in: 900,
});`;
}
if (path.includes('/register') && method === 'post') {
return `
const body = (await request.json()) as any;
const newUser = {
id: \`new-user-\${Date.now()}\`,
email: body.email,
first_name: body.first_name,
last_name: body.last_name || null,
phone_number: body.phone_number || null,
is_active: true,
is_superuser: false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
last_login: null,
organization_count: 0,
};
setCurrentUser(newUser);
return HttpResponse.json({
user: newUser,
access_token: \`demo-access-\${Date.now()}\`,
refresh_token: \`demo-refresh-\${Date.now()}\`,
token_type: 'bearer',
expires_in: 900,
});`;
}
if (path.includes('/refresh') && method === 'post') {
return `
return HttpResponse.json({
access_token: \`demo-access-refreshed-\${Date.now()}\`,
refresh_token: \`demo-refresh-refreshed-\${Date.now()}\`,
token_type: 'bearer',
expires_in: 900,
});`;
}
// Generic auth success
return `
return HttpResponse.json({
success: true,
message: 'Operation successful',
});`;
}
// User endpoints
if (category === 'users') {
if (path === '/api/v1/users/me' && method === 'get') {
return `
if (!currentUser) {
return HttpResponse.json({ detail: 'Not authenticated' }, { status: 401 });
}
return HttpResponse.json(currentUser);`;
}
if (path === '/api/v1/users/me' && method === 'patch') {
return `
if (!currentUser) {
return HttpResponse.json({ detail: 'Not authenticated' }, { status: 401 });
}
const body = (await request.json()) as any;
updateCurrentUser(body);
return HttpResponse.json(currentUser);`;
}
if (path === '/api/v1/organizations/me' && method === 'get') {
return `
if (!currentUser) {
return HttpResponse.json({ detail: 'Not authenticated' }, { status: 401 });
}
const orgs = getUserOrganizations(currentUser.id);
return HttpResponse.json(orgs);`;
}
if (path === '/api/v1/sessions' && method === 'get') {
return `
if (!currentUser) {
return HttpResponse.json({ detail: 'Not authenticated' }, { status: 401 });
}
return HttpResponse.json({ sessions: [] });`;
}
}
// Admin endpoints
if (category === 'admin') {
const authCheck = `
if (!currentUser?.is_superuser) {
return HttpResponse.json({ detail: 'Admin access required' }, { status: 403 });
}`;
if (path === '/api/v1/admin/stats' && method === 'get') {
return `${authCheck}
return HttpResponse.json(adminStats);`;
}
if (path === '/api/v1/admin/users' && method === 'get') {
return `${authCheck}
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') || '1');
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
const start = (page - 1) * pageSize;
const end = start + pageSize;
const paginatedUsers = sampleUsers.slice(start, end);
return HttpResponse.json({
data: paginatedUsers,
pagination: {
total: sampleUsers.length,
page,
page_size: pageSize,
total_pages: Math.ceil(sampleUsers.length / pageSize),
has_next: end < sampleUsers.length,
has_prev: page > 1,
},
});`;
}
if (path === '/api/v1/admin/organizations' && method === 'get') {
return `${authCheck}
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') || '1');
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
const start = (page - 1) * pageSize;
const end = start + pageSize;
const paginatedOrgs = sampleOrganizations.slice(start, end);
return HttpResponse.json({
data: paginatedOrgs,
pagination: {
total: sampleOrganizations.length,
page,
page_size: pageSize,
total_pages: Math.ceil(sampleOrganizations.length / pageSize),
has_next: end < sampleOrganizations.length,
has_prev: page > 1,
},
});`;
}
}
// Generic success response
return `
return HttpResponse.json({
success: true,
message: 'Operation successful'
});`;
}
function generateHandlers(spec: OpenAPISpec): string {
const handlers: string[] = [];
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000';
for (const [pathPattern, pathItem] of Object.entries(spec.paths)) {
for (const [method, operation] of Object.entries(pathItem)) {
if (!['get', 'post', 'put', 'patch', 'delete'].includes(method.toLowerCase())) {
continue;
}
if (shouldSkipEndpoint(pathPattern, method)) {
continue;
}
const mswPath = convertPathToMSWPattern(pathPattern);
const httpMethod = getMethodName(method);
const summary = operation.summary || `${method.toUpperCase()} ${pathPattern}`;
const mockResponse = generateMockResponse(pathPattern, method, operation);
const handler = `
/**
* ${summary}
*/
http.${httpMethod}(\`\${API_BASE_URL}${mswPath}\`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
${mockResponse}
}),`;
handlers.push(handler);
}
}
return handlers.join('\n');
}
function generateHandlerFile(spec: OpenAPISpec): string {
const handlersCode = generateHandlers(spec);
return `/**
* Auto-generated MSW Handlers
*
* ⚠️ DO NOT EDIT THIS FILE MANUALLY
*
* This file is automatically generated from the OpenAPI specification.
* To regenerate: npm run generate:api
*
* For custom handler behavior, use src/mocks/handlers/overrides.ts
*
* Generated: ${new Date().toISOString()}
*/
import { http, HttpResponse, delay } from 'msw';
import {
validateCredentials,
setCurrentUser,
updateCurrentUser,
currentUser,
sampleUsers,
} from '../data/users';
import {
sampleOrganizations,
getUserOrganizations,
getOrganizationMembersList,
} from '../data/organizations';
import { adminStats } from '../data/stats';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000';
const NETWORK_DELAY = 300; // ms - simulate realistic network delay
/**
* Auto-generated request handlers
* Covers all endpoints defined in OpenAPI spec
*/
export const generatedHandlers = [${handlersCode}
];
`;
}
// Main execution
function main() {
const specPath = process.argv[2] || '/tmp/openapi.json';
if (!fs.existsSync(specPath)) {
console.error(`❌ OpenAPI spec not found at: ${specPath}`);
console.error(' Make sure backend is running and OpenAPI spec is available');
process.exit(1);
}
console.log('📖 Reading OpenAPI specification...');
const spec = parseOpenAPISpec(specPath);
console.log('🔨 Generating MSW handlers...');
const handlerCode = generateHandlerFile(spec);
const outputPath = path.join(__dirname, '../src/mocks/handlers/generated.ts');
fs.writeFileSync(outputPath, handlerCode);
console.log(`✅ Generated MSW handlers: ${outputPath}`);
console.log(`📊 Generated ${Object.keys(spec.paths).length} endpoint handlers`);
}
main();