diff --git a/.changeset/cyan-comics-sneeze.md b/.changeset/cyan-comics-sneeze.md new file mode 100644 index 000000000000..fcf473755309 --- /dev/null +++ b/.changeset/cyan-comics-sneeze.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Add whoami command diff --git a/packages/wrangler/src/__tests__/index.test.ts b/packages/wrangler/src/__tests__/index.test.ts index 28de0d0f4457..daec88f662b2 100644 --- a/packages/wrangler/src/__tests__/index.test.ts +++ b/packages/wrangler/src/__tests__/index.test.ts @@ -17,6 +17,7 @@ describe("wrangler", () => { Commands: wrangler init [name] 📥 Create a wrangler.toml configuration file + wrangler whoami 🕵️ Retrieve your user info and test your auth config wrangler dev 👂 Start a local server for developing your worker wrangler publish [script] 🆙 Publish your Worker to Cloudflare. wrangler tail [name] 🦚 Starts a log tailing session for a deployed Worker. @@ -49,6 +50,7 @@ describe("wrangler", () => { Commands: wrangler init [name] 📥 Create a wrangler.toml configuration file + wrangler whoami 🕵️ Retrieve your user info and test your auth config wrangler dev 👂 Start a local server for developing your worker wrangler publish [script] 🆙 Publish your Worker to Cloudflare. wrangler tail [name] 🦚 Starts a log tailing session for a deployed Worker. diff --git a/packages/wrangler/src/__tests__/whoami.test.tsx b/packages/wrangler/src/__tests__/whoami.test.tsx new file mode 100644 index 000000000000..d085a6b917b5 --- /dev/null +++ b/packages/wrangler/src/__tests__/whoami.test.tsx @@ -0,0 +1,130 @@ +import React from "react"; +import os from "node:os"; +import path from "node:path"; +import { render } from "ink-testing-library"; +import type { UserInfo } from "../whoami"; +import { getUserInfo, WhoAmI } from "../whoami"; +import { runInTempDir } from "./run-in-tmp"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { setMockResponse } from "./mock-cfetch"; +import { initialise } from "../user"; + +const ORIGINAL_CF_API_TOKEN = process.env.CF_API_TOKEN; +const ORIGINAL_CF_ACCOUNT_ID = process.env.CF_ACCOUNT_ID; + +describe("getUserInfo()", () => { + runInTempDir(); + + beforeEach(() => { + // Clear the environment variables, so we can control them in the tests + delete process.env.CF_API_TOKEN; + delete process.env.CF_ACCOUNT_ID; + // Override where the home directory is so that we can specify a user config + mkdirSync("./home"); + jest.spyOn(os, "homedir").mockReturnValue("./home"); + }); + + afterEach(() => { + // Reset any changes to the environment variables + process.env.CF_API_TOKEN = ORIGINAL_CF_API_TOKEN; + process.env.CF_ACCOUNT_ID = ORIGINAL_CF_ACCOUNT_ID; + }); + + it("should return undefined if there is no config file", async () => { + await initialise(); + const userInfo = await getUserInfo(); + expect(userInfo).toBeUndefined(); + }); + + it("should return undefined if there is an empty config file", async () => { + writeUserConfig(); + await initialise(); + const userInfo = await getUserInfo(); + expect(userInfo).toBeUndefined(); + }); + + it("should return the user's email and accounts if authenticated via config token", async () => { + writeUserConfig("some-oauth-token"); + setMockResponse("/user", () => { + return { email: "user@example.com" }; + }); + setMockResponse("/accounts", () => { + return [ + { name: "Account One", id: "account-1" }, + { name: "Account Two", id: "account-2" }, + { name: "Account Three", id: "account-3" }, + ]; + }); + + await initialise(); + const userInfo = await getUserInfo(); + + expect(userInfo).toEqual({ + authType: "OAuth", + apiToken: "some-oauth-token", + email: "user@example.com", + accounts: [ + { name: "Account One", id: "account-1" }, + { name: "Account Two", id: "account-2" }, + { name: "Account Three", id: "account-3" }, + ], + }); + }); +}); + +describe("WhoAmI component", () => { + it("should return undefined if there is no user", async () => { + const { lastFrame } = render(); + + expect(lastFrame()).toMatchInlineSnapshot( + `"You are not authenticated. Please run \`wrangler login\`."` + ); + }); + + it("should display the user's email and accounts", async () => { + const user: UserInfo = { + authType: "OAuth", + apiToken: "some-oauth-token", + email: "user@example.com", + accounts: [ + { name: "Account One", id: "account-1" }, + { name: "Account Two", id: "account-2" }, + { name: "Account Three", id: "account-3" }, + ], + }; + + const { lastFrame } = render(); + + expect(lastFrame()).toContain( + "You are logged in with an OAuth Token, associated with the email 'user@example.com'!" + ); + expect(lastFrame()).toMatch(/Account Name .+ Account ID/); + expect(lastFrame()).toMatch(/Account One .+ account-1/); + expect(lastFrame()).toMatch(/Account Two .+ account-2/); + expect(lastFrame()).toMatch(/Account Three .+ account-3/); + }); +}); + +function writeUserConfig( + oauth_token?: string, + refresh_token?: string, + expiration_time?: string +) { + const lines: string[] = []; + if (oauth_token) { + lines.push(`oauth_token = "${oauth_token}"`); + } + if (refresh_token) { + lines.push(`refresh_token = "${refresh_token}"`); + } + if (expiration_time) { + lines.push(`expiration_time = "${expiration_time}"`); + } + const configPath = path.join(os.homedir(), ".wrangler/config"); + mkdirSync(configPath, { recursive: true }); + writeFileSync( + path.join(configPath, "default.toml"), + lines.join("\n"), + "utf-8" + ); +} diff --git a/packages/wrangler/src/index.tsx b/packages/wrangler/src/index.tsx index d6e621e8921b..660be9b4f939 100644 --- a/packages/wrangler/src/index.tsx +++ b/packages/wrangler/src/index.tsx @@ -45,6 +45,7 @@ import onExit from "signal-exit"; import { setTimeout } from "node:timers/promises"; import * as fs from "node:fs"; import { execa } from "execa"; +import { whoami } from "./whoami"; const resetColor = "\x1b[0m"; const fgGreenColor = "\x1b[32m"; @@ -364,11 +365,10 @@ export async function main(argv: string[]): Promise { // whoami wrangler.command( "whoami", - false, // we don't need to show this the menu - // "🕵️ Retrieve your user info and test your auth config", + "🕵️ Retrieve your user info and test your auth config", () => {}, - (args) => { - console.log(":whoami", args); + async () => { + await whoami(); } ); diff --git a/packages/wrangler/src/whoami.tsx b/packages/wrangler/src/whoami.tsx new file mode 100644 index 000000000000..241e88566ccc --- /dev/null +++ b/packages/wrangler/src/whoami.tsx @@ -0,0 +1,69 @@ +import { Text, render } from "ink"; +import Table from "ink-table"; +import React from "react"; +import { fetchListResult, fetchResult } from "./cfetch"; +import { getAPIToken } from "./user"; + +export async function whoami() { + console.log("Getting User settings..."); + const user = await getUserInfo(); + render(); +} + +export function WhoAmI({ user }: { user: UserInfo | undefined }) { + return user ? ( + <> + + + + ) : ( + You are not authenticated. Please run `wrangler login`. + ); +} + +function Email(props: { tokenType: string; email: string }) { + return ( + + 👋 You are logged in with an {props.tokenType} Token, associated with the + email '{props.email}'! + + ); +} + +function Accounts(props: { accounts: AccountInfo[] }) { + const accounts = props.accounts.map((account) => ({ + "Account Name": account.name, + "Account ID": account.id, + })); + return
; +} + +export interface UserInfo { + apiToken: string; + authType: string; + email: string; + accounts: AccountInfo[]; +} + +export async function getUserInfo(): Promise { + const apiToken = getAPIToken(); + return apiToken + ? { + apiToken, + authType: "OAuth", + email: await getEmail(), + accounts: await getAccounts(), + } + : undefined; +} + +async function getEmail(): Promise { + const { email } = await fetchResult<{ email: string }>("/user"); + return email; +} + +type AccountInfo = { name: string; id: string }; + +async function getAccounts(): Promise { + return await fetchListResult("/accounts"); +}