Backend is served at https://plugincompass.com (port 443) via reverse proxy, not https://plugincompass.com:9445
612 lines
15 KiB
JavaScript
612 lines
15 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
|
|
async function apiClient(endpoint, options = {}) {
|
|
const url = endpoint.startsWith('http') ? endpoint : `${BACKEND_BASE_URL}${endpoint}`;
|
|
|
|
const token = await getAuthToken();
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
|
...(options.headers || {})
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers,
|
|
credentials: 'include'
|
|
});
|
|
|
|
// Handle token expiration
|
|
if (response.status === 401) {
|
|
const refreshed = await refreshAccessToken();
|
|
if (refreshed) {
|
|
// Retry with new token
|
|
const newToken = await getAuthToken();
|
|
headers['Authorization'] = `Bearer ${newToken}`;
|
|
const retryResponse = await fetch(url, {
|
|
...options,
|
|
headers,
|
|
credentials: 'include'
|
|
});
|
|
return handleResponse(retryResponse);
|
|
} else {
|
|
// Token refresh failed, clear auth
|
|
await clearAuth();
|
|
throw new Error('Session expired. Please sign in again.');
|
|
}
|
|
}
|
|
|
|
return handleResponse(response);
|
|
} catch (error) {
|
|
console.error(`[API] Request failed: ${endpoint}`, error);
|
|
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() {
|
|
try {
|
|
// First check stored user
|
|
const { value } = await Preferences.get({ key: STORAGE_KEYS.USER });
|
|
if (value) {
|
|
const user = JSON.parse(value);
|
|
|
|
// Verify token is still valid by fetching fresh user data
|
|
try {
|
|
const freshUser = await apiClient('/api/me');
|
|
if (freshUser) {
|
|
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);
|
|
}
|
|
|
|
return user;
|
|
}
|
|
return null;
|
|
} catch (e) {
|
|
console.error('[AUTH] Failed to get current user:', e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function isAuthenticated() {
|
|
const token = await getAuthToken();
|
|
if (!token) return false;
|
|
|
|
const user = await getCurrentUser();
|
|
return !!user;
|
|
}
|
|
|
|
// 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
|
|
};
|