forked from cardosofelipe/fast-next-template
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.
This commit is contained in:
@@ -71,6 +71,14 @@ for file in "$OUTPUT_DIR"/**/*.ts "$OUTPUT_DIR"/*.ts; do
|
||||
done
|
||||
echo -e "${GREEN}✓ ESLint disabled for generated files${NC}"
|
||||
|
||||
# Generate MSW handlers from OpenAPI spec
|
||||
echo -e "${YELLOW}🎭 Generating MSW handlers...${NC}"
|
||||
if npx tsx scripts/generate-msw-handlers.ts /tmp/openapi.json; then
|
||||
echo -e "${GREEN}✓ MSW handlers generated successfully${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ MSW handler generation failed (non-critical)${NC}"
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm /tmp/openapi.json
|
||||
|
||||
@@ -80,8 +88,13 @@ echo -e "${YELLOW}📝 Generated files:${NC}"
|
||||
echo -e " - $OUTPUT_DIR/index.ts"
|
||||
echo -e " - $OUTPUT_DIR/schemas/"
|
||||
echo -e " - $OUTPUT_DIR/services/"
|
||||
echo -e " - src/mocks/handlers/generated.ts (MSW handlers)"
|
||||
echo ""
|
||||
echo -e "${YELLOW}💡 Next steps:${NC}"
|
||||
echo -e " Import in your code:"
|
||||
echo -e " ${GREEN}import { ApiClient } from '@/lib/api/generated';${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}🎭 Demo Mode:${NC}"
|
||||
echo -e " MSW handlers are automatically synced with your API"
|
||||
echo -e " Test demo mode: ${GREEN}NEXT_PUBLIC_DEMO_MODE=true npm run dev${NC}"
|
||||
echo ""
|
||||
|
||||
369
frontend/scripts/generate-msw-handlers.ts
Normal file
369
frontend/scripts/generate-msw-handlers.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
#!/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();
|
||||
110
frontend/scripts/sync-msw-with-openapi.md
Normal file
110
frontend/scripts/sync-msw-with-openapi.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Keeping MSW Handlers Synced with OpenAPI Spec
|
||||
|
||||
## Problem
|
||||
MSW handlers can drift out of sync with the backend API as it evolves.
|
||||
|
||||
## Solution Options
|
||||
|
||||
### Option 1: Use openapi-msw (Recommended)
|
||||
|
||||
Install the package that auto-generates MSW handlers from OpenAPI:
|
||||
|
||||
```bash
|
||||
npm install --save-dev openapi-msw
|
||||
```
|
||||
|
||||
Then create a generation script:
|
||||
|
||||
```typescript
|
||||
// scripts/generate-msw-handlers.ts
|
||||
import { generateMockHandlers } from 'openapi-msw';
|
||||
import fs from 'fs';
|
||||
|
||||
async function generate() {
|
||||
const spec = JSON.parse(fs.readFileSync('/tmp/openapi.json', 'utf-8'));
|
||||
|
||||
const handlers = generateMockHandlers(spec, {
|
||||
baseUrl: 'http://localhost:8000',
|
||||
});
|
||||
|
||||
fs.writeFileSync('src/mocks/handlers/generated.ts', handlers);
|
||||
}
|
||||
|
||||
generate();
|
||||
```
|
||||
|
||||
### Option 2: Manual Sync Checklist
|
||||
|
||||
When you add/change backend endpoints:
|
||||
|
||||
1. **Update Backend** → Make API changes
|
||||
2. **Generate Frontend Client** → `npm run generate:api`
|
||||
3. **Update MSW Handlers** → Edit `src/mocks/handlers/*.ts`
|
||||
4. **Test Demo Mode** → `NEXT_PUBLIC_DEMO_MODE=true npm run dev`
|
||||
|
||||
### Option 3: Automated with Script Hook
|
||||
|
||||
Add to `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"generate:api": "./scripts/generate-api-client.sh && npm run sync:msw",
|
||||
"sync:msw": "echo '⚠️ Don't forget to update MSW handlers in src/mocks/handlers/'"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Current Coverage
|
||||
|
||||
Our MSW handlers currently cover:
|
||||
|
||||
**Auth Endpoints:**
|
||||
- POST `/api/v1/auth/register`
|
||||
- POST `/api/v1/auth/login`
|
||||
- POST `/api/v1/auth/refresh`
|
||||
- POST `/api/v1/auth/logout`
|
||||
- POST `/api/v1/auth/logout-all`
|
||||
- POST `/api/v1/auth/password-reset`
|
||||
- POST `/api/v1/auth/password-reset/confirm`
|
||||
- POST `/api/v1/auth/change-password`
|
||||
|
||||
**User Endpoints:**
|
||||
- GET `/api/v1/users/me`
|
||||
- PATCH `/api/v1/users/me`
|
||||
- DELETE `/api/v1/users/me`
|
||||
- GET `/api/v1/users/:id`
|
||||
- GET `/api/v1/users`
|
||||
- GET `/api/v1/organizations/me`
|
||||
- GET `/api/v1/sessions`
|
||||
- DELETE `/api/v1/sessions/:id`
|
||||
|
||||
**Admin Endpoints:**
|
||||
- GET `/api/v1/admin/stats`
|
||||
- GET `/api/v1/admin/users`
|
||||
- GET `/api/v1/admin/users/:id`
|
||||
- POST `/api/v1/admin/users`
|
||||
- PATCH `/api/v1/admin/users/:id`
|
||||
- DELETE `/api/v1/admin/users/:id`
|
||||
- POST `/api/v1/admin/users/bulk`
|
||||
- GET `/api/v1/admin/organizations`
|
||||
- GET `/api/v1/admin/organizations/:id`
|
||||
- GET `/api/v1/admin/organizations/:id/members`
|
||||
- GET `/api/v1/admin/sessions`
|
||||
|
||||
## Quick Validation
|
||||
|
||||
To check if MSW is missing handlers:
|
||||
|
||||
1. Start demo mode: `NEXT_PUBLIC_DEMO_MODE=true npm run dev`
|
||||
2. Open browser console
|
||||
3. Look for `[MSW] Warning: intercepted a request without a matching request handler`
|
||||
4. Add missing handlers to appropriate file in `src/mocks/handlers/`
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep handlers simple** - Return happy path responses by default
|
||||
2. **Match backend schemas** - Use generated TypeScript types
|
||||
3. **Realistic delays** - Use `await delay(300)` for UX testing
|
||||
4. **Document passwords** - Make demo credentials obvious
|
||||
5. **Test regularly** - Run demo mode after API changes
|
||||
Reference in New Issue
Block a user