xref: /expo/packages/@expo/cli/src/api/user/actions.ts (revision 8a424beb)
18d307f52SEvan Baconimport assert from 'assert';
28d307f52SEvan Baconimport chalk from 'chalk';
38d307f52SEvan Bacon
4*8a424bebSJames Ideimport { retryUsernamePasswordAuthWithOTPAsync } from './otp';
5*8a424bebSJames Ideimport { Actor, getUserAsync, loginAsync, ssoLoginAsync } from './user';
68d307f52SEvan Baconimport * as Log from '../../log';
7e32ccf9fSEvan Baconimport { env } from '../../utils/env';
8e32ccf9fSEvan Baconimport { CommandError } from '../../utils/errors';
98d307f52SEvan Baconimport { learnMore } from '../../utils/link';
108d307f52SEvan Baconimport promptAsync, { Question } from '../../utils/prompts';
118d307f52SEvan Baconimport { ApiV2Error } from '../rest/client';
128d307f52SEvan Bacon
138d307f52SEvan Bacon/** Show login prompt while prompting for missing credentials. */
148d307f52SEvan Baconexport async function showLoginPromptAsync({
158d307f52SEvan Bacon  printNewLine = false,
168d307f52SEvan Bacon  otp,
178d307f52SEvan Bacon  ...options
188d307f52SEvan Bacon}: {
198d307f52SEvan Bacon  printNewLine?: boolean;
208d307f52SEvan Bacon  username?: string;
218d307f52SEvan Bacon  password?: string;
228d307f52SEvan Bacon  otp?: string;
23d88ac65dSlzkb  sso?: boolean | undefined;
248d307f52SEvan Bacon} = {}): Promise<void> {
25e32ccf9fSEvan Bacon  if (env.EXPO_OFFLINE) {
26e32ccf9fSEvan Bacon    throw new CommandError('OFFLINE', 'Cannot authenticate in offline-mode');
27e32ccf9fSEvan Bacon  }
288d307f52SEvan Bacon  const hasCredentials = options.username && options.password;
29d88ac65dSlzkb  const sso = options.sso;
308d307f52SEvan Bacon
318d307f52SEvan Bacon  if (printNewLine) {
328d307f52SEvan Bacon    Log.log();
338d307f52SEvan Bacon  }
348d307f52SEvan Bacon
35d88ac65dSlzkb  if (sso) {
36d88ac65dSlzkb    await ssoLoginAsync();
37d88ac65dSlzkb    return;
38d88ac65dSlzkb  }
39d88ac65dSlzkb
40d88ac65dSlzkb  Log.log(
41d88ac65dSlzkb    hasCredentials
42d88ac65dSlzkb      ? `Logging in to EAS with email or username (exit and run 'eas login' for other options)`
43d88ac65dSlzkb      : `Log in to EAS with email or username (exit and run 'eas login' for other options)`
44d88ac65dSlzkb  );
458d307f52SEvan Bacon
468d307f52SEvan Bacon  let username = options.username;
478d307f52SEvan Bacon  let password = options.password;
488d307f52SEvan Bacon
498d307f52SEvan Bacon  if (!hasCredentials) {
508d307f52SEvan Bacon    const resolved = await promptAsync(
518d307f52SEvan Bacon      [
528d307f52SEvan Bacon        !options.username && {
538d307f52SEvan Bacon          type: 'text',
548d307f52SEvan Bacon          name: 'username',
558d307f52SEvan Bacon          message: 'Email or username',
568d307f52SEvan Bacon        },
578d307f52SEvan Bacon        !options.password && {
588d307f52SEvan Bacon          type: 'password',
598d307f52SEvan Bacon          name: 'password',
608d307f52SEvan Bacon          message: 'Password',
618d307f52SEvan Bacon        },
628d307f52SEvan Bacon      ].filter(Boolean) as Question<string>[],
638d307f52SEvan Bacon      {
648d307f52SEvan Bacon        nonInteractiveHelp: `Use the EXPO_TOKEN environment variable to authenticate in CI (${learnMore(
658d307f52SEvan Bacon          'https://docs.expo.dev/accounts/programmatic-access/'
668d307f52SEvan Bacon        )})`,
678d307f52SEvan Bacon      }
688d307f52SEvan Bacon    );
6929975bfdSEvan Bacon    username ??= resolved.username;
7029975bfdSEvan Bacon    password ??= resolved.password;
718d307f52SEvan Bacon  }
7229975bfdSEvan Bacon  // This is just for the types.
7329975bfdSEvan Bacon  assert(username && password);
748d307f52SEvan Bacon
758d307f52SEvan Bacon  try {
768d307f52SEvan Bacon    await loginAsync({
778d307f52SEvan Bacon      username,
788d307f52SEvan Bacon      password,
798d307f52SEvan Bacon      otp,
808d307f52SEvan Bacon    });
818d307f52SEvan Bacon  } catch (e) {
828d307f52SEvan Bacon    if (e instanceof ApiV2Error && e.expoApiV2ErrorCode === 'ONE_TIME_PASSWORD_REQUIRED') {
838d307f52SEvan Bacon      await retryUsernamePasswordAuthWithOTPAsync(
848d307f52SEvan Bacon        username,
858d307f52SEvan Bacon        password,
868d307f52SEvan Bacon        e.expoApiV2ErrorMetadata as any
878d307f52SEvan Bacon      );
888d307f52SEvan Bacon    } else {
898d307f52SEvan Bacon      throw e;
908d307f52SEvan Bacon    }
918d307f52SEvan Bacon  }
928d307f52SEvan Bacon}
938d307f52SEvan Bacon
948d307f52SEvan Bacon/** Ensure the user is logged in, if not, prompt to login. */
958d307f52SEvan Baconexport async function ensureLoggedInAsync(): Promise<Actor> {
968d307f52SEvan Bacon  let user = await getUserAsync().catch(() => null);
978d307f52SEvan Bacon
988d307f52SEvan Bacon  if (!user) {
998d307f52SEvan Bacon    Log.warn(chalk.yellow`An Expo user account is required to proceed.`);
1008d307f52SEvan Bacon    await showLoginPromptAsync({ printNewLine: true });
1018d307f52SEvan Bacon    user = await getUserAsync();
1028d307f52SEvan Bacon  }
1038d307f52SEvan Bacon
1048d307f52SEvan Bacon  assert(user, 'User should be logged in');
1058d307f52SEvan Bacon  return user;
1068d307f52SEvan Bacon}
107