Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Add Oauth login! #4

Merged
merged 1 commit into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 19 additions & 17 deletions src/commands/LogIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>("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();
}
}
6 changes: 5 additions & 1 deletion src/features/VercelManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
VercelEnvironmentInformation,
VercelResponse,
} from "./models";
import { getTokenOauth } from "../utils/oauth";

export class VercelManager {
public onDidEnvironmentsUpdated: () => void = () => {};
Expand Down Expand Up @@ -41,11 +42,14 @@ export class VercelManager {
};
}

async logIn(apiToken: string): Promise<void> {
async logIn(): Promise<boolean> {
const apiToken = await getTokenOauth();
if (!apiToken) return false;
await this.token.setAuth(apiToken);
this.onDidDeploymentsUpdated();
this.onDidEnvironmentsUpdated();
await this.token.onDidLogIn();
return true;
}

/**
Expand Down
80 changes: 80 additions & 0 deletions src/utils/oauth.ts
Original file line number Diff line number Diff line change
@@ -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<URL> {
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(
"<html><body>Authentication successful! You can now close this window.</body></html>"
);
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<string | undefined> {
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;
}