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:
@@ -52,7 +52,7 @@ const demoCategories = [
|
||||
features: ['Login & logout', 'Registration', 'Password reset', 'Session tokens'],
|
||||
credentials: {
|
||||
email: 'demo@example.com',
|
||||
password: 'Demo123!',
|
||||
password: 'DemoPass1234!',
|
||||
role: 'Regular User',
|
||||
},
|
||||
},
|
||||
@@ -64,7 +64,7 @@ const demoCategories = [
|
||||
features: ['Profile editing', 'Password changes', 'Active sessions', 'Preferences'],
|
||||
credentials: {
|
||||
email: 'demo@example.com',
|
||||
password: 'Demo123!',
|
||||
password: 'DemoPass1234!',
|
||||
role: 'Regular User',
|
||||
},
|
||||
},
|
||||
@@ -76,7 +76,7 @@ const demoCategories = [
|
||||
features: ['User management', 'Analytics charts', 'Bulk operations', 'Organization control'],
|
||||
credentials: {
|
||||
email: 'admin@example.com',
|
||||
password: 'Admin123!',
|
||||
password: 'AdminPass1234!',
|
||||
role: 'Admin',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ export { ChartCard } from './ChartCard';
|
||||
export { UserGrowthChart } from './UserGrowthChart';
|
||||
export type { UserGrowthData } from './UserGrowthChart';
|
||||
export { OrganizationDistributionChart } from './OrganizationDistributionChart';
|
||||
export type { OrganizationDistributionData } from './OrganizationDistributionChart';
|
||||
export type { OrgDistributionData } from './OrganizationDistributionChart';
|
||||
export { RegistrationActivityChart } from './RegistrationActivityChart';
|
||||
export type { RegistrationActivityData } from './RegistrationActivityChart';
|
||||
export { UserStatusChart } from './UserStatusChart';
|
||||
|
||||
@@ -44,7 +44,7 @@ export function DemoModeBanner() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Demo Credentials (any password ≥8 chars works):
|
||||
Demo Credentials (any password ≥12 chars works):
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
<code className="block rounded bg-muted px-2 py-1.5 text-xs font-mono">
|
||||
|
||||
@@ -27,8 +27,8 @@ export function DemoCredentialsModal({ open, onClose }: DemoCredentialsModalProp
|
||||
const [copiedRegular, setCopiedRegular] = useState(false);
|
||||
const [copiedAdmin, setCopiedAdmin] = useState(false);
|
||||
|
||||
const regularCredentials = 'demo@example.com\nDemo123!';
|
||||
const adminCredentials = 'admin@example.com\nAdmin123!';
|
||||
const regularCredentials = 'demo@example.com\nDemoPass1234!';
|
||||
const adminCredentials = 'admin@example.com\nAdminPass1234!';
|
||||
|
||||
const copyToClipboard = async (text: string, type: 'regular' | 'admin') => {
|
||||
try {
|
||||
@@ -83,7 +83,7 @@ export function DemoCredentialsModal({ open, onClose }: DemoCredentialsModalProp
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<span className="text-xs font-sans text-muted-foreground/70">Password:</span>
|
||||
<span className="text-foreground">Demo123!</span>
|
||||
<span className="text-foreground">DemoPass1234!</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@@ -123,7 +123,7 @@ export function DemoCredentialsModal({ open, onClose }: DemoCredentialsModalProp
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<span className="text-xs font-sans text-muted-foreground/70">Password:</span>
|
||||
<span className="text-foreground">Admin123!</span>
|
||||
<span className="text-foreground">AdminPass1234!</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@@ -141,12 +141,12 @@ export function DemoCredentialsModal({ open, onClose }: DemoCredentialsModalProp
|
||||
<DialogFooter>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 w-full">
|
||||
<Button asChild variant="default" className="w-full">
|
||||
<Link href="/login?email=demo@example.com&password=Demo123!" onClick={onClose}>
|
||||
<Link href="/login?email=demo@example.com&password=DemoPass1234!" onClick={onClose}>
|
||||
Login as User
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="default" className="w-full">
|
||||
<Link href="/login?email=admin@example.com&password=Admin123!" onClick={onClose}>
|
||||
<Link href="/login?email=admin@example.com&password=AdminPass1234!" onClick={onClose}>
|
||||
Login as Admin
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -124,8 +124,8 @@ export const config = {
|
||||
enabled: parseBool(ENV.DEMO_MODE, false),
|
||||
// Demo credentials
|
||||
credentials: {
|
||||
user: { email: 'demo@example.com', password: 'DemoPass123' },
|
||||
admin: { email: 'admin@example.com', password: 'AdminPass123' },
|
||||
user: { email: 'demo@example.com', password: 'DemoPass1234!' },
|
||||
admin: { email: 'admin@example.com', password: 'AdminPass1234!' },
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { UserResponse } from '@/lib/api/client';
|
||||
|
||||
/**
|
||||
* Demo user (regular user)
|
||||
* Credentials: demo@example.com / DemoPass123
|
||||
* Credentials: demo@example.com / DemoPass1234!
|
||||
*/
|
||||
export const demoUser: UserResponse = {
|
||||
id: 'demo-user-id-1',
|
||||
@@ -20,13 +20,11 @@ export const demoUser: UserResponse = {
|
||||
is_superuser: false,
|
||||
created_at: '2024-01-15T10:00:00Z',
|
||||
updated_at: '2024-01-20T15:30:00Z',
|
||||
last_login: '2025-01-24T08:00:00Z',
|
||||
organization_count: 2,
|
||||
};
|
||||
|
||||
/**
|
||||
* Demo admin user (superuser)
|
||||
* Credentials: admin@example.com / AdminPass123
|
||||
* Credentials: admin@example.com / AdminPass1234!
|
||||
*/
|
||||
export const demoAdmin: UserResponse = {
|
||||
id: 'demo-admin-id-1',
|
||||
@@ -38,8 +36,6 @@ export const demoAdmin: UserResponse = {
|
||||
is_superuser: true,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-24T10:00:00Z',
|
||||
last_login: '2025-01-24T09:00:00Z',
|
||||
organization_count: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -58,8 +54,6 @@ export const sampleUsers: UserResponse[] = [
|
||||
is_superuser: false,
|
||||
created_at: '2024-02-01T12:00:00Z',
|
||||
updated_at: '2024-02-05T14:30:00Z',
|
||||
last_login: '2025-01-23T16:45:00Z',
|
||||
organization_count: 1,
|
||||
},
|
||||
{
|
||||
id: 'user-4',
|
||||
@@ -71,8 +65,6 @@ export const sampleUsers: UserResponse[] = [
|
||||
is_superuser: false,
|
||||
created_at: '2024-03-10T08:30:00Z',
|
||||
updated_at: '2024-03-15T11:00:00Z',
|
||||
last_login: '2025-01-22T10:20:00Z',
|
||||
organization_count: 3,
|
||||
},
|
||||
{
|
||||
id: 'user-5',
|
||||
@@ -84,8 +76,6 @@ export const sampleUsers: UserResponse[] = [
|
||||
is_superuser: false,
|
||||
created_at: '2024-01-20T14:00:00Z',
|
||||
updated_at: '2024-06-01T09:00:00Z',
|
||||
last_login: '2024-06-01T09:00:00Z',
|
||||
organization_count: 0,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -120,23 +110,23 @@ export function updateCurrentUser(updates: Partial<UserResponse>) {
|
||||
* In demo mode, we're lenient with passwords to improve UX
|
||||
*/
|
||||
export function validateCredentials(email: string, password: string): UserResponse | null {
|
||||
// Demo user - accept documented password or any password >= 8 chars
|
||||
// Demo user - accept documented password or any password >= 12 chars
|
||||
if (email === 'demo@example.com') {
|
||||
if (password === 'DemoPass123' || password.length >= 8) {
|
||||
if (password === 'DemoPass1234!' || password.length >= 12) {
|
||||
return demoUser;
|
||||
}
|
||||
}
|
||||
|
||||
// Demo admin - accept documented password or any password >= 8 chars
|
||||
// Demo admin - accept documented password or any password >= 12 chars
|
||||
if (email === 'admin@example.com') {
|
||||
if (password === 'AdminPass123' || password.length >= 8) {
|
||||
if (password === 'AdminPass1234!' || password.length >= 12) {
|
||||
return demoAdmin;
|
||||
}
|
||||
}
|
||||
|
||||
// Sample users - accept any valid password (it's a demo!)
|
||||
const user = sampleUsers.find((u) => u.email === email);
|
||||
if (user && password.length >= 8) {
|
||||
if (user && password.length >= 12) {
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,492 +0,0 @@
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
});
|
||||
}),
|
||||
];
|
||||
@@ -1,324 +0,0 @@
|
||||
/**
|
||||
* MSW Auth Endpoint Handlers
|
||||
*
|
||||
* Mirrors backend auth endpoints for demo mode
|
||||
* Consistent with E2E test mocks in e2e/helpers/auth.ts
|
||||
*/
|
||||
|
||||
import { http, HttpResponse, delay } from 'msw';
|
||||
import type {
|
||||
LoginRequest,
|
||||
TokenResponse,
|
||||
UserCreate,
|
||||
RegisterResponse,
|
||||
RefreshTokenRequest,
|
||||
LogoutRequest,
|
||||
MessageResponse,
|
||||
PasswordResetRequest,
|
||||
PasswordResetConfirm,
|
||||
} from '@/lib/api/client';
|
||||
import {
|
||||
validateCredentials,
|
||||
setCurrentUser,
|
||||
currentUser,
|
||||
demoUser,
|
||||
demoAdmin,
|
||||
sampleUsers,
|
||||
} from '../data/users';
|
||||
|
||||
// API base URL (same as app config)
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000';
|
||||
|
||||
// Simulate network delay (realistic UX)
|
||||
const NETWORK_DELAY = 300;
|
||||
|
||||
// In-memory session store (resets on page reload, which is fine for demo)
|
||||
let activeTokens = new Set<string>();
|
||||
|
||||
/**
|
||||
* Auth endpoint handlers
|
||||
*/
|
||||
export const authHandlers = [
|
||||
/**
|
||||
* POST /api/v1/auth/register - Register new user
|
||||
*/
|
||||
http.post(`${API_BASE_URL}/api/v1/auth/register`, async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const body = (await request.json()) as UserCreate;
|
||||
|
||||
// Validate required fields
|
||||
if (!body.email || !body.password || !body.first_name) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Missing required fields',
|
||||
},
|
||||
{ status: 422 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
const existingUser = sampleUsers.find((u) => u.email === body.email);
|
||||
if (existingUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'User with this email already exists',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create new user (in real app, this would be persisted)
|
||||
const newUser: RegisterResponse['user'] = {
|
||||
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,
|
||||
};
|
||||
|
||||
// Generate tokens
|
||||
const accessToken = `demo-access-${Date.now()}`;
|
||||
const refreshToken = `demo-refresh-${Date.now()}`;
|
||||
activeTokens.add(accessToken);
|
||||
activeTokens.add(refreshToken);
|
||||
|
||||
// Set as current user
|
||||
setCurrentUser(newUser);
|
||||
|
||||
const response: RegisterResponse = {
|
||||
user: newUser,
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
token_type: 'bearer',
|
||||
expires_in: 900, // 15 minutes
|
||||
};
|
||||
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/login - Login with email and password
|
||||
*/
|
||||
http.post(`${API_BASE_URL}/api/v1/auth/login`, async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const body = (await request.json()) as LoginRequest;
|
||||
|
||||
// Validate credentials
|
||||
const user = validateCredentials(body.email, body.password);
|
||||
|
||||
if (!user) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Incorrect email or password',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user is active
|
||||
if (!user.is_active) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Account is deactivated',
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const accessToken = `demo-access-${user.id}-${Date.now()}`;
|
||||
const refreshToken = `demo-refresh-${user.id}-${Date.now()}`;
|
||||
activeTokens.add(accessToken);
|
||||
activeTokens.add(refreshToken);
|
||||
|
||||
// Update last login
|
||||
const updatedUser = {
|
||||
...user,
|
||||
last_login: new Date().toISOString(),
|
||||
};
|
||||
setCurrentUser(updatedUser);
|
||||
|
||||
const response: TokenResponse = {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
token_type: 'bearer',
|
||||
expires_in: 900, // 15 minutes
|
||||
};
|
||||
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/refresh - Refresh access token
|
||||
*/
|
||||
http.post(`${API_BASE_URL}/api/v1/auth/refresh`, async ({ request }) => {
|
||||
await delay(100); // Fast refresh
|
||||
|
||||
const body = (await request.json()) as RefreshTokenRequest;
|
||||
|
||||
// Validate refresh token
|
||||
if (!body.refresh_token || !activeTokens.has(body.refresh_token)) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Invalid or expired refresh token',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
const newAccessToken = `demo-access-refreshed-${Date.now()}`;
|
||||
const newRefreshToken = `demo-refresh-refreshed-${Date.now()}`;
|
||||
|
||||
// Remove old tokens, add new ones
|
||||
activeTokens.delete(body.refresh_token);
|
||||
activeTokens.add(newAccessToken);
|
||||
activeTokens.add(newRefreshToken);
|
||||
|
||||
const response: TokenResponse = {
|
||||
access_token: newAccessToken,
|
||||
refresh_token: newRefreshToken,
|
||||
token_type: 'bearer',
|
||||
expires_in: 900,
|
||||
};
|
||||
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/logout - Logout (revoke tokens)
|
||||
*/
|
||||
http.post(`${API_BASE_URL}/api/v1/auth/logout`, async ({ request }) => {
|
||||
await delay(100);
|
||||
|
||||
const body = (await request.json()) as LogoutRequest;
|
||||
|
||||
// Remove token from active set
|
||||
if (body.refresh_token) {
|
||||
activeTokens.delete(body.refresh_token);
|
||||
}
|
||||
|
||||
// Clear current user
|
||||
setCurrentUser(null);
|
||||
|
||||
const response: MessageResponse = {
|
||||
success: true,
|
||||
message: 'Logged out successfully',
|
||||
};
|
||||
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/logout-all - Logout from all devices
|
||||
*/
|
||||
http.post(`${API_BASE_URL}/api/v1/auth/logout-all`, async () => {
|
||||
await delay(100);
|
||||
|
||||
// Clear all tokens
|
||||
activeTokens.clear();
|
||||
setCurrentUser(null);
|
||||
|
||||
const response: MessageResponse = {
|
||||
success: true,
|
||||
message: 'Logged out from all devices',
|
||||
};
|
||||
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/password-reset - Request password reset
|
||||
*/
|
||||
http.post(`${API_BASE_URL}/api/v1/auth/password-reset`, async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const body = (await request.json()) as PasswordResetRequest;
|
||||
|
||||
// In demo mode, always return success (don't reveal if email exists)
|
||||
const response: MessageResponse = {
|
||||
success: true,
|
||||
message: 'If an account exists with that email, you will receive a password reset link.',
|
||||
};
|
||||
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/password-reset/confirm - Confirm password reset
|
||||
*/
|
||||
http.post(`${API_BASE_URL}/api/v1/auth/password-reset/confirm`, async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const body = (await request.json()) as PasswordResetConfirm;
|
||||
|
||||
// Validate token (in demo, accept any token that looks valid)
|
||||
if (!body.token || body.token.length < 10) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Invalid or expired reset token',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate password requirements
|
||||
if (!body.new_password || body.new_password.length < 8) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Password must be at least 8 characters',
|
||||
},
|
||||
{ status: 422 }
|
||||
);
|
||||
}
|
||||
|
||||
const response: MessageResponse = {
|
||||
success: true,
|
||||
message: 'Password reset successfully',
|
||||
};
|
||||
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/change-password - Change password (authenticated)
|
||||
*/
|
||||
http.post(`${API_BASE_URL}/api/v1/auth/change-password`, async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
// Check if user is authenticated
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Not authenticated',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'User not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const response: MessageResponse = {
|
||||
success: true,
|
||||
message: 'Password changed successfully',
|
||||
};
|
||||
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
];
|
||||
@@ -1,16 +1,21 @@
|
||||
/**
|
||||
* MSW Handlers Index
|
||||
*
|
||||
* Exports all request handlers for Mock Service Worker
|
||||
* Organized by domain: auth, users, admin
|
||||
* Combines auto-generated handlers with custom overrides.
|
||||
*
|
||||
* Architecture:
|
||||
* - generated.ts: Auto-generated from OpenAPI spec (DO NOT EDIT)
|
||||
* - overrides.ts: Custom handler logic (EDIT AS NEEDED)
|
||||
*
|
||||
* Overrides take precedence over generated handlers.
|
||||
*/
|
||||
|
||||
import { authHandlers } from './auth';
|
||||
import { userHandlers } from './users';
|
||||
import { adminHandlers } from './admin';
|
||||
import { generatedHandlers } from './generated';
|
||||
import { overrideHandlers } from './overrides';
|
||||
|
||||
/**
|
||||
* All request handlers for MSW
|
||||
* Order matters: more specific handlers should come first
|
||||
*
|
||||
* Order matters: overrides come first to take precedence
|
||||
*/
|
||||
export const handlers = [...authHandlers, ...userHandlers, ...adminHandlers];
|
||||
export const handlers = [...overrideHandlers, ...generatedHandlers];
|
||||
|
||||
38
frontend/src/mocks/handlers/overrides.ts
Normal file
38
frontend/src/mocks/handlers/overrides.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* MSW Handler Overrides
|
||||
*
|
||||
* Custom handlers that override or extend auto-generated ones.
|
||||
* Use this file for complex logic that can't be auto-generated.
|
||||
*
|
||||
* Examples:
|
||||
* - Complex validation logic
|
||||
* - Stateful interactions
|
||||
* - Error simulation scenarios
|
||||
* - Special edge cases
|
||||
*/
|
||||
|
||||
import { http, HttpResponse, delay } from 'msw';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000';
|
||||
|
||||
/**
|
||||
* Custom handler overrides
|
||||
*
|
||||
* These handlers take precedence over generated ones.
|
||||
* Add custom implementations here as needed.
|
||||
*/
|
||||
export const overrideHandlers = [
|
||||
// Example: Custom error simulation for testing
|
||||
// http.post(`${API_BASE_URL}/api/v1/auth/login`, async ({ request }) => {
|
||||
// // Simulate rate limiting 10% of the time
|
||||
// if (Math.random() < 0.1) {
|
||||
// return HttpResponse.json(
|
||||
// { detail: 'Too many login attempts' },
|
||||
// { status: 429 }
|
||||
// );
|
||||
// }
|
||||
// // Otherwise, use generated handler (by not returning anything)
|
||||
// }),
|
||||
|
||||
// Add your custom handlers here...
|
||||
];
|
||||
@@ -1,301 +0,0 @@
|
||||
/**
|
||||
* MSW User Endpoint Handlers
|
||||
*
|
||||
* Handles user profile, organizations, and session management
|
||||
*/
|
||||
|
||||
import { http, HttpResponse, delay } from 'msw';
|
||||
import type {
|
||||
UserResponse,
|
||||
UserUpdate,
|
||||
OrganizationResponse,
|
||||
SessionResponse,
|
||||
MessageResponse,
|
||||
} from '@/lib/api/client';
|
||||
import { currentUser, updateCurrentUser, sampleUsers } from '../data/users';
|
||||
import { getUserOrganizations } from '../data/organizations';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000';
|
||||
const NETWORK_DELAY = 200;
|
||||
|
||||
// In-memory session store for demo
|
||||
const mockSessions: SessionResponse[] = [
|
||||
{
|
||||
id: 'session-1',
|
||||
user_id: 'demo-user-id-1',
|
||||
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(), // 7 days ago
|
||||
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days from now
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
id: 'session-2',
|
||||
user_id: 'demo-user-id-1',
|
||||
device_name: 'Safari on iPhone',
|
||||
device_id: 'device-2',
|
||||
ip_address: '192.168.1.101',
|
||||
location_city: 'San Francisco',
|
||||
location_country: 'United States',
|
||||
last_used_at: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // 1 day ago
|
||||
created_at: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
expires_at: new Date(Date.now() + 6 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
is_active: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if request is authenticated
|
||||
*/
|
||||
function isAuthenticated(request: Request): boolean {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
return Boolean(authHeader && authHeader.startsWith('Bearer '));
|
||||
}
|
||||
|
||||
/**
|
||||
* User endpoint handlers
|
||||
*/
|
||||
export const userHandlers = [
|
||||
/**
|
||||
* GET /api/v1/users/me - Get current user profile
|
||||
*/
|
||||
http.get(`${API_BASE_URL}/api/v1/users/me`, async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
if (!isAuthenticated(request)) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Not authenticated',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'User not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return HttpResponse.json(currentUser);
|
||||
}),
|
||||
|
||||
/**
|
||||
* PATCH /api/v1/users/me - Update current user profile
|
||||
*/
|
||||
http.patch(`${API_BASE_URL}/api/v1/users/me`, async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
if (!isAuthenticated(request)) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Not authenticated',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'User not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = (await request.json()) as UserUpdate;
|
||||
|
||||
// Update user profile
|
||||
updateCurrentUser(body);
|
||||
|
||||
return HttpResponse.json(currentUser);
|
||||
}),
|
||||
|
||||
/**
|
||||
* DELETE /api/v1/users/me - Delete current user account
|
||||
*/
|
||||
http.delete(`${API_BASE_URL}/api/v1/users/me`, async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
if (!isAuthenticated(request)) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Not authenticated',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const response: MessageResponse = {
|
||||
success: true,
|
||||
message: 'Account deleted successfully',
|
||||
};
|
||||
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
/**
|
||||
* GET /api/v1/users/:id - Get user by ID (public profile)
|
||||
*/
|
||||
http.get(`${API_BASE_URL}/api/v1/users/:id`, async ({ request, params }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
if (!isAuthenticated(request)) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Not authenticated',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}),
|
||||
|
||||
/**
|
||||
* GET /api/v1/users - List users (paginated)
|
||||
*/
|
||||
http.get(`${API_BASE_URL}/api/v1/users`, async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
if (!isAuthenticated(request)) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Not authenticated',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// 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') || '20');
|
||||
|
||||
// Simple pagination
|
||||
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,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
/**
|
||||
* GET /api/v1/organizations/me - Get current user's organizations
|
||||
*/
|
||||
http.get(`${API_BASE_URL}/api/v1/organizations/me`, async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
if (!isAuthenticated(request)) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Not authenticated',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json([], { status: 200 });
|
||||
}
|
||||
|
||||
const organizations = getUserOrganizations(currentUser.id);
|
||||
return HttpResponse.json(organizations);
|
||||
}),
|
||||
|
||||
/**
|
||||
* GET /api/v1/sessions - Get current user's sessions
|
||||
*/
|
||||
http.get(`${API_BASE_URL}/api/v1/sessions`, async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
if (!isAuthenticated(request)) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Not authenticated',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json({ sessions: [] });
|
||||
}
|
||||
|
||||
// Filter sessions for current user
|
||||
const userSessions = mockSessions.filter((s) => s.user_id === currentUser.id);
|
||||
|
||||
return HttpResponse.json({
|
||||
sessions: userSessions,
|
||||
});
|
||||
}),
|
||||
|
||||
/**
|
||||
* DELETE /api/v1/sessions/:id - Revoke a session
|
||||
*/
|
||||
http.delete(`${API_BASE_URL}/api/v1/sessions/:id`, async ({ request, params }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
if (!isAuthenticated(request)) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Not authenticated',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
|
||||
// Find session
|
||||
const sessionIndex = mockSessions.findIndex((s) => s.id === id);
|
||||
if (sessionIndex === -1) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
detail: 'Session not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Remove session
|
||||
mockSessions.splice(sessionIndex, 1);
|
||||
|
||||
const response: MessageResponse = {
|
||||
success: true,
|
||||
message: 'Session revoked successfully',
|
||||
};
|
||||
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
];
|
||||
Reference in New Issue
Block a user