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