From 973e9b7ebe2eeee147a3da1c705b09ad1f083c73 Mon Sep 17 00:00:00 2001 From: Sergei Date: Fri, 9 Jan 2026 17:06:35 -0800 Subject: [PATCH] Fix Developer Mode WebView authentication - Add legacy dashboard API methods (eluxnetworks.net) - Implement JWT token validation before using cached credentials - Clear invalid tokens (non-JWT strings like "0") and force re-login - Use correct credentials (anandk/anandk_8) - Add 30-minute token refresh interval when WebView is active - Fix avatar upload using expo-file-system instead of FileReader - Handle address field as both string and object --- app/(tabs)/beneficiaries/[id]/index.tsx | 92 ++++++++++--- services/api.ts | 174 ++++++++++++++++++++++-- 2 files changed, 232 insertions(+), 34 deletions(-) diff --git a/app/(tabs)/beneficiaries/[id]/index.tsx b/app/(tabs)/beneficiaries/[id]/index.tsx index d9ebc00..cc48757 100644 --- a/app/(tabs)/beneficiaries/[id]/index.tsx +++ b/app/(tabs)/beneficiaries/[id]/index.tsx @@ -63,7 +63,12 @@ export default function BeneficiaryDetailScreen() { const [error, setError] = useState(null); const [showWebView, setShowWebView] = useState(false); const [isWebViewReady, setIsWebViewReady] = useState(false); - const [authToken, setAuthToken] = useState(null); + const [legacyCredentials, setLegacyCredentials] = useState<{ + token: string; + userName: string; + userId: string; + } | null>(null); + const [isRefreshingToken, setIsRefreshingToken] = useState(false); // Edit modal state const [isEditModalVisible, setIsEditModalVisible] = useState(false); @@ -71,21 +76,67 @@ export default function BeneficiaryDetailScreen() { const webViewRef = useRef(null); - // Load legacy token for WebView dashboard - useEffect(() => { - const loadLegacyToken = async () => { - try { - const token = await api.getLegacyToken(); - setAuthToken(token); - setIsWebViewReady(true); - } catch (err) { - console.log('[BeneficiaryDetail] Legacy token not available'); - setIsWebViewReady(true); + // Load legacy credentials for WebView dashboard + const loadLegacyCredentials = useCallback(async () => { + try { + // Check if token is expiring soon + const isExpiring = await api.isLegacyTokenExpiringSoon(); + if (isExpiring) { + console.log('[DevMode] Legacy token expiring, refreshing...'); + await api.refreshLegacyToken(); } - }; - loadLegacyToken(); + + const credentials = await api.getLegacyWebViewCredentials(); + if (credentials) { + setLegacyCredentials(credentials); + console.log('[DevMode] Legacy credentials loaded:', credentials.userName); + } + setIsWebViewReady(true); + } catch (err) { + console.log('[DevMode] Failed to load legacy credentials:', err); + setIsWebViewReady(true); + } }, []); + useEffect(() => { + loadLegacyCredentials(); + + // Periodically refresh token (every 30 minutes) + const tokenCheckInterval = setInterval(async () => { + if (!showWebView) return; // Only refresh if WebView is active + + const isExpiring = await api.isLegacyTokenExpiringSoon(); + if (isExpiring && !isRefreshingToken) { + console.log('[DevMode] Periodic check: refreshing legacy token...'); + setIsRefreshingToken(true); + const result = await api.refreshLegacyToken(); + if (result.ok) { + const credentials = await api.getLegacyWebViewCredentials(); + if (credentials) { + setLegacyCredentials(credentials); + // Re-inject token into WebView + const injectScript = ` + (function() { + var authData = { + username: '${credentials.userName}', + token: '${credentials.token}', + user_id: ${credentials.userId} + }; + localStorage.setItem('auth2', JSON.stringify(authData)); + console.log('Token auto-refreshed'); + })(); + true; + `; + webViewRef.current?.injectJavaScript(injectScript); + } + } + setIsRefreshingToken(false); + } + }, 30 * 60 * 1000); // 30 minutes + + return () => clearInterval(tokenCheckInterval); + }, [loadLegacyCredentials, showWebView, isRefreshingToken]); + const loadBeneficiary = useCallback(async (showLoadingIndicator = true) => { if (!id) return; @@ -241,15 +292,18 @@ export default function BeneficiaryDetailScreen() { }; // JavaScript to inject token into localStorage for WebView - const injectedJavaScript = authToken + // Web app expects auth2 as JSON: {username, token, user_id} + const injectedJavaScript = legacyCredentials ? ` (function() { try { var authData = { - token: '${authToken}' + username: '${legacyCredentials.userName}', + token: '${legacyCredentials.token}', + user_id: ${legacyCredentials.userId} }; localStorage.setItem('auth2', JSON.stringify(authData)); - console.log('Auth data injected'); + console.log('Auth data injected:', authData.username); } catch(e) { console.error('Failed to inject token:', e); } @@ -343,10 +397,10 @@ export default function BeneficiaryDetailScreen() { {/* Content area - WebView or MockDashboard */} {showWebView ? ( - isWebViewReady && authToken ? ( + isWebViewReady && legacyCredentials ? ( - {isWebViewReady ? 'Loading dashboard...' : 'Connecting to sensors...'} + {isWebViewReady ? 'Authenticating...' : 'Connecting to sensors...'} ) diff --git a/services/api.ts b/services/api.ts index eb2c94d..121b2e1 100644 --- a/services/api.ts +++ b/services/api.ts @@ -1,5 +1,6 @@ import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashboardData, ChatResponse, DashboardSingleResponse, NotificationSettings } from '@/types'; import * as Crypto from 'expo-crypto'; +import * as FileSystem from 'expo-file-system'; import * as SecureStore from 'expo-secure-store'; // Callback for handling unauthorized responses (401) @@ -609,10 +610,10 @@ class ApiService { const beneficiaries: Beneficiary[] = (data.beneficiaries || []).map((item: any) => ({ id: item.id, name: item.name || item.email, - avatar: undefined, // No auto-generated avatars - only show if user uploaded one + avatar: item.avatarUrl || undefined, // Use uploaded avatar from server status: 'offline' as const, email: item.email, - address: item.address?.street ? `${item.address.street}, ${item.address.city}` : undefined, + address: typeof item.address === 'string' ? item.address : (item.address?.street ? `${item.address.street}, ${item.address.city}` : undefined), subscription: item.subscription, // Equipment status from orders equipmentStatus: item.equipmentStatus, @@ -660,10 +661,10 @@ class ApiService { const beneficiary: Beneficiary = { id: data.id, name: data.name || data.email, - avatar: undefined, // No auto-generated avatars - only show if user uploaded one + avatar: data.avatarUrl || undefined, status: 'offline' as const, email: data.email, - address: data.address?.street ? `${data.address.street}, ${data.address.city}` : undefined, + address: typeof data.address === 'string' ? data.address : (data.address?.street ? `${data.address.street}, ${data.address.city}` : undefined), subscription: data.subscription ? { status: data.subscription.status, plan: data.subscription.plan, @@ -775,21 +776,23 @@ class ApiService { let base64Image: string | null = null; if (imageUri) { - // Convert file URI to base64 - const response = await fetch(imageUri); - const blob = await response.blob(); - - // Convert blob to base64 - const base64 = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result as string); - reader.onerror = reject; - reader.readAsDataURL(blob); + // Read file as base64 string using expo-file-system + const base64Data = await FileSystem.readAsStringAsync(imageUri, { + encoding: FileSystem.EncodingType.Base64, }); - base64Image = base64; + // Determine mime type from URI extension + const extension = imageUri.split('.').pop()?.toLowerCase() || 'jpeg'; + const mimeType = extension === 'png' ? 'image/png' : 'image/jpeg'; + + // Create data URI + base64Image = `data:${mimeType};base64,${base64Data}`; + + console.log('[API] Avatar converted to base64, length:', base64Image.length); } + console.log('[API] Uploading avatar for beneficiary:', id); + const apiResponse = await fetch(`${WELLNUO_API_URL}/me/beneficiaries/${id}/avatar`, { method: 'PATCH', headers: { @@ -801,6 +804,8 @@ class ApiService { const data = await apiResponse.json(); + console.log('[API] Avatar upload response:', apiResponse.status, data); + if (!apiResponse.ok) { return { ok: false, error: { message: data.error || 'Failed to update avatar' } }; } @@ -1284,6 +1289,145 @@ class ApiService { }; } } + + // ========================================== + // Legacy Dashboard Methods (Developer Mode) + // For eluxnetworks.net dashboard WebView + // ========================================== + + // Demo credentials for legacy dashboard + private readonly DEMO_LEGACY_USER = 'anandk'; + private readonly DEMO_LEGACY_PASSWORD = 'anandk_8'; + private readonly DEMO_DEPLOYMENT_ID = 21; // Ferdinand's deployment + + // Login to legacy dashboard API + async loginToLegacyDashboard(): Promise> { + try { + const formData = new URLSearchParams(); + formData.append('function', 'credentials'); + formData.append('user_name', this.DEMO_LEGACY_USER); + formData.append('ps', this.DEMO_LEGACY_PASSWORD); + formData.append('clientId', CLIENT_ID); + formData.append('nonce', this.generateNonce()); + + 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(); + console.log('[API] Legacy login response:', data.status, 'token type:', typeof data.access_token); + + // Check that access_token is a valid JWT string (not 0 or empty) + if (data.status === '200 OK' && data.access_token && typeof data.access_token === 'string' && data.access_token.includes('.')) { + // Save legacy credentials + await SecureStore.setItemAsync('legacyAccessToken', data.access_token); + await SecureStore.setItemAsync('legacyUserId', String(data.user_id)); + await SecureStore.setItemAsync('legacyUserName', this.DEMO_LEGACY_USER); + console.log('[API] Legacy credentials saved successfully'); + + return { data: data as AuthResponse, ok: true }; + } + + console.log('[API] Legacy login failed - invalid token:', data.access_token); + return { + ok: false, + error: { message: data.message || 'Legacy login failed - invalid credentials' }, + }; + } catch (error) { + console.error('[API] Legacy login error:', error); + return { + ok: false, + error: { message: 'Failed to connect to dashboard API' }, + }; + } + } + + // Refresh legacy token + async refreshLegacyToken(): Promise> { + console.log('[API] Refreshing legacy token...'); + return this.loginToLegacyDashboard(); + } + + // Check if legacy token is expiring soon (within 1 hour) + async isLegacyTokenExpiringSoon(): Promise { + try { + const token = await this.getLegacyToken(); + 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; + + const isExpiring = (exp - now) < oneHour; + console.log('[API] Legacy token expiring soon:', isExpiring, 'expires in:', Math.round((exp - now) / 60), 'min'); + return isExpiring; + } catch (e) { + console.log('[API] Error checking legacy token:', e); + return true; + } + } + + // Get legacy credentials for WebView injection + async getLegacyWebViewCredentials(): Promise<{ + token: string; + userName: string; + userId: string; + } | null> { + try { + const token = await SecureStore.getItemAsync('legacyAccessToken'); + const userName = await SecureStore.getItemAsync('legacyUserName'); + const userId = await SecureStore.getItemAsync('legacyUserId'); + + // Check if credentials exist AND token is valid JWT (contains dots) + const isValidToken = token && typeof token === 'string' && token.includes('.'); + + if (!isValidToken || !userName || !userId) { + console.log('[API] Legacy credentials missing or invalid token, logging in...'); + console.log('[API] Token valid:', isValidToken, 'userName:', !!userName, 'userId:', !!userId); + + // Clear any invalid cached credentials + if (token && !isValidToken) { + console.log('[API] Clearing invalid cached token:', token); + await SecureStore.deleteItemAsync('legacyAccessToken'); + await SecureStore.deleteItemAsync('legacyUserName'); + await SecureStore.deleteItemAsync('legacyUserId'); + } + + const loginResult = await this.loginToLegacyDashboard(); + if (!loginResult.ok) return null; + + // Get freshly saved credentials + const newToken = await SecureStore.getItemAsync('legacyAccessToken'); + const newUserName = await SecureStore.getItemAsync('legacyUserName'); + const newUserId = await SecureStore.getItemAsync('legacyUserId'); + + if (!newToken || !newUserName || !newUserId) return null; + return { token: newToken, userName: newUserName, userId: newUserId }; + } + + console.log('[API] Legacy credentials found:', userName, 'token length:', token.length); + return { token, userName, userId }; + } catch (e) { + console.error('[API] Error getting legacy credentials:', e); + return null; + } + } + + // Get demo deployment ID + getDemoDeploymentId(): number { + return this.DEMO_DEPLOYMENT_ID; + } } export const api = new ApiService();