@expo/xdl
Version:
The Expo Development Library
569 lines (434 loc) • 14.5 kB
JavaScript
"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 _Analytics() {
const data = _interopRequireDefault(require("./Analytics"));
_Analytics = function () {
return data;
};
return data;
}
function _ApiV() {
const data = _interopRequireDefault(require("./ApiV2"));
_ApiV = function () {
return data;
};
return data;
}
function _Config() {
const data = _interopRequireDefault(require("./Config"));
_Config = function () {
return data;
};
return data;
}
function _Logger() {
const data = _interopRequireDefault(require("./Logger"));
_Logger = function () {
return data;
};
return data;
}
function _UserSettings() {
const data = _interopRequireDefault(require("./UserSettings"));
_UserSettings = function () {
return data;
};
return data;
}
function _Utils() {
const data = require("./Utils");
_Utils = function () {
return data;
};
return data;
}
function _XDLError() {
const data = _interopRequireDefault(require("./XDLError"));
_XDLError = 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 (_Utils().Semaphore)());
_defineProperty(this, "_interactiveAuthenticationCallbackAsync", void 0);
}
static getGlobalInstance() {
if (!__globalInstance) {
__globalInstance = new UserManagerInstance();
}
return __globalInstance;
}
initialize() {
this._currentUser = null;
this._getSessionLock = new (_Utils().Semaphore)();
}
/**
* 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 = _ApiV().default.clientForUser();
const loginResp = await apiAnonymous.postAsync('auth/loginAsync', {
username: loginArgs.username,
password: loginArgs.password,
otp: loginArgs.otp
});
if (loginResp.error) {
throw new (_XDLError().default)('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 (_XDLError().default)('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 (_Config().default.offline) {
throw new (_XDLError().default)('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 (_XDLError().default)('NOT_LOGGED_IN', 'Not logged in');
}
return user;
}
setInteractiveAuthenticationCallback(callback) {
this._interactiveAuthenticationCallbackAsync = callback;
}
async _readUserData() {
let auth = await _UserSettings().default.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 _UserSettings().default.getAsync('auth', null);
}
if (typeof auth === 'undefined') {
return null;
}
return auth;
}
/**
* 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 (_Config().default.offline) {
return null;
}
const data = await this._readUserData();
const accessToken = _UserSettings().default.accessToken(); // No token, no session, no current user. Need to login
if (!accessToken && !(data === null || data === void 0 ? 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)) {
_Logger().default.global.warn('Fetching the user profile failed');
_Logger().default.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 (_XDLError().default)('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 (_XDLError().default)('USER_ACCOUNT_ERROR', 'This action is not supported for normal users.');
}
return user;
}
async getCurrentUsernameAsync() {
const token = _UserSettings().default.accessToken();
if (token) {
const user = await this.getCurrentUserAsync();
if (user === null || user === void 0 ? void 0 : user.username) {
return user.username;
}
}
const data = await this._readUserData();
if (data === null || data === void 0 ? void 0 : data.username) {
return data.username;
}
return null;
}
async getSessionAsync() {
const token = _UserSettings().default.accessToken();
if (token) {
return {
accessToken: token
};
}
const data = await this._readUserData();
if (data === null || data === void 0 ? 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 (_XDLError().default)('ROBOT_ACCOUNT_ERROR', 'This action is not available for robot users');
}
const api = _ApiV().default.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 (_XDLError().default)('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 ? void 0 : _this$_currentUser2.accessToken)) {
_Analytics().default.logEvent('Logout', {
userId: this._currentUser.userId,
username: this._currentUser.username,
currentConnection: this._currentUser.currentConnection
});
}
this._currentUser = null; // Delete saved auth info
await _UserSettings().default.deleteKeyAsync('auth');
}
/**
* Forgot Password
*/
async forgotPasswordAsync(usernameOrEmail) {
const apiAnonymous = _ApiV().default.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 = _ApiV().default.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 _UserSettings().default.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
_Analytics().default.logEvent('Login', {
userId: user.userId,
currentConnection: user.currentConnection,
username: user.username
});
}
_Analytics().default.setUserProperties(user.username, {
userId: user.userId,
currentConnection: user.currentConnection,
username: user.username,
userType: user.kind
});
}
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=__sourcemaps__/User.js.map