xref: /expo/packages/@expo/cli/src/api/user/user.ts (revision 63fb203d)
1import { promises as fs } from 'fs';
2import gql from 'graphql-tag';
3
4import UserSettings from './UserSettings';
5import { getSessionUsingBrowserAuthFlowAsync } from './expoSsoLauncher';
6import { CurrentUserQuery } from '../../graphql/generated';
7import * as Log from '../../log';
8import * as Analytics from '../../utils/analytics/rudderstackClient';
9import { getDevelopmentCodeSigningDirectory } from '../../utils/codesigning';
10import { env } from '../../utils/env';
11import { getExpoWebsiteBaseUrl } from '../endpoint';
12import { graphqlClient } from '../graphql/client';
13import { UserQuery } from '../graphql/queries/UserQuery';
14import { fetchAsync } from '../rest/client';
15
16export type Actor = NonNullable<CurrentUserQuery['meActor']>;
17
18let currentUser: Actor | undefined;
19
20export const ANONYMOUS_USERNAME = 'anonymous';
21
22/**
23 * Resolve the name of the actor, either normal user or robot user.
24 * This should be used whenever the "current user" needs to be displayed.
25 * The display name CANNOT be used as project owner.
26 */
27export function getActorDisplayName(user?: Actor): string {
28  switch (user?.__typename) {
29    case 'User':
30      return user.username;
31    case 'SSOUser':
32      return user.username;
33    case 'Robot':
34      return user.firstName ? `${user.firstName} (robot)` : 'robot';
35    default:
36      return ANONYMOUS_USERNAME;
37  }
38}
39
40export async function getUserAsync(): Promise<Actor | undefined> {
41  const hasCredentials = UserSettings.getAccessToken() || UserSettings.getSession()?.sessionSecret;
42  if (!env.EXPO_OFFLINE && !currentUser && hasCredentials) {
43    const user = await UserQuery.currentUserAsync();
44    currentUser = user ?? undefined;
45    if (user) {
46      await Analytics.setUserDataAsync(user.id, {
47        username: getActorDisplayName(user),
48        user_id: user.id,
49        user_type: user.__typename,
50      });
51    }
52  }
53  return currentUser;
54}
55
56export async function loginAsync(json: {
57  username: string;
58  password: string;
59  otp?: string;
60}): Promise<void> {
61  const res = await fetchAsync('auth/loginAsync', {
62    method: 'POST',
63    body: JSON.stringify(json),
64  });
65  const {
66    data: { sessionSecret },
67  } = await res.json();
68
69  const userData = await fetchUserAsync({ sessionSecret });
70
71  await UserSettings.setSessionAsync({
72    sessionSecret,
73    userId: userData.id,
74    username: userData.username,
75    currentConnection: 'Username-Password-Authentication',
76  });
77}
78
79export async function ssoLoginAsync(): Promise<void> {
80  const sessionSecret = await getSessionUsingBrowserAuthFlowAsync({
81    expoWebsiteUrl: getExpoWebsiteBaseUrl(),
82  });
83  const userData = await fetchUserAsync({ sessionSecret });
84
85  await UserSettings.setSessionAsync({
86    sessionSecret,
87    userId: userData.id,
88    username: userData.username,
89    currentConnection: 'Browser-Flow-Authentication',
90  });
91}
92
93export async function logoutAsync(): Promise<void> {
94  currentUser = undefined;
95  await Promise.all([
96    fs.rm(getDevelopmentCodeSigningDirectory(), { recursive: true, force: true }),
97    UserSettings.setSessionAsync(undefined),
98  ]);
99  Log.log('Logged out');
100}
101
102async function fetchUserAsync({
103  sessionSecret,
104}: {
105  sessionSecret: string;
106}): Promise<{ id: string; username: string }> {
107  const result = await graphqlClient
108    .query(
109      gql`
110        query UserQuery {
111          meUserActor {
112            id
113            username
114          }
115        }
116      `,
117      {},
118      {
119        fetchOptions: {
120          headers: {
121            'expo-session': sessionSecret,
122          },
123        },
124        additionalTypenames: [] /* UserQuery has immutable fields */,
125      }
126    )
127    .toPromise();
128  const { data } = result;
129  return {
130    id: data.meUserActor.id,
131    username: data.meUserActor.username,
132  };
133}
134