WellNuo/services/api.ts

500 lines
16 KiB
TypeScript

import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashboardData, ChatResponse, DashboardSingleResponse } from '@/types';
import * as Crypto from 'expo-crypto';
import * as SecureStore from 'expo-secure-store';
// Callback for handling unauthorized responses (401)
let onUnauthorizedCallback: (() => void) | null = null;
export function setOnUnauthorizedCallback(callback: () => void) {
onUnauthorizedCallback = callback;
}
const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api';
const CLIENT_ID = 'MA_001';
// WellNuo Backend API (our own API for auth, OTP, etc.)
// TODO: Update to production URL when deployed
const WELLNUO_API_URL = 'https://wellnuo.smartlaunchhub.com/api';
// Avatar images for elderly beneficiaries - grandmothers (бабушки)
const ELDERLY_AVATARS = [
'https://images.unsplash.com/photo-1566616213894-2d4e1baee5d8?w=200&h=200&fit=crop&crop=face', // grandmother with gray hair
'https://images.unsplash.com/photo-1544027993-37dbfe43562a?w=200&h=200&fit=crop&crop=face', // elderly woman smiling
'https://images.unsplash.com/photo-1491308056676-205b7c9a7dc1?w=200&h=200&fit=crop&crop=face', // senior woman portrait
'https://images.unsplash.com/photo-1580489944761-15a19d654956?w=200&h=200&fit=crop&crop=face', // older woman glasses
'https://images.unsplash.com/photo-1548142813-c348350df52b?w=200&h=200&fit=crop&crop=face', // grandmother portrait
];
// Get consistent avatar based on deployment_id
function getAvatarForBeneficiary(deploymentId: number): string {
const index = deploymentId % ELDERLY_AVATARS.length;
return ELDERLY_AVATARS[index];
}
// Helper function to format time ago
function formatTimeAgo(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins} min ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
}
class ApiService {
private async getToken(): Promise<string | null> {
try {
return await SecureStore.getItemAsync('accessToken');
} catch {
return null;
}
}
private async getUserName(): Promise<string | null> {
try {
return await SecureStore.getItemAsync('userName');
} catch {
return null;
}
}
private generateNonce(): string {
const randomBytes = Crypto.getRandomBytes(16);
return Array.from(randomBytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
private async makeRequest<T>(params: Record<string, string>): Promise<ApiResponse<T>> {
try {
const formData = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
formData.append(key, value);
});
const response = await fetch(API_BASE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.toString(),
});
const data = await response.json();
// Handle 401 Unauthorized - trigger logout
if (response.status === 401 || data.status === '401' || data.error === 'Unauthorized') {
if (onUnauthorizedCallback) {
onUnauthorizedCallback();
}
return {
ok: false,
error: {
message: 'Session expired. Please login again.',
code: 'UNAUTHORIZED',
status: 401,
},
};
}
if (data.status === '200 OK' || data.ok === true) {
return { data: data as T, ok: true };
}
return {
ok: false,
error: {
message: data.message || data.error || 'Request failed',
status: response.status,
},
};
} catch (error) {
const apiError: ApiError = {
message: error instanceof Error ? error.message : 'Network error',
code: 'NETWORK_ERROR',
};
return { ok: false, error: apiError };
}
}
// Authentication
async login(username: string, password: string): Promise<ApiResponse<AuthResponse>> {
const response = await this.makeRequest<AuthResponse>({
function: 'credentials',
user_name: username,
ps: password,
clientId: CLIENT_ID,
nonce: this.generateNonce(),
});
if (response.ok && response.data) {
// Save credentials to SecureStore
await SecureStore.setItemAsync('accessToken', response.data.access_token);
await SecureStore.setItemAsync('userId', response.data.user_id.toString());
await SecureStore.setItemAsync('userName', username);
await SecureStore.setItemAsync('privileges', response.data.privileges);
await SecureStore.setItemAsync('maxRole', response.data.max_role.toString());
}
return response;
}
async logout(): Promise<void> {
await SecureStore.deleteItemAsync('accessToken');
await SecureStore.deleteItemAsync('userId');
await SecureStore.deleteItemAsync('userName');
await SecureStore.deleteItemAsync('privileges');
await SecureStore.deleteItemAsync('maxRole');
await SecureStore.deleteItemAsync('userEmail');
await SecureStore.deleteItemAsync('onboardingCompleted');
}
// Save user email (for OTP auth flow)
async saveEmail(email: string): Promise<void> {
await SecureStore.setItemAsync('userEmail', email);
}
// Get stored email
async getStoredEmail(): Promise<string | null> {
try {
return await SecureStore.getItemAsync('userEmail');
} catch {
return null;
}
}
// Onboarding completion flag - persists across app restarts
async setOnboardingCompleted(completed: boolean): Promise<void> {
await SecureStore.setItemAsync('onboardingCompleted', completed ? '1' : '0');
}
async isOnboardingCompleted(): Promise<boolean> {
try {
const value = await SecureStore.getItemAsync('onboardingCompleted');
return value === '1';
} catch {
return false;
}
}
// Save mock user (for dev mode OTP flow)
async saveMockUser(user: { user_id: string; user_name: string; email: string; max_role: string; privileges: string[] }): Promise<void> {
await SecureStore.setItemAsync('accessToken', `mock-token-${user.user_id}`);
await SecureStore.setItemAsync('userId', user.user_id);
await SecureStore.setItemAsync('userName', user.user_name);
await SecureStore.setItemAsync('privileges', user.privileges.join(','));
await SecureStore.setItemAsync('maxRole', user.max_role);
await SecureStore.setItemAsync('userEmail', user.email);
}
// ==================== OTP Authentication (WellNuo Backend) ====================
// Check if email exists in database
async checkEmail(email: string): Promise<ApiResponse<{ exists: boolean; name?: string | null }>> {
try {
const response = await fetch(`${WELLNUO_API_URL}/auth/check-email`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
const data = await response.json();
if (response.ok) {
return { data, ok: true };
}
return {
ok: false,
error: { message: data.error || 'Failed to check email' },
};
} catch (error) {
return {
ok: false,
error: { message: 'Network error. Please check your connection.' },
};
}
}
// Request OTP code - sends email via Brevo
async requestOTP(email: string): Promise<ApiResponse<{ message: string }>> {
try {
const response = await fetch(`${WELLNUO_API_URL}/auth/request-otp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
const data = await response.json();
if (response.ok) {
return { data, ok: true };
}
return {
ok: false,
error: { message: data.error || 'Failed to send OTP' },
};
} catch (error) {
return {
ok: false,
error: { message: 'Network error. Please check your connection.' },
};
}
}
// Verify OTP code and get JWT token
async verifyOTP(email: string, code: string): Promise<ApiResponse<{ token: string; user: { id: string; email: string; first_name?: string; last_name?: string } }>> {
try {
const payload = { email: email.trim().toLowerCase(), code };
console.log('[API] verifyOTP request:', JSON.stringify(payload));
const response = await fetch(`${WELLNUO_API_URL}/auth/verify-otp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
const data = await response.json();
console.log('[API] verifyOTP response:', JSON.stringify(data));
if (response.ok && data.token) {
// Save auth data
await SecureStore.setItemAsync('accessToken', data.token);
// Ensure user_id is string to prevent type errors
await SecureStore.setItemAsync('userId', String(data.user.id));
await SecureStore.setItemAsync('userName', data.user.first_name || email.split('@')[0]);
await SecureStore.setItemAsync('userEmail', email);
await SecureStore.setItemAsync('maxRole', 'USER');
await SecureStore.setItemAsync('privileges', '');
return { data, ok: true };
}
return {
ok: false,
error: { message: data.error || data.message || 'Invalid or expired code' },
};
} catch (error) {
console.error('[API] verifyOTP network error:', error);
return {
ok: false,
error: { message: 'Network error. Please check your connection.' },
};
}
}
async isAuthenticated(): Promise<boolean> {
const token = await this.getToken();
return !!token;
}
// Get stored user info
async getStoredUser() {
try {
const userId = await SecureStore.getItemAsync('userId');
const userName = await SecureStore.getItemAsync('userName');
const privileges = await SecureStore.getItemAsync('privileges');
const maxRole = await SecureStore.getItemAsync('maxRole');
const email = await SecureStore.getItemAsync('userEmail');
if (!userId || !userName) return null;
return {
user_id: parseInt(userId, 10),
user_name: userName,
email: email || undefined,
privileges: privileges || '',
max_role: parseInt(maxRole || '0', 10),
};
} catch {
return null;
}
}
// Beneficiaries (elderly people being monitored)
async getBeneficiaries(): Promise<ApiResponse<{ beneficiaries: Beneficiary[] }>> {
const token = await this.getToken();
if (!token) {
return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } };
}
// Note: Using mock data since API structure is not fully documented
// Replace with actual API call when available
const mockBeneficiaries: Beneficiary[] = [
{
id: 1,
name: 'Julia Smith',
status: 'online',
relationship: 'Mother',
last_activity: '2 min ago',
sensor_data: {
motion_detected: true,
last_motion: '2 min ago',
door_status: 'closed',
temperature: 22,
humidity: 45,
},
},
{
id: 2,
name: 'Robert Johnson',
status: 'offline',
relationship: 'Father',
last_activity: '1 hour ago',
sensor_data: {
motion_detected: false,
last_motion: '1 hour ago',
door_status: 'closed',
temperature: 21,
humidity: 50,
},
},
];
return { data: { beneficiaries: mockBeneficiaries }, ok: true };
}
async getBeneficiary(id: number): Promise<ApiResponse<Beneficiary>> {
// Use real API data via getBeneficiaryDashboard
const response = await this.getBeneficiaryDashboard(id.toString());
if (!response.ok || !response.data) {
return { ok: false, error: response.error || { message: 'Beneficiary not found', code: 'NOT_FOUND' } };
}
const data = response.data;
// Determine if beneficiary is "online" based on last_detected_time
const lastDetected = data.last_detected_time ? new Date(data.last_detected_time) : null;
const isRecent = lastDetected && (Date.now() - lastDetected.getTime()) < 30 * 60 * 1000; // 30 min
const deploymentId = parseInt(data.deployment_id, 10);
const beneficiary: Beneficiary = {
id: deploymentId,
name: data.name,
avatar: getAvatarForBeneficiary(deploymentId),
status: isRecent ? 'online' : 'offline',
address: data.address,
timezone: data.time_zone,
wellness_score: data.wellness_score_percent,
wellness_descriptor: data.wellness_descriptor,
last_location: data.last_location,
temperature: data.temperature,
units: data.units,
sleep_hours: data.sleep_hours,
bedroom_temperature: data.bedroom_temperature,
before_last_location: data.before_last_location,
last_detected_time: data.last_detected_time,
last_activity: data.last_detected_time
? formatTimeAgo(new Date(data.last_detected_time))
: undefined,
};
return { data: beneficiary, ok: true };
}
// Get beneficiary dashboard data by deployment_id
async getBeneficiaryDashboard(deploymentId: string): Promise<ApiResponse<BeneficiaryDashboardData>> {
const token = await this.getToken();
const userName = await this.getUserName();
if (!token || !userName) {
return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } };
}
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
const response = await this.makeRequest<DashboardSingleResponse>({
function: 'dashboard_single',
user_name: userName,
token: token,
deployment_id: deploymentId,
date: today,
nonce: this.generateNonce(),
});
if (response.ok && response.data?.result_list?.[0]) {
return { data: response.data.result_list[0], ok: true };
}
return {
ok: false,
error: response.error || { message: 'Failed to get beneficiary data' },
};
}
// Get all beneficiaries using deployments_list API (single fast request)
async getAllBeneficiaries(): Promise<ApiResponse<Beneficiary[]>> {
const token = await this.getToken();
const userName = await this.getUserName();
if (!token || !userName) {
return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } };
}
// Use deployments_list API - single request for all beneficiaries
const response = await this.makeRequest<{
result_list: Array<{
deployment_id: number;
email: string;
first_name: string;
last_name: string;
}>
}>({
function: 'deployments_list',
user_name: userName,
token: token,
first: '0',
last: '100',
});
if (!response.ok || !response.data?.result_list) {
return { ok: false, error: response.error || { message: 'Failed to get beneficiaries' } };
}
const beneficiaries: Beneficiary[] = response.data.result_list.map(item => ({
id: item.deployment_id,
name: `${item.first_name} ${item.last_name}`.trim(),
avatar: getAvatarForBeneficiary(item.deployment_id),
status: 'offline' as const, // Will be updated when dashboard is loaded
email: item.email,
}));
return { data: beneficiaries, ok: true };
}
// AI Chat - deploymentId is required, no default value for security
async sendMessage(question: string, deploymentId: string): Promise<ApiResponse<ChatResponse>> {
if (!deploymentId) {
return { ok: false, error: { message: 'Please select a beneficiary first', code: 'NO_BENEFICIARY_SELECTED' } };
}
const token = await this.getToken();
const userName = await this.getUserName();
if (!token || !userName) {
return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } };
}
return this.makeRequest<ChatResponse>({
function: 'voice_ask',
clientId: CLIENT_ID,
user_name: userName,
token: token,
question: question,
deployment_id: deploymentId,
});
}
}
export const api = new ApiService();