xref: /expo/packages/@expo/cli/src/api/user/actions.ts (revision fe1ef024)
1import assert from 'assert';
2import chalk from 'chalk';
3
4import * as Log from '../../log';
5import { env } from '../../utils/env';
6import { CommandError } from '../../utils/errors';
7import { learnMore } from '../../utils/link';
8import promptAsync, { Question } from '../../utils/prompts';
9import { ApiV2Error } from '../rest/client';
10import { retryUsernamePasswordAuthWithOTPAsync } from './otp';
11import { Actor, getUserAsync, loginAsync, ssoLoginAsync } from './user';
12
13/** Show login prompt while prompting for missing credentials. */
14export async function showLoginPromptAsync({
15  printNewLine = false,
16  otp,
17  ...options
18}: {
19  printNewLine?: boolean;
20  username?: string;
21  password?: string;
22  otp?: string;
23  sso?: boolean | undefined;
24} = {}): Promise<void> {
25  if (env.EXPO_OFFLINE) {
26    throw new CommandError('OFFLINE', 'Cannot authenticate in offline-mode');
27  }
28  const hasCredentials = options.username && options.password;
29  const sso = options.sso;
30
31  if (printNewLine) {
32    Log.log();
33  }
34
35  if (sso) {
36    await ssoLoginAsync();
37    return;
38  }
39
40  Log.log(
41    hasCredentials
42      ? `Logging in to EAS with email or username (exit and run 'eas login' for other options)`
43      : `Log in to EAS with email or username (exit and run 'eas login' for other options)`
44  );
45
46  let username = options.username;
47  let password = options.password;
48
49  if (!hasCredentials) {
50    const resolved = await promptAsync(
51      [
52        !options.username && {
53          type: 'text',
54          name: 'username',
55          message: 'Email or username',
56        },
57        !options.password && {
58          type: 'password',
59          name: 'password',
60          message: 'Password',
61        },
62      ].filter(Boolean) as Question<string>[],
63      {
64        nonInteractiveHelp: `Use the EXPO_TOKEN environment variable to authenticate in CI (${learnMore(
65          'https://docs.expo.dev/accounts/programmatic-access/'
66        )})`,
67      }
68    );
69    username ??= resolved.username;
70    password ??= resolved.password;
71  }
72  // This is just for the types.
73  assert(username && password);
74
75  try {
76    await loginAsync({
77      username,
78      password,
79      otp,
80    });
81  } catch (e) {
82    if (e instanceof ApiV2Error && e.expoApiV2ErrorCode === 'ONE_TIME_PASSWORD_REQUIRED') {
83      await retryUsernamePasswordAuthWithOTPAsync(
84        username,
85        password,
86        e.expoApiV2ErrorMetadata as any
87      );
88    } else {
89      throw e;
90    }
91  }
92}
93
94/** Ensure the user is logged in, if not, prompt to login. */
95export async function ensureLoggedInAsync(): Promise<Actor> {
96  let user = await getUserAsync().catch(() => null);
97
98  if (!user) {
99    Log.warn(chalk.yellow`An Expo user account is required to proceed.`);
100    await showLoginPromptAsync({ printNewLine: true });
101    user = await getUserAsync();
102  }
103
104  assert(user, 'User should be logged in');
105  return user;
106}
107