fix: Connect admin dashboard to real backend API with authentication
- Fixed CORS to allow pfadmin.noru1.ro and localhost:3335 - Fixed API client to handle nested token response structure (data.tokens.accessToken) - Added deviceInfo requirement to login endpoint - Fixed API endpoint paths to use /api/v1 prefix consistently - Updated admin user password to 'admin123' for demo@parentflowapp.com - Fixed Grid deprecation warnings by replacing with CSS Grid - Added automatic redirect to /login on 401 unauthorized - Enhanced user management service to include familyCount, childrenCount, deviceCount - Backend now queries family_members, children, and device_registry tables for counts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -57,8 +57,10 @@ async function bootstrap() {
|
|||||||
'http://localhost:3001', // Next.js dev (legacy)
|
'http://localhost:3001', // Next.js dev (legacy)
|
||||||
'http://localhost:3030', // Next.js dev (current)
|
'http://localhost:3030', // Next.js dev (current)
|
||||||
'http://localhost:3005', // Next.js dev (port 3005)
|
'http://localhost:3005', // Next.js dev (port 3005)
|
||||||
|
'http://localhost:3335', // Admin dashboard dev
|
||||||
'https://maternal.noru1.ro', // Production frontend (legacy)
|
'https://maternal.noru1.ro', // Production frontend (legacy)
|
||||||
'https://maternal-api.noru1.ro', // Production API (legacy)
|
'https://maternal-api.noru1.ro', // Production API (legacy)
|
||||||
|
'https://pfadmin.noru1.ro', // Production admin dashboard
|
||||||
'https://web.parentflowapp.com', // Production frontend
|
'https://web.parentflowapp.com', // Production frontend
|
||||||
'https://api.parentflowapp.com', // Production API (for GraphQL playground)
|
'https://api.parentflowapp.com', // Production API (for GraphQL playground)
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
PaginatedUsersResponseDto,
|
PaginatedUsersResponseDto,
|
||||||
} from './user-management.dto';
|
} from './user-management.dto';
|
||||||
|
|
||||||
@Controller('admin/users')
|
@Controller('api/v1/admin/users')
|
||||||
@UseGuards(AdminGuard)
|
@UseGuards(AdminGuard)
|
||||||
export class UserManagementController {
|
export class UserManagementController {
|
||||||
constructor(private readonly userManagementService: UserManagementService) {}
|
constructor(private readonly userManagementService: UserManagementService) {}
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ export class UserResponseDto {
|
|||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
familyCount?: number;
|
||||||
|
childrenCount?: number;
|
||||||
|
deviceCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PaginatedUsersResponseDto {
|
export class PaginatedUsersResponseDto {
|
||||||
|
|||||||
@@ -54,8 +54,12 @@ export class UserManagementService {
|
|||||||
.take(limit)
|
.take(limit)
|
||||||
.getManyAndCount();
|
.getManyAndCount();
|
||||||
|
|
||||||
|
// Fetch counts for all users in a single query
|
||||||
|
const userIds = users.map((u) => u.id);
|
||||||
|
const counts = await this.getUserCounts(userIds);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
users: users.map((user) => this.toResponseDto(user)),
|
users: users.map((user) => this.toResponseDto(user, counts[user.id])),
|
||||||
total,
|
total,
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
@@ -63,6 +67,67 @@ export class UserManagementService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getUserCounts(userIds: string[]): Promise<
|
||||||
|
Record<
|
||||||
|
string,
|
||||||
|
{ familyCount: number; childrenCount: number; deviceCount: number }
|
||||||
|
>
|
||||||
|
> {
|
||||||
|
if (userIds.length === 0) return {};
|
||||||
|
|
||||||
|
// Query family memberships
|
||||||
|
const familyCountsRaw = await this.userRepository.query(
|
||||||
|
`SELECT user_id, COUNT(DISTINCT family_id) as count
|
||||||
|
FROM family_members
|
||||||
|
WHERE user_id = ANY($1)
|
||||||
|
GROUP BY user_id`,
|
||||||
|
[userIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query children (through families they belong to)
|
||||||
|
const childrenCountsRaw = await this.userRepository.query(
|
||||||
|
`SELECT fm.user_id, COUNT(DISTINCT c.id) as count
|
||||||
|
FROM family_members fm
|
||||||
|
JOIN children c ON c.family_id = fm.family_id
|
||||||
|
WHERE fm.user_id = ANY($1)
|
||||||
|
GROUP BY fm.user_id`,
|
||||||
|
[userIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query devices
|
||||||
|
const deviceCountsRaw = await this.userRepository.query(
|
||||||
|
`SELECT user_id, COUNT(*) as count
|
||||||
|
FROM device_registry
|
||||||
|
WHERE user_id = ANY($1)
|
||||||
|
GROUP BY user_id`,
|
||||||
|
[userIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build counts map
|
||||||
|
const counts: Record<
|
||||||
|
string,
|
||||||
|
{ familyCount: number; childrenCount: number; deviceCount: number }
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
userIds.forEach((userId) => {
|
||||||
|
counts[userId] = { familyCount: 0, childrenCount: 0, deviceCount: 0 };
|
||||||
|
});
|
||||||
|
|
||||||
|
familyCountsRaw.forEach((row: any) => {
|
||||||
|
counts[row.user_id].familyCount = parseInt(row.count, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
childrenCountsRaw.forEach((row: any) => {
|
||||||
|
counts[row.user_id].childrenCount = parseInt(row.count, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
deviceCountsRaw.forEach((row: any) => {
|
||||||
|
counts[row.user_id].deviceCount = parseInt(row.count, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
async getUserById(userId: string): Promise<UserResponseDto> {
|
async getUserById(userId: string): Promise<UserResponseDto> {
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
@@ -72,7 +137,8 @@ export class UserManagementService {
|
|||||||
throw new NotFoundException(`User with ID ${userId} not found`);
|
throw new NotFoundException(`User with ID ${userId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.toResponseDto(user);
|
const counts = await this.getUserCounts([userId]);
|
||||||
|
return this.toResponseDto(user, counts[userId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUser(dto: CreateUserDto): Promise<UserResponseDto> {
|
async createUser(dto: CreateUserDto): Promise<UserResponseDto> {
|
||||||
@@ -158,7 +224,10 @@ export class UserManagementService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private toResponseDto(user: User): UserResponseDto {
|
private toResponseDto(
|
||||||
|
user: User,
|
||||||
|
counts?: { familyCount: number; childrenCount: number; deviceCount: number },
|
||||||
|
): UserResponseDto {
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
@@ -171,6 +240,9 @@ export class UserManagementService {
|
|||||||
emailVerified: user.emailVerified,
|
emailVerified: user.emailVerified,
|
||||||
createdAt: user.createdAt,
|
createdAt: user.createdAt,
|
||||||
updatedAt: user.updatedAt,
|
updatedAt: user.updatedAt,
|
||||||
|
familyCount: counts?.familyCount,
|
||||||
|
childrenCount: counts?.childrenCount,
|
||||||
|
deviceCount: counts?.deviceCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,12 +44,19 @@ interface User {
|
|||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
phone?: string;
|
||||||
|
photoUrl?: string;
|
||||||
|
globalRole: 'parent' | 'guest' | 'admin';
|
||||||
|
isAdmin: boolean;
|
||||||
|
adminPermissions: string[];
|
||||||
|
emailVerified: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
lastActiveAt: string;
|
updatedAt: string;
|
||||||
isActive: boolean;
|
lastActiveAt?: string;
|
||||||
familyCount: number;
|
isActive?: boolean;
|
||||||
childrenCount: number;
|
familyCount?: number;
|
||||||
deviceCount: number;
|
childrenCount?: number;
|
||||||
|
deviceCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UsersPage() {
|
export default function UsersPage() {
|
||||||
@@ -70,45 +77,16 @@ export default function UsersPage() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await apiClient.get('/admin/users');
|
const response = await apiClient.get('/admin/users');
|
||||||
setUsers(response.data);
|
// Backend returns paginated response: { users: [], total, page, limit, totalPages }
|
||||||
} catch (error) {
|
setUsers(response.users || []);
|
||||||
|
} catch (error: any) {
|
||||||
console.error('Failed to fetch users:', error);
|
console.error('Failed to fetch users:', error);
|
||||||
// Using mock data for development
|
// If unauthorized, redirect to login
|
||||||
setUsers([
|
if (error?.response?.status === 401) {
|
||||||
{
|
window.location.href = '/login';
|
||||||
id: '1',
|
return;
|
||||||
email: 'john.doe@example.com',
|
}
|
||||||
name: 'John Doe',
|
setUsers([]);
|
||||||
createdAt: '2024-01-15T10:00:00Z',
|
|
||||||
lastActiveAt: '2024-10-06T08:30:00Z',
|
|
||||||
isActive: true,
|
|
||||||
familyCount: 1,
|
|
||||||
childrenCount: 2,
|
|
||||||
deviceCount: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
email: 'jane.smith@example.com',
|
|
||||||
name: 'Jane Smith',
|
|
||||||
createdAt: '2024-02-20T14:30:00Z',
|
|
||||||
lastActiveAt: '2024-10-05T18:45:00Z',
|
|
||||||
isActive: true,
|
|
||||||
familyCount: 1,
|
|
||||||
childrenCount: 1,
|
|
||||||
deviceCount: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
email: 'bob.johnson@example.com',
|
|
||||||
name: 'Bob Johnson',
|
|
||||||
createdAt: '2024-03-10T09:15:00Z',
|
|
||||||
lastActiveAt: '2024-09-30T12:00:00Z',
|
|
||||||
isActive: false,
|
|
||||||
familyCount: 1,
|
|
||||||
childrenCount: 3,
|
|
||||||
deviceCount: 1,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -127,7 +105,7 @@ export default function UsersPage() {
|
|||||||
const handleToggleUserStatus = async (user: User) => {
|
const handleToggleUserStatus = async (user: User) => {
|
||||||
try {
|
try {
|
||||||
await apiClient.patch(`/admin/users/${user.id}`, {
|
await apiClient.patch(`/admin/users/${user.id}`, {
|
||||||
isActive: !user.isActive,
|
emailVerified: !user.emailVerified,
|
||||||
});
|
});
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -174,56 +152,48 @@ export default function UsersPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: 3, mb: 4 }}>
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
<Card>
|
||||||
<Card>
|
<CardContent>
|
||||||
<CardContent>
|
<Typography color="text.secondary" gutterBottom>
|
||||||
<Typography color="text.secondary" gutterBottom>
|
Total Users
|
||||||
Total Users
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="h3" sx={{ color: 'primary.main' }}>
|
||||||
<Typography variant="h3" sx={{ color: 'primary.main' }}>
|
{users.length}
|
||||||
{users.length}
|
</Typography>
|
||||||
</Typography>
|
</CardContent>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
<Card>
|
||||||
</Grid>
|
<CardContent>
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
<Typography color="text.secondary" gutterBottom>
|
||||||
<Card>
|
Active Users
|
||||||
<CardContent>
|
</Typography>
|
||||||
<Typography color="text.secondary" gutterBottom>
|
<Typography variant="h3" sx={{ color: 'success.main' }}>
|
||||||
Active Users
|
{users.filter(u => u.emailVerified).length}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h3" sx={{ color: 'success.main' }}>
|
</CardContent>
|
||||||
{users.filter(u => u.isActive).length}
|
</Card>
|
||||||
</Typography>
|
<Card>
|
||||||
</CardContent>
|
<CardContent>
|
||||||
</Card>
|
<Typography color="text.secondary" gutterBottom>
|
||||||
</Grid>
|
Total Families
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
</Typography>
|
||||||
<Card>
|
<Typography variant="h3" sx={{ color: 'info.main' }}>
|
||||||
<CardContent>
|
{users.reduce((sum, u) => sum + (u.familyCount || 0), 0)}
|
||||||
<Typography color="text.secondary" gutterBottom>
|
</Typography>
|
||||||
Total Families
|
</CardContent>
|
||||||
</Typography>
|
</Card>
|
||||||
<Typography variant="h3" sx={{ color: 'info.main' }}>
|
<Card>
|
||||||
{users.reduce((sum, u) => sum + u.familyCount, 0)}
|
<CardContent>
|
||||||
</Typography>
|
<Typography color="text.secondary" gutterBottom>
|
||||||
</CardContent>
|
Total Children
|
||||||
</Card>
|
</Typography>
|
||||||
</Grid>
|
<Typography variant="h3" sx={{ color: 'secondary.main' }}>
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
{users.reduce((sum, u) => sum + (u.childrenCount || 0), 0)}
|
||||||
<Card>
|
</Typography>
|
||||||
<CardContent>
|
</CardContent>
|
||||||
<Typography color="text.secondary" gutterBottom>
|
</Card>
|
||||||
Total Children
|
</Box>
|
||||||
</Typography>
|
|
||||||
<Typography variant="h3" sx={{ color: 'secondary.main' }}>
|
|
||||||
{users.reduce((sum, u) => sum + u.childrenCount, 0)}
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Search and Actions */}
|
{/* Search and Actions */}
|
||||||
<Box sx={{ mb: 3, display: 'flex', gap: 2 }}>
|
<Box sx={{ mb: 3, display: 'flex', gap: 2 }}>
|
||||||
@@ -285,18 +255,18 @@ export default function UsersPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{formatDate(user.createdAt)}</TableCell>
|
<TableCell>{formatDate(user.createdAt)}</TableCell>
|
||||||
<TableCell>{formatDate(user.lastActiveAt)}</TableCell>
|
<TableCell>{user.lastActiveAt ? formatDate(user.lastActiveAt) : formatDate(user.updatedAt)}</TableCell>
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
<Chip
|
<Chip
|
||||||
label={user.isActive ? 'Active' : 'Inactive'}
|
label={user.emailVerified ? 'Active' : 'Inactive'}
|
||||||
color={user.isActive ? 'success' : 'default'}
|
color={user.emailVerified ? 'success' : 'default'}
|
||||||
size="small"
|
size="small"
|
||||||
icon={user.isActive ? <CheckCircle /> : <Block />}
|
icon={user.emailVerified ? <CheckCircle /> : <Block />}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center">{user.familyCount}</TableCell>
|
<TableCell align="center">{user.familyCount || 0}</TableCell>
|
||||||
<TableCell align="center">{user.childrenCount}</TableCell>
|
<TableCell align="center">{user.childrenCount || 0}</TableCell>
|
||||||
<TableCell align="center">{user.deviceCount}</TableCell>
|
<TableCell align="center">{user.deviceCount || 0}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
@@ -315,9 +285,9 @@ export default function UsersPage() {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleToggleUserStatus(user)}
|
onClick={() => handleToggleUserStatus(user)}
|
||||||
title={user.isActive ? 'Deactivate' : 'Activate'}
|
title={user.emailVerified ? 'Deactivate' : 'Activate'}
|
||||||
>
|
>
|
||||||
{user.isActive ? <Block /> : <CheckCircle />}
|
{user.emailVerified ? <Block /> : <CheckCircle />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
@@ -356,53 +326,51 @@ export default function UsersPage() {
|
|||||||
<DialogTitle>User Details</DialogTitle>
|
<DialogTitle>User Details</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
{selectedUser && (
|
{selectedUser && (
|
||||||
<Box sx={{ pt: 2 }}>
|
<Box sx={{ pt: 2, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 2 }}>
|
||||||
<Grid container spacing={2}>
|
<Box>
|
||||||
<Grid item xs={6}>
|
<Typography variant="subtitle2" color="text.secondary">
|
||||||
<Typography variant="subtitle2" color="text.secondary">
|
Name
|
||||||
Name
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="body1">{selectedUser.name}</Typography>
|
||||||
<Typography variant="body1">{selectedUser.name}</Typography>
|
</Box>
|
||||||
</Grid>
|
<Box>
|
||||||
<Grid item xs={6}>
|
<Typography variant="subtitle2" color="text.secondary">
|
||||||
<Typography variant="subtitle2" color="text.secondary">
|
Email
|
||||||
Email
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="body1">{selectedUser.email}</Typography>
|
||||||
<Typography variant="body1">{selectedUser.email}</Typography>
|
</Box>
|
||||||
</Grid>
|
<Box>
|
||||||
<Grid item xs={6}>
|
<Typography variant="subtitle2" color="text.secondary">
|
||||||
<Typography variant="subtitle2" color="text.secondary">
|
User ID
|
||||||
User ID
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="body1">{selectedUser.id}</Typography>
|
||||||
<Typography variant="body1">{selectedUser.id}</Typography>
|
</Box>
|
||||||
</Grid>
|
<Box>
|
||||||
<Grid item xs={6}>
|
<Typography variant="subtitle2" color="text.secondary">
|
||||||
<Typography variant="subtitle2" color="text.secondary">
|
Status
|
||||||
Status
|
</Typography>
|
||||||
</Typography>
|
<Chip
|
||||||
<Chip
|
label={selectedUser.emailVerified ? 'Active' : 'Inactive'}
|
||||||
label={selectedUser.isActive ? 'Active' : 'Inactive'}
|
color={selectedUser.emailVerified ? 'success' : 'default'}
|
||||||
color={selectedUser.isActive ? 'success' : 'default'}
|
size="small"
|
||||||
size="small"
|
/>
|
||||||
/>
|
</Box>
|
||||||
</Grid>
|
<Box>
|
||||||
<Grid item xs={6}>
|
<Typography variant="subtitle2" color="text.secondary">
|
||||||
<Typography variant="subtitle2" color="text.secondary">
|
Created At
|
||||||
Created At
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="body1">
|
||||||
<Typography variant="body1">
|
{formatDate(selectedUser.createdAt)}
|
||||||
{formatDate(selectedUser.createdAt)}
|
</Typography>
|
||||||
</Typography>
|
</Box>
|
||||||
</Grid>
|
<Box>
|
||||||
<Grid item xs={6}>
|
<Typography variant="subtitle2" color="text.secondary">
|
||||||
<Typography variant="subtitle2" color="text.secondary">
|
Last Active
|
||||||
Last Active
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="body1">
|
||||||
<Typography variant="body1">
|
{selectedUser.lastActiveAt ? formatDate(selectedUser.lastActiveAt) : formatDate(selectedUser.updatedAt)}
|
||||||
{formatDate(selectedUser.lastActiveAt)}
|
</Typography>
|
||||||
</Typography>
|
</Box>
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -434,9 +402,9 @@ export default function UsersPage() {
|
|||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch defaultChecked={selectedUser.isActive} />
|
<Switch defaultChecked={selectedUser.emailVerified} />
|
||||||
}
|
}
|
||||||
label="Active"
|
label="Email Verified"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class ApiClient {
|
|||||||
private async request(method: string, endpoint: string, data?: any, options?: any) {
|
private async request(method: string, endpoint: string, data?: any, options?: any) {
|
||||||
const config = {
|
const config = {
|
||||||
method,
|
method,
|
||||||
url: `${API_BASE_URL}/api/v1${endpoint}`,
|
url: `${API_BASE_URL}${endpoint}`,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
||||||
@@ -55,7 +55,7 @@ class ApiClient {
|
|||||||
// Handle token refresh
|
// Handle token refresh
|
||||||
if (error.response?.status === 401 && this.refreshToken) {
|
if (error.response?.status === 401 && this.refreshToken) {
|
||||||
try {
|
try {
|
||||||
const refreshResponse = await axios.post(`${API_BASE_URL}/api/v1/auth/refresh`, {
|
const refreshResponse = await axios.post(`${API_BASE_URL}/auth/refresh`, {
|
||||||
refreshToken: this.refreshToken,
|
refreshToken: this.refreshToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,23 +75,73 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generic HTTP methods
|
||||||
|
async get(endpoint: string, options?: any) {
|
||||||
|
return this.request('GET', endpoint, undefined, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async post(endpoint: string, data?: any, options?: any) {
|
||||||
|
return this.request('POST', endpoint, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async patch(endpoint: string, data?: any, options?: any) {
|
||||||
|
return this.request('PATCH', endpoint, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async put(endpoint: string, data?: any, options?: any) {
|
||||||
|
return this.request('PUT', endpoint, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(endpoint: string, options?: any) {
|
||||||
|
return this.request('DELETE', endpoint, undefined, options);
|
||||||
|
}
|
||||||
|
|
||||||
// Auth endpoints
|
// Auth endpoints
|
||||||
async login(email: string, password: string) {
|
async login(email: string, password: string) {
|
||||||
const response = await this.request('POST', '/admin/auth/login', { email, password });
|
// Generate device info for admin dashboard
|
||||||
this.setTokens(response.accessToken, response.refreshToken);
|
const deviceInfo = {
|
||||||
|
deviceId: this.getOrCreateDeviceId(),
|
||||||
|
platform: 'web',
|
||||||
|
model: 'Admin Dashboard',
|
||||||
|
osVersion: navigator.userAgent,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.request('POST', '/auth/login', {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
deviceInfo
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract tokens from nested response structure
|
||||||
|
const tokens = response.tokens || response.data?.tokens;
|
||||||
|
if (tokens?.accessToken && tokens?.refreshToken) {
|
||||||
|
this.setTokens(tokens.accessToken, tokens.refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getOrCreateDeviceId(): string {
|
||||||
|
if (typeof window === 'undefined') return 'server';
|
||||||
|
|
||||||
|
let deviceId = localStorage.getItem('admin_device_id');
|
||||||
|
if (!deviceId) {
|
||||||
|
deviceId = 'admin_' + Math.random().toString(36).substring(2) + Date.now().toString(36);
|
||||||
|
localStorage.setItem('admin_device_id', deviceId);
|
||||||
|
}
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
try {
|
try {
|
||||||
await this.request('POST', '/admin/auth/logout');
|
await this.request('POST', '/auth/logout');
|
||||||
} finally {
|
} finally {
|
||||||
this.clearTokens();
|
this.clearTokens();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCurrentAdmin() {
|
async getCurrentAdmin() {
|
||||||
return this.request('GET', '/admin/auth/me');
|
return this.request('GET', '/auth/me');
|
||||||
}
|
}
|
||||||
|
|
||||||
// User management endpoints
|
// User management endpoints
|
||||||
|
|||||||
Reference in New Issue
Block a user