- Add CORS headers to backend server to allow mobile app requests - Implement request timeout (10s) in capacitor-bridge.js to prevent hanging - Add comprehensive logging throughout authentication flow - Add detailed error reporting in initApp for better debugging - Log all API requests with request IDs for traceability This fixes the 'Loading Plugin Compass...' infinite loop issue caused by missing CORS headers and unhandled network timeouts.
662 lines
17 KiB
JavaScript
662 lines
17 KiB
JavaScript
// Capacitor Bridge for Plugin Compass Mobile App
|
|
// Connects to backend at plugincompass.com
|
|
|
|
const BACKEND_BASE_URL = window.BACKEND_BASE_URL || 'https://plugincompass.com';
|
|
|
|
// Storage keys
|
|
const STORAGE_KEYS = {
|
|
USER: 'plugin_compass_user',
|
|
TOKEN: 'plugin_compass_token',
|
|
REFRESH_TOKEN: 'plugin_compass_refresh_token',
|
|
SESSION: 'plugin_compass_session'
|
|
};
|
|
|
|
let PreferencesImpl = null;
|
|
|
|
async function getPreferences() {
|
|
if (PreferencesImpl) return PreferencesImpl;
|
|
|
|
try {
|
|
const module = await import('@capacitor/preferences');
|
|
PreferencesImpl = module.Preferences;
|
|
return PreferencesImpl;
|
|
} catch {
|
|
// Fallback to localStorage
|
|
PreferencesImpl = {
|
|
async get({ key }) {
|
|
return { value: localStorage.getItem(key) };
|
|
},
|
|
async set({ key, value }) {
|
|
localStorage.setItem(key, value);
|
|
return;
|
|
},
|
|
async remove({ key }) {
|
|
localStorage.removeItem(key);
|
|
return;
|
|
},
|
|
async keys() {
|
|
return { keys: Object.keys(localStorage) };
|
|
},
|
|
async clear() {
|
|
localStorage.clear();
|
|
return;
|
|
}
|
|
};
|
|
return PreferencesImpl;
|
|
}
|
|
}
|
|
|
|
export const Preferences = {
|
|
async get(options) {
|
|
const pref = await getPreferences();
|
|
return pref.get(options);
|
|
},
|
|
async set(options) {
|
|
const pref = await getPreferences();
|
|
return pref.set(options);
|
|
},
|
|
async remove(options) {
|
|
const pref = await getPreferences();
|
|
return pref.remove(options);
|
|
},
|
|
async keys() {
|
|
const pref = await getPreferences();
|
|
return pref.keys();
|
|
},
|
|
async clear() {
|
|
const pref = await getPreferences();
|
|
return pref.clear();
|
|
}
|
|
};
|
|
|
|
// Token management
|
|
async function getAuthToken() {
|
|
try {
|
|
const { value } = await Preferences.get({ key: STORAGE_KEYS.TOKEN });
|
|
return value;
|
|
} catch (e) {
|
|
console.error('[AUTH] Failed to get token:', e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function setAuthToken(token) {
|
|
try {
|
|
await Preferences.set({ key: STORAGE_KEYS.TOKEN, value: token });
|
|
} catch (e) {
|
|
console.error('[AUTH] Failed to set token:', e);
|
|
}
|
|
}
|
|
|
|
async function getRefreshToken() {
|
|
try {
|
|
const { value } = await Preferences.get({ key: STORAGE_KEYS.REFRESH_TOKEN });
|
|
return value;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function setRefreshToken(token) {
|
|
try {
|
|
await Preferences.set({ key: STORAGE_KEYS.REFRESH_TOKEN, value: token });
|
|
} catch (e) {
|
|
console.error('[AUTH] Failed to set refresh token:', e);
|
|
}
|
|
}
|
|
|
|
async function clearAuth() {
|
|
try {
|
|
await Preferences.remove({ key: STORAGE_KEYS.TOKEN });
|
|
await Preferences.remove({ key: STORAGE_KEYS.REFRESH_TOKEN });
|
|
await Preferences.remove({ key: STORAGE_KEYS.USER });
|
|
await Preferences.remove({ key: STORAGE_KEYS.SESSION });
|
|
} catch (e) {
|
|
console.error('[AUTH] Failed to clear auth:', e);
|
|
}
|
|
}
|
|
|
|
// API client with authentication and timeout
|
|
const REQUEST_TIMEOUT = 10000; // 10 second timeout
|
|
|
|
function fetchWithTimeout(url, options, timeout = REQUEST_TIMEOUT) {
|
|
return Promise.race([
|
|
fetch(url, options),
|
|
new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout)
|
|
)
|
|
]);
|
|
}
|
|
|
|
async function apiClient(endpoint, options = {}) {
|
|
const url = endpoint.startsWith('http') ? endpoint : `${BACKEND_BASE_URL}${endpoint}`;
|
|
const requestId = Math.random().toString(36).substring(7);
|
|
|
|
console.log(`[API:${requestId}] Starting request to ${endpoint}`);
|
|
console.log(`[API:${requestId}] Full URL: ${url}`);
|
|
|
|
const token = await getAuthToken();
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
|
...(options.headers || {})
|
|
};
|
|
|
|
try {
|
|
console.log(`[API:${requestId}] Sending ${options.method || 'GET'} request...`);
|
|
const startTime = Date.now();
|
|
|
|
const response = await fetchWithTimeout(url, {
|
|
...options,
|
|
headers,
|
|
credentials: 'include'
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
console.log(`[API:${requestId}] Response received in ${duration}ms - Status: ${response.status}`);
|
|
|
|
// Handle token expiration
|
|
if (response.status === 401) {
|
|
console.log(`[API:${requestId}] Token expired, attempting refresh...`);
|
|
const refreshed = await refreshAccessToken();
|
|
if (refreshed) {
|
|
console.log(`[API:${requestId}] Token refreshed, retrying request...`);
|
|
// Retry with new token
|
|
const newToken = await getAuthToken();
|
|
headers['Authorization'] = `Bearer ${newToken}`;
|
|
const retryResponse = await fetchWithTimeout(url, {
|
|
...options,
|
|
headers,
|
|
credentials: 'include'
|
|
});
|
|
return handleResponse(retryResponse);
|
|
} else {
|
|
// Token refresh failed, clear auth
|
|
console.error(`[API:${requestId}] Token refresh failed`);
|
|
await clearAuth();
|
|
throw new Error('Session expired. Please sign in again.');
|
|
}
|
|
}
|
|
|
|
return handleResponse(response);
|
|
} catch (error) {
|
|
console.error(`[API:${requestId}] Request failed: ${endpoint}`, error);
|
|
console.error(`[API:${requestId}] Error details:`, {
|
|
message: error.message,
|
|
name: error.name,
|
|
stack: error.stack
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function handleResponse(response) {
|
|
const text = await response.text();
|
|
let json;
|
|
try {
|
|
json = text ? JSON.parse(text) : {};
|
|
} catch (parseErr) {
|
|
throw new Error(`Invalid JSON response: ${parseErr.message}`);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const err = new Error(json.error || json.message || `HTTP ${response.status}`);
|
|
err.status = response.status;
|
|
err.data = json;
|
|
throw err;
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
async function refreshAccessToken() {
|
|
try {
|
|
const refreshToken = await getRefreshToken();
|
|
if (!refreshToken) return false;
|
|
|
|
const response = await fetch(`${BACKEND_BASE_URL}/api/auth/refresh`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refreshToken })
|
|
});
|
|
|
|
if (!response.ok) {
|
|
return false;
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (data.token) {
|
|
await setAuthToken(data.token);
|
|
if (data.refreshToken) {
|
|
await setRefreshToken(data.refreshToken);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
console.error('[AUTH] Token refresh failed:', e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Authentication functions
|
|
export async function loginWithEmail(email, password, remember = false) {
|
|
try {
|
|
const response = await apiClient('/api/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email, password, remember })
|
|
});
|
|
|
|
if (response.ok && response.user && response.token) {
|
|
// Store tokens
|
|
await setAuthToken(response.token);
|
|
if (response.refreshToken) {
|
|
await setRefreshToken(response.refreshToken);
|
|
}
|
|
|
|
// Store user info
|
|
await Preferences.set({
|
|
key: STORAGE_KEYS.USER,
|
|
value: JSON.stringify(response.user)
|
|
});
|
|
|
|
return { success: true, user: response.user };
|
|
} else {
|
|
return { success: false, error: response.error || 'Login failed' };
|
|
}
|
|
} catch (error) {
|
|
console.error('[AUTH] Login error:', error);
|
|
return { success: false, error: error.message || 'Login failed' };
|
|
}
|
|
}
|
|
|
|
export async function signupWithEmail(email, password) {
|
|
try {
|
|
const response = await apiClient('/api/signup', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email, password })
|
|
});
|
|
|
|
if (response.ok && response.user && response.token) {
|
|
await setAuthToken(response.token);
|
|
if (response.refreshToken) {
|
|
await setRefreshToken(response.refreshToken);
|
|
}
|
|
|
|
await Preferences.set({
|
|
key: STORAGE_KEYS.USER,
|
|
value: JSON.stringify(response.user)
|
|
});
|
|
|
|
return { success: true, user: response.user };
|
|
} else {
|
|
return { success: false, error: response.error || 'Signup failed' };
|
|
}
|
|
} catch (error) {
|
|
console.error('[AUTH] Signup error:', error);
|
|
return { success: false, error: error.message || 'Signup failed' };
|
|
}
|
|
}
|
|
|
|
export async function logout() {
|
|
try {
|
|
// Call logout endpoint if available
|
|
await apiClient('/api/logout', {
|
|
method: 'POST',
|
|
headers: {}
|
|
}).catch(() => {});
|
|
} finally {
|
|
await clearAuth();
|
|
}
|
|
}
|
|
|
|
export async function getCurrentUser() {
|
|
console.log('[AUTH] Getting current user...');
|
|
try {
|
|
// First check stored user
|
|
console.log('[AUTH] Checking stored user...');
|
|
const { value } = await Preferences.get({ key: STORAGE_KEYS.USER });
|
|
if (value) {
|
|
const user = JSON.parse(value);
|
|
console.log('[AUTH] Found stored user:', user.email || user.id);
|
|
|
|
// Verify token is still valid by fetching fresh user data
|
|
try {
|
|
console.log('[AUTH] Fetching fresh user data from /api/me...');
|
|
const freshUser = await apiClient('/api/me');
|
|
if (freshUser) {
|
|
console.log('[AUTH] Fresh user data received:', freshUser.email || freshUser.id);
|
|
await Preferences.set({
|
|
key: STORAGE_KEYS.USER,
|
|
value: JSON.stringify(freshUser)
|
|
});
|
|
return freshUser;
|
|
}
|
|
} catch (e) {
|
|
console.warn('[AUTH] Could not refresh user data:', e.message);
|
|
console.log('[AUTH] Falling back to cached user data');
|
|
}
|
|
|
|
return user;
|
|
}
|
|
console.log('[AUTH] No stored user found');
|
|
return null;
|
|
} catch (e) {
|
|
console.error('[AUTH] Failed to get current user:', e);
|
|
console.error('[AUTH] Error details:', {
|
|
message: e.message,
|
|
name: e.name,
|
|
stack: e.stack
|
|
});
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function isAuthenticated() {
|
|
console.log('[AUTH] Checking authentication status...');
|
|
const token = await getAuthToken();
|
|
console.log('[AUTH] Token exists:', !!token);
|
|
|
|
if (!token) {
|
|
console.log('[AUTH] No token found, user is not authenticated');
|
|
return false;
|
|
}
|
|
|
|
console.log('[AUTH] Token found, fetching current user...');
|
|
const user = await getCurrentUser();
|
|
const isAuth = !!user;
|
|
console.log('[AUTH] Authentication result:', isAuth, user ? `(User: ${user.email || user.id})` : '');
|
|
return isAuth;
|
|
}
|
|
|
|
// OAuth helpers
|
|
export function getOAuthUrl(provider, redirectUri = null) {
|
|
const params = new URLSearchParams();
|
|
if (redirectUri) {
|
|
params.set('redirect_uri', redirectUri);
|
|
}
|
|
params.set('mobile', 'true');
|
|
return `${BACKEND_BASE_URL}/auth/${provider}?${params.toString()}`;
|
|
}
|
|
|
|
export async function handleOAuthCallback(provider, code, state) {
|
|
try {
|
|
const response = await apiClient(`/auth/${provider}/callback`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ code, state })
|
|
});
|
|
|
|
if (response.token) {
|
|
await setAuthToken(response.token);
|
|
if (response.refreshToken) {
|
|
await setRefreshToken(response.refreshToken);
|
|
}
|
|
|
|
if (response.user) {
|
|
await Preferences.set({
|
|
key: STORAGE_KEYS.USER,
|
|
value: JSON.stringify(response.user)
|
|
});
|
|
}
|
|
|
|
return { success: true, user: response.user };
|
|
} else {
|
|
return { success: false, error: response.error || 'OAuth failed' };
|
|
}
|
|
} catch (error) {
|
|
console.error('[AUTH] OAuth error:', error);
|
|
return { success: false, error: error.message || 'OAuth failed' };
|
|
}
|
|
}
|
|
|
|
// Plugin/App management
|
|
export async function getUserApps() {
|
|
try {
|
|
const apps = await apiClient('/api/apps');
|
|
return { success: true, apps: apps || [] };
|
|
} catch (error) {
|
|
console.error('[APPS] Failed to load apps:', error);
|
|
return { success: false, error: error.message, apps: [] };
|
|
}
|
|
}
|
|
|
|
export async function getApp(appId) {
|
|
try {
|
|
const app = await apiClient(`/api/apps/${appId}`);
|
|
return { success: true, app };
|
|
} catch (error) {
|
|
console.error('[APPS] Failed to load app:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
export async function createApp(appData) {
|
|
try {
|
|
const app = await apiClient('/api/apps', {
|
|
method: 'POST',
|
|
body: JSON.stringify(appData)
|
|
});
|
|
return { success: true, app };
|
|
} catch (error) {
|
|
console.error('[APPS] Failed to create app:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
export async function updateApp(appId, appData) {
|
|
try {
|
|
const app = await apiClient(`/api/apps/${appId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(appData)
|
|
});
|
|
return { success: true, app };
|
|
} catch (error) {
|
|
console.error('[APPS] Failed to update app:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
export async function deleteApp(appId) {
|
|
try {
|
|
await apiClient(`/api/apps/${appId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('[APPS] Failed to delete app:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
export async function syncApp(appId) {
|
|
if (!appId || appId.trim() === '') {
|
|
throw new Error('appId is required');
|
|
}
|
|
|
|
try {
|
|
// Get app from backend instead of local storage
|
|
const result = await getApp(appId);
|
|
if (!result.success || !result.app) {
|
|
throw new Error(`App not found: ${appId}`);
|
|
}
|
|
|
|
// Sync with backend
|
|
const response = await apiClient('/desktop/apps/sync', {
|
|
method: 'POST',
|
|
body: JSON.stringify(result.app)
|
|
});
|
|
|
|
return response;
|
|
} catch (error) {
|
|
console.error('[SYNC] Failed to sync app:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function runOpencodeTask(appId, taskName, args = []) {
|
|
if (!appId || appId.trim() === '') {
|
|
throw new Error('appId is required');
|
|
}
|
|
if (!taskName || taskName.trim() === '') {
|
|
throw new Error('taskName is required');
|
|
}
|
|
|
|
throw new Error('OpenCode execution is not supported on mobile devices. Please use the desktop app for this feature.');
|
|
}
|
|
|
|
// Session management
|
|
export async function getSessions() {
|
|
try {
|
|
const sessions = await apiClient('/api/sessions');
|
|
return { success: true, sessions: sessions || [] };
|
|
} catch (error) {
|
|
console.error('[SESSIONS] Failed to load sessions:', error);
|
|
return { success: false, error: error.message, sessions: [] };
|
|
}
|
|
}
|
|
|
|
export async function getSession(sessionId) {
|
|
try {
|
|
const session = await apiClient(`/api/sessions/${sessionId}`);
|
|
return { success: true, session };
|
|
} catch (error) {
|
|
console.error('[SESSIONS] Failed to load session:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
export async function createSession(sessionData) {
|
|
try {
|
|
const session = await apiClient('/api/sessions', {
|
|
method: 'POST',
|
|
body: JSON.stringify(sessionData)
|
|
});
|
|
return { success: true, session };
|
|
} catch (error) {
|
|
console.error('[SESSIONS] Failed to create session:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
export async function sendMessage(sessionId, messageData) {
|
|
try {
|
|
const result = await apiClient(`/api/sessions/${sessionId}/messages`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(messageData)
|
|
});
|
|
return { success: true, ...result };
|
|
} catch (error) {
|
|
console.error('[MESSAGES] Failed to send message:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
// User account
|
|
export async function getUserAccount() {
|
|
try {
|
|
const account = await apiClient('/api/account');
|
|
return { success: true, account };
|
|
} catch (error) {
|
|
console.error('[ACCOUNT] Failed to load account:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
export async function updateUserAccount(accountData) {
|
|
try {
|
|
const account = await apiClient('/api/account', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(accountData)
|
|
});
|
|
return { success: true, account };
|
|
} catch (error) {
|
|
console.error('[ACCOUNT] Failed to update account:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
export async function getUsageSummary() {
|
|
try {
|
|
const usage = await apiClient('/api/account/usage');
|
|
return { success: true, usage };
|
|
} catch (error) {
|
|
console.error('[USAGE] Failed to load usage:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
// Legacy exports for backward compatibility
|
|
window.nativeBridge = {
|
|
Preferences,
|
|
syncApp,
|
|
runOpencodeTask,
|
|
loginWithEmail,
|
|
signupWithEmail,
|
|
logout,
|
|
getCurrentUser,
|
|
isAuthenticated,
|
|
getUserApps,
|
|
getApp,
|
|
createApp,
|
|
updateApp,
|
|
deleteApp,
|
|
getSessions,
|
|
createSession,
|
|
sendMessage,
|
|
getOAuthUrl,
|
|
handleOAuthCallback,
|
|
getUserAccount,
|
|
updateUserAccount,
|
|
getUsageSummary,
|
|
apiClient
|
|
};
|
|
|
|
// Expose all functions globally for the web UI
|
|
Object.assign(window, {
|
|
loginWithEmail,
|
|
signupWithEmail,
|
|
logout,
|
|
getCurrentUser,
|
|
isAuthenticated,
|
|
getUserApps,
|
|
getApp,
|
|
createApp,
|
|
updateApp,
|
|
deleteApp,
|
|
getSessions,
|
|
getSession,
|
|
createSession,
|
|
sendMessage,
|
|
getOAuthUrl,
|
|
handleOAuthCallback,
|
|
getUserAccount,
|
|
updateUserAccount,
|
|
getUsageSummary,
|
|
apiClient
|
|
});
|
|
|
|
export {
|
|
loginWithEmail,
|
|
signupWithEmail,
|
|
logout,
|
|
getCurrentUser,
|
|
isAuthenticated,
|
|
getUserApps,
|
|
getApp,
|
|
createApp,
|
|
updateApp,
|
|
deleteApp,
|
|
getSessions,
|
|
getSession,
|
|
createSession,
|
|
sendMessage,
|
|
getOAuthUrl,
|
|
handleOAuthCallback,
|
|
getUserAccount,
|
|
updateUserAccount,
|
|
getUsageSummary,
|
|
apiClient,
|
|
BACKEND_BASE_URL,
|
|
STORAGE_KEYS
|
|
};
|