Basic GitHub integration flow
This commit is contained in:
282
src/ipc/handlers/github_handlers.ts
Normal file
282
src/ipc/handlers/github_handlers.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import {
|
||||
ipcMain,
|
||||
IpcMainEvent,
|
||||
BrowserWindow,
|
||||
IpcMainInvokeEvent,
|
||||
} from "electron";
|
||||
import fetch from "node-fetch"; // Use node-fetch for making HTTP requests in main process
|
||||
import { writeSettings } from "../../main/settings";
|
||||
|
||||
// --- GitHub Device Flow Constants ---
|
||||
// TODO: Fetch this securely, e.g., from environment variables or a config file
|
||||
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || "Ov23liWV2HdC0RBLecWx";
|
||||
const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
|
||||
const GITHUB_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
||||
const GITHUB_SCOPES = "repo,user"; // Define the scopes needed
|
||||
|
||||
// --- State Management (Simple in-memory, consider alternatives for robustness) ---
|
||||
interface DeviceFlowState {
|
||||
deviceCode: string;
|
||||
interval: number;
|
||||
timeoutId: NodeJS.Timeout | null;
|
||||
isPolling: boolean;
|
||||
window: BrowserWindow | null; // Reference to the window that initiated the flow
|
||||
}
|
||||
|
||||
// Simple map to track ongoing flows (key could be appId or a unique flow ID if needed)
|
||||
// For simplicity, let's assume only one flow can happen at a time for now.
|
||||
let currentFlowState: DeviceFlowState | null = null;
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
// function event.sender.send(channel: string, data: any) {
|
||||
// if (currentFlowState?.window && !currentFlowState.window.isDestroyed()) {
|
||||
// currentFlowState.window.webContents.send(channel, data);
|
||||
// }
|
||||
// }
|
||||
|
||||
async function pollForAccessToken(event: IpcMainInvokeEvent) {
|
||||
if (!currentFlowState || !currentFlowState.isPolling) {
|
||||
console.log("[GitHub Handler] Polling stopped or no active flow.");
|
||||
return;
|
||||
}
|
||||
|
||||
const { deviceCode, interval } = currentFlowState;
|
||||
|
||||
console.log(
|
||||
`[GitHub Handler] Polling for token with device code: ${deviceCode}`
|
||||
);
|
||||
event.sender.send("github:flow-update", {
|
||||
message: "Polling GitHub for authorization...",
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(GITHUB_ACCESS_TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: GITHUB_CLIENT_ID,
|
||||
device_code: deviceCode,
|
||||
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.access_token) {
|
||||
// --- SUCCESS ---
|
||||
console.log(
|
||||
"[GitHub Handler] Successfully obtained GitHub Access Token."
|
||||
); // TODO: Store this token securely!
|
||||
event.sender.send("github:flow-success", {
|
||||
message: "Successfully connected!",
|
||||
});
|
||||
writeSettings({
|
||||
githubSettings: {
|
||||
secrets: {
|
||||
accessToken: data.access_token,
|
||||
},
|
||||
},
|
||||
});
|
||||
// TODO: Associate token with appId if provided
|
||||
stopPolling();
|
||||
return;
|
||||
} else if (data.error) {
|
||||
switch (data.error) {
|
||||
case "authorization_pending":
|
||||
console.log("[GitHub Handler] Authorization pending...");
|
||||
event.sender.send("github:flow-update", {
|
||||
message: "Waiting for user authorization...",
|
||||
});
|
||||
// Schedule next poll
|
||||
currentFlowState.timeoutId = setTimeout(
|
||||
() => pollForAccessToken(event),
|
||||
interval * 1000
|
||||
);
|
||||
break;
|
||||
case "slow_down":
|
||||
const newInterval = interval + 5;
|
||||
console.log(
|
||||
`[GitHub Handler] Slow down requested. New interval: ${newInterval}s`
|
||||
);
|
||||
currentFlowState.interval = newInterval; // Update interval
|
||||
event.sender.send("github:flow-update", {
|
||||
message: `GitHub asked to slow down. Retrying in ${newInterval}s...`,
|
||||
});
|
||||
currentFlowState.timeoutId = setTimeout(
|
||||
() => pollForAccessToken(event),
|
||||
newInterval * 1000
|
||||
);
|
||||
break;
|
||||
case "expired_token":
|
||||
console.error("[GitHub Handler] Device code expired.");
|
||||
event.sender.send("github:flow-error", {
|
||||
error: "Verification code expired. Please try again.",
|
||||
});
|
||||
stopPolling();
|
||||
break;
|
||||
case "access_denied":
|
||||
console.error("[GitHub Handler] Access denied by user.");
|
||||
event.sender.send("github:flow-error", {
|
||||
error: "Authorization denied by user.",
|
||||
});
|
||||
stopPolling();
|
||||
break;
|
||||
default:
|
||||
console.error(
|
||||
`[GitHub Handler] Unknown GitHub error: ${
|
||||
data.error_description || data.error
|
||||
}`
|
||||
);
|
||||
event.sender.send("github:flow-error", {
|
||||
error: `GitHub authorization error: ${
|
||||
data.error_description || data.error
|
||||
}`,
|
||||
});
|
||||
stopPolling();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown response structure: ${JSON.stringify(data)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[GitHub Handler] Error polling for GitHub access token:",
|
||||
error
|
||||
);
|
||||
event.sender.send("github:flow-error", {
|
||||
error: `Network or unexpected error during polling: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
});
|
||||
stopPolling();
|
||||
}
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (currentFlowState) {
|
||||
if (currentFlowState.timeoutId) {
|
||||
clearTimeout(currentFlowState.timeoutId);
|
||||
}
|
||||
currentFlowState.isPolling = false;
|
||||
currentFlowState.timeoutId = null;
|
||||
// Maybe keep window reference for a bit if needed, or clear it
|
||||
// currentFlowState.window = null;
|
||||
console.log("[GitHub Handler] Polling stopped.");
|
||||
}
|
||||
// Setting to null signifies no active flow
|
||||
// currentFlowState = null; // Decide if you want to clear immediately or allow potential restart
|
||||
}
|
||||
|
||||
// --- IPC Handlers ---
|
||||
|
||||
function handleStartGithubFlow(
|
||||
event: IpcMainInvokeEvent,
|
||||
args: { appId: number | null }
|
||||
) {
|
||||
console.log(
|
||||
`[GitHub Handler] Received github:start-flow for appId: ${args.appId}`
|
||||
);
|
||||
|
||||
// If a flow is already in progress, maybe cancel it or send an error
|
||||
if (currentFlowState && currentFlowState.isPolling) {
|
||||
console.warn(
|
||||
"[GitHub Handler] Another GitHub flow is already in progress."
|
||||
);
|
||||
event.sender.send("github:flow-error", {
|
||||
error: "Another connection process is already active.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the window that initiated the request
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
if (!window) {
|
||||
console.error("[GitHub Handler] Could not get BrowserWindow instance.");
|
||||
return;
|
||||
}
|
||||
|
||||
currentFlowState = {
|
||||
deviceCode: "",
|
||||
interval: 5, // Default interval
|
||||
timeoutId: null,
|
||||
isPolling: false,
|
||||
window: window,
|
||||
};
|
||||
|
||||
event.sender.send("github:flow-update", {
|
||||
message: "Requesting device code from GitHub...",
|
||||
});
|
||||
|
||||
fetch(GITHUB_DEVICE_CODE_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: GITHUB_CLIENT_ID,
|
||||
scope: GITHUB_SCOPES,
|
||||
}),
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
return res.json().then((errData) => {
|
||||
throw new Error(
|
||||
`GitHub API Error: ${errData.error_description || res.statusText}`
|
||||
);
|
||||
});
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
console.log("[GitHub Handler] Received device code response:", data);
|
||||
if (!currentFlowState) return; // Flow might have been cancelled
|
||||
|
||||
currentFlowState.deviceCode = data.device_code;
|
||||
currentFlowState.interval = data.interval || 5;
|
||||
currentFlowState.isPolling = true;
|
||||
|
||||
// Send user code and verification URI to renderer
|
||||
event.sender.send("github:flow-update", {
|
||||
userCode: data.user_code,
|
||||
verificationUri: data.verification_uri,
|
||||
message: "Please authorize in your browser.",
|
||||
});
|
||||
|
||||
// Start polling after the initial interval
|
||||
currentFlowState.timeoutId = setTimeout(
|
||||
() => pollForAccessToken(event),
|
||||
currentFlowState.interval * 1000
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"[GitHub Handler] Error initiating GitHub device flow:",
|
||||
error
|
||||
);
|
||||
event.sender.send("github:flow-error", {
|
||||
error: `Failed to start GitHub connection: ${error.message}`,
|
||||
});
|
||||
stopPolling(); // Ensure polling stops on initial error
|
||||
currentFlowState = null; // Clear state on initial error
|
||||
});
|
||||
}
|
||||
|
||||
// Optional: Handle cancellation from renderer
|
||||
// function handleCancelGithubFlow(event: IpcMainEvent) {
|
||||
// console.log('[GitHub Handler] Received github:cancel-flow');
|
||||
// stopPolling();
|
||||
// currentFlowState = null; // Clear state on cancel
|
||||
// // Optionally send confirmation back
|
||||
// event.sender.send('github:flow-cancelled', { message: 'GitHub flow cancelled.' });
|
||||
// }
|
||||
|
||||
// --- Registration ---
|
||||
export function registerGithubHandlers() {
|
||||
ipcMain.handle("github:start-flow", handleStartGithubFlow);
|
||||
// ipcMain.on('github:cancel-flow', handleCancelGithubFlow); // Uncomment if you add cancellation
|
||||
}
|
||||
@@ -29,6 +29,20 @@ export interface AppStreamCallbacks {
|
||||
onOutput: (output: AppOutput) => void;
|
||||
}
|
||||
|
||||
export interface GitHubDeviceFlowUpdateData {
|
||||
userCode?: string;
|
||||
verificationUri?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface GitHubDeviceFlowSuccessData {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface GitHubDeviceFlowErrorData {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export class IpcClient {
|
||||
private static instance: IpcClient;
|
||||
private ipcRenderer: IpcRenderer;
|
||||
@@ -505,4 +519,58 @@ export class IpcClient {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// --- GitHub Device Flow ---
|
||||
public startGithubDeviceFlow(appId: number | null): void {
|
||||
this.ipcRenderer.invoke("github:start-flow", { appId });
|
||||
}
|
||||
|
||||
public onGithubDeviceFlowUpdate(
|
||||
callback: (data: GitHubDeviceFlowUpdateData) => void
|
||||
): () => void {
|
||||
const listener = (data: any) => {
|
||||
console.log("github:flow-update", data);
|
||||
callback(data as GitHubDeviceFlowUpdateData);
|
||||
};
|
||||
this.ipcRenderer.on("github:flow-update", listener);
|
||||
// Return a function to remove the listener
|
||||
return () => {
|
||||
this.ipcRenderer.removeListener("github:flow-update", listener);
|
||||
};
|
||||
}
|
||||
|
||||
public onGithubDeviceFlowSuccess(
|
||||
callback: (data: GitHubDeviceFlowSuccessData) => void
|
||||
): () => void {
|
||||
const listener = (data: any) => {
|
||||
console.log("github:flow-success", data);
|
||||
callback(data as GitHubDeviceFlowSuccessData);
|
||||
};
|
||||
this.ipcRenderer.on("github:flow-success", listener);
|
||||
return () => {
|
||||
this.ipcRenderer.removeListener("github:flow-success", listener);
|
||||
};
|
||||
}
|
||||
|
||||
public onGithubDeviceFlowError(
|
||||
callback: (data: GitHubDeviceFlowErrorData) => void
|
||||
): () => void {
|
||||
const listener = (data: any) => {
|
||||
console.log("github:flow-error", data);
|
||||
callback(data as GitHubDeviceFlowErrorData);
|
||||
};
|
||||
this.ipcRenderer.on("github:flow-error", listener);
|
||||
return () => {
|
||||
this.ipcRenderer.removeListener("github:flow-error", listener);
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Implement cancel method if needed
|
||||
// public cancelGithubDeviceFlow(): void {
|
||||
// this.ipcRenderer.sendMessage("github:cancel-flow");
|
||||
// }
|
||||
// --- End GitHub Device Flow ---
|
||||
|
||||
// Example methods for listening to events (if needed)
|
||||
// public on(channel: string, func: (...args: any[]) => void): void {
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { registerChatStreamHandlers } from "./handlers/chat_stream_handlers";
|
||||
import { registerSettingsHandlers } from "./handlers/settings_handlers";
|
||||
import { registerShellHandlers } from "./handlers/shell_handler";
|
||||
import { registerDependencyHandlers } from "./handlers/dependency_handlers";
|
||||
import { registerGithubHandlers } from "./handlers/github_handlers";
|
||||
|
||||
export function registerIpcHandlers() {
|
||||
// Register all IPC handlers by category
|
||||
@@ -13,4 +14,5 @@ export function registerIpcHandlers() {
|
||||
registerSettingsHandlers();
|
||||
registerShellHandlers();
|
||||
registerDependencyHandlers();
|
||||
registerGithubHandlers();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user