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:
Felipe Cardoso
2025-11-24 19:20:28 +01:00
parent 372af25aaa
commit 5b0ae54365
25 changed files with 1499 additions and 1167 deletions

View File

@@ -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 ""

View 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();

View 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