- Add refreshToken() method to automatically refresh expired tokens - Add isTokenExpiringSoon() to check if token expires within 1 hour - Store password in SecureStore for auto-refresh capability - Add periodic token check every 30 minutes in WebView - Intercept WebView navigation to login pages and refresh instead - Re-inject fresh token into WebView localStorage after refresh - Add metro.config.js to fix Metro bundler path resolution This prevents the app from showing web login page when session expires.
299 lines
8.7 KiB
TypeScript
299 lines
8.7 KiB
TypeScript
import * as SecureStore from 'expo-secure-store';
|
|
import type { AuthResponse, ChatResponse, Beneficiary, ApiResponse, ApiError } from '@/types';
|
|
|
|
const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api';
|
|
const CLIENT_ID = 'MA_001';
|
|
|
|
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 {
|
|
return Math.floor(Math.random() * 1000000).toString();
|
|
}
|
|
|
|
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();
|
|
|
|
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 (including password for auto-refresh)
|
|
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('userPassword', password); // Store for token refresh
|
|
await SecureStore.setItemAsync('privileges', response.data.privileges);
|
|
await SecureStore.setItemAsync('maxRole', response.data.max_role.toString());
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
// Refresh token using stored credentials
|
|
async refreshToken(): Promise<ApiResponse<AuthResponse>> {
|
|
try {
|
|
const userName = await SecureStore.getItemAsync('userName');
|
|
const password = await SecureStore.getItemAsync('userPassword');
|
|
|
|
if (!userName || !password) {
|
|
return { ok: false, error: { message: 'No stored credentials', code: 'NO_CREDENTIALS' } };
|
|
}
|
|
|
|
console.log('Refreshing token for user:', userName);
|
|
return await this.login(userName, password);
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
error: { message: 'Failed to refresh token', code: 'REFRESH_ERROR' }
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check if token is about to expire (within 1 hour)
|
|
async isTokenExpiringSoon(): Promise<boolean> {
|
|
try {
|
|
const token = await this.getToken();
|
|
if (!token) return true;
|
|
|
|
// Decode JWT to get expiration
|
|
const parts = token.split('.');
|
|
if (parts.length !== 3) return true;
|
|
|
|
const payload = JSON.parse(atob(parts[1]));
|
|
const exp = payload.exp;
|
|
if (!exp) return true;
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const oneHour = 60 * 60;
|
|
|
|
return (exp - now) < oneHour;
|
|
} catch {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Get all credentials for WebView injection
|
|
async getWebViewCredentials(): Promise<{
|
|
token: string;
|
|
userName: string;
|
|
userId: string;
|
|
} | null> {
|
|
try {
|
|
const token = await SecureStore.getItemAsync('accessToken');
|
|
const userName = await SecureStore.getItemAsync('userName');
|
|
const userId = await SecureStore.getItemAsync('userId');
|
|
|
|
if (!token || !userName || !userId) return null;
|
|
|
|
return { token, userName, userId };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async logout(): Promise<void> {
|
|
await SecureStore.deleteItemAsync('accessToken');
|
|
await SecureStore.deleteItemAsync('userId');
|
|
await SecureStore.deleteItemAsync('userName');
|
|
await SecureStore.deleteItemAsync('userPassword');
|
|
await SecureStore.deleteItemAsync('privileges');
|
|
await SecureStore.deleteItemAsync('maxRole');
|
|
}
|
|
|
|
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');
|
|
|
|
if (!userId || !userName) return null;
|
|
|
|
return {
|
|
user_id: parseInt(userId, 10),
|
|
user_name: userName,
|
|
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>> {
|
|
const response = await this.getBeneficiaries();
|
|
if (!response.ok || !response.data) {
|
|
return { ok: false, error: response.error };
|
|
}
|
|
|
|
const beneficiary = response.data.beneficiaries.find((b) => b.id === id);
|
|
if (!beneficiary) {
|
|
return { ok: false, error: { message: 'Beneficiary not found', code: 'NOT_FOUND' } };
|
|
}
|
|
|
|
return { data: beneficiary, ok: true };
|
|
}
|
|
|
|
// Get all beneficiaries using deployments_list API (real data)
|
|
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' } };
|
|
}
|
|
|
|
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(),
|
|
status: 'offline' as const,
|
|
email: item.email,
|
|
}));
|
|
|
|
return { data: beneficiaries, ok: true };
|
|
}
|
|
|
|
// AI Chat
|
|
async sendMessage(question: string, deploymentId: string = '21'): Promise<ApiResponse<ChatResponse>> {
|
|
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();
|