Fix capacitor build: Implement proper backend connectivity and authentication

- Update BACKEND_BASE_URL to plugincompass.com:9445
- Add comprehensive authentication system (login, signup, logout)
- Implement JWT token management with automatic refresh
- Add plugin/app management API (CRUD operations)
- Add session management for chat/builder
- Update mobile UI with real backend integration
- Add user account and usage tracking
- Enable plugin loading and editing from backend
- Fix module type for ES6 support
This commit is contained in:
Developer
2026-02-16 22:02:46 +00:00
parent 3f6e649965
commit eced327702
3 changed files with 918 additions and 154 deletions

View File

@@ -1,4 +1,15 @@
const BACKEND_BASE_URL = window.BACKEND_BASE_URL || 'https://api.example.com';
// Capacitor Bridge for Plugin Compass Mobile App
// Connects to backend at plugincompass.com:9445
const BACKEND_BASE_URL = window.BACKEND_BASE_URL || 'https://plugincompass.com:9445';
// 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;
@@ -58,31 +69,378 @@ export const Preferences = {
}
};
// 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');
}
const { value } = await Preferences.get({ key: `app_${appId}` });
if (!value) {
throw new Error(`App not found: ${appId}`);
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;
}
const appData = JSON.parse(value);
const response = await fetch(`${BACKEND_BASE_URL}/desktop/apps/sync`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(appData),
});
if (!response.ok) {
throw new Error(`Sync failed: ${response.status}`);
}
return response.json();
}
export async function runOpencodeTask(appId, taskName, args = []) {
@@ -96,8 +454,158 @@ export async function runOpencodeTask(appId, taskName, args = []) {
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
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
};