Initial Commit

This commit is contained in:
Draculabo committed 2025-12-23 01:38:53 +08:00
commit d0417339c1
158 files changed
+31368

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
import http from 'http';
import { logger } from '../../utils/logger';
import { ipcContext } from '../context';
export class AuthServer {
private static server: http.Server | null = null;
private static PORT = 8888;
static start() {
if (this.server) {
logger.warn('AuthServer: Server already running');
return;
}
this.server = http.createServer((req, res) => {
const url = new URL(req.url || '', `http://localhost:${this.PORT}`);
if (url.pathname === '/oauth-callback') {
const code = url.searchParams.get('code');
const error = url.searchParams.get('error');
if (code) {
logger.info('AuthServer: Received authorization code');
// Send code to renderer
if (ipcContext.mainWindow) {
ipcContext.mainWindow.webContents.send('GOOGLE_AUTH_CODE', code);
} else {
logger.error('AuthServer: Main window not found, cannot send code');
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<html>
<body style="font-family: sans-serif; text-align: center; padding-top: 50px;">
<h1>Login Successful</h1>
<p>You can close this window and return to Antigravity Manager.</p>
<script>
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>
`);
} else if (error) {
logger.error(`AuthServer: OAuth error: ${error}`);
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<html>
<body>
<h1>Login Failed</h1>
<p>Error: ${error}</p>
</body>
</html>
`);
} else {
res.writeHead(400);
res.end('Missing code parameter');
}
} else {
res.writeHead(404);
res.end('Not Found');
}
});
this.server.on('error', (err) => {
logger.error('AuthServer: Server error', err);
});
this.server.listen(this.PORT, () => {
logger.info(`AuthServer: Listening on http://localhost:${this.PORT}`);
});
}
static stop() {
if (this.server) {
this.server.close();
this.server = null;
logger.info('AuthServer: Stopped');
}
}
}
+259
View File
@@ -0,0 +1,259 @@
import { v4 as uuidv4 } from 'uuid';
import { CloudAccountRepo } from '../../ipc/database/cloudHandler';
import { GoogleAPIService } from '../../services/GoogleAPIService';
import { CloudAccount } from '../../types/cloudAccount';
import { logger } from '../../utils/logger';
import { shell } from 'electron';
import fs from 'fs';
import { closeAntigravity, startAntigravity } from '../../ipc/process/handler';
import { updateTrayMenu } from '../../ipc/tray/handler';
import { getAntigravityDbPaths } from '../../utils/paths';
// Fallback constants if service constants are not available or for direct usage
const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
const REDIRECT_URI = 'http://localhost:8888/oauth-callback';
const SCOPE = [
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/cclog',
'https://www.googleapis.com/auth/experimentsandconfigs',
].join(' ');
// Helper to update tray
function notifyTrayUpdate(account: CloudAccount) {
try {
// Fetch language setting. Default to 'en' if not set.
const lang = CloudAccountRepo.getSetting<string>('language', 'en');
updateTrayMenu(account, lang);
} catch (e) {
logger.warn('Failed to update tray', e);
}
}
export async function addGoogleAccount(authCode: string): Promise<CloudAccount> {
try {
// 1. Exchange code for tokens
const tokenResp = await GoogleAPIService.exchangeCode(authCode);
// 2. Get User Info
const userInfo = await GoogleAPIService.getUserInfo(tokenResp.access_token);
// 3. Construct CloudAccount Object
const now = Math.floor(Date.now() / 1000);
const account: CloudAccount = {
id: uuidv4(),
provider: 'google',
email: userInfo.email,
name: userInfo.name || userInfo.email,
avatar_url: userInfo.picture,
token: {
access_token: tokenResp.access_token,
refresh_token: tokenResp.refresh_token || '', // prompt=consent guarantees this, but we fallback safely
expires_in: tokenResp.expires_in,
expiry_timestamp: now + tokenResp.expires_in,
token_type: tokenResp.token_type,
email: userInfo.email,
},
created_at: now,
last_used: now,
};
if (!account.token.refresh_token) {
logger.warn(`No refresh token received for ${account.email}. Account will expire in 1 hour.`);
}
// 4. Save to DB
await CloudAccountRepo.addAccount(account);
// 5. Initial Quota Check (Async, best effort)
try {
const quota = await GoogleAPIService.fetchQuota(account.token.access_token);
account.quota = quota;
await CloudAccountRepo.updateQuota(account.id, quota);
notifyTrayUpdate(account);
} catch (e) {
logger.warn('Failed to fetch initial quota', e);
}
return account;
} catch (error) {
logger.error('Failed to add Google account', error);
throw error;
}
}
export async function listCloudAccounts(): Promise<CloudAccount[]> {
return CloudAccountRepo.getAccounts();
}
export async function deleteCloudAccount(accountId: string): Promise<void> {
await CloudAccountRepo.removeAccount(accountId);
}
export async function refreshAccountQuota(accountId: string): Promise<CloudAccount> {
const account = await CloudAccountRepo.getAccount(accountId);
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
// Check if token needs refresh
let now = Math.floor(Date.now() / 1000);
if (account.token.expiry_timestamp < now + 300) {
// 5 minutes buffer
logger.info(`Token for ${account.email} near expiry, refreshing...`);
try {
const newTokenData = await GoogleAPIService.refreshAccessToken(account.token.refresh_token);
// Update token in memory object
account.token.access_token = newTokenData.access_token;
account.token.expires_in = newTokenData.expires_in;
account.token.expiry_timestamp = now + newTokenData.expires_in;
// Save to DB
await CloudAccountRepo.updateToken(account.id, account.token);
} catch (e) {
logger.error(`Failed to refresh token during time-check for ${account.email}`, e);
}
}
try {
const quota = await GoogleAPIService.fetchQuota(account.token.access_token);
account.quota = quota;
await CloudAccountRepo.updateQuota(account.id, quota);
notifyTrayUpdate(account);
return account;
} catch (error: any) {
if (error.message === 'UNAUTHORIZED') {
logger.warn(`Got 401 Unauthorized for ${account.email}, forcing token refresh...`);
try {
// Force Refresh
const newTokenData = await GoogleAPIService.refreshAccessToken(account.token.refresh_token);
now = Math.floor(Date.now() / 1000);
account.token.access_token = newTokenData.access_token;
account.token.expires_in = newTokenData.expires_in;
account.token.expiry_timestamp = now + newTokenData.expires_in;
await CloudAccountRepo.updateToken(account.id, account.token);
// Retry Quota
const quota = await GoogleAPIService.fetchQuota(account.token.access_token);
account.quota = quota;
await CloudAccountRepo.updateQuota(account.id, quota);
return account;
} catch (refreshError) {
logger.error(
`Failed to force refresh token or retry quota for ${account.email}`,
refreshError,
);
throw refreshError;
}
} else if (error.message === 'FORBIDDEN') {
logger.warn(
`Got 403 Forbidden for ${account.email}, marking as rate limited (if implemented) or just ignoring.`,
);
// Return existing account to avoid crash
return account;
}
logger.error(`Failed to refresh quota for ${account.email}`, error);
throw error;
}
}
export async function switchCloudAccount(accountId: string): Promise<void> {
try {
const account = await CloudAccountRepo.getAccount(accountId);
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
logger.info(`Switching to cloud account: ${account.email} (${account.id})`);
// 1. Ensure token is fresh before injecting
const now = Math.floor(Date.now() / 1000);
if (account.token.expiry_timestamp < now + 300) {
logger.info(`Token for ${account.email} near expiry, refreshing before switch...`);
try {
const newTokenData = await GoogleAPIService.refreshAccessToken(account.token.refresh_token);
account.token.access_token = newTokenData.access_token;
account.token.expires_in = newTokenData.expires_in;
account.token.expiry_timestamp = now + newTokenData.expires_in;
await CloudAccountRepo.updateToken(account.id, account.token);
} catch (e) {
logger.warn('Failed to refresh token before switch, trying with existing token', e);
}
}
// 2. Stop Antigravity Process
await closeAntigravity(); // This waits for it to close
// 3. Backup Database (New Logic)
const dbPaths = getAntigravityDbPaths();
// Find the valid DB path
let dbPath: string | null = null;
for (const p of dbPaths) {
if (fs.existsSync(p)) {
dbPath = p;
break;
}
}
if (dbPath) {
try {
const backupPath = `${dbPath}.backup`;
fs.copyFileSync(dbPath, backupPath);
logger.info(`Backed up database to ${backupPath}`);
} catch (e) {
logger.error('Failed to backup database', e);
}
}
// 3. Inject Token
// injectedCloudToken uses direct DB/FS access which is sync better-sqlite3.
CloudAccountRepo.injectCloudToken(account.token);
// 4. Update usage and active status
CloudAccountRepo.updateLastUsed(account.id);
CloudAccountRepo.setActive(account.id);
// 5. Restart Process
await startAntigravity();
logger.info(`Successfully switched to cloud account: ${account.email}`);
notifyTrayUpdate(account);
} catch (err: any) {
logger.error('Failed to switch cloud account', err);
throw new Error(`Switch failed: ${err.message || 'Unknown error'}`);
}
}
export function getAutoSwitchEnabled(): boolean {
return CloudAccountRepo.getSetting<boolean>('auto_switch_enabled', false);
}
export async function setAutoSwitchEnabled(enabled: boolean): Promise<void> {
CloudAccountRepo.setSetting('auto_switch_enabled', enabled);
// Trigger an immediate check if enabled
if (enabled) {
const { CloudMonitorService } = await import('../../services/CloudMonitorService');
CloudMonitorService.poll().catch((err: any) =>
logger.error('Failed to poll after enabling auto-switch', err),
);
}
}
export async function forcePollCloudMonitor(): Promise<void> {
const { CloudMonitorService } = await import('../../services/CloudMonitorService');
await CloudMonitorService.poll();
}
export async function startAuthFlow(): Promise<void> {
const url = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=${SCOPE}&access_type=offline&prompt=consent&include_granted_scopes=true`;
logger.info(`Starting auth flow, opening URL: ${url}`);
await shell.openExternal(url);
}
+79
View File
@@ -0,0 +1,79 @@
import { z } from 'zod';
import { os } from '@orpc/server';
import {
addGoogleAccount,
listCloudAccounts,
deleteCloudAccount,
refreshAccountQuota,
switchCloudAccount,
getAutoSwitchEnabled,
setAutoSwitchEnabled,
forcePollCloudMonitor,
startAuthFlow,
} from './handler';
import { CloudAccountSchema } from '../../types/cloudAccount';
import { CloudAccountRepo } from '../database/cloudHandler';
export const cloudRouter = os.router({
addGoogleAccount: os
.input(z.object({ authCode: z.string() }))
.output(CloudAccountSchema)
.handler(async ({ input }) => {
return addGoogleAccount(input.authCode);
}),
listCloudAccounts: os.output(z.array(CloudAccountSchema)).handler(async () => {
return listCloudAccounts();
}),
deleteCloudAccount: os
.input(z.object({ accountId: z.string() }))
.output(z.void())
.handler(async ({ input }) => {
await deleteCloudAccount(input.accountId);
}),
refreshAccountQuota: os
.input(z.object({ accountId: z.string() }))
.output(CloudAccountSchema)
.handler(async ({ input }) => {
return refreshAccountQuota(input.accountId);
}),
switchCloudAccount: os
.input(z.object({ accountId: z.string() }))
.output(z.void())
.handler(async ({ input }) => {
await switchCloudAccount(input.accountId);
}),
getAutoSwitchEnabled: os.output(z.boolean()).handler(async () => {
return getAutoSwitchEnabled();
}),
setAutoSwitchEnabled: os
.input(z.object({ enabled: z.boolean() }))
.output(z.void())
.handler(async ({ input }) => {
await setAutoSwitchEnabled(input.enabled);
}),
forcePollCloudMonitor: os.output(z.void()).handler(async () => {
await forcePollCloudMonitor();
}),
startAuthFlow: os.output(z.void()).handler(async () => {
await startAuthFlow();
}),
syncLocalAccount: os.output(CloudAccountSchema.nullable()).handler(async () => {
try {
const result = await CloudAccountRepo.syncFromIDE();
return result;
} catch (error: any) {
console.error('[ORPC] syncLocalAccount error:', error.message, error.stack);
throw error;
}
}),
});