packages/skygear-core/lib/auth.js
/**
* Copyright 2015 Oursky Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const _ = require('lodash');
import {EventHandle, toJSON} from './util';
import {ErrorCodes} from './error';
import Role from './role';
export const USER_CHANGED = 'userChanged';
/**
* Auth container
*
* Provides User authentications and user roles API.
*/
export class AuthContainer {
constructor(container) {
/**
* @private
*/
this.container = container;
this._accessToken = null;
this._user = null;
}
/**
* Currently logged-in user
* @type {Record}
*/
get currentUser() {
return this._user;
}
/**
* Current access token
* @type {String}
*/
get accessToken() {
return this._accessToken;
}
/**
* Registers listener which user record changed.
*
* @param {function()} listener
* @return {EventHandle}
*/
onUserChanged(listener) {
this.container.ee.on(USER_CHANGED, listener);
return new EventHandle(this.container.ee, USER_CHANGED, listener);
}
/**
* Creates a user account with the specified auth data, password and user
* record data.
*
* @param {Object} authData - unique identifier of the user
* @param {String} password - password of the user
* @param {Object} [data={}] - data saved to the user record
* @return {Promise<Record>} promise with created user record
*/
async signup(authData, password, data = {}) {
const authResponse = await this.container.makeRequest('auth:signup', {
auth_data: authData, // eslint-disable-line camelcase
password: password,
profile: toJSON(data)
});
return this._authResolve(authResponse);
}
/**
* Creates a user account with the specified username, password and user
* record data.
*
* @param {String} username - username of the user
* @param {String} password - password of the user
* @param {Object} [data={}] - data saved to the user record
* @return {Promise<Record>} promise with the created user record
*/
async signupWithUsername(username, password, data = {}) {
return this.signup({
username: username
}, password, data);
}
/**
* Creates a user account with the specified email, password and user record
* data.
*
* @param {String} email - email of the user
* @param {String} password - password of the user
* @param {Object} [data={}] - data saved to the user record
* @return {Promise<Record>} promise with the created user record
*/
async signupWithEmail(email, password, data = {}) {
return this.signup({
email: email
}, password, data);
}
/**
* Creates an anonymous user account and log in as the created user.
*
* @return {Promise<Record>} promise with the created user record
*/
async signupAnonymously() {
return this.signup(null, null, null);
}
/**
* Logs in to an existing user account with the specified auth data and
* password.
*
* @param {Object} authData - unique identifier of the user
* @param {String} password - password of the user
* @return {Promise<Record>} promise with the logged in user record
*/
async login(authData, password) {
const authResponse = await this.container.makeRequest('auth:login', {
auth_data: authData, // eslint-disable-line camelcase
password: password
});
return this._authResolve(authResponse);
}
/**
* Logs in to an existing user account with the specified username and
* password.
*
* @param {String} username - username of the user
* @param {String} password - password of the user
* @return {Promise<Record>} promise with the logged in user record
*/
async loginWithUsername(username, password) {
return this.login({
username: username
}, password);
}
/**
* Logs in to an existing user account with the specified email and
* password.
*
* @param {String} email - email of the user
* @param {String} password - password of the user
* @return {Promise<Record>} promise with the logged in user record
*/
async loginWithEmail(email, password) {
return this.login({
email: email
}, password);
}
/**
* Logs in to an existing user account with custom auth provider.
*
* @param {String} provider - provider name
* @param {Object} authData - provider auth data
* @return {Promise<Record>} promise with the logged in user record
*/
async loginWithProvider(provider, authData) {
const authResponse = await this.container.makeRequest('auth:login', {
provider: provider,
provider_auth_data: authData // eslint-disable-line camelcase
});
return this._authResolve(authResponse);
}
/**
* Logs out the current user of this container.
*
* @return {Promise} promise
*/
async logout() {
try {
try {
await this.container.push.unregisterDevice();
} catch (error) {
if (error.code === ErrorCodes.InvalidArgument &&
error.message === 'Missing device id'
) {
// fallthrough
} else {
throw error;
}
}
this.container.clearCache();
await this.container.makeRequest('auth:logout', {});
return null;
} finally {
await Promise.all([
this._setAccessToken(null),
this._setUser(null)
]);
}
}
/**
* Retrieves current user record from server.
*
* @return {Promise<Record>} promise with current user record
*/
async whoami() {
const authResponse = await this.container.makeRequest('me', {});
return this._authResolve(authResponse);
}
/**
* Changes the password of the current user.
*
* @param {String} oldPassword - old password of current user
* @param {String} newPassword - new password of current user
* @param {Boolean} [invalidate=false] - not implemented
* @return {Promise<Record>} promise with current user record
*/
async changePassword(oldPassword, newPassword, invalidate = false) {
if (invalidate) {
throw Error('Invalidate is not yet implemented');
}
const authResponse = await this.container.makeRequest('auth:password', {
old_password: oldPassword, // eslint-disable-line camelcase
password: newPassword
});
return this._authResolve(authResponse);
}
/**
* Reset user password, require master key.
*
* @param {Record|String} user - target user or user id
* @param {String} newPassword - new password of target user
* @return {Promise<String>} promise with target user id
*/
async adminResetPassword(user, newPassword) {
const userId = user._id || user;
await this.container.makeRequest('auth:reset_password', {
auth_id: userId, // eslint-disable-line camelcase
password: newPassword
});
return userId;
}
/**
* Defines roles to have admin right.
*
* @param {Role[]} roles - roles to have admin right
* @return {Promise<String[]>} promise with role names
*/
async setAdminRole(roles) {
let roleNames = _.map(roles, function (perRole) {
return perRole.name;
});
const body = await this.container.makeRequest('role:admin', {
roles: roleNames
});
return body.result;
}
/**
* Sets default roles for new registered users.
*
* @param {Role[]} roles - default roles
* @return {Promise<String[]>} promise with role names
*/
async setDefaultRole(roles) {
let roleNames = _.map(roles, function (perRole) {
return perRole.name;
});
const body = await this.container.makeRequest('role:default', {
roles: roleNames
});
return body.result;
}
/**
* Gets roles of users from server.
*
* @param {Record[]|String[]} users - user records or user ids
* @return {Promise<Object>} promise with userIDs-to-roles map
*/
async fetchUserRole(users) {
let userIds = _.map(users, function (perUser) {
// accept either user record or user id
return perUser._id || perUser;
});
const body = await this.container.makeRequest('role:get', {
users: userIds
});
return Object.keys(body.result)
.map((key) => [key, body.result[key]])
.reduce((acc, pairs) => ({
...acc || {},
[pairs[0]]: pairs[1].map((name) => new Role(name))
}), null);
}
/**
* Assigns roles to users.
*
* @param {Record[]|String[]} users - target users
* @param {Role[]|String[]} roles - roles to be assigned
* @return {Promise<String[]>} proimse with the target users
*/
async assignUserRole(users, roles) {
let userIds = _.map(users, function (perUser) {
// accept either user record or user id
return perUser._id || perUser;
});
let roleNames = _.map(roles, function (perRole) {
// accept either role object or role name
return perRole.name || perRole;
});
const body = await this.container.makeRequest('role:assign', {
users: userIds,
roles: roleNames
});
return body.result;
}
/**
* Revokes roles from users.
*
* @param {Record[]|String[]} users - target users
* @param {Role[]|String[]} roles - roles to be revoked
* @return {Promise<String[]>} promise with target users
*/
async revokeUserRole(users, roles) {
let userIds = _.map(users, function (perUser) {
// accept either user record or user id
return perUser._id || perUser;
});
let roleNames = _.map(roles, function (perRole) {
// accept either role object or role name
return perRole.name || perRole;
});
const body = await this.container.makeRequest('role:revoke', {
users: userIds,
roles: roleNames
});
return body.result;
}
/**
* Enable user account of a user.
*
* This function is intended for admin use.
*
* @param {Record|String} user - target user
* @return {Promise<String>} promise with target user
*/
async adminEnableUser(user) {
const userId = user._id || user;
await this.container.makeRequest('auth:disable:set', {
auth_id: userId, // eslint-disable-line camelcase
disabled: false
});
return userId;
}
/**
* Disable user account of a user.
*
* This function is intended for admin use.
*
* @param {Record|String} user - target user
* @param {String} [message] - message to be shown to user
* @param {Date} [expiry] - date and time when the user is automatically
* enabled
* @return {Promise<String>} promise with target user
*/
async adminDisableUser(user, message, expiry) {
const userId = user._id || user;
let payload = {
auth_id: userId, // eslint-disable-line camelcase
disabled: true
};
if (message) {
payload.message = message;
}
if (expiry) {
payload.expiry = expiry.toJSON();
}
await this.container.makeRequest('auth:disable:set', payload);
return userId;
}
async _getAccessToken() {
try {
const token = await this.container.store.getItem('skygear-accesstoken');
this._accessToken = token;
return token;
} catch (err) {
console.warn('Failed to get access', err);
this._accessToken = null;
return null;
}
}
async _setAccessToken(value) {
try {
this._accessToken = value;
if (value === null) {
await this.container.store.removeItem('skygear-accesstoken');
} else {
await this.container.store.setItem('skygear-accesstoken', value);
}
} catch (err) {
console.warn('Failed to persist accesstoken', err);
}
return value;
}
async _authResolve(body) {
await Promise.all([
this._setUser(body.result.profile),
this._setAccessToken(body.result.access_token)
]);
this.container.pubsub._reconfigurePubsubIfNeeded();
return this.currentUser;
}
async _getUser() {
try {
const userJSON = await this.container.store.getItem('skygear-user');
if (!userJSON) {
this._user = null;
return null;
}
let attrs = JSON.parse(userJSON);
if (!attrs) {
this._user = null;
return null;
}
this._user = new this._User(attrs);
return this._user;
} catch (err) {
console.warn('Failed to get user', err);
this._user = null;
return null;
}
}
async _setUser(attrs) {
let value;
if (attrs) {
this._user = new this._User(attrs);
value = JSON.stringify(this._user.toJSON());
} else {
this._user = null;
value = null;
}
try {
if (value === null) {
await this.container.store.removeItem('skygear-user');
} else {
await this.container.store.setItem('skygear-user', value);
}
this.container.ee.emit(USER_CHANGED, this._user);
} catch (err) {
console.warn('Failed to persist user', err);
}
}
get _User() {
return this.container.UserRecord;
}
get _Query() {
return this.container.Query;
}
}