Skip to content

Commit db3f422

Browse files
authored
Add "whoami" command (#572)
- [x] Merge replayio/docs#211 to add the new API keys redirect page ### [Loom demo](https://www.loom.com/share/448e43d722e84fae975a2013cbbd73cb) ### Screenshots | Scenario | | | :--- | :--- | | Not authenticated | ![Not authenticated](https://github.com/replayio/replay-cli/assets/29597/aa0c5ab6-feb8-4a16-9987-dbdc4f7538d0) | | Authenticated by email and password | ![Authenticated by email and password](https://github.com/replayio/replay-cli/assets/29597/2caee8ce-891b-4b06-907f-c33f2c721daf) | Team API key | ![Team API key](https://github.com/replayio/replay-cli/assets/29597/08df1946-cf6c-4bda-80a1-a11f124976db) | Personal API key | ![Personal API key](https://github.com/replayio/replay-cli/assets/29597/a39723cc-ad23-4efa-b6bd-a5ad30c0c5a7)
1 parent 18d4972 commit db3f422

File tree

13 files changed

+173
-33
lines changed

13 files changed

+173
-33
lines changed

.changeset/pink-lemons-bathe.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"replayio": patch
3+
---
4+
5+
Add "whoami" command to print information about the current user and API key

packages/replayio/src/bin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { initLogger } from "@replay-cli/shared/logger";
12
import { exitProcess } from "@replay-cli/shared/process/exitProcess";
23
import { setUserAgent } from "@replay-cli/shared/userAgent";
34
import { name, version } from "../package.json";
@@ -14,7 +15,7 @@ import "./commands/remove";
1415
import "./commands/update";
1516
import "./commands/upload";
1617
import "./commands/upload-source-maps";
17-
import { initLogger } from "@replay-cli/shared/logger";
18+
import "./commands/whoami";
1819

1920
initLogger(name, version);
2021

packages/replayio/src/commands/login.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
import { getAccessToken } from "@replay-cli/shared/authentication/getAccessToken";
12
import { exitProcess } from "@replay-cli/shared/process/exitProcess";
23
import { registerCommand } from "../utils/commander/registerCommand";
3-
import { checkAuthentication } from "../utils/initialization/checkAuthentication";
44
import { promptForAuthentication } from "../utils/initialization/promptForAuthentication";
55

66
registerCommand("login").description("Log into your Replay account (or register)").action(login);
77

88
async function login() {
9-
const authenticated = await checkAuthentication();
10-
if (authenticated) {
9+
const { accessToken } = await getAccessToken();
10+
if (accessToken) {
1111
console.log("You are already signed in!");
1212
} else {
1313
await promptForAuthentication();

packages/replayio/src/commands/logout.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ registerCommand("logout").description("Log out of your Replay account").action(l
99
async function logout() {
1010
await logoutIfAuthenticated();
1111

12-
const token = await getAccessToken();
13-
if (token) {
14-
const name = process.env.REPLAY_API_KEY ? "REPLAY_API_KEY" : "RECORD_REPLAY_API_KEY";
15-
12+
const { accessToken, apiKeySource } = await getAccessToken();
13+
if (accessToken && apiKeySource) {
1614
console.log(
17-
`You are now signed out but still authenticated via the ${highlight(name)} env variable`
15+
`You have been signed out but you are still authenticated by the ${highlight(
16+
apiKeySource
17+
)} env variable`
1818
);
1919
} else {
2020
console.log("You are now signed out");

packages/replayio/src/commands/upload-source-maps.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@ async function uploadSourceMaps(
4141
root?: string;
4242
}
4343
) {
44+
const { accessToken } = await getAccessToken();
4445
const uploadPromise = uploadSourceMapsExternal({
4546
extensions,
4647
filepaths: filePaths,
4748
group,
4849
ignore,
49-
key: await getAccessToken(),
50+
key: accessToken,
5051
root,
5152
server: replayApiServer,
5253
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { getAccessToken } from "@replay-cli/shared/authentication/getAccessToken";
2+
import { getAuthInfo } from "@replay-cli/shared/graphql/getAuthInfo";
3+
import { exitProcess } from "@replay-cli/shared/process/exitProcess";
4+
import { dim, emphasize, highlight, link } from "@replay-cli/shared/theme";
5+
import { name as packageName } from "../../package.json";
6+
import { registerCommand } from "../utils/commander/registerCommand";
7+
import { fetchViewerFromGraphQL } from "../utils/graphql/fetchViewerFromGraphQL";
8+
9+
registerCommand("whoami", {
10+
checkForNpmUpdate: false,
11+
checkForRuntimeUpdate: false,
12+
requireAuthentication: false,
13+
})
14+
.description("Display info about the current user")
15+
.action(info);
16+
17+
const DOCS_URL = "https://docs.replay.io/reference/api-keys";
18+
19+
async function info() {
20+
const { accessToken, apiKeySource } = await getAccessToken();
21+
if (accessToken) {
22+
const authInfo = await getAuthInfo(accessToken);
23+
24+
const { userEmail, userName, teamName } = await fetchViewerFromGraphQL(accessToken);
25+
26+
if (apiKeySource) {
27+
console.log(`You are authenticated by API key ${dim(`(process.env.${apiKeySource})`)}`);
28+
console.log("");
29+
if (authInfo.type === "user") {
30+
console.log(`This API key belongs to ${emphasize(userName)} (${userEmail})`);
31+
console.log(`Recordings you upload are ${emphasize("private")} by default`);
32+
} else {
33+
console.log(`This API key belongs to the team named ${emphasize(teamName)}`);
34+
console.log(`Recordings you upload are ${emphasize("shared")} with other team members`);
35+
}
36+
console.log("");
37+
console.log(`Learn more about API keys at ${link(DOCS_URL)}`);
38+
} else {
39+
console.log(`You signed in as ${emphasize(userName)} (${userEmail})`);
40+
console.log("");
41+
console.log(`Recordings you upload are ${emphasize("private")} by default`);
42+
console.log("");
43+
console.log(`Learn about other ways to sign in at ${link(DOCS_URL)}`);
44+
}
45+
} else {
46+
console.log("You are not authenticated");
47+
console.log("");
48+
console.log(`Sign in by running ${highlight(`${packageName} login`)}`);
49+
console.log("");
50+
console.log("You can also authenticate with an API key");
51+
console.log(`Learn more at ${link(DOCS_URL)}`);
52+
}
53+
54+
await exitProcess(0);
55+
}

packages/replayio/src/utils/browser/reportBrowserCrash.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import { getAccessToken } from "@replay-cli/shared/authentication/getAccessToken";
12
import { getReplayPath } from "@replay-cli/shared/getReplayPath";
23
import { logger } from "@replay-cli/shared/logger";
4+
import { getUserAgent } from "@replay-cli/shared/userAgent";
35
import { readFile, writeFileSync } from "fs-extra";
46
import { File, FormData, fetch } from "undici";
57
import { replayApiServer } from "../../config";
6-
import { getUserAgent } from "@replay-cli/shared/userAgent";
7-
import { checkAuthentication } from "../../utils/initialization/checkAuthentication";
88
import { getCurrentRuntimeMetadata } from "../../utils/initialization/getCurrentRuntimeMetadata";
99
import { runtimeMetadata } from "../../utils/installation/config";
1010
import { findMostRecentFile } from "../findMostRecentFile";
@@ -13,8 +13,7 @@ export async function reportBrowserCrash(stderr: string) {
1313
const errorLogPath = getReplayPath("recorder-crash.log");
1414
writeFileSync(errorLogPath, stderr, "utf8");
1515

16-
const accessToken = await checkAuthentication();
17-
16+
const { accessToken } = await getAccessToken();
1817
if (!accessToken) {
1918
return {
2019
errorLogPath,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { GraphQLError } from "@replay-cli/shared/graphql/GraphQLError";
2+
import { queryGraphQL } from "@replay-cli/shared/graphql/queryGraphQL";
3+
import { logger } from "@replay-cli/shared/logger";
4+
5+
export type AuthInfo = {
6+
userEmail: string | undefined;
7+
userName: string | undefined;
8+
teamName: string | undefined;
9+
};
10+
11+
export async function fetchViewerFromGraphQL(accessToken: string): Promise<AuthInfo> {
12+
logger.debug("Fetching viewer info from GraphQL");
13+
14+
const { data, errors } = await queryGraphQL(
15+
"ViewerInfo",
16+
`
17+
query ViewerInfo {
18+
viewer {
19+
email
20+
user {
21+
name
22+
}
23+
}
24+
auth {
25+
workspaces {
26+
edges {
27+
node {
28+
name
29+
}
30+
}
31+
}
32+
}
33+
}
34+
`,
35+
{},
36+
accessToken
37+
);
38+
39+
if (errors) {
40+
throw new GraphQLError("Failed to fetch auth info", errors);
41+
}
42+
43+
const response = data as {
44+
viewer: {
45+
email: string;
46+
user: {
47+
name: string;
48+
} | null;
49+
};
50+
auth: {
51+
workspaces: {
52+
edges: {
53+
node: {
54+
name: string;
55+
};
56+
}[];
57+
};
58+
};
59+
};
60+
61+
const { viewer, auth } = response;
62+
63+
return {
64+
userEmail: viewer?.email,
65+
userName: viewer?.user?.name,
66+
teamName: auth?.workspaces?.edges?.[0]?.node?.name,
67+
};
68+
}

packages/replayio/src/utils/initialization/checkAuthentication.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/replayio/src/utils/initialization/initialize.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { raceWithTimeout } from "@replay-cli/shared/async/raceWithTimeout";
2+
import { getAccessToken } from "@replay-cli/shared/authentication/getAccessToken";
23
import { initLaunchDarklyFromAccessToken } from "@replay-cli/shared/launch-darkly/initLaunchDarklyFromAccessToken";
34
import { initMixpanelForUserSession } from "@replay-cli/shared/mixpanel/initMixpanelForUserSession";
45
import { name as packageName, version as packageVersion } from "../../../package.json";
56
import { logPromise } from "../async/logPromise";
6-
import { checkAuthentication } from "./checkAuthentication";
77
import { checkForNpmUpdate } from "./checkForNpmUpdate";
88
import { checkForRuntimeUpdate } from "./checkForRuntimeUpdate";
99
import { promptForAuthentication } from "./promptForAuthentication";
@@ -22,7 +22,7 @@ export async function initialize({
2222
// These initialization steps can run in parallel to improve startup time
2323
// None of them should log anything though; that would interfere with the initialization-in-progress message
2424
const promises = Promise.all([
25-
checkAuthentication(),
25+
getAccessToken().then(({ accessToken }) => accessToken),
2626
shouldCheckForRuntimeUpdate
2727
? raceWithTimeout(checkForRuntimeUpdate(), 5_000)
2828
: Promise.resolve(),

0 commit comments

Comments
 (0)