UNPKG

kyper-matter

Version:

Library to provide simple application functionality like authentication and local/session/token storage for Tesselate applications.

679 lines (659 loc) 21.2 kB
import config from './config' import logger from './utils/logger' import * as dom from './utils/dom' import * as request from './utils/request' import * as ProviderAuth from './utils/providerAuth' import token from './utils/token' import * as envStorage from './utils/envStorage' import { isString, isArray, isObject, has, some, every } from 'lodash' export default class Matter { /** Constructor * @param {String|Object} project Project name or object containing project name and owner */ constructor (project, opts) { if (!project) { logger.error({ description: 'Project name required to use Matter.', func: 'constructor', obj: 'Matter' }) throw new Error('Project name is required to use Matter') } if (isObject(project)) { this.name = project.name this.owner = project.owner || null } else { this.name = project } if (opts) { this.options = opts config.applySettings(opts) } logger.debug({ description: 'Matter object built.', matter: this, func: 'constructor', obj: 'Matter' }) } /** Get current logged in status * @return {Boolean} * @example * //Check if there is an account currently logged in * if(matter.isLoggedIn){ * console.log('There is currently an account logged in.') * } else { * console.warn('There is no account currently logged in.') * } */ get isLoggedIn () { return isString(this.token.string) } /** Endpoint generation that handles default/provided settings and environment * @return {String} endpoint - endpoint for tessellate application */ get endpoint () { // Remove url if host is a tessellate server if (typeof window !== 'undefined' && has(window, 'location') && window.location.host.indexOf('tessellate') !== -1) { logger.info({ description: 'App is Tessellate and Host is Tessellate Server, serverUrl simplified!', func: 'endpoint', obj: 'Matter' }) return '' } // Handle tessellate as name if (this.name !== 'tessellate') { return this.owner ? `${config.serverUrl}/users/${this.owner}/projects/${this.name}` : `${config.serverUrl}/projects/${this.name}` } return config.serverUrl } /** Save current user (handled automatically by default) * @param {Object} userData - Account data to set for current user * @example * //Save account response to current user * matter.currentUser = {username: 'testuser1', email: 'test@email.com'} * console.log('New current user set:', matter.currentUser) */ set currentUser (userData) { logger.debug({ description: 'Current User set.', user: userData, func: 'currentUser', obj: 'Matter' }) envStorage.setItem(config.tokenUserDataName, userData) } /** Get currently logged in user or returns null * @return {Object|null} * @example * //Return account if logged in * if(matter.isLoggedIn){ * console.log('Current user account: ', matter.currentUser) * } else { * console.log('No current user. Current user: ', matter.currentUser) * } * matter.currentUser * console.log('New current user set:', matter.currentUser) */ get currentUser () { if (this.storage.getItem(config.tokenUserDataName)) { return this.storage.getItem(config.tokenUserDataName) } return null } /* Utility to handle safley writing to localStorage, sessionStorage, and cookies * @return {Object} */ get storage () { return envStorage } /** Utility to handle token writing/deleting/decoding * @return {Object} */ get token () { return token } /** Utils placed in base library * @return {Object} */ get utils () { return { logger, request, storage: envStorage, dom } } /** Signup a new user * @param {Object} signupData - Object containing data to use while signing up to application. * @param {String} signupData.username - Username of new user (error will be returned if username is taken) * @param {String} signupData.email - Email of new user (error will be returned if email is already used) * @param {String} signupData.password - Password to be used with account (will be encrypted). * @return {Promise} * @example * //Signup a new user * var signupData = {username: 'testuser1', email:'test@email.com', password: 'testpassword'} * matter.signup(signupData).then(function(signupRes){ * console.log('New user signed up successfully. New account: ', signupRes.account) * }, function(err){ * console.error('Error signing up:', err) * }) */ signup (signupData) { logger.debug({ description: 'Signup called.', signupData, func: 'signup', obj: 'Matter' }) if (!signupData) { logger.error({ description: 'Signup information is required to signup.', func: 'signup', obj: 'Matter' }) return Promise.reject({ message: 'Signup data is required to signup.', status: 'NULL_DATA' }) } if (isString(signupData)) { return this.authUsingProvider(signupData) } // Handle no username or email if (!signupData.username || !signupData.email) { logger.error({ description: 'Email and Username required to signup.', func: 'signup', obj: 'Matter' }) return Promise.reject({ message: 'Email and Username required to signup.', status: 'EMPTY_DATA' }) } if (!signupData.password) { logger.error({ description: 'Password is required to signup.', func: 'signup', obj: 'Matter' }) return Promise.reject({ message: 'Password is required to signup.', status: 'PASS_REQUIRED' }) } return request.post(`${this.endpoint}/signup`, signupData).then(response => { if (response.token) { this.token.string = response.token } if (response.user) { this.currentUser = response.user } logger.info({ description: 'Signup successful.', user: this.currentUser, func: 'signup', obj: 'Matter' }) return this.currentUser })['catch'](error => { logger.error({ description: 'Error requesting signup.', error, signupData, func: 'signup', obj: 'Matter' }) return Promise.reject(error) }) } /** Login by username/email * @param {Object} loginData - Object containing data to use while logging in to application. * @param {String} loginData.username - Username of user to login as * @param {String} loginData.email - Email of new user (Optional instead of username) * @param {String} loginData.password - Password to be used with account (will be encrypted). * @return {Promise} * @example * //Login as 'testuser1' * var loginData = {username: 'testuser1', password: 'testpassword'} * matter.login(loginData).then(function(loginRes){ * console.log('New user logged in succesfully. Account: ', loginRes.user) * }, function(err){ * console.error('Error logging in:', err) * }) */ login (loginData) { if (!loginData || (!isObject(loginData) && !isString(loginData))) { logger.error({ description: 'Username/Email and Password are required to login', func: 'login', obj: 'Matter' }) return Promise.reject({ message: 'Login data is required to login.', status: 'DATA_REQUIRED' }) } // Provider login if (isString(loginData)) { return this.authUsingProvider(loginData) } // No username or email if (!loginData.username && !loginData.email) { logger.error({ description: 'Email or Username required to login.', func: 'login', obj: 'Matter' }) return Promise.reject({ message: 'Email or Username required to login.', status: 'ID_REQUIRED' }) } // Handle null or invalid password if (!loginData.password || loginData.password === '') { return Promise.reject({ message: 'Password is required to login.', status: 'PASS_REQUIRED' }) } // Username/Email Login return request.put(`${this.endpoint}/login`, loginData) .then(response => { if (response.data && response.data.status && response.data.status === 409) { logger.error({ description: 'User not found.', response, func: 'login', obj: 'Matter' }) return Promise.reject(response.data) } if (response.token) { this.token.string = response.token } if (response.user) { this.currentUser = response.user } logger.info({ description: 'Successful login.', user: this.currentUser, func: 'login', obj: 'Matter' }) return this.currentUser })['catch'](error => { logger.error({ description: 'Error requesting login.', error, func: 'login', obj: 'Matter' }) if (error.status === 409 || error.status === 400) { error = error.response.text } return Promise.reject(error) }) } /** logout * @description Log out of currently logged in user account * @return {Promise} * @example * //Logout of currently logged in account * matter.logout().then(function(loginRes){ * console.log('Logged out successfully') * }, function(err){ * console.error('Error logging out:', err) * }) */ logout () { // TODO: Handle logging out of providers if (!this.isLoggedIn) { logger.warn({ description: 'No logged in account to log out.', func: 'logout', obj: 'Matter' }) return Promise.reject({ message: 'No logged in account to log out.', status: 'NULL_ACCOUNT' }) } return request.put(`${this.endpoint}/logout`).then(response => { logger.info({ description: 'Logout successful.', response, func: 'logout', obj: 'Matter' }) this.currentUser = null this.token.delete() return response })['catch'](error => { logger.error({ description: 'Error requesting log out: ', error, func: 'logout', obj: 'Matter' }) this.storage.removeItem(config.tokenUserDataName) this.token.delete() return Promise.reject(error) }) } /** Authenticate using external provider * @param {String} provider - Provider name * @return {Promise} * @example * //Signup using google * matter.authUsingProvider('google').then(function(signupRes){ * console.log('New user logged in succesfully. Account: ', signupRes.user) * }, function(err){ * console.error('Error logging in:', err) * }) */ authUsingProvider (provider) { if (!provider) { logger.info({ description: 'Provider required to sign up.', func: 'authUsingProvider', obj: 'Matter' }) return Promise.reject({message: 'Provider data is required to signup.'}) } return ProviderAuth.authWithServer(provider).then(response => { logger.info({ description: 'Provider login successful.', response, func: 'authUsingProvider', obj: 'Matter' }) if (response && response.token) { this.token.string = response.token } if (response && response.user || response.data) { this.currentUser = response.data || response.user } return this.currentUser }, error => { logger.error({ description: 'Provider signup error.', error, func: 'authUsingProvider', obj: 'Matter' }) return Promise.reject(error) }) } /** getCurrentUser * @return {Promise} * @example * //Logout of currently logged in account * matter.getCurrentUser().then(function(currentAccount){ * console.log('Currently logged in account:', currentAccount) * }, function(err){ * console.error('Error logging out:', err) * }) */ getCurrentUser () { if (this.currentUser) { logger.debug({ description: 'Current is already available. Returning user.', func: 'currentUser', obj: 'Matter' }) return Promise.resolve(this.currentUser) } if (!this.isLoggedIn) { logger.debug({ description: 'Current user is null.', func: 'currentUser', obj: 'Matter' }) return Promise.resolve(null) } return request.get(`${this.endpoint}/user`).then(response => { // TODO: Save user information locally logger.log({ description: 'Current User Request responded.', response, func: 'currentUser', obj: 'Matter' }) this.currentUser = response return response })['catch'](error => { if (error.status === 401) { logger.warn({ description: 'Called for current user without token.', error, func: 'currentUser', obj: 'Matter' }) token.delete() return Promise.resolve(null) } logger.error({ description: 'Error requesting current user.', error, func: 'currentUser', obj: 'Matter' }) return Promise.reject(error) }) } /** updateAccount * @param {Object} updateData - Data to update within profile (only provided data will be modified). * @return {Promise} * @example * //Update current account's profile * matter.updateAccount().then(function(updatedAccount){ * console.log('Currently logged in account:', updatedAccount) * }, function(err){ * console.error('Error updating profile:', err) * }) */ updateAccount (updateData) { if (!this.isLoggedIn) { logger.error({ description: 'No current user profile to update.', func: 'updateAccount', obj: 'Matter' }) return Promise.reject({ message: 'Must be logged in to update account.' }) } if (!updateData) { logger.error({ description: 'Data is required to update profile.', func: 'updateAccount', obj: 'Matter' }) return Promise.reject({ message: 'Data required to update account.', status: 'NULL_DATA' }) } // Send update request return request.put(`${this.endpoint}/users/${this.currentUser.username}`, updateData) .then(response => { logger.info({ description: 'Update profile request responded.', response, func: 'updateAccount', obj: 'Matter' }) this.currentUser = response return response })['catch'](error => { logger.error({ description: 'Error requesting current user.', error, func: 'updateAccount', obj: 'Matter' }) return Promise.reject(error) }) } /** uploadAvatar * @description Upload account avatar to Tessellate * @param {Object} file - File object to upload * @return {Promise} * @example * //Upload image to tessellate * matter.uploadAvatar(file).then(function(imgUrl){ * console.log('Currently logged in account:', imgUrl) * }, function(err){ * console.error('Error uploading image:', err) * }) */ uploadAvatar (file) { if (!this.isLoggedIn) { logger.error({ description: 'Must be logged in to upload an image.', func: 'uploadAvatar', obj: 'Matter' }) return Promise.reject({ message: 'Must be logged in to upload image.' }) } if (!file) { logger.error({ description: 'File is required to upload Avatar.', func: 'uploadAvatar', obj: 'Matter' }) return Promise.reject({ message: 'Data required to update profile.', status: 'NULL_DATA' }) } const reqData = { files: [ { key: 'image', file } ] } // Send update request return request.put(`${this.endpoint}/users/${this.currentUser.username}/avatar`, reqData) } /** changePassword * @param {String} updateData New password for account. * @return {Promise} * @example * //Update current account's password * var newPassword = 'asdfasdfasdf' * matter.changePassword(newPassword).then(function(updatedAccount){ * console.log('Currently logged in account:', updatedAccount) * }, function(err){ * console.error('Error changing password:', err) * }) */ changePassword (newPassword) { if (!this.isLoggedIn) { logger.error({ description: 'No current user profile for which to change password.', func: 'changePassword', obj: 'Matter' }) return Promise.reject({ message: 'Must be logged in to change password.' }) } // Send update request return request.put(`${this.endpoint}/user/password`, newPassword) } /** recoverAccount * @param {String} updateData New password for account. * @return {Promise} * @example * //Recover current users password * matter.recoverAccount().then(function(updatedAccount){ * console.log('Currently logged in account:', updatedAccount) * }, function(err){ * console.error('Error updating profile:', err) * }) */ recoverAccount (accountData) { if (!accountData) { logger.error({ description: 'Account data is required to recover an account.', func: 'recoverAccount', obj: 'Matter' }) return Promise.reject({message: 'Account data is required to recover an account.'}) } let account = {} if (isString(accountData)) { account = accountData.indexOf('@') !== -1 ? { email: accountData } : { username: accountData } } else { account = accountData } logger.debug({ description: 'Requesting recovery of account.', account, func: 'recoverAccount', obj: 'Matter' }) return request.put(`${this.endpoint}/user/recover`, account) } /** Check that user is in a single group or in all of a list of groups * @param {Array} checkGroups - List of groups to check for account membership * @return {Boolean} * @example * //Check for group membership * var isBoth = * if(matter.isInGroup('admins')){ * console.log('Current account is an admin!') * } else { * console.warn('Current account is not an admin.') * } */ isInGroup (checkGroups) { if (!this.isLoggedIn) { logger.error({ description: 'No logged in user to check for groups.', func: 'isInGroup', obj: 'Matter' }) return false } if (!checkGroups) { logger.log({ description: 'Invalid group(s).', func: 'isInGroup', obj: 'Matter' }) return false } // Check if user is within groups if (isString(checkGroups)) { const groupName = checkGroups // Single role or string list of roles const groupsArray = groupName.split(',') if (groupsArray.length > 1) { // String list of groupts logger.info({ description: 'String list of groups.', list: groupsArray, func: 'isInGroup', obj: 'Matter' }) return this.isInGroups(groupsArray) } // Single group let groups = this.token.data.groups || [] logger.log({ description: 'Checking if user is in group.', group: groupName, userGroups: this.token.data.groups, func: 'isInGroup', obj: 'Matter' }) return some(groups, (group) => { return groupName === group.name }) } if (isArray(checkGroups)) { return this.isInGroups(checkGroups) } return false } /** Check that user is in all of a list of groups * @param {Array|String} checkGroups - List of groups to check for account membership * @return {Boolean} * @example * //Check for group membership * var isBoth = matter.isInGroups(['admins', 'users']) * if(isBoth){ * console.log('Current account is both an admin and a user') * } else { * console.warn('Current account is not both an admin and a user') * } */ isInGroups (checkGroups) { if (!this.isLoggedIn) { logger.log({ description: 'No logged in user to check.', func: 'isInGroups', obj: 'Matter' }) return false } if (!checkGroups) { logger.log({ description: 'Invalid group(s).', func: 'isInGroup', obj: 'Matter' }) return false } // Check if user is in some of the provided groups if (isArray(checkGroups)) { return every(checkGroups.map(group => { if (isString(group)) { // Group is string return this.isInGroup(group) } // Group is object if (has(group, 'name')) { return this.isInGroup(group.name) } logger.error({ description: 'Invalid group object.', group: group, func: 'isInGroups', obj: 'Matter' }) return false }), true) } if (isString(checkGroups)) { // TODO: Handle spaces within string list const groupsArray = checkGroups.split(',') if (groupsArray.length > 1) { return this.isInGroups(groupsArray) } return this.isInGroup(groupsArray[0]) } logger.error({ description: 'Invalid groups list.', func: 'isInGroups', obj: 'Matter' }) return false } }