diff --git a/src/commands/LogIn.ts b/src/commands/LogIn.ts index 154ea07..b3a8ef5 100644 --- a/src/commands/LogIn.ts +++ b/src/commands/LogIn.ts @@ -2,27 +2,29 @@ import * as vscode from "vscode"; import type { Command } from "../CommandManager"; import type { VercelManager } from "../features/VercelManager"; +const viewSidebar = () => + vscode.commands.executeCommand("workbench.view.extension.vercel-sidebar"); +/** @return HasAccessToken */ +async function warnDeprecatedConfig() { + const config = vscode.workspace.getConfiguration("vercel"); + const apiToken = config.get("AccessToken"); + if (!apiToken) return; + const msg = + "The vercel.AccessToken configuration has been removed. Auto-removing from configuration."; + void vscode.window.showWarningMessage(msg); + await config.update( + "AccessToken", + undefined, + vscode.ConfigurationTarget.Global + ); +} export class LogIn implements Command { public readonly id = "vercel.logIn"; constructor(private readonly vercel: VercelManager) {} async execute() { - const apiToken = vscode.workspace - .getConfiguration("vercel") - .get("AccessToken") as string; - //TODO Add support for signing in through website - if (apiToken) { - await this.vercel - .logIn(apiToken) - .then(() => - vscode.commands.executeCommand( - "workbench.view.extension.vercel-sidebar" - ) - ); - } else { - await vscode.window.showErrorMessage( - "Please provide vscode-vercel.AccessToken in settings.json." - ); - } + await warnDeprecatedConfig(); + await this.vercel.logIn(); + await viewSidebar(); } } diff --git a/src/features/VercelManager.ts b/src/features/VercelManager.ts index 724e1ee..a068a1a 100644 --- a/src/features/VercelManager.ts +++ b/src/features/VercelManager.ts @@ -8,6 +8,7 @@ import type { VercelEnvironmentInformation, VercelResponse, } from "./models"; +import { getTokenOauth } from "../utils/oauth"; export class VercelManager { public onDidEnvironmentsUpdated: () => void = () => {}; @@ -41,11 +42,14 @@ export class VercelManager { }; } - async logIn(apiToken: string): Promise { + async logIn(): Promise { + const apiToken = await getTokenOauth(); + if (!apiToken) return false; await this.token.setAuth(apiToken); this.onDidDeploymentsUpdated(); this.onDidEnvironmentsUpdated(); await this.token.onDidLogIn(); + return true; } /** diff --git a/src/utils/oauth.ts b/src/utils/oauth.ts new file mode 100644 index 0000000..4ae9021 --- /dev/null +++ b/src/utils/oauth.ts @@ -0,0 +1,80 @@ +import * as vscode from "vscode"; +import http from "http"; +const OAUTH_PORT = 9615; +const OAUTH_PATH = "/oauth-complete"; +const OAUTH_URL = `http://localhost:${OAUTH_PORT}${OAUTH_PATH}`; +const CLIENT_ID = "oac_dJy0AdgVEITrkrnYF5Y4nSlo"; +const CLIENT_SEC = "NPBb5J2ZNrlhX3W98DCPS1o1"; + +const vercelSlug = "vercel-project-manager"; +const link = `https://vercel.com/integrations/${vercelSlug}/new`; + +/** successMessage is html */ +async function serveResponse( + port: number, + pathname?: string // If present, will ignore all other paths +): Promise { + return await new Promise((resolve, reject) => { + const server = http.createServer(async function (req, res) { + const url = req.url && new URL(req.url, `http://${req.headers.host}`); + if (!url || url.pathname !== pathname) { + res.writeHead(404); + res.end(); + if (!url) reject(new Error("No URL provided")); + return; + } + res.writeHead(200, { "Content-Type": "text/html" }); + res.end( + "Authentication successful! You can now close this window." + ); + server.close(); + return resolve(url); + }); + server.on("error", e => { + if (!("code" in e) || e.code !== "EADDRINUSE") return; + // console.error("Address in use, retrying..."); + let tries = 0; + setTimeout(() => { + if (tries++ > 5) return reject(e); // Retry up to 5 times + server.close(); + server.listen(port); + }, 1000); + }); + server.listen(port); + }); +} +async function getTokenFromCode(code: string): Promise { + const url = new URL("https://api.vercel.com/v2/oauth/access_token"); + const headers = { + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + }; + const params = new URLSearchParams({ + code: code, + redirect_uri: OAUTH_URL, // eslint-disable-line @typescript-eslint/naming-convention + client_id: CLIENT_ID, // eslint-disable-line @typescript-eslint/naming-convention + client_secret: CLIENT_SEC, // eslint-disable-line @typescript-eslint/naming-convention + }); + const res = await fetch(url, { headers, method: "POST", body: params }); + if (!res.ok) { + return; + } + // eslint-disable-next-line @typescript-eslint/naming-convention + const jsonRes = (await res.json()) as { access_token: string }; + const accessToken = jsonRes.access_token; + return accessToken; +} +export async function getTokenOauth() { + const resUrl = serveResponse(OAUTH_PORT, OAUTH_PATH); // don't await it here! We need to open the url before it's meaningful. + await vscode.env.openExternal(vscode.Uri.parse(link)); // open in a browser + const code = (await resUrl).searchParams.get("code"); + if (!code) { + const msg = "Failed to authenticate with Vercel (Couldn't get code)."; + return await vscode.window.showErrorMessage(msg); + } + const accessToken = await getTokenFromCode(code); + if (!accessToken) { + const msg = `Failed to authenticate with Vercel. (Couldn't get access token)`; + return await vscode.window.showErrorMessage(msg); + } + return accessToken; +}