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