Files
syndarix/frontend/src/mocks/handlers/admin.ts
Felipe Cardoso 487c8a3863 Add demo mode support with MSW integration and documentation
- Integrated Mock Service Worker (MSW) for frontend-only demo mode, allowing API call interception without requiring a backend.
- Added `DemoModeBanner` component to indicate active demo mode and display demo credentials.
- Enhanced configuration with `DEMO_MODE` flag and demo credentials for user and admin access.
- Updated ESLint configuration to exclude MSW-related files from linting and coverage.
- Created comprehensive `DEMO_MODE.md` documentation for setup and usage guidelines, including deployment instructions and troubleshooting.
- Updated package dependencies to include MSW and related libraries.
2025-11-24 18:42:05 +01:00

493 lines
12 KiB
TypeScript

/**
* MSW Admin Endpoint Handlers
*
* Handles admin dashboard, user management, org management
* Only accessible to superusers (is_superuser = true)
*/
import { http, HttpResponse, delay } from 'msw';
import type {
UserResponse,
OrganizationResponse,
UserCreate,
UserUpdate,
OrganizationCreate,
OrganizationUpdate,
AdminStatsResponse,
BulkUserAction,
BulkActionResult,
} from '@/lib/api/client';
import { currentUser, sampleUsers } from '../data/users';
import { sampleOrganizations, 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 = 200;
/**
* Check if request is from a superuser
*/
function isSuperuser(request: Request): boolean {
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return false;
}
return currentUser?.is_superuser === true;
}
/**
* Admin endpoint handlers
*/
export const adminHandlers = [
/**
* GET /api/v1/admin/stats - Get dashboard statistics
*/
http.get(`${API_BASE_URL}/api/v1/admin/stats`, async ({ request }) => {
await delay(NETWORK_DELAY);
if (!isSuperuser(request)) {
return HttpResponse.json(
{
detail: 'Admin access required',
},
{ status: 403 }
);
}
return HttpResponse.json(adminStats);
}),
/**
* GET /api/v1/admin/users - List all users (paginated)
*/
http.get(`${API_BASE_URL}/api/v1/admin/users`, async ({ request }) => {
await delay(NETWORK_DELAY);
if (!isSuperuser(request)) {
return HttpResponse.json(
{
detail: 'Admin access required',
},
{ status: 403 }
);
}
// Parse query params
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') || '1');
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
const search = url.searchParams.get('search') || '';
const isActive = url.searchParams.get('is_active');
// Filter users
let filteredUsers = [...sampleUsers];
if (search) {
filteredUsers = filteredUsers.filter(
(u) =>
u.email.toLowerCase().includes(search.toLowerCase()) ||
u.first_name.toLowerCase().includes(search.toLowerCase()) ||
u.last_name?.toLowerCase().includes(search.toLowerCase())
);
}
if (isActive !== null) {
const activeFilter = isActive === 'true';
filteredUsers = filteredUsers.filter((u) => u.is_active === activeFilter);
}
// Paginate
const start = (page - 1) * pageSize;
const end = start + pageSize;
const paginatedUsers = filteredUsers.slice(start, end);
return HttpResponse.json({
data: paginatedUsers,
pagination: {
total: filteredUsers.length,
page,
page_size: pageSize,
total_pages: Math.ceil(filteredUsers.length / pageSize),
has_next: end < filteredUsers.length,
has_prev: page > 1,
},
});
}),
/**
* GET /api/v1/admin/users/:id - Get user by ID
*/
http.get(`${API_BASE_URL}/api/v1/admin/users/:id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
if (!isSuperuser(request)) {
return HttpResponse.json(
{
detail: 'Admin access required',
},
{ status: 403 }
);
}
const { id } = params;
const user = sampleUsers.find((u) => u.id === id);
if (!user) {
return HttpResponse.json(
{
detail: 'User not found',
},
{ status: 404 }
);
}
return HttpResponse.json(user);
}),
/**
* POST /api/v1/admin/users - Create new user
*/
http.post(`${API_BASE_URL}/api/v1/admin/users`, async ({ request }) => {
await delay(NETWORK_DELAY);
if (!isSuperuser(request)) {
return HttpResponse.json(
{
detail: 'Admin access required',
},
{ status: 403 }
);
}
const body = (await request.json()) as UserCreate;
// Check if email exists
if (sampleUsers.some((u) => u.email === body.email)) {
return HttpResponse.json(
{
detail: 'User with this email already exists',
},
{ status: 400 }
);
}
// Create user (in-memory, will be lost on reload)
const newUser: UserResponse = {
id: `user-new-${Date.now()}`,
email: body.email,
first_name: body.first_name,
last_name: body.last_name || null,
phone_number: body.phone_number || null,
is_active: body.is_active !== false,
is_superuser: body.is_superuser === true,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
last_login: null,
organization_count: 0,
};
sampleUsers.push(newUser);
return HttpResponse.json(newUser, { status: 201 });
}),
/**
* PATCH /api/v1/admin/users/:id - Update user
*/
http.patch(`${API_BASE_URL}/api/v1/admin/users/:id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
if (!isSuperuser(request)) {
return HttpResponse.json(
{
detail: 'Admin access required',
},
{ status: 403 }
);
}
const { id } = params;
const userIndex = sampleUsers.findIndex((u) => u.id === id);
if (userIndex === -1) {
return HttpResponse.json(
{
detail: 'User not found',
},
{ status: 404 }
);
}
const body = (await request.json()) as UserUpdate;
// Update user
sampleUsers[userIndex] = {
...sampleUsers[userIndex],
...body,
updated_at: new Date().toISOString(),
};
return HttpResponse.json(sampleUsers[userIndex]);
}),
/**
* DELETE /api/v1/admin/users/:id - Delete user
*/
http.delete(`${API_BASE_URL}/api/v1/admin/users/:id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
if (!isSuperuser(request)) {
return HttpResponse.json(
{
detail: 'Admin access required',
},
{ status: 403 }
);
}
const { id } = params;
const userIndex = sampleUsers.findIndex((u) => u.id === id);
if (userIndex === -1) {
return HttpResponse.json(
{
detail: 'User not found',
},
{ status: 404 }
);
}
sampleUsers.splice(userIndex, 1);
return HttpResponse.json({
success: true,
message: 'User deleted successfully',
});
}),
/**
* POST /api/v1/admin/users/bulk - Bulk user action
*/
http.post(`${API_BASE_URL}/api/v1/admin/users/bulk`, async ({ request }) => {
await delay(NETWORK_DELAY);
if (!isSuperuser(request)) {
return HttpResponse.json(
{
detail: 'Admin access required',
},
{ status: 403 }
);
}
const body = (await request.json()) as BulkUserAction;
const { action, user_ids } = body;
let affected = 0;
let failed = 0;
for (const userId of user_ids) {
const userIndex = sampleUsers.findIndex((u) => u.id === userId);
if (userIndex !== -1) {
switch (action) {
case 'activate':
sampleUsers[userIndex].is_active = true;
affected++;
break;
case 'deactivate':
sampleUsers[userIndex].is_active = false;
affected++;
break;
case 'delete':
sampleUsers.splice(userIndex, 1);
affected++;
break;
}
} else {
failed++;
}
}
const result: BulkActionResult = {
success: failed === 0,
affected_count: affected,
failed_count: failed,
message: `${action} completed: ${affected} users affected`,
failed_ids: [],
};
return HttpResponse.json(result);
}),
/**
* GET /api/v1/admin/organizations - List all organizations (paginated)
*/
http.get(`${API_BASE_URL}/api/v1/admin/organizations`, async ({ request }) => {
await delay(NETWORK_DELAY);
if (!isSuperuser(request)) {
return HttpResponse.json(
{
detail: 'Admin access required',
},
{ status: 403 }
);
}
// Parse query params
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') || '1');
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
const search = url.searchParams.get('search') || '';
// Filter organizations
let filteredOrgs = [...sampleOrganizations];
if (search) {
filteredOrgs = filteredOrgs.filter(
(o) =>
o.name.toLowerCase().includes(search.toLowerCase()) ||
o.slug.toLowerCase().includes(search.toLowerCase())
);
}
// Paginate
const start = (page - 1) * pageSize;
const end = start + pageSize;
const paginatedOrgs = filteredOrgs.slice(start, end);
return HttpResponse.json({
data: paginatedOrgs,
pagination: {
total: filteredOrgs.length,
page,
page_size: pageSize,
total_pages: Math.ceil(filteredOrgs.length / pageSize),
has_next: end < filteredOrgs.length,
has_prev: page > 1,
},
});
}),
/**
* GET /api/v1/admin/organizations/:id - Get organization by ID
*/
http.get(`${API_BASE_URL}/api/v1/admin/organizations/:id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
if (!isSuperuser(request)) {
return HttpResponse.json(
{
detail: 'Admin access required',
},
{ status: 403 }
);
}
const { id } = params;
const org = sampleOrganizations.find((o) => o.id === id);
if (!org) {
return HttpResponse.json(
{
detail: 'Organization not found',
},
{ status: 404 }
);
}
return HttpResponse.json(org);
}),
/**
* GET /api/v1/admin/organizations/:id/members - Get organization members
*/
http.get(
`${API_BASE_URL}/api/v1/admin/organizations/:id/members`,
async ({ request, params }) => {
await delay(NETWORK_DELAY);
if (!isSuperuser(request)) {
return HttpResponse.json(
{
detail: 'Admin access required',
},
{ status: 403 }
);
}
const { id } = params as { id: string };
const members = getOrganizationMembersList(id);
// Parse pagination params
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') || '1');
const pageSize = parseInt(url.searchParams.get('page_size') || '20');
const start = (page - 1) * pageSize;
const end = start + pageSize;
const paginatedMembers = members.slice(start, end);
return HttpResponse.json({
data: paginatedMembers,
pagination: {
total: members.length,
page,
page_size: pageSize,
total_pages: Math.ceil(members.length / pageSize),
has_next: end < members.length,
has_prev: page > 1,
},
});
}
),
/**
* GET /api/v1/admin/sessions - Get all sessions (admin view)
*/
http.get(`${API_BASE_URL}/api/v1/admin/sessions`, async ({ request }) => {
await delay(NETWORK_DELAY);
if (!isSuperuser(request)) {
return HttpResponse.json(
{
detail: 'Admin access required',
},
{ status: 403 }
);
}
// Mock session data
const sessions = [
{
id: 'session-1',
user_id: 'demo-user-id-1',
user_email: 'demo@example.com',
user_full_name: 'Demo User',
device_name: 'Chrome on macOS',
device_id: 'device-1',
ip_address: '192.168.1.100',
location_city: 'San Francisco',
location_country: 'United States',
last_used_at: new Date().toISOString(),
created_at: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
is_active: true,
},
];
return HttpResponse.json({
data: sessions,
pagination: {
total: sessions.length,
page: 1,
page_size: 100,
total_pages: 1,
has_next: false,
has_prev: false,
},
});
}),
];