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
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
|