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