xref: /expo/packages/@expo/cli/src/api/user/user.ts (revision 8a424beb)
1e377ff85SWill Schurmanimport { promises as fs } from 'fs';
28d307f52SEvan Baconimport gql from 'graphql-tag';
38d307f52SEvan Bacon
4*8a424bebSJames Ideimport UserSettings from './UserSettings';
5*8a424bebSJames Ideimport { getSessionUsingBrowserAuthFlowAsync } from './expoSsoLauncher';
68fd977adSWill Schurmanimport { CurrentUserQuery } from '../../graphql/generated';
78d307f52SEvan Baconimport * as Log from '../../log';
88d307f52SEvan Baconimport * as Analytics from '../../utils/analytics/rudderstackClient';
9e377ff85SWill Schurmanimport { getDevelopmentCodeSigningDirectory } from '../../utils/codesigning';
10e32ccf9fSEvan Baconimport { env } from '../../utils/env';
110b7c94a9SWill Schurmanimport { getExpoWebsiteBaseUrl } from '../endpoint';
128d307f52SEvan Baconimport { graphqlClient } from '../graphql/client';
138d307f52SEvan Baconimport { UserQuery } from '../graphql/queries/UserQuery';
148d307f52SEvan Baconimport { fetchAsync } from '../rest/client';
158d307f52SEvan Bacon
168d307f52SEvan Baconexport type Actor = NonNullable<CurrentUserQuery['meActor']>;
178d307f52SEvan Bacon
188d307f52SEvan Baconlet currentUser: Actor | undefined;
198d307f52SEvan Bacon
208d307f52SEvan Baconexport const ANONYMOUS_USERNAME = 'anonymous';
218d307f52SEvan Bacon
228d307f52SEvan Bacon/**
238d307f52SEvan Bacon * Resolve the name of the actor, either normal user or robot user.
248d307f52SEvan Bacon * This should be used whenever the "current user" needs to be displayed.
258d307f52SEvan Bacon * The display name CANNOT be used as project owner.
268d307f52SEvan Bacon */
278d307f52SEvan Baconexport function getActorDisplayName(user?: Actor): string {
288d307f52SEvan Bacon  switch (user?.__typename) {
298d307f52SEvan Bacon    case 'User':
308d307f52SEvan Bacon      return user.username;
31d88ac65dSlzkb    case 'SSOUser':
32d88ac65dSlzkb      return user.username;
338d307f52SEvan Bacon    case 'Robot':
348d307f52SEvan Bacon      return user.firstName ? `${user.firstName} (robot)` : 'robot';
358d307f52SEvan Bacon    default:
368d307f52SEvan Bacon      return ANONYMOUS_USERNAME;
378d307f52SEvan Bacon  }
388d307f52SEvan Bacon}
398d307f52SEvan Bacon
408d307f52SEvan Baconexport async function getUserAsync(): Promise<Actor | undefined> {
4174e3651eSCedric van Putten  const hasCredentials = UserSettings.getAccessToken() || UserSettings.getSession()?.sessionSecret;
42e32ccf9fSEvan Bacon  if (!env.EXPO_OFFLINE && !currentUser && hasCredentials) {
438d307f52SEvan Bacon    const user = await UserQuery.currentUserAsync();
448d307f52SEvan Bacon    currentUser = user ?? undefined;
458d307f52SEvan Bacon    if (user) {
468d307f52SEvan Bacon      await Analytics.setUserDataAsync(user.id, {
478d307f52SEvan Bacon        username: getActorDisplayName(user),
488d307f52SEvan Bacon        user_id: user.id,
498d307f52SEvan Bacon        user_type: user.__typename,
508d307f52SEvan Bacon      });
518d307f52SEvan Bacon    }
528d307f52SEvan Bacon  }
538d307f52SEvan Bacon  return currentUser;
548d307f52SEvan Bacon}
558d307f52SEvan Bacon
568d307f52SEvan Baconexport async function loginAsync(json: {
578d307f52SEvan Bacon  username: string;
588d307f52SEvan Bacon  password: string;
598d307f52SEvan Bacon  otp?: string;
608d307f52SEvan Bacon}): Promise<void> {
618d307f52SEvan Bacon  const res = await fetchAsync('auth/loginAsync', {
628d307f52SEvan Bacon    method: 'POST',
638d307f52SEvan Bacon    body: JSON.stringify(json),
648d307f52SEvan Bacon  });
658d307f52SEvan Bacon  const {
668d307f52SEvan Bacon    data: { sessionSecret },
678d307f52SEvan Bacon  } = await res.json();
68d88ac65dSlzkb
69d88ac65dSlzkb  const userData = await fetchUserAsync({ sessionSecret });
70d88ac65dSlzkb
71d88ac65dSlzkb  await UserSettings.setSessionAsync({
72d88ac65dSlzkb    sessionSecret,
73d88ac65dSlzkb    userId: userData.id,
74d88ac65dSlzkb    username: userData.username,
75d88ac65dSlzkb    currentConnection: 'Username-Password-Authentication',
76d88ac65dSlzkb  });
77d88ac65dSlzkb}
78d88ac65dSlzkb
79d88ac65dSlzkbexport async function ssoLoginAsync(): Promise<void> {
800b7c94a9SWill Schurman  const sessionSecret = await getSessionUsingBrowserAuthFlowAsync({
81d88ac65dSlzkb    expoWebsiteUrl: getExpoWebsiteBaseUrl(),
820b7c94a9SWill Schurman  });
83d88ac65dSlzkb  const userData = await fetchUserAsync({ sessionSecret });
84d88ac65dSlzkb
85d88ac65dSlzkb  await UserSettings.setSessionAsync({
86d88ac65dSlzkb    sessionSecret,
87d88ac65dSlzkb    userId: userData.id,
88d88ac65dSlzkb    username: userData.username,
89d88ac65dSlzkb    currentConnection: 'Browser-Flow-Authentication',
90d88ac65dSlzkb  });
91d88ac65dSlzkb}
92d88ac65dSlzkb
93d88ac65dSlzkbexport async function logoutAsync(): Promise<void> {
94d88ac65dSlzkb  currentUser = undefined;
95d88ac65dSlzkb  await Promise.all([
96d88ac65dSlzkb    fs.rm(getDevelopmentCodeSigningDirectory(), { recursive: true, force: true }),
97d88ac65dSlzkb    UserSettings.setSessionAsync(undefined),
98d88ac65dSlzkb  ]);
99d88ac65dSlzkb  Log.log('Logged out');
100d88ac65dSlzkb}
101d88ac65dSlzkb
102d88ac65dSlzkbasync function fetchUserAsync({
103d88ac65dSlzkb  sessionSecret,
104d88ac65dSlzkb}: {
105d88ac65dSlzkb  sessionSecret: string;
106d88ac65dSlzkb}): Promise<{ id: string; username: string }> {
1078d307f52SEvan Bacon  const result = await graphqlClient
1088d307f52SEvan Bacon    .query(
1098d307f52SEvan Bacon      gql`
1108d307f52SEvan Bacon        query UserQuery {
111d88ac65dSlzkb          meUserActor {
1128d307f52SEvan Bacon            id
1138d307f52SEvan Bacon            username
1148d307f52SEvan Bacon          }
1158d307f52SEvan Bacon        }
1168d307f52SEvan Bacon      `,
1178d307f52SEvan Bacon      {},
1188d307f52SEvan Bacon      {
1198d307f52SEvan Bacon        fetchOptions: {
1208d307f52SEvan Bacon          headers: {
1218d307f52SEvan Bacon            'expo-session': sessionSecret,
1228d307f52SEvan Bacon          },
1238d307f52SEvan Bacon        },
1248d307f52SEvan Bacon        additionalTypenames: [] /* UserQuery has immutable fields */,
1258d307f52SEvan Bacon      }
1268d307f52SEvan Bacon    )
1278d307f52SEvan Bacon    .toPromise();
128d88ac65dSlzkb  const { data } = result;
129d88ac65dSlzkb  return {
130d88ac65dSlzkb    id: data.meUserActor.id,
131d88ac65dSlzkb    username: data.meUserActor.username,
132d88ac65dSlzkb  };
1338d307f52SEvan Bacon}
134