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
This commit is contained in:
parent
24e7f057e7
commit
973e9b7ebe
@ -63,7 +63,12 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showWebView, setShowWebView] = useState(false);
|
const [showWebView, setShowWebView] = useState(false);
|
||||||
const [isWebViewReady, setIsWebViewReady] = useState(false);
|
const [isWebViewReady, setIsWebViewReady] = useState(false);
|
||||||
const [authToken, setAuthToken] = useState<string | null>(null);
|
const [legacyCredentials, setLegacyCredentials] = useState<{
|
||||||
|
token: string;
|
||||||
|
userName: string;
|
||||||
|
userId: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [isRefreshingToken, setIsRefreshingToken] = useState(false);
|
||||||
|
|
||||||
// Edit modal state
|
// Edit modal state
|
||||||
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
||||||
@ -71,21 +76,67 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
|
|
||||||
const webViewRef = useRef<WebView>(null);
|
const webViewRef = useRef<WebView>(null);
|
||||||
|
|
||||||
// Load legacy token for WebView dashboard
|
// Load legacy credentials for WebView dashboard
|
||||||
useEffect(() => {
|
const loadLegacyCredentials = useCallback(async () => {
|
||||||
const loadLegacyToken = async () => {
|
|
||||||
try {
|
try {
|
||||||
const token = await api.getLegacyToken();
|
// Check if token is expiring soon
|
||||||
setAuthToken(token);
|
const isExpiring = await api.isLegacyTokenExpiringSoon();
|
||||||
|
if (isExpiring) {
|
||||||
|
console.log('[DevMode] Legacy token expiring, refreshing...');
|
||||||
|
await api.refreshLegacyToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = await api.getLegacyWebViewCredentials();
|
||||||
|
if (credentials) {
|
||||||
|
setLegacyCredentials(credentials);
|
||||||
|
console.log('[DevMode] Legacy credentials loaded:', credentials.userName);
|
||||||
|
}
|
||||||
setIsWebViewReady(true);
|
setIsWebViewReady(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('[BeneficiaryDetail] Legacy token not available');
|
console.log('[DevMode] Failed to load legacy credentials:', err);
|
||||||
setIsWebViewReady(true);
|
setIsWebViewReady(true);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
loadLegacyToken();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
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) => {
|
const loadBeneficiary = useCallback(async (showLoadingIndicator = true) => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
@ -241,15 +292,18 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// JavaScript to inject token into localStorage for WebView
|
// 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() {
|
(function() {
|
||||||
try {
|
try {
|
||||||
var authData = {
|
var authData = {
|
||||||
token: '${authToken}'
|
username: '${legacyCredentials.userName}',
|
||||||
|
token: '${legacyCredentials.token}',
|
||||||
|
user_id: ${legacyCredentials.userId}
|
||||||
};
|
};
|
||||||
localStorage.setItem('auth2', JSON.stringify(authData));
|
localStorage.setItem('auth2', JSON.stringify(authData));
|
||||||
console.log('Auth data injected');
|
console.log('Auth data injected:', authData.username);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error('Failed to inject token:', e);
|
console.error('Failed to inject token:', e);
|
||||||
}
|
}
|
||||||
@ -343,10 +397,10 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
{/* Content area - WebView or MockDashboard */}
|
{/* Content area - WebView or MockDashboard */}
|
||||||
<View style={styles.dashboardContent}>
|
<View style={styles.dashboardContent}>
|
||||||
{showWebView ? (
|
{showWebView ? (
|
||||||
isWebViewReady && authToken ? (
|
isWebViewReady && legacyCredentials ? (
|
||||||
<WebView
|
<WebView
|
||||||
ref={webViewRef}
|
ref={webViewRef}
|
||||||
source={{ uri: getDashboardUrl(FERDINAND_DEPLOYMENT_ID) }}
|
source={{ uri: getDashboardUrl(api.getDemoDeploymentId()) }}
|
||||||
style={styles.webView}
|
style={styles.webView}
|
||||||
javaScriptEnabled={true}
|
javaScriptEnabled={true}
|
||||||
domStorageEnabled={true}
|
domStorageEnabled={true}
|
||||||
@ -368,7 +422,7 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
<View style={styles.webViewLoading}>
|
<View style={styles.webViewLoading}>
|
||||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||||
<Text style={styles.webViewLoadingText}>
|
<Text style={styles.webViewLoadingText}>
|
||||||
{isWebViewReady ? 'Loading dashboard...' : 'Connecting to sensors...'}
|
{isWebViewReady ? 'Authenticating...' : 'Connecting to sensors...'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
174
services/api.ts
174
services/api.ts
@ -1,5 +1,6 @@
|
|||||||
import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashboardData, ChatResponse, DashboardSingleResponse, NotificationSettings } from '@/types';
|
import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashboardData, ChatResponse, DashboardSingleResponse, NotificationSettings } from '@/types';
|
||||||
import * as Crypto from 'expo-crypto';
|
import * as Crypto from 'expo-crypto';
|
||||||
|
import * as FileSystem from 'expo-file-system';
|
||||||
import * as SecureStore from 'expo-secure-store';
|
import * as SecureStore from 'expo-secure-store';
|
||||||
|
|
||||||
// Callback for handling unauthorized responses (401)
|
// Callback for handling unauthorized responses (401)
|
||||||
@ -609,10 +610,10 @@ class ApiService {
|
|||||||
const beneficiaries: Beneficiary[] = (data.beneficiaries || []).map((item: any) => ({
|
const beneficiaries: Beneficiary[] = (data.beneficiaries || []).map((item: any) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name || item.email,
|
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,
|
status: 'offline' as const,
|
||||||
email: item.email,
|
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,
|
subscription: item.subscription,
|
||||||
// Equipment status from orders
|
// Equipment status from orders
|
||||||
equipmentStatus: item.equipmentStatus,
|
equipmentStatus: item.equipmentStatus,
|
||||||
@ -660,10 +661,10 @@ class ApiService {
|
|||||||
const beneficiary: Beneficiary = {
|
const beneficiary: Beneficiary = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
name: data.name || data.email,
|
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,
|
status: 'offline' as const,
|
||||||
email: data.email,
|
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 ? {
|
subscription: data.subscription ? {
|
||||||
status: data.subscription.status,
|
status: data.subscription.status,
|
||||||
plan: data.subscription.plan,
|
plan: data.subscription.plan,
|
||||||
@ -775,21 +776,23 @@ class ApiService {
|
|||||||
let base64Image: string | null = null;
|
let base64Image: string | null = null;
|
||||||
|
|
||||||
if (imageUri) {
|
if (imageUri) {
|
||||||
// Convert file URI to base64
|
// Read file as base64 string using expo-file-system
|
||||||
const response = await fetch(imageUri);
|
const base64Data = await FileSystem.readAsStringAsync(imageUri, {
|
||||||
const blob = await response.blob();
|
encoding: FileSystem.EncodingType.Base64,
|
||||||
|
|
||||||
// Convert blob to base64
|
|
||||||
const base64 = await new Promise<string>((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => resolve(reader.result as string);
|
|
||||||
reader.onerror = reject;
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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`, {
|
const apiResponse = await fetch(`${WELLNUO_API_URL}/me/beneficiaries/${id}/avatar`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
@ -801,6 +804,8 @@ class ApiService {
|
|||||||
|
|
||||||
const data = await apiResponse.json();
|
const data = await apiResponse.json();
|
||||||
|
|
||||||
|
console.log('[API] Avatar upload response:', apiResponse.status, data);
|
||||||
|
|
||||||
if (!apiResponse.ok) {
|
if (!apiResponse.ok) {
|
||||||
return { ok: false, error: { message: data.error || 'Failed to update avatar' } };
|
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<ApiResponse<AuthResponse>> {
|
||||||
|
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<ApiResponse<AuthResponse>> {
|
||||||
|
console.log('[API] Refreshing legacy token...');
|
||||||
|
return this.loginToLegacyDashboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if legacy token is expiring soon (within 1 hour)
|
||||||
|
async isLegacyTokenExpiringSoon(): Promise<boolean> {
|
||||||
|
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();
|
export const api = new ApiService();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user