You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

469 lines
15 KiB

"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.UserManagerInstance = exports.ANONYMOUS_USERNAME = void 0;
function _camelCase() {
const data = _interopRequireDefault(require("lodash/camelCase"));
_camelCase = function () {
return data;
};
return data;
}
function _isEmpty() {
const data = _interopRequireDefault(require("lodash/isEmpty"));
_isEmpty = function () {
return data;
};
return data;
}
function _snakeCase() {
const data = _interopRequireDefault(require("lodash/snakeCase"));
_snakeCase = function () {
return data;
};
return data;
}
function _internal() {
const data = require("./internal");
_internal = function () {
return data;
};
return data;
}
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
const ANONYMOUS_USERNAME = 'anonymous';
exports.ANONYMOUS_USERNAME = ANONYMOUS_USERNAME;
class UserManagerInstance {
constructor() {
_defineProperty(this, "_currentUser", null);
_defineProperty(this, "_getSessionLock", new (_internal().Semaphore)());
_defineProperty(this, "_interactiveAuthenticationCallbackAsync", void 0);
_defineProperty(this, "getCachedUserDataAsync", async () => {
await this._getSessionLock.acquire();
try {
const currentUser = this._currentUser;
// If user is cached and there is an accessToken or sessionSecret, return the user
if (currentUser && (currentUser.accessToken || currentUser.sessionSecret)) {
return currentUser;
}
const userData = await this._readUserData();
// // No token, no session, no current user. Need to login
if (!(userData !== null && userData !== void 0 && userData.sessionSecret)) {
return null;
}
return userData;
} catch (e) {
_internal().Logger.global.warn(e);
return null;
} finally {
this._getSessionLock.release();
}
});
}
static getGlobalInstance() {
if (!__globalInstance) {
__globalInstance = new UserManagerInstance();
}
return __globalInstance;
}
initialize() {
this._currentUser = null;
this._getSessionLock = new (_internal().Semaphore)();
}
/**
* Get the account and project name using a user and Expo config.
* This will validate if the owner field is set when using a robot account.
*/
getProjectOwner(user, exp) {
if (user.kind === 'robot' && !exp.owner) {
throw new (_internal().XDLError)('ROBOT_OWNER_ERROR', 'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner');
}
return exp.owner || user.username;
}
/**
* Logs in a user for a given login type.
*
* Valid login types are:
* - "user-pass": Username and password authentication
*
* If the login type is "user-pass", we directly make the request to www
* to login a user.
*/
async loginAsync(loginType, loginArgs) {
if (loginType === 'user-pass') {
if (!loginArgs) {
throw new Error(`The 'user-pass' login type requires a username and password.`);
}
const apiAnonymous = _internal().ApiV2.clientForUser();
const loginResp = await apiAnonymous.postAsync('auth/loginAsync', {
username: loginArgs.username,
password: loginArgs.password,
otp: loginArgs.otp
});
if (loginResp.error) {
throw new (_internal().XDLError)('INVALID_USERNAME_PASSWORD', loginResp['error_description']);
}
const user = await this._getProfileAsync({
currentConnection: 'Username-Password-Authentication',
sessionSecret: loginResp.sessionSecret
});
return user;
} else {
throw new Error(`Invalid login type provided. Must be 'user-pass'.`);
}
}
async registerAsync(userData, user = null) {
let actor = user;
if (!actor) {
actor = await this.getCurrentUserAsync();
}
if (actor) {
await this.logoutAsync();
actor = null;
}
try {
// Create or update the profile
let registeredUser = await this.createOrUpdateUserAsync({
connection: 'Username-Password-Authentication',
// Always create/update username password
email: userData.email,
givenName: userData.givenName,
familyName: userData.familyName,
username: userData.username,
password: userData.password
});
registeredUser = await this.loginAsync('user-pass', {
username: userData.username,
password: userData.password
});
return registeredUser;
} catch (e) {
console.error(e);
throw new (_internal().XDLError)('REGISTRATION_ERROR', 'Error registering user: ' + e.message);
}
}
/**
* Ensure user is logged in and has a valid token.
*
* If there are any issues with the login, this method throws.
*/
async ensureLoggedInAsync() {
if (_internal().ConnectionStatus.isOffline()) {
throw new (_internal().XDLError)('NETWORK_REQUIRED', "Can't verify user without network access");
}
let user = await this.getCurrentUserAsync({
silent: true
});
if (!user && this._interactiveAuthenticationCallbackAsync) {
user = await this._interactiveAuthenticationCallbackAsync();
}
if (!user) {
throw new (_internal().XDLError)('NOT_LOGGED_IN', 'Not logged in');
}
return user;
}
setInteractiveAuthenticationCallback(callback) {
this._interactiveAuthenticationCallbackAsync = callback;
}
async _readUserData() {
let auth = await _internal().UserSettings.getAsync('auth', null);
if ((0, _isEmpty().default)(auth)) {
// XXX(ville):
// We sometimes read an empty string from ~/.expo/state.json,
// even though it has valid credentials in it.
// We don't know why.
// An empty string can't be parsed as JSON, so an empty default object is returned.
// In this case, retrying usually helps.
auth = await _internal().UserSettings.getAsync('auth', null);
}
if (typeof auth === 'undefined') {
return null;
}
return auth;
}
/**
* Returns cached user data without hitting our backend. Only works for 'Username-Password-Authentication' flow. Does not work with 'Access-Token-Authentication' flow.
*/
/**
* Get the current user based on the available token.
* If there is no current token, returns null.
*/
async getCurrentUserAsync(options) {
await this._getSessionLock.acquire();
try {
const currentUser = this._currentUser;
// If user is cached and there is an accessToken or sessionSecret, return the user
if (currentUser && (currentUser.accessToken || currentUser.sessionSecret)) {
return currentUser;
}
if (_internal().ConnectionStatus.isOffline()) {
return null;
}
const data = await this._readUserData();
const accessToken = _internal().UserSettings.accessToken();
// No token, no session, no current user. Need to login
if (!accessToken && !(data !== null && data !== void 0 && data.sessionSecret)) {
return null;
}
try {
if (accessToken) {
return await this._getProfileAsync({
accessToken,
currentConnection: 'Access-Token-Authentication'
});
}
return await this._getProfileAsync({
currentConnection: data === null || data === void 0 ? void 0 : data.currentConnection,
sessionSecret: data === null || data === void 0 ? void 0 : data.sessionSecret
});
} catch (e) {
if (!(options && options.silent)) {
_internal().Logger.global.warn('Fetching the user profile failed');
_internal().Logger.global.warn(e);
}
if (e.code === 'UNAUTHORIZED_ERROR') {
return null;
}
throw e;
}
} finally {
this._getSessionLock.release();
}
}
/**
* Get the current user and check if it's a robot.
* If the user is not a robot, it will throw an error.
*/
async getCurrentUserOnlyAsync() {
const user = await this.getCurrentUserAsync();
if (user && user.kind !== 'user') {
throw new (_internal().XDLError)('ROBOT_ACCOUNT_ERROR', 'This action is not supported for robot users.');
}
return user;
}
/**
* Get the current user and check if it's a robot.
* If the user is not a robot, it will throw an error.
*/
async getCurrentRobotUserOnlyAsync() {
const user = await this.getCurrentUserAsync();
if (user && user.kind !== 'robot') {
throw new (_internal().XDLError)('USER_ACCOUNT_ERROR', 'This action is not supported for normal users.');
}
return user;
}
async getCurrentUsernameAsync() {
const token = _internal().UserSettings.accessToken();
if (token) {
const user = await this.getCurrentUserAsync();
if (user !== null && user !== void 0 && user.username) {
return user.username;
}
}
const data = await this._readUserData();
if (data !== null && data !== void 0 && data.username) {
return data.username;
}
return null;
}
async getSessionAsync() {
const token = _internal().UserSettings.accessToken();
if (token) {
return {
accessToken: token
};
}
const data = await this._readUserData();
if (data !== null && data !== void 0 && data.sessionSecret) {
return {
sessionSecret: data.sessionSecret
};
}
return null;
}
/**
* Create or update a user.
*/
async createOrUpdateUserAsync(userData) {
var _currentUser;
let currentUser = this._currentUser;
if (!currentUser) {
// attempt to get the current user
currentUser = await this.getCurrentUserAsync();
}
if (((_currentUser = currentUser) === null || _currentUser === void 0 ? void 0 : _currentUser.kind) === 'robot') {
throw new (_internal().XDLError)('ROBOT_ACCOUNT_ERROR', 'This action is not available for robot users');
}
const api = _internal().ApiV2.clientForUser(currentUser);
const {
user: updatedUser
} = await api.postAsync('auth/createOrUpdateUser', {
userData: _prepareAuth0Profile(userData)
});
this._currentUser = {
...this._currentUser,
..._parseAuth0Profile(updatedUser),
kind: 'user'
};
return this._currentUser;
}
/**
* Logout
*/
async logoutAsync() {
var _this$_currentUser, _this$_currentUser2;
if (((_this$_currentUser = this._currentUser) === null || _this$_currentUser === void 0 ? void 0 : _this$_currentUser.kind) === 'robot') {
throw new (_internal().XDLError)('ROBOT_ACCOUNT_ERROR', 'This action is not available for robot users');
}
// Only send logout events events for users without access tokens
if (this._currentUser && !((_this$_currentUser2 = this._currentUser) !== null && _this$_currentUser2 !== void 0 && _this$_currentUser2.accessToken)) {
_internal().Analytics.logEvent('Logout', {
userId: this._currentUser.userId,
currentConnection: this._currentUser.currentConnection
});
}
this._currentUser = null;
// Delete saved auth info
await _internal().UserSettings.deleteKeyAsync('auth');
}
async getFeatureGatingAsync() {
const user = await this.ensureLoggedInAsync();
const api = _internal().ApiV2.clientForUser(user);
const {
featureGates
} = await api.getAsync('auth/user-feature-gates');
return new (_internal().FeatureGating)(featureGates, new (_internal().FeatureGateEnvOverrides)());
}
/**
* Forgot Password
*/
async forgotPasswordAsync(usernameOrEmail) {
const apiAnonymous = _internal().ApiV2.clientForUser();
return apiAnonymous.postAsync('auth/forgotPasswordAsync', {
usernameOrEmail
});
}
/**
* Get profile given token data. Errors if token is not valid or if no
* user profile is returned.
*
* This method is called by all public authentication methods of `UserManager`
* except `logoutAsync`. Therefore, we use this method as a way to:
* - update the UserSettings store with the current token and user id
* - update UserManager._currentUser
* - Fire login analytics events
*
* Also updates UserManager._currentUser.
*
* @private
*/
async _getProfileAsync({
currentConnection,
sessionSecret,
accessToken
}) {
let user;
const api = _internal().ApiV2.clientForUser({
sessionSecret,
accessToken
});
user = await api.getAsync('auth/userInfo');
if (!user) {
throw new Error('Unable to fetch user.');
}
user = {
..._parseAuth0Profile(user),
// We need to inherit the "robot" type only, the rest is considered "user" but returned as "person".
kind: user.user_type === 'robot' ? 'robot' : 'user',
currentConnection,
sessionSecret,
accessToken
};
// Create a "username" to use in current terminal UI (e.g. expo whoami)
if (user.kind === 'robot') {
user.username = user.givenName ? `${user.givenName} (robot)` : 'robot';
}
// note: do not persist the authorization token, must be env-var only
if (!accessToken) {
await _internal().UserSettings.setAsync('auth', {
userId: user.userId,
username: user.username,
currentConnection,
sessionSecret
});
}
// If no currentUser, or currentUser.id differs from profiles
// user id, that means we have a new login
if ((!this._currentUser || this._currentUser.userId !== user.userId) && user.username && user.username !== '') {
if (!accessToken) {
// Only send login events for users without access tokens
_internal().Analytics.logEvent('Login', {
userId: user.userId,
currentConnection: user.currentConnection
});
}
_internal().UnifiedAnalytics.identifyUser(user.userId, {
userId: user.userId,
currentConnection: user.currentConnection,
username: user.username,
userType: user.kind,
primaryAccountId: user.primaryAccountId
});
_internal().Analytics.identifyUser(user.userId, {
userId: user.userId,
currentConnection: user.currentConnection,
username: user.username,
userType: user.kind,
primaryAccountId: user.primaryAccountId
});
}
this._currentUser = user;
return user;
}
}
exports.UserManagerInstance = UserManagerInstance;
let __globalInstance;
var _default = UserManagerInstance.getGlobalInstance();
/** Private Methods **/
exports.default = _default;
function _parseAuth0Profile(rawProfile) {
if (!rawProfile || typeof rawProfile !== 'object') {
return rawProfile;
}
return Object.keys(rawProfile).reduce((p, key) => {
p[(0, _camelCase().default)(key)] = _parseAuth0Profile(rawProfile[key]);
return p;
}, {});
}
function _prepareAuth0Profile(niceProfile) {
if (typeof niceProfile !== 'object') {
return niceProfile;
}
return Object.keys(niceProfile).reduce((p, key) => {
p[(0, _snakeCase().default)(key)] = _prepareAuth0Profile(niceProfile[key]);
return p;
}, {});
}
//# sourceMappingURL=User.js.map