- 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>
233 lines
6.7 KiB
TypeScript
233 lines
6.7 KiB
TypeScript
import axios from 'axios';
|
|
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020/api/v1';
|
|
|
|
class ApiClient {
|
|
private token: string | null = null;
|
|
private refreshToken: string | null = null;
|
|
|
|
constructor() {
|
|
// Initialize tokens from localStorage if available
|
|
if (typeof window !== 'undefined') {
|
|
this.token = localStorage.getItem('admin_access_token');
|
|
this.refreshToken = localStorage.getItem('admin_refresh_token');
|
|
}
|
|
}
|
|
|
|
setTokens(accessToken: string, refreshToken: string) {
|
|
this.token = accessToken;
|
|
this.refreshToken = refreshToken;
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.setItem('admin_access_token', accessToken);
|
|
localStorage.setItem('admin_refresh_token', refreshToken);
|
|
}
|
|
}
|
|
|
|
clearTokens() {
|
|
this.token = null;
|
|
this.refreshToken = null;
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.removeItem('admin_access_token');
|
|
localStorage.removeItem('admin_refresh_token');
|
|
}
|
|
}
|
|
|
|
private async request(method: string, endpoint: string, data?: any, options?: any) {
|
|
const config = {
|
|
method,
|
|
url: `${API_BASE_URL}${endpoint}`,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
|
...options?.headers,
|
|
},
|
|
...options,
|
|
};
|
|
|
|
if (data) {
|
|
config.data = data;
|
|
}
|
|
|
|
try {
|
|
const response = await axios(config);
|
|
return response.data;
|
|
} catch (error: any) {
|
|
// Handle token refresh
|
|
if (error.response?.status === 401 && this.refreshToken) {
|
|
try {
|
|
const refreshResponse = await axios.post(`${API_BASE_URL}/auth/refresh`, {
|
|
refreshToken: this.refreshToken,
|
|
});
|
|
|
|
this.setTokens(refreshResponse.data.accessToken, refreshResponse.data.refreshToken);
|
|
|
|
// Retry original request
|
|
config.headers.Authorization = `Bearer ${this.token}`;
|
|
const response = await axios(config);
|
|
return response.data;
|
|
} catch (refreshError) {
|
|
this.clearTokens();
|
|
window.location.href = '/login';
|
|
throw refreshError;
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 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
|
|
async login(email: string, password: string) {
|
|
// Generate device info for admin dashboard
|
|
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;
|
|
}
|
|
|
|
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() {
|
|
try {
|
|
await this.request('POST', '/auth/logout');
|
|
} finally {
|
|
this.clearTokens();
|
|
}
|
|
}
|
|
|
|
async getCurrentAdmin() {
|
|
return this.request('GET', '/auth/me');
|
|
}
|
|
|
|
// User management endpoints
|
|
async getUsers(params?: { page?: number; limit?: number; search?: string }) {
|
|
const queryString = params ? '?' + new URLSearchParams(params as any).toString() : '';
|
|
return this.request('GET', `/admin/users${queryString}`);
|
|
}
|
|
|
|
async getUserById(id: string) {
|
|
return this.request('GET', `/admin/users/${id}`);
|
|
}
|
|
|
|
async updateUser(id: string, data: any) {
|
|
return this.request('PATCH', `/admin/users/${id}`, data);
|
|
}
|
|
|
|
async deleteUser(id: string) {
|
|
return this.request('DELETE', `/admin/users/${id}`);
|
|
}
|
|
|
|
// Invite code endpoints
|
|
async getInviteCodes(params?: { page?: number; limit?: number; status?: string }) {
|
|
const queryString = params ? '?' + new URLSearchParams(params as any).toString() : '';
|
|
return this.request('GET', `/admin/invite-codes${queryString}`);
|
|
}
|
|
|
|
async createInviteCode(data: {
|
|
code: string;
|
|
maxUses?: number;
|
|
expiresAt?: string;
|
|
metadata?: any;
|
|
}) {
|
|
return this.request('POST', '/admin/invite-codes', data);
|
|
}
|
|
|
|
async updateInviteCode(id: string, data: any) {
|
|
return this.request('PATCH', `/admin/invite-codes/${id}`, data);
|
|
}
|
|
|
|
async deleteInviteCode(id: string) {
|
|
return this.request('DELETE', `/admin/invite-codes/${id}`);
|
|
}
|
|
|
|
// Analytics endpoints
|
|
async getAnalytics(params?: { startDate?: string; endDate?: string }) {
|
|
const queryString = params ? '?' + new URLSearchParams(params as any).toString() : '';
|
|
return this.request('GET', `/admin/analytics${queryString}`);
|
|
}
|
|
|
|
async getUserGrowth() {
|
|
return this.request('GET', '/admin/analytics/user-growth');
|
|
}
|
|
|
|
async getActivityStats() {
|
|
return this.request('GET', '/admin/analytics/activity-stats');
|
|
}
|
|
|
|
async getSystemHealth() {
|
|
return this.request('GET', '/admin/analytics/system-health');
|
|
}
|
|
|
|
// Family management
|
|
async getFamilies(params?: { page?: number; limit?: number; search?: string }) {
|
|
const queryString = params ? '?' + new URLSearchParams(params as any).toString() : '';
|
|
return this.request('GET', `/admin/families${queryString}`);
|
|
}
|
|
|
|
async getFamilyById(id: string) {
|
|
return this.request('GET', `/admin/families/${id}`);
|
|
}
|
|
|
|
// Activity logs
|
|
async getActivityLogs(params?: { page?: number; limit?: number; userId?: string }) {
|
|
const queryString = params ? '?' + new URLSearchParams(params as any).toString() : '';
|
|
return this.request('GET', `/admin/logs${queryString}`);
|
|
}
|
|
|
|
// System settings
|
|
async getSettings() {
|
|
return this.request('GET', '/admin/settings');
|
|
}
|
|
|
|
async updateSettings(data: any) {
|
|
return this.request('PATCH', '/admin/settings', data);
|
|
}
|
|
}
|
|
|
|
export const apiClient = new ApiClient();
|
|
export default apiClient; |