xref: /expo/packages/@expo/cli/src/api/user/user.ts (revision f2de99f7)
1import { promises as fs } from 'fs';
2import gql from 'graphql-tag';
3
4import { CurrentUserQuery } from '../../graphql/generated';
5import * as Log from '../../log';
6import * as Analytics from '../../utils/analytics/rudderstackClient';
7import { getDevelopmentCodeSigningDirectory } from '../../utils/codesigning';
8import { env } from '../../utils/env';
9import { getExpoWebsiteBaseUrl, getSsoLocalServerPortAsync } from '../endpoint';
10import { graphqlClient } from '../graphql/client';
11import { UserQuery } from '../graphql/queries/UserQuery';
12import { fetchAsync } from '../rest/client';
13import UserSettings from './UserSettings';
14import { getSessionUsingBrowserAuthFlowAsync } from './expoSsoLauncher';
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 config = {
81    expoWebsiteUrl: getExpoWebsiteBaseUrl(),
82    serverPort: await getSsoLocalServerPortAsync(),
83  };
84  const sessionSecret = await getSessionUsingBrowserAuthFlowAsync(config);
85  const userData = await fetchUserAsync({ sessionSecret });
86
87  await UserSettings.setSessionAsync({
88    sessionSecret,
89    userId: userData.id,
90    username: userData.username,
91    currentConnection: 'Browser-Flow-Authentication',
92  });
93}
94
95export async function logoutAsync(): Promise<void> {
96  currentUser = undefined;
97  await Promise.all([
98    fs.rm(getDevelopmentCodeSigningDirectory(), { recursive: true, force: true }),
99    UserSettings.setSessionAsync(undefined),
100  ]);
101  Log.log('Logged out');
102}
103
104async function fetchUserAsync({
105  sessionSecret,
106}: {
107  sessionSecret: string;
108}): Promise<{ id: string; username: string }> {
109  const result = await graphqlClient
110    .query(
111      gql`
112        query UserQuery {
113          meUserActor {
114            id
115            username
116          }
117        }
118      `,
119      {},
120      {
121        fetchOptions: {
122          headers: {
123            'expo-session': sessionSecret,
124          },
125        },
126        additionalTypenames: [] /* UserQuery has immutable fields */,
127      }
128    )
129    .toPromise();
130  const { data } = result;
131  return {
132    id: data.meUserActor.id,
133    username: data.meUserActor.username,
134  };
135}
136