├── .eslintrc ├── .gitignore ├── README.md ├── babelrc.js ├── build-release.js ├── package-lock.json ├── package.json ├── src ├── Components │ ├── Alert.js │ ├── AppBarHeader.js │ ├── CopyNotification.js │ ├── DetailPassword.js │ ├── DetailTextField.js │ ├── Logger.js │ ├── Login.js │ ├── SearchResultDetail.js │ ├── SearchResultsList.js │ └── SearchSelect.js ├── Container │ └── Home.js ├── app.js ├── extension │ ├── Promote.png │ └── manifest.xml ├── helpers.js ├── index.html ├── redux │ ├── alert.js │ ├── auth.js │ ├── create.js │ ├── middleware.js │ ├── passwords.js │ ├── reducer.js │ └── search.js └── strings.js ├── webpack-dev.config.js └── webpack.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": false, 6 | "es6": true 7 | }, 8 | "parserOptions": { 9 | "sourceType": "module", 10 | "ecmaVersion": 2019 11 | }, 12 | "extends": [ 13 | "plugin:promise/recommended", "plugin:import/warnings", "plugin:import/errors", "plugin:react/recommended" 14 | ], 15 | "plugins": ["promise", "import"], 16 | "rules": { 17 | "promise/always-return": "off", 18 | "promise/avoid-new": "off", 19 | "promise/no-nesting": "off", 20 | "import/default": "off", 21 | "space-before-function-paren": 0, 22 | "object-curly-spacing": 0, 23 | "arrow-body-style": 0, 24 | "linebreak-style": 0, 25 | "consistent-return": "off", 26 | "prefer-template": "warn", 27 | "global-require": "off", 28 | "no-case-declarations": "off", 29 | "no-underscore-dangle": "off", 30 | "no-var": "error", 31 | "prefer-const": "error", 32 | "one-var": ["error", "never"], 33 | "template-curly-spacing": ["error", "never"], 34 | "no-shadow": [ 35 | "error", { 36 | "allow": [ 37 | "then", "catch", "done" 38 | ] 39 | } 40 | ], 41 | "max-len": [ 42 | "error", { 43 | "code": 140, 44 | "ignoreComments": true 45 | } 46 | ], 47 | // use ide formatting 48 | "indent": [ 49 | 2, 2, { 50 | "SwitchCase": 1 51 | } 52 | ], 53 | "quotes": [ 54 | 2, "single" 55 | ], 56 | "new-cap": 0, 57 | "no-prototype-builtins": 0, 58 | "no-restricted-syntax": [ 59 | "error", "WithStatement" 60 | ], 61 | "no-use-before-define": [ 62 | "error", { 63 | "functions": false, 64 | "classes": true 65 | } 66 | ] 67 | }, 68 | "settings": { 69 | "react": { 70 | "version": "detect" 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | .env.test 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | 74 | # next.js build output 75 | .next 76 | 77 | # nuxt.js build output 78 | .nuxt 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless/ 85 | 86 | # FuseBox cache 87 | .fusebox/ 88 | 89 | # DynamoDB Local files 90 | .dynamodb/ 91 | .idea 92 | scratch 93 | .babel 94 | stage 95 | release 96 | dist 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # connectwise-control-itglue-helper 2 | 3 | #### Install 4 | 5 | - Download latest release 6 | - Extract zip to `C:\Program Files (x86)\ScreenConnect\App_Extensions` 7 | - Enable extension in web console 8 | 9 | #### Prerequisites 10 | - On-prem (?). 11 | - Currently **unable** to support SSO. 12 | - May or may not work without 2FA/MFA enabled (if you don't have it enabled, you should turn it on.) 13 | 14 | #### Install Build Prerequisites 15 | ``` 16 | > npm install 17 | ``` 18 | 19 | #### Build 20 | ``` 21 | > npm run build 22 | ``` 23 | 24 | #### Run in development 25 | Note: uses [CORS Anywhere](https://github.com/k-grube/cors-anywhere) in development mode to proxy requests 26 | 27 | ``` 28 | > npm run start 29 | ``` 30 | 31 | ### Control Helper Notes 32 | 33 | When running as an extension helper, `window.external` contains several useful properties and functions: 34 | 35 | ```javascript 36 | helperText 37 | sessionID 38 | sessionTitle 39 | sessionType 40 | participantName 41 | ``` 42 | ```javascript 43 | void addNote(string text) 44 | void sendChatMessage(string text) 45 | void sendText(string text) 46 | void sendFiles() 47 | void sendFolder() 48 | void receiveFiles() 49 | void receiveFolder() 50 | void sendSystemKeyCode() 51 | void runTool(string itemPath, bool sharedOrPersonalToolbox, bool shouldRunElevated) 52 | void sendCredentials(string domain, string userName, string password) 53 | void showMessageBox(string message, string title) 54 | string getSettingValue(string key) 55 | void setSettingValue(string key, string value) 56 | ``` 57 | 58 | Size of the helper when pinned 59 | - 250px by however tall the window is (usually around 800px on standard screen) 60 | 61 | Size of the helper when moused over: 62 | - 650px by 350px 63 | 64 | 65 | 66 | ### TODOs 67 | 68 | - make organization detail search useful 69 | - add pagination and/or internal scroll bars 70 | - add error handling for pretty much everything 71 | - add bindings from intermediate states during loading where applicable 72 | - ~~change login form to use 'subdomain' instead of full domain~~ 73 | - ~~log out on 401 error (see redux middleware)~~ 74 | - ~~fix SearchSelect timeout to prevent duplicate searches running~~ 75 | - add remember me 76 | -------------------------------------------------------------------------------- /babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cacheDirectory: '.babel', 3 | 'presets': [[ 4 | '@babel/preset-env', { 5 | 'targets': 'ie >= 11', 6 | useBuiltIns: 'entry', 7 | 'corejs': 3, 8 | }, 9 | ], '@babel/preset-react'], 10 | 'plugins': [[ 11 | '@babel/plugin-transform-runtime', { 12 | 'absoluteRuntime': true, 13 | 'corejs': 3, 14 | 'helpers': true, 15 | 'regenerator': true, 16 | 'useESModules': false, 17 | }, 18 | ], 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /build-release.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const archiver = require('archiver'); 4 | const webpack = require('webpack'); 5 | const webpackConfig = require('./webpack.config'); 6 | 7 | // create stage and release folders 8 | // delete stage and release files 9 | try { 10 | if (!fs.existsSync(path.join(__dirname, '/stage'))) { 11 | fs.mkdirSync(path.join(__dirname, '/stage')); 12 | } 13 | if (!fs.existsSync(path.join(__dirname, '/release'))) { 14 | fs.mkdirSync(path.join(__dirname, '/release')); 15 | } 16 | 17 | fs.unlinkSync(path.join(__dirname, '/release/release.zip')); 18 | fs.unlinkSync(path.join(__dirname, '/stage/ITGlue.html')); 19 | fs.unlinkSync(path.join(__dirname, '/stage/manifest.xml')); 20 | fs.unlinkSync(path.join(__dirname, '/stage/Promote.png')); 21 | } catch (e) { 22 | } 23 | 24 | webpack(webpackConfig, (err, stats) => { 25 | if (err) { 26 | console.error('Fatal error during compile.'); 27 | throw err; 28 | } 29 | 30 | // copy new build to stage 31 | fs.copyFileSync(path.join(__dirname, '/dist/ITGlue.html'), path.join(__dirname, '/stage/ITGlue.html')); 32 | fs.copyFileSync(path.join(__dirname, '/src/extension/manifest.xml'), path.join(__dirname, '/stage/manifest.xml')); 33 | fs.copyFileSync(path.join(__dirname, '/src/extension/Promote.png'), path.join(__dirname, '/stage/Promote.png')); 34 | 35 | buildZip(); 36 | }); 37 | 38 | 39 | function buildZip() { 40 | const output = fs.createWriteStream(path.join(__dirname, '/release/release.zip')); 41 | const archive = archiver('zip', {zlib: {level: 9}}); 42 | 43 | output.on('close', () => console.log('release.zip created.')); 44 | 45 | archive.on('warning', (err) => { 46 | console.warn(err); 47 | }); 48 | archive.on('error', (err) => { 49 | throw err; 50 | }); 51 | archive.pipe(output); 52 | archive.directory('stage/', '0e68472d-e8e4-4d7a-894e-c0eec88cf731'); 53 | archive.finalize(); 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connectwise-control-itglue-helper", 3 | "version": "1.0.0-alpha4", 4 | "description": "", 5 | "scripts": { 6 | "start": "webpack-dev-server --open --hot --mode development --config webpack-dev.config.js", 7 | "build": "webpack --config webpack.config.js --mode production", 8 | "release": "node build-release.js" 9 | }, 10 | "browser": "src/app.js", 11 | "keywords": [], 12 | "author": "Kevin Grube", 13 | "license": "MIT", 14 | "repository": "https://github.com/mspgeek/connectwise-control-itglue-helper.git", 15 | "dependencies": { 16 | "@babel/plugin-transform-runtime": "^7.5.5", 17 | "@babel/runtime-corejs3": "^7.5.5", 18 | "@material-ui/core": "^4.3.0", 19 | "@material-ui/icons": "^4.2.1", 20 | "@material-ui/styles": "^4.3.0", 21 | "clipboard-copy": "^3.1.0", 22 | "lodash": "^4.17.15", 23 | "node-itglue": "^1.0.1", 24 | "prop-types": "^15.7.2", 25 | "react": "^16.8.6", 26 | "react-dom": "^16.8.6", 27 | "react-redux": "^7.1.0", 28 | "redux": "^4.0.4" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.5.5", 32 | "@babel/preset-env": "^7.5.5", 33 | "@babel/preset-react": "^7.0.0", 34 | "archiver": "^3.1.1", 35 | "babel-eslint": "^10.0.2", 36 | "babel-loader": "^8.0.6", 37 | "clean-webpack-plugin": "^3.0.0", 38 | "core-js": "^3.1.4", 39 | "eslint": "^6.1.0", 40 | "eslint-loader": "^2.2.1", 41 | "eslint-plugin-import": "^2.18.2", 42 | "eslint-plugin-promise": "^4.2.1", 43 | "eslint-plugin-react": "^7.14.3", 44 | "html-webpack-inline-source-plugin": "0.0.10", 45 | "html-webpack-plugin": "^3.2.0", 46 | "react-hot-loader": "^4.12.10", 47 | "redux-devtools-extension": "^2.13.8", 48 | "regenerator-runtime": "^0.13.3", 49 | "uglifyjs-webpack-plugin": "^2.1.3", 50 | "webpack": "^4.38.0", 51 | "webpack-cli": "^3.3.6", 52 | "webpack-dev-server": "^3.7.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Components/Alert.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Paper from '@material-ui/core/Paper'; 4 | import Typography from '@material-ui/core/Typography'; 5 | import makeStyles from '@material-ui/core/styles/makeStyles'; 6 | import {connect} from 'react-redux'; 7 | import IconButton from '@material-ui/core/IconButton'; 8 | import IconDismiss from '@material-ui/icons/Close'; 9 | 10 | import {dismissAlert} from '../redux/alert'; 11 | 12 | const useStyles = makeStyles(theme => ({ 13 | paper: { 14 | padding: theme.spacing(1), 15 | backgroundColor: theme.palette.error.light, 16 | display: 'flex', 17 | justifyContent: 'center', 18 | alignItems: 'center', 19 | }, 20 | typography: { 21 | flexGrow: 1, 22 | paddingLeft: theme.spacing(2), 23 | }, 24 | })); 25 | 26 | function Alert(props) { 27 | const classes = useStyles(); 28 | const {message, show} = props; 29 | 30 | let messageClean = message; 31 | if (typeof message === 'object') { 32 | if (message.message) { 33 | messageClean = message.message; 34 | } else if (message.title) { 35 | messageClean = message.title; 36 | if (message.detail) { 37 | messageClean = `${messageClean} - ${message.detail}`; 38 | } 39 | } else if (message.data) { 40 | if (typeof message.data === 'string') { 41 | messageClean = message.data; 42 | } else if (message.data.error_message) { 43 | messageClean = message.data.error_message; 44 | } else if (message.data.error) { 45 | messageClean = message.data.error; 46 | 47 | if (message.data.status) { 48 | messageClean = `${message.data.status} ${message.data.error}`; 49 | } 50 | } 51 | } else if (message.statusText) { 52 | messageClean = message.statusText; 53 | } else { 54 | messageClean = JSON.stringify(message); 55 | } 56 | } 57 | 58 | // one final fallback 59 | if (typeof messageClean !== 'string') { 60 | messageClean = JSON.stringify(messageClean); 61 | } 62 | 63 | return ( 64 |
65 | {show && 66 | 67 | 68 | {messageClean} 69 | 70 | 71 | 72 | 73 | } 74 |
75 | ); 76 | } 77 | 78 | Alert.propTypes = { 79 | // actions 80 | dismissAlert: PropTypes.func, 81 | 82 | message: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), 83 | show: PropTypes.bool, 84 | }; 85 | 86 | Alert.defaultProps = { 87 | message: '', 88 | show: false, 89 | }; 90 | 91 | export default connect(state => ({...state.alert}), {dismissAlert})(Alert); 92 | -------------------------------------------------------------------------------- /src/Components/AppBarHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {connect} from 'react-redux'; 4 | import AppBar from '@material-ui/core/AppBar'; 5 | import Toolbar from '@material-ui/core/Toolbar'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import IconChevronLeft from '@material-ui/icons/ChevronLeft'; 8 | import IconChevronRight from '@material-ui/icons/ChevronRight'; 9 | import IconMenu from '@material-ui/icons/Menu'; 10 | import Button from '@material-ui/core/Button'; 11 | import {makeStyles} from '@material-ui/styles'; 12 | 13 | import {logout, resetAll} from '../redux/auth'; 14 | import {handleNavigation, setActiveComponent} from '../redux/search'; 15 | import {Typography} from '@material-ui/core'; 16 | 17 | const useStyles = makeStyles(theme => ({ 18 | root: { 19 | // flexGrow: 1, 20 | // paddingBottom: theme.spacing(1), 21 | }, 22 | menuButton: { 23 | marginRight: theme.spacing(1), 24 | marginLeft: theme.spacing(1), 25 | }, 26 | componentRoot: { 27 | display: 'flex', 28 | flexDirection: 'row', 29 | flexGrow: 1, 30 | }, 31 | header: { 32 | alignItems: 'center', 33 | }, 34 | })); 35 | 36 | function HeaderComponent(props) { 37 | const {direction, onNavigation} = props; 38 | 39 | const classes = useStyles(); 40 | 41 | return ( 42 | <> 43 | onNavigation(direction)} 49 | > 50 | {direction === 'left' ? : } 51 | 52 | {props.children} 53 | 54 | ); 55 | } 56 | 57 | HeaderComponent.propTypes = { 58 | children: PropTypes.element, 59 | direction: PropTypes.string, 60 | onNavigation: PropTypes.func, 61 | }; 62 | 63 | HeaderComponent.defaultProps = { 64 | direction: 'left', 65 | }; 66 | 67 | function AppBarHeader(props) { 68 | const classes = useStyles(); 69 | const {activeComponent, selectedOrganization, password} = props; 70 | 71 | const title = (selectedOrganization && selectedOrganization.name) 72 | || (password && password.attributes && password.attributes['organization-name']) 73 | || 'IT Glue Helper'; 74 | 75 | return ( 76 | 77 | 78 | {activeComponent === 'utils' && 79 | 80 | {props.loggedIn && } 81 | 82 | 83 | } 84 | {activeComponent !== 'utils' && 85 | 86 | 87 | {title} 88 | 89 | 90 | } 91 | 92 | 93 | ); 94 | } 95 | 96 | AppBarHeader.propTypes = { 97 | //actions 98 | logout: PropTypes.func, 99 | resetAll: PropTypes.func, 100 | setActiveComponent: PropTypes.func, 101 | handleNavigation: PropTypes.func, 102 | 103 | loginPending: PropTypes.bool, 104 | loggedIn: PropTypes.bool, 105 | searchContext: PropTypes.string, 106 | selectedOrganization: PropTypes.object, 107 | activeComponent: PropTypes.string, 108 | password: PropTypes.object, 109 | }; 110 | 111 | AppBarHeader.defaultProps = { 112 | searchContext: 'global', 113 | activeComponent: 'header', 114 | }; 115 | 116 | export default connect( 117 | state => ({ 118 | ...state.auth, 119 | searchContext: state.search.searchContext, 120 | selectedType: state.search.selectedType, 121 | selectedOrganization: state.search.selectedOrganization, 122 | activeComponent: state.search.activeComponent, 123 | password: state.passwords.password, 124 | }), 125 | {logout, resetAll, setActiveComponent, handleNavigation})(AppBarHeader); 126 | -------------------------------------------------------------------------------- /src/Components/CopyNotification.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Snackbar from '@material-ui/core/Snackbar'; 4 | import makeStyles from '@material-ui/styles/makeStyles'; 5 | import IconButton from '@material-ui/core/IconButton'; 6 | import IconClose from '@material-ui/icons/Close'; 7 | 8 | const useStyles = makeStyles(theme => ({ 9 | close: { 10 | padding: theme.spacing(0.5), 11 | }, 12 | })); 13 | 14 | function CopyNotification(props) { 15 | const classes = useStyles(); 16 | 17 | function handleClose(event, reason) { 18 | if (reason === 'clickaway') { 19 | return; 20 | } 21 | 22 | props.onClose(); 23 | } 24 | 25 | return ( 26 | {props.message}} 35 | action={[ 36 | 43 | 44 | , 45 | ]} 46 | /> 47 | ); 48 | } 49 | 50 | CopyNotification.propTypes = { 51 | message: PropTypes.string, 52 | open: PropTypes.bool.isRequired, 53 | onClose: PropTypes.func.isRequired, 54 | }; 55 | 56 | CopyNotification.defaultProps = { 57 | message: 'Copied to clipboard', 58 | }; 59 | 60 | export default CopyNotification; 61 | -------------------------------------------------------------------------------- /src/Components/DetailPassword.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import {connect} from 'react-redux'; 4 | import copy from 'clipboard-copy'; 5 | import {makeStyles} from '@material-ui/styles'; 6 | import IconAccountBox from '@material-ui/icons/AccountBox'; 7 | import IconAssignmentReturn from '@material-ui/icons/AssignmentReturn'; 8 | import IconKey from '@material-ui/icons/VpnKey'; 9 | import IconButton from '@material-ui/core/IconButton'; 10 | import IconOpenInNew from '@material-ui/icons/OpenInNew'; 11 | import {Typography} from '@material-ui/core'; 12 | import Grid from '@material-ui/core/Grid'; 13 | import Tooltip from '@material-ui/core/Tooltip'; 14 | import LinearProgress from '@material-ui/core/LinearProgress'; 15 | 16 | import {getPasswordById, sendCredentials, sendText} from '../helpers'; 17 | import DetailTextField from './DetailTextField'; 18 | import CopyNotification from './CopyNotification'; 19 | import Paper from '@material-ui/core/Paper'; 20 | 21 | /** 22 | * Created by kgrube on 8/2/2019 23 | */ 24 | 25 | const useStyles = makeStyles(theme => { 26 | return { 27 | root: { 28 | padding: theme.spacing(1), 29 | paddingTop: theme.spacing(1), 30 | }, 31 | gridItem: { 32 | display: 'flex', 33 | flexDirection: 'row', 34 | }, 35 | actions: { 36 | flewGrow: 1, 37 | }, 38 | progressRoot: { 39 | display: 'flex', 40 | justifyContent: 'center', 41 | height: theme.spacing(8), 42 | alignItems: 'center', 43 | }, 44 | progress: { 45 | flexGrow: 1, 46 | }, 47 | }; 48 | }); 49 | 50 | function DetailPassword(props) { 51 | const {password, passwordLoaded, passwordLoading} = props; 52 | const classes = useStyles(); 53 | const [copyNotificationOpen, setCopyNotificationOpen] = React.useState(false); 54 | const [copyMessage, setCopyMessage] = React.useState('Copied to clipboard'); 55 | 56 | function handleCloseCopyNotification() { 57 | setCopyNotificationOpen(false); 58 | } 59 | 60 | /** 61 | * 62 | * @param value specify value to copy, and value to pass to callback 63 | * @param message specify popup message 64 | * @param copyText specify 65 | * @param loadPassword should we try to load the password field? 66 | * @param callback callback(value) 67 | * @returns {Function} 68 | */ 69 | function wrapButtonClick({value, message, copyText = false, loadPassword = false, callback}) { 70 | return () => { 71 | return Promise.resolve() 72 | .then(() => { 73 | if (loadPassword) { 74 | const {token} = props; 75 | return getPasswordById({token, passwordId: password.id, showPassword: true}) 76 | .then(result => result.attributes.password); 77 | } 78 | return value; 79 | }) 80 | .then(result => { 81 | if (copyText) { 82 | return copy(result); 83 | } 84 | return result; 85 | }) 86 | .then(result => { 87 | if (callback) { 88 | // eslint-disable-next-line promise/no-callback-in-promise 89 | callback(result); 90 | } 91 | 92 | setCopyNotificationOpen(true); 93 | setCopyMessage(message); 94 | }) 95 | .catch(error => { 96 | props.dispatch({ 97 | types: ['detail/COPY', 'detail/COPY_SUCCESS', 'detail/COPY_FAIL'], 98 | promise: promise.catch(() => error), 99 | }); 100 | }); 101 | }; 102 | } 103 | 104 | return ( 105 | 106 | 107 | {passwordLoading && !passwordLoaded && 108 |
109 | 110 |
111 | } 112 | {passwordLoaded && !passwordLoading && 113 | <> 114 | {password.attributes.name} 115 | 116 | 117 |
118 | 119 | sendCredentials(password.attributes.username, value), 125 | })} 126 | > 127 | 128 | 129 | 130 | 131 | sendText(value), 137 | })} 138 | > 139 | 140 | 141 | 142 | 143 | sendText(value), 149 | })} 150 | > 151 | 152 | 153 | 154 | 155 | window.open(password.attributes['resource-url'])} 158 | > 159 | 160 | 161 | 162 |
163 |
164 | 165 | 170 | 171 | 172 | 177 | 178 | 179 | 185 | 186 |
187 | 188 | } 189 |
190 | ); 191 | } 192 | 193 | DetailPassword.propTypes = { 194 | password: PropTypes.object, 195 | token: PropTypes.string, 196 | 197 | passwordLoaded: PropTypes.bool, 198 | passwordLoading: PropTypes.bool, 199 | 200 | dispatch: PropTypes.func, 201 | }; 202 | 203 | DetailPassword.defaultProps = { 204 | password: { 205 | attributes: {}, 206 | }, 207 | }; 208 | 209 | export default connect(state => ({ 210 | token: state.auth.token, 211 | password: state.passwords.password, 212 | passwordLoaded: state.passwords.passwordLoaded, 213 | passwordLoading: state.passwords.passwordLoading, 214 | }))(DetailPassword); 215 | -------------------------------------------------------------------------------- /src/Components/DetailTextField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import FormControl from '@material-ui/core/FormControl'; 4 | import InputLabel from '@material-ui/core/InputLabel'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import IconCopy from '@material-ui/icons/FileCopy'; 8 | import makeStyles from '@material-ui/core/styles/makeStyles'; 9 | import InputBase from '@material-ui/core/InputBase'; 10 | import CopyNotification from './CopyNotification'; 11 | import Tooltip from '@material-ui/core/Tooltip'; 12 | import copy from 'clipboard-copy'; 13 | 14 | const useStyles = makeStyles(theme => ({ 15 | formControl: { 16 | color: theme.palette.text.primary, 17 | flexGrow: 1, 18 | }, 19 | formValue: { 20 | // position: 'relative', 21 | }, 22 | label: { 23 | color: theme.palette.text.primary, 24 | }, 25 | textFieldRoot: { 26 | display: 'flex', 27 | flexDirection: 'row', 28 | }, 29 | inputRoot: { 30 | color: 'inherit', 31 | width: '100%', 32 | height: '100%', 33 | }, 34 | inputInput: { 35 | padding: theme.spacing(1, 0, 0, 0), 36 | width: '100%', 37 | }, 38 | })); 39 | 40 | function DetailTextField(props) { 41 | const {value, label, type, showCopy, multiline, wrapButtonClick} = props; 42 | const classes = useStyles(); 43 | const [copyNotificationOpen, setCopyNotificationOpen] = React.useState(false); 44 | 45 | function handleCloseCopyNotification() { 46 | setCopyNotificationOpen(false); 47 | } 48 | 49 | return ( 50 |
51 | 52 | {label} 53 | 63 | 64 | {showCopy && 65 | 66 | 79 | 80 | 81 | } 82 |
83 | ); 84 | } 85 | 86 | DetailTextField.propTypes = { 87 | value: PropTypes.string, 88 | label: PropTypes.string, 89 | type: PropTypes.string, 90 | showCopy: PropTypes.bool, 91 | multiline: PropTypes.bool, 92 | 93 | wrapButtonClick: PropTypes.func, 94 | }; 95 | 96 | DetailTextField.defaultProps = { 97 | multiline: false, 98 | showCopy: true, 99 | value: '', 100 | }; 101 | 102 | export default DetailTextField; 103 | -------------------------------------------------------------------------------- /src/Components/Logger.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | export default (props) => { 5 | 6 | } 7 | 8 | export function log(...data) { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/Components/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import TextField from '@material-ui/core/TextField'; 4 | import Button from '@material-ui/core/Button'; 5 | import {makeStyles} from '@material-ui/styles'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import LinearProgress from '@material-ui/core/LinearProgress'; 8 | 9 | import {setAuth, login} from '../redux/auth'; 10 | import {connect} from 'react-redux'; 11 | 12 | const useStyles = makeStyles(theme => ({ 13 | paper: { 14 | marginTop: theme.spacing(1), 15 | padding: theme.spacing(0, 2), 16 | display: 'flex', 17 | flexDirection: 'column', 18 | alignItems: 'center', 19 | }, 20 | form: { 21 | width: '100%', // Fix IE 11 issue. 22 | marginTop: theme.spacing(1), 23 | }, 24 | submit: { 25 | margin: theme.spacing(3, 0, 2), 26 | }, 27 | progressRoot: { 28 | display: 'flex', 29 | justifyContent: 'center', 30 | height: theme.spacing(8), 31 | alignItems: 'center', 32 | }, 33 | progress: { 34 | flexGrow: 1, 35 | }, 36 | })); 37 | 38 | function Login(props) { 39 | const handleChange = name => event => props.setAuth(name, event.target.value); 40 | const handleSubmit = (event) => { 41 | event.preventDefault(); 42 | props.login(); 43 | }; 44 | 45 | const {email, subdomain, otp, password, loginPending} = props; 46 | 47 | const classes = useStyles(); 48 | return ( 49 | <> 50 | {loginPending && 51 |
52 | 53 |
} 54 |
55 |
56 | 57 | 58 | 71 | 72 | 73 | 85 | 86 | 87 | 100 | 101 | 102 | 114 | 115 | 116 | 126 | 127 | 128 |
129 |
130 | 131 | ); 132 | } 133 | 134 | Login.propTypes = { 135 | // actions 136 | setAuth: PropTypes.func.isRequired, 137 | login: PropTypes.func.isRequired, 138 | 139 | // values 140 | subdomain: PropTypes.string, 141 | email: PropTypes.string, 142 | password: PropTypes.string, 143 | otp: PropTypes.string, 144 | 145 | // state 146 | loginPending: PropTypes.bool, 147 | 148 | }; 149 | 150 | Login.defaultProps = { 151 | subdomain: '', 152 | email: '', 153 | password: '', 154 | otp: '', 155 | loginPending: false, 156 | }; 157 | 158 | export default connect( 159 | state => ({...state.auth.user, loginPending: state.auth.loginPending}), 160 | {setAuth, login})(Login); 161 | -------------------------------------------------------------------------------- /src/Components/SearchResultDetail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kgrube on 8/2/2019 3 | */ 4 | 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import {connect} from 'react-redux'; 8 | 9 | import DetailPassword from './DetailPassword'; 10 | 11 | // show password detail, or list of client passwords 12 | function SearchResultDetail(props) { 13 | const {selectedPassword, selectedType} = props; 14 | return ( 15 | <> 16 | {selectedType === 'password' && 17 | } 20 | 21 | ); 22 | } 23 | 24 | SearchResultDetail.propTypes = { 25 | // state variables 26 | selectedPassword: PropTypes.object, 27 | selectedItem: PropTypes.object, 28 | selectedType: PropTypes.string, 29 | }; 30 | 31 | export default connect(state => ({ 32 | selectedItem: state.search.selectedItem, 33 | selectedType: state.search.selectedType, 34 | }), null)(SearchResultDetail); 35 | -------------------------------------------------------------------------------- /src/Components/SearchResultsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import MenuItem from '@material-ui/core/MenuItem'; 4 | import Typography from '@material-ui/core/Typography'; 5 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 6 | import IconHome from '@material-ui/icons/Home'; 7 | import IconKey from '@material-ui/icons/VpnKey'; 8 | import Paper from '@material-ui/core/Paper'; 9 | import MenuList from '@material-ui/core/MenuList'; 10 | import {connect} from 'react-redux'; 11 | import {makeStyles} from '@material-ui/core'; 12 | import {hideSearchResult, selectOrganization, selectPassword, setActiveComponent, setSearchContext} from '../redux/search'; 13 | import {loadPasswordById} from '../redux/passwords'; 14 | import {showAlert} from '../redux/alert'; 15 | import LinearProgress from '@material-ui/core/LinearProgress'; 16 | 17 | const useStyles = makeStyles(theme => ({ 18 | loading: { 19 | alignItems: 'center', 20 | padding: theme.spacing(1), 21 | }, 22 | resultItem: { 23 | display: 'flex', 24 | padding: theme.spacing(0.5, 0.5), 25 | '& div': { 26 | minWidth: 0, 27 | }, 28 | }, 29 | resultItemLabel: { 30 | flexGrow: 1, 31 | }, 32 | resultList: { 33 | padding: theme.spacing(0, 1), 34 | }, 35 | progressRoot: { 36 | display: 'flex', 37 | justifyContent: 'center', 38 | height: theme.spacing(8), 39 | alignItems: 'center', 40 | }, 41 | progress: { 42 | flexGrow: 1, 43 | }, 44 | })); 45 | 46 | function ResultItem(props) { 47 | const {classes, item, handleClick} = props; 48 | return ( 49 | handleClick(item)}> 50 | { 51 | item.class === 'organization' ? 52 | `${item.name}` : `${item.name} (${item.organization_name})` 53 | } 54 | 55 | {item.class === 'organization' ? : } 56 | 57 | 58 | ); 59 | } 60 | 61 | ResultItem.propTypes = { 62 | item: PropTypes.object, 63 | classes: PropTypes.object, 64 | handleClick: PropTypes.func, 65 | }; 66 | 67 | function SearchResultsList(props) { 68 | const {searchResults, searchLoading, searchLoaded} = props; 69 | const classes = useStyles(); 70 | 71 | function handleClick(item) { 72 | console.log('selected item', item); 73 | // call action 74 | if (item.class === 'organization') { 75 | props.selectOrganization(item); 76 | } else if (item.class === 'password') { 77 | props.setActiveComponent('password'); 78 | props.selectPassword(item); 79 | props.loadPasswordById(item.id); 80 | props.hideSearchResult(); 81 | } else { 82 | // this shouldn't happen 83 | props.showAlert('Invalid result selected.'); 84 | } 85 | } 86 | 87 | if (!props.searchResultOpen) { 88 | return null; 89 | } 90 | 91 | return ( 92 | <> 93 | {searchLoading && 94 |
95 | 96 |
97 | } 98 | 99 | {!searchLoading && searchLoaded && 100 | 103 | {searchLoaded && searchResults.length > 0 && searchResults.map((item, idx) => { 104 | const label = `${item.label}-${idx}`; 105 | return ( 106 | 112 | ); 113 | })} 114 | {searchLoaded && searchResults.length === 0 && 115 | 116 | No results 117 | } 118 | 119 | } 120 | 121 | 122 | ); 123 | } 124 | 125 | SearchResultsList.propTypes = { 126 | searchResultOpen: PropTypes.bool.isRequired, 127 | searchResults: PropTypes.array.isRequired, 128 | searchLoading: PropTypes.bool.isRequired, 129 | searchLoaded: PropTypes.bool.isRequired, 130 | 131 | // actions 132 | showAlert: PropTypes.func, 133 | selectItem: PropTypes.func, 134 | hideSearchResult: PropTypes.func, 135 | selectOrganization: PropTypes.func, 136 | selectPassword: PropTypes.func, 137 | loadOrganizationPasswords: PropTypes.func, 138 | loadPasswordById: PropTypes.func, 139 | setActiveComponent: PropTypes.func, 140 | setSearchContext: PropTypes.func, 141 | }; 142 | 143 | SearchResultsList.defaultProps = { 144 | showSearchResult: false, 145 | searchResults: [], 146 | }; 147 | 148 | export default connect( 149 | // state => ({...state.search}) 150 | null, 151 | { 152 | setActiveComponent, 153 | hideSearchResult, 154 | selectPassword, 155 | selectOrganization, 156 | loadPasswordById, 157 | showAlert, 158 | setSearchContext, 159 | })(SearchResultsList); 160 | -------------------------------------------------------------------------------- /src/Components/SearchSelect.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {fade, makeStyles} from '@material-ui/core/styles'; 4 | import IconSearch from '@material-ui/icons/Search'; 5 | import IconGlobe from '@material-ui/icons/Language'; 6 | import InputBase from '@material-ui/core/InputBase'; 7 | 8 | import {connect} from 'react-redux'; 9 | 10 | import {loadSearch, setSearchText, toggleSearchContext} from '../redux/search'; 11 | import Paper from '@material-ui/core/Paper'; 12 | import IconButton from '@material-ui/core/IconButton'; 13 | 14 | const useStyles = makeStyles(theme => ({ 15 | paperRoot: { 16 | padding: theme.spacing(1), 17 | display: 'flex', 18 | }, 19 | iconButton: { 20 | padding: 10, 21 | }, 22 | inputBase: { 23 | marginLeft: 8, 24 | flex: 1, 25 | }, 26 | })); 27 | 28 | function SearchSelect(props) { 29 | const [searchTimeout, setSearchTimeout] = React.useState([]); 30 | 31 | function handleChange(event) { 32 | // check if they're still typing, delete the last request if so 33 | if (searchTimeout.length > 0) { 34 | searchTimeout.forEach(id => clearTimeout(id)); 35 | } 36 | 37 | const searchText = event.target.value; 38 | props.setSearchText(searchText); 39 | 40 | setSearchTimeout([...searchTimeout, setTimeout(() => { 41 | props.loadSearch(); 42 | setSearchTimeout([]); 43 | }, 150)]); 44 | } 45 | 46 | function handleSearchContextChange() { 47 | props.toggleSearchContext(); 48 | } 49 | 50 | const classes = useStyles(); 51 | 52 | return ( 53 | 54 | 55 | {props.searchContext === 'organization' 56 | ? 57 | : 58 | } 59 | 60 | 69 | 70 | ); 71 | } 72 | 73 | SearchSelect.propTypes = { 74 | // actions 75 | loadSearch: PropTypes.func, 76 | setSearchText: PropTypes.func, 77 | toggleSearchContext: PropTypes.func, 78 | 79 | // state 80 | searchLoading: PropTypes.bool, 81 | searchLoaded: PropTypes.bool, 82 | 83 | // values 84 | searchResults: PropTypes.array, 85 | searchText: PropTypes.string, 86 | searchContext: PropTypes.oneOf(['global', 'organization']), 87 | selectedOrganization: PropTypes.object, 88 | }; 89 | 90 | SearchSelect.defaultProps = { 91 | searchLoading: false, 92 | searchLoaded: false, 93 | searchContext: 'global', 94 | 95 | searchText: '', 96 | searchResults: [], 97 | }; 98 | 99 | export default connect( 100 | state => ({...state.search}), 101 | {loadSearch, setSearchText, toggleSearchContext})(SearchSelect); 102 | -------------------------------------------------------------------------------- /src/Container/Home.js: -------------------------------------------------------------------------------- 1 | import {hot} from 'react-hot-loader/root'; 2 | import React, {useEffect} from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import Container from '@material-ui/core/Container'; 5 | import CssBaseline from '@material-ui/core/CssBaseline'; 6 | import ThemeProvider from '@material-ui/styles/ThemeProvider'; 7 | import createMuiTheme from '@material-ui/core/styles/createMuiTheme'; 8 | import {connect} from 'react-redux'; 9 | import makeStyles from '@material-ui/core/styles/makeStyles'; 10 | 11 | import Login from '../Components/Login'; 12 | import Alert from '../Components/Alert'; 13 | 14 | import {checkToken} from '../redux/auth'; 15 | import AppBarHeader from '../Components/AppBarHeader'; 16 | import SearchResultsList from '../Components/SearchResultsList'; 17 | import SearchResultDetail from '../Components/SearchResultDetail'; 18 | import SearchSelect from '../Components/SearchSelect'; 19 | 20 | const theme = createMuiTheme({ 21 | breakpoints: { 22 | xs: 0, 23 | sm: 250, 24 | md: 960, 25 | lg: 1280, 26 | xl: 1920, 27 | }, 28 | typography: { 29 | fontSize: 13, 30 | }, 31 | }); 32 | 33 | const useStyles = makeStyles((currentTheme) => ({ 34 | '@global': { 35 | body: { 36 | backgroundColor: theme.palette.common.white, 37 | }, 38 | }, 39 | containerRoot: { 40 | padding: currentTheme.spacing(0, 1), 41 | [theme.breakpoints.only('xs')]: { 42 | 'overflowY': 'scroll', 43 | height: 500, 44 | }, 45 | [theme.breakpoints.only('sm')]: { 46 | 'overflowY': 'scroll', 47 | height: 300, 48 | }, 49 | }, 50 | })); 51 | 52 | function Home(props) { 53 | const { 54 | loggedIn, 55 | token, 56 | loginPending, 57 | user: {subdomain}, 58 | selectedPassword, 59 | searchResultOpen, 60 | searchLoaded, 61 | searchLoading, 62 | searchResults, 63 | } = props; 64 | const classes = useStyles(); 65 | 66 | useEffect(() => { 67 | // // if there's a saved token, check if it's valid 68 | // // @TODO need to check token is valid more often 69 | props.checkToken(); 70 | 71 | console.log('token changed', token); 72 | }, [token]); 73 | 74 | return ( 75 | 76 | 77 | 78 | 79 | 80 | {!loggedIn && !loginPending && 81 | } 82 | {loggedIn && !selectedPassword && 83 | <> 84 | 85 | 91 | } 92 | {loggedIn && selectedPassword && 93 | } 94 | 95 | 96 | ); 97 | } 98 | 99 | Home.propTypes = { 100 | // actions 101 | checkToken: PropTypes.func, 102 | 103 | // state 104 | loggedIn: PropTypes.bool, 105 | loginPending: PropTypes.bool, 106 | searchResultOpen: PropTypes.bool, 107 | searchLoading: PropTypes.bool, 108 | searchLoaded: PropTypes.bool, 109 | 110 | // values 111 | searchResults: PropTypes.array, 112 | user: PropTypes.object, 113 | token: PropTypes.string, 114 | selectedPassword: PropTypes.object, 115 | }; 116 | 117 | Home.defaultProps = { 118 | loggedIn: false, 119 | loginPending: false, 120 | token: undefined, 121 | searchResultOpen: false, 122 | selectedOrganization: undefined, 123 | }; 124 | 125 | export default connect(state => ({ 126 | ...state.auth, 127 | selectedPassword: state.search.selectedPassword, 128 | searchResultOpen: state.search.searchResultOpen, 129 | searchResults: state.search.searchResults, 130 | searchLoading: state.search.searchLoading, 131 | searchLoaded: state.search.searchLoaded, 132 | }), {checkToken})(hot(Home)); 133 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import 'core-js/stable'; 2 | import 'regenerator-runtime/runtime'; 3 | import 'react-hot-loader'; 4 | import ReactDOM from 'react-dom'; 5 | import React from 'react'; 6 | import Home from './Container/Home'; 7 | import {Provider} from 'react-redux'; 8 | 9 | import {getSavedToken} from './helpers'; 10 | import create from './redux/create'; 11 | 12 | // load from localSettings 13 | const store = create(); 14 | 15 | const App = () => ; 16 | 17 | ReactDOM.render(, document.getElementById('app')); 18 | -------------------------------------------------------------------------------- /src/extension/Promote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mspgeek/connectwise-control-itglue-helper/f4e1a50dd0ea355e6158935b2395673f939192cf/src/extension/Promote.png -------------------------------------------------------------------------------- /src/extension/manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.1.101 4 | Extension Developer (Clone) 5 | ConnectWise Labs 6 | Development suite for Extensions. Create, edit, and publish your own Extensions. 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | /* global __DEV__ */ 2 | /* global __CORS_ANYWHERE__ */ 3 | import ITGlue from 'node-itglue'; 4 | 5 | import {SETTING_TOKEN, SETTING_REDUX, CORS_ANYWHERE} from './strings'; 6 | 7 | /** 8 | * @param {string} key 9 | * @param {string} value 10 | */ 11 | export function setSCSettingValue(key, value) { 12 | try { 13 | if (__DEV__) { 14 | window.localStorage.setItem(key, value); 15 | } else { 16 | window.external.setSettingValue(key, value); 17 | } 18 | } catch (e) { 19 | console.error('setSCSettingValue', e); 20 | } 21 | } 22 | 23 | /** 24 | * @param {string} key 25 | */ 26 | export function getSCSettingValue(key) { 27 | let value; 28 | try { 29 | if (__DEV__) { 30 | value = window.localStorage.getItem(key); 31 | } else { 32 | value = window.external.getSettingValue(key); 33 | } 34 | } catch (e) { 35 | console.error('getSCSettingValue', e); 36 | } 37 | return value; 38 | } 39 | 40 | /** 41 | * @param {string} key 42 | */ 43 | export function deleteSCSettingValue(key) { 44 | try { 45 | if (__DEV__) { 46 | window.localStorage.setItem(key, ''); 47 | } else { 48 | window.external.setSettingValue(key, ''); 49 | } 50 | } catch (e) { 51 | console.error('deleteSCSettingValue', e); 52 | } 53 | } 54 | 55 | // @TODO first argument is domain, parse domain? 56 | export function sendCredentials(username, password) { 57 | try { 58 | if (__DEV__) { 59 | alert(`send credentials: ${username}:${password}`); 60 | } else { 61 | window.external.sendCredentials(null, username, password); 62 | } 63 | } catch (e) { 64 | } 65 | } 66 | 67 | export function sendText(text) { 68 | try { 69 | if (__DEV__) { 70 | alert(`typing text ${text}`); 71 | } else { 72 | window.external.sendText(text); 73 | } 74 | } catch (e) { 75 | 76 | } 77 | } 78 | 79 | export function saveToken(token) { 80 | console.log('saveToken', token); 81 | if (token) { 82 | setSCSettingValue(SETTING_TOKEN, token); 83 | } else { 84 | setSCSettingValue(SETTING_TOKEN, ''); 85 | } 86 | 87 | } 88 | 89 | export function getSavedToken() { 90 | const token = getSCSettingValue(SETTING_TOKEN); 91 | console.log('getSavedToken', token); 92 | return token; 93 | } 94 | 95 | export function deleteToken() { 96 | deleteSCSettingValue(SETTING_TOKEN); 97 | } 98 | 99 | export function saveStore(store) { 100 | // @TODO this doesn't work live 101 | // this is too big? 102 | setSCSettingValue(SETTING_REDUX, JSON.stringify(store)); 103 | } 104 | 105 | export function saveTokenFromStore(store) { 106 | const {auth: {token}} = store; 107 | saveToken(token); 108 | } 109 | 110 | export function getStore() { 111 | try { 112 | return JSON.parse(getSCSettingValue(SETTING_REDUX)); 113 | } catch (err) { 114 | return undefined; 115 | } 116 | } 117 | 118 | export function clearStore() { 119 | setSCSettingValue(SETTING_REDUX, ''); 120 | } 121 | 122 | /** 123 | * @param token 124 | * @returns {Promise} 125 | */ 126 | export function verifyToken(token) { 127 | const itg = new ITGlue({ 128 | mode: 'bearer', 129 | token, 130 | }); 131 | 132 | return itg.get({path: '/organizations'}) 133 | .then(() => true) 134 | .catch(() => false); 135 | } 136 | 137 | export function getOrganizationPasswords({token, orgId}) { 138 | const itg = new ITGlue({ 139 | mode: 'bearer', 140 | token, 141 | }); 142 | 143 | return itg.get({ 144 | path: `/organizations/${orgId}/relationships/passwords`, 145 | params: { 146 | 'page[size]': 1000, 147 | 'page[number]': 1, 148 | 'sort': 'name', 149 | }, 150 | }) 151 | .then(results => results.data.map((password) => ({ 152 | // shape match 153 | // class, class_name, id, name, username, hint, organization_name 154 | id: password.id, 155 | class: 'password', 156 | class_name: 'Password', 157 | orgId: password.attributes['organization-id'], 158 | organization_name: password.attributes['organization-name'], 159 | name: password.attributes.name, 160 | username: password.attributes.username, 161 | category: password.attributes['password-category-name'], 162 | }))); 163 | } 164 | 165 | export function getPasswordById({token, passwordId, showPassword = false}) { 166 | const itg = new ITGlue({ 167 | mode: 'bearer', 168 | token, 169 | }); 170 | 171 | return itg.get({path: `/passwords/${passwordId}`, params: {'show_password': showPassword}}) 172 | .then(result => { 173 | return result.data; 174 | }); 175 | } 176 | 177 | export function getItGlueJsonWebToken({subdomain, otp, email, password}) { 178 | const config = { 179 | mode: 'user', 180 | companyUrl: `https://${subdomain}.itglue.com`, 181 | user: { 182 | email, 183 | password, 184 | otp, 185 | }, 186 | }; 187 | 188 | if (__CORS_ANYWHERE__) { 189 | config.companyUrl = `${CORS_ANYWHERE}${config.companyUrl}`; 190 | } 191 | 192 | const itg = new ITGlue(config); 193 | 194 | return itg.getItGlueJsonWebToken({email, password, otp}); 195 | } 196 | 197 | export function getSearch({subdomain, token, searchText, kind = 'organizations,passwords', orgId}) { 198 | const config = { 199 | companyUrl: `https://${subdomain}.itglue.com`, 200 | token, 201 | mode: 'user-bearer', 202 | }; 203 | 204 | if (__CORS_ANYWHERE__) { 205 | config.companyUrl = `${CORS_ANYWHERE}${config.companyUrl}`; 206 | } 207 | const itg = new ITGlue(config); 208 | 209 | return itg.search({ 210 | query: searchText, 211 | limit: 50, 212 | kind, 213 | related: true, 214 | filter_organization_id: orgId, 215 | }); 216 | } 217 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ITGlue Helper 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/redux/alert.js: -------------------------------------------------------------------------------- 1 | const ALERT_SHOW = 'alert/ALERT_SHOW'; 2 | const ALERT_HIDE = 'alert/ALERT_HIDE'; 3 | 4 | const RESET = 'alert/RESET'; 5 | 6 | export const initialState = { 7 | show: false, 8 | message: '', 9 | }; 10 | export default function reducer(state = initialState, action = {}) { 11 | switch (action.type) { 12 | case ALERT_SHOW: 13 | return { 14 | ...state, 15 | message: action.message, 16 | show: true, 17 | }; 18 | case ALERT_HIDE: 19 | return initialState; 20 | case RESET: 21 | return initialState; 22 | default: 23 | return state; 24 | } 25 | 26 | } 27 | 28 | export function showAlert(message) { 29 | return { 30 | type: ALERT_SHOW, 31 | message, 32 | }; 33 | } 34 | 35 | export function dismissAlert() { 36 | return { 37 | type: ALERT_HIDE, 38 | }; 39 | } 40 | 41 | export function reset() { 42 | return { 43 | type: RESET, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/redux/auth.js: -------------------------------------------------------------------------------- 1 | import {saveToken, verifyToken, getItGlueJsonWebToken, clearStore, getSavedToken} from '../helpers'; 2 | import {reset as resetAlert, dismissAlert} from './alert'; 3 | import {reset as resetPasswords} from './passwords'; 4 | import {reset as resetSearch, setActiveComponent} from './search'; 5 | 6 | const LOGOUT = 'auth/LOGOUT'; 7 | 8 | const LOGIN = 'auth/LOGIN'; 9 | const LOGIN_SUCCESS = 'auth/LOGIN_SUCCESS'; 10 | const LOGIN_FAIL = 'auth/LOGIN_FAIL'; 11 | 12 | const SET_AUTH = 'auth/SET_AUTH'; 13 | const NO_TOKEN = 'auth/NO_TOKEN'; 14 | 15 | const LOGIN_TOKEN = 'auth/LOGIN_TOKEN'; 16 | const LOGIN_TOKEN_SUCCESS = 'auth/LOGIN_TOKEN_SUCCESS'; 17 | const LOGIN_TOKEN_FAIL = 'auth/LOGIN_TOKEN_FAIL'; 18 | 19 | const RESET = 'auth/RESET'; 20 | 21 | export const initialState = { 22 | user: { 23 | subdomain: '', 24 | otp: '', 25 | email: '', 26 | password: '', 27 | }, 28 | loginPending: false, 29 | loginError: undefined, 30 | loggedIn: false, 31 | 32 | token: getSavedToken(), 33 | }; 34 | 35 | export default function reducer(state = initialState, action = {}) { 36 | switch (action.type) { 37 | case LOGIN: 38 | return { 39 | ...state, 40 | loginPending: true, 41 | loginError: undefined, 42 | loggedIn: false, 43 | token: undefined, 44 | }; 45 | case LOGIN_SUCCESS: 46 | saveToken(action.result); 47 | return { 48 | ...state, 49 | loginPending: false, 50 | loginError: undefined, 51 | loggedIn: true, 52 | token: action.result, 53 | }; 54 | case LOGIN_FAIL: 55 | saveToken(''); 56 | return { 57 | ...state, 58 | loginPending: false, 59 | loginError: action.error, 60 | loggedIn: false, 61 | token: undefined, 62 | }; 63 | case SET_AUTH: 64 | return { 65 | ...state, 66 | user: { 67 | ...state.user, 68 | [action.name]: action.value, 69 | }, 70 | }; 71 | case NO_TOKEN: 72 | saveToken(''); 73 | return { 74 | ...state, 75 | token: undefined, 76 | }; 77 | case LOGIN_TOKEN: 78 | return { 79 | ...state, 80 | loginPending: true, 81 | loginError: undefined, 82 | loggedIn: false, 83 | }; 84 | case LOGIN_TOKEN_SUCCESS: 85 | return { 86 | ...state, 87 | loginPending: false, 88 | loginError: undefined, 89 | loggedIn: true, 90 | }; 91 | case LOGIN_TOKEN_FAIL: 92 | saveToken(''); 93 | return { 94 | ...state, 95 | loginPending: false, 96 | loginError: action.error, 97 | loggedIn: false, 98 | token: undefined, 99 | }; 100 | case LOGOUT: 101 | return { 102 | ...initialState, 103 | token: undefined, 104 | }; 105 | case RESET: 106 | return { 107 | ...initialState, 108 | token: undefined, 109 | }; 110 | default: 111 | return state; 112 | } 113 | } 114 | 115 | export function login() { 116 | return (dispatch, getState) => { 117 | const {subdomain, otp, email, password} = getState().auth.user; 118 | 119 | return dispatch({ 120 | types: [LOGIN, LOGIN_SUCCESS, LOGIN_FAIL], 121 | promise: getItGlueJsonWebToken({subdomain, otp, email, password}) 122 | .then((token) => { 123 | dispatch(dismissAlert()); 124 | dispatch(setActiveComponent('header')); 125 | saveToken(token); 126 | return token; 127 | }), 128 | }); 129 | }; 130 | } 131 | 132 | export function checkToken() { 133 | return (dispatch, getState) => { 134 | const {token} = getState().auth; 135 | if (!token) { 136 | return dispatch({type: NO_TOKEN}); 137 | } 138 | 139 | return dispatch({ 140 | types: [LOGIN_TOKEN, LOGIN_TOKEN_SUCCESS, LOGIN_TOKEN_FAIL], 141 | promise: verifyToken(token) 142 | .then(() => { 143 | dispatch(dismissAlert()); 144 | }), 145 | }); 146 | }; 147 | } 148 | 149 | export function setAuth(name, value) { 150 | return { 151 | type: SET_AUTH, 152 | name, 153 | value, 154 | }; 155 | } 156 | 157 | export function logout() { 158 | clearStore(); 159 | saveToken(''); 160 | return (dispatch) => { 161 | dispatch({type: LOGOUT}); 162 | }; 163 | } 164 | 165 | export function reset() { 166 | return { 167 | type: RESET, 168 | }; 169 | } 170 | 171 | export function resetAll() { 172 | saveToken(''); 173 | return (dispatch) => { 174 | dispatch(resetAlert()); 175 | dispatch(resetPasswords()); 176 | dispatch(resetSearch()); 177 | dispatch(reset()); 178 | }; 179 | } 180 | -------------------------------------------------------------------------------- /src/redux/create.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware, compose} from 'redux'; 2 | import {composeWithDevTools} from 'redux-devtools-extension'; 3 | import reducer from './reducer'; 4 | import {promiseThunkMiddleware, errorHandler, cacheHandler} from './middleware'; 5 | import {getSavedToken, getStore} from '../helpers'; 6 | import {initialState as alert} from './alert'; 7 | import {initialState as auth} from './auth'; 8 | import {initialState as passwords} from './passwords'; 9 | import {initialState as search} from './search'; 10 | 11 | // load saved state 12 | let initialState = getStore(); 13 | 14 | if (!initialState) { 15 | initialState = { 16 | alert, auth, passwords, search, 17 | }; 18 | } 19 | 20 | const token = getSavedToken(); 21 | 22 | console.log('initialState', initialState); 23 | 24 | const middleware = [promiseThunkMiddleware(), errorHandler(), cacheHandler()]; 25 | 26 | export default function create() { 27 | return createStore( 28 | reducer, 29 | { 30 | ...initialState, 31 | // strip auth variables to prevent weird login pending issues 32 | auth: { 33 | user: { 34 | ...initialState.auth.user, 35 | }, 36 | token, 37 | }, 38 | }, 39 | composeWithDevTools(applyMiddleware(...middleware))); 40 | } 41 | -------------------------------------------------------------------------------- /src/redux/middleware.js: -------------------------------------------------------------------------------- 1 | /* global __DEV__ */ 2 | import {saveStore, saveTokenFromStore} from '../helpers'; 3 | import {showAlert} from '../redux/alert'; 4 | import {logout} from '../redux/auth'; 5 | import isArray from 'lodash/isArray'; 6 | 7 | export function promiseThunkMiddleware() { 8 | return ({dispatch, getState}) => next => action => { 9 | if (typeof action === 'function') { 10 | return action(dispatch, getState); 11 | } 12 | 13 | const {promise, types, ...rest} = action; 14 | 15 | if (!promise) { 16 | return next(action); 17 | } 18 | 19 | const [REQUEST, SUCCESS, FAILURE] = types; 20 | 21 | next({...rest, type: REQUEST}); 22 | 23 | /* eslint-disable promise/no-callback-in-promise */ 24 | Promise.all([promise]) 25 | .then(result => { 26 | next({...rest, result: result[0], type: SUCCESS}); 27 | }) 28 | .catch(error => { 29 | let raisedError = error; 30 | if (isArray(error)) { 31 | raisedError = error[0]; 32 | } 33 | next({...rest, error: raisedError, type: FAILURE}); 34 | 35 | if (raisedError.status === 401) { 36 | dispatch(logout()); 37 | } 38 | 39 | console.error('MIDDLEWARE ERROR', raisedError); 40 | }); 41 | }; 42 | } 43 | 44 | export function errorHandler() { 45 | return ({dispatch, getState}) => next => action => { 46 | if (action.error) { 47 | dispatch(showAlert(action.error)); 48 | } 49 | return next(action); 50 | }; 51 | } 52 | 53 | export function cacheHandler() { 54 | return ({dispatch, getState}) => next => action => { 55 | const oldState = getState(); 56 | const oldToken = getState().auth.token; 57 | 58 | next(action); 59 | 60 | const newState = getState(); 61 | const newToken = getState().auth.token; 62 | 63 | if (oldToken !== newToken) { 64 | saveTokenFromStore(getState()); 65 | } 66 | // @TODO make this semi-intelligent 67 | saveStore(getState()); 68 | }; 69 | } -------------------------------------------------------------------------------- /src/redux/passwords.js: -------------------------------------------------------------------------------- 1 | import {getPasswordById} from '../helpers'; 2 | 3 | const GET_PASSWORD = 'passwords/GET_PASSWORD'; 4 | const GET_PASSWORD_SUCCESS = 'passwords/GET_PASSWORD_SUCCESS'; 5 | const GET_PASSWORD_FAIL = 'passwords/GET_PASSWORD_FAIL'; 6 | 7 | const SELECT_PASSWORD = 'passwords/SELECT_PASSWORD'; 8 | const DESELECT_PASSWORD = 'passwords/DESELECT_PASSWORD'; 9 | 10 | const RESET = 'passwords/RESET'; 11 | 12 | export const initialState = { 13 | passwords: [], 14 | passwordsLoading: false, 15 | passwordsLoaded: false, 16 | passwordsLoadError: undefined, 17 | 18 | password: undefined, 19 | passwordLoaded: false, 20 | passwordLoading: false, 21 | passwordLoadError: undefined, 22 | 23 | passwordId: undefined, 24 | }; 25 | 26 | export default function reducer(state = initialState, action = {}) { 27 | switch (action.type) { 28 | case SELECT_PASSWORD: 29 | return { 30 | // reset to initial state 31 | ...initialState, 32 | passwordId: action.passwordId, 33 | }; 34 | case DESELECT_PASSWORD: 35 | return initialState; 36 | case GET_PASSWORD: 37 | return { 38 | ...state, 39 | password: undefined, 40 | passwordLoaded: false, 41 | passwordLoading: true, 42 | passwordLoadError: undefined, 43 | }; 44 | case GET_PASSWORD_SUCCESS: 45 | return { 46 | ...state, 47 | password: action.result, 48 | passwordLoaded: true, 49 | passwordLoading: false, 50 | passwordLoadError: undefined, 51 | }; 52 | case GET_PASSWORD_FAIL: 53 | return { 54 | ...state, 55 | password: undefined, 56 | passwordLoaded: false, 57 | passwordLoading: false, 58 | passwordLoadError: action.error, 59 | }; 60 | case RESET: 61 | return initialState; 62 | default: 63 | return state; 64 | } 65 | } 66 | 67 | export function loadPasswordById(passwordId) { 68 | return (dispatch, getState) => { 69 | const {auth: {token}} = getState(); 70 | dispatch({ 71 | types: [GET_PASSWORD, GET_PASSWORD_SUCCESS, GET_PASSWORD_FAIL], 72 | promise: getPasswordById({token, passwordId, showPassword: false}), 73 | }); 74 | }; 75 | } 76 | 77 | export function deselectPassword() { 78 | return { 79 | type: DESELECT_PASSWORD, 80 | }; 81 | } 82 | 83 | export function reset() { 84 | return { 85 | type: RESET, 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/redux/reducer.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | 3 | import auth from './auth'; 4 | import alert from './alert'; 5 | import passwords from './passwords'; 6 | import search from './search'; 7 | 8 | export default combineReducers({ 9 | auth, 10 | alert, 11 | passwords, 12 | search, 13 | }); 14 | -------------------------------------------------------------------------------- /src/redux/search.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kgrube on 8/1/2019 3 | */ 4 | import {getSearch} from '../helpers'; 5 | import {deselectPassword as passwordDeselect} from './passwords'; 6 | 7 | const LOAD_SEARCH = 'search/LOAD_SEARCH'; 8 | const LOAD_SEARCH_SUCCESS = 'search/LOAD_SEARCH_SUCCESS'; 9 | const LOAD_SEARCH_FAIL = 'search/LOAD_SEARCH_FAIL'; 10 | 11 | const SET_SEARCH_TEXT = 'search/SET_SEARCH_TEXT'; 12 | const SET_SEARCH_CONTEXT = 'search/SET_SEARCH_CONTEXT'; 13 | const SHOW_SEARCH_RESULT = 'search/SHOW_SEARCH_RESULT'; 14 | const HIDE_SEARCH_RESULT = 'search/HIDE_SEARCH_RESULT'; 15 | const SET_ACTIVE_COMPONENT = 'search/SET_ACTIVE_COMPONENT'; 16 | const CLEAR_RESULTS = 'search/CLEAR_RESULTS'; 17 | 18 | const SELECT_PASSWORD = 'search/SELECT_PASSWORD'; 19 | const DESELECT_PASSWORD = 'search/DESELECT_PASSWORD'; 20 | const SELECT_ORGANIZATION = 'search/SELECT_ORGANIZATION'; 21 | const DESELECT_ORGANIZATION = 'search/DESELECT_ORGANIZATION'; 22 | 23 | const RESET = 'search/RESET'; 24 | 25 | export const initialState = { 26 | searchText: '', 27 | searchResults: [], 28 | 29 | searchLoading: false, 30 | searchLoaded: false, 31 | 32 | searchResultOpen: false, 33 | // the context in which to search 34 | // 'global' => use /search.json 35 | // 'organization' => include filter_organization_id 36 | searchContext: 'global', // ['global', 'organization'] 37 | selectedOrganization: undefined, 38 | selectedPassword: undefined, 39 | selectedType: undefined, // ['password','organization'] 40 | 41 | activeComponent: 'header', 42 | }; 43 | 44 | export default function reducer(state = initialState, action = {}) { 45 | switch (action.type) { 46 | case LOAD_SEARCH: 47 | return { 48 | ...state, 49 | searchResults: [], 50 | searchLoading: true, 51 | searchLoaded: false, 52 | }; 53 | case LOAD_SEARCH_SUCCESS: 54 | return { 55 | ...state, 56 | searchResults: action.result, 57 | searchLoading: false, 58 | searchLoaded: true, 59 | }; 60 | case LOAD_SEARCH_FAIL: 61 | return { 62 | ...state, 63 | searchResults: [], 64 | searchLoading: false, 65 | searchLoaded: false, 66 | }; 67 | case SET_SEARCH_TEXT: 68 | return { 69 | ...state, 70 | searchText: action.searchText, 71 | }; 72 | case SELECT_PASSWORD: 73 | return { 74 | ...state, 75 | searchContext: 'organization', 76 | selectedType: 'password', 77 | selectedPassword: action.password, 78 | }; 79 | case DESELECT_PASSWORD: 80 | return { 81 | ...state, 82 | selectedType: undefined, 83 | selectedPassword: undefined, 84 | }; 85 | case SELECT_ORGANIZATION: 86 | return { 87 | ...state, 88 | searchContext: 'organization', 89 | selectedType: 'organization', 90 | selectedOrganization: action.organization, 91 | }; 92 | case DESELECT_ORGANIZATION: 93 | return { 94 | ...state, 95 | selectedType: undefined, 96 | selectedOrganization: undefined, 97 | }; 98 | case CLEAR_RESULTS: 99 | return { 100 | ...state, 101 | searchResults: [], 102 | }; 103 | case HIDE_SEARCH_RESULT: 104 | return { 105 | ...state, 106 | searchResultOpen: false, 107 | }; 108 | case SHOW_SEARCH_RESULT: 109 | return { 110 | ...state, 111 | searchResultOpen: true, 112 | }; 113 | case SET_SEARCH_CONTEXT: 114 | return { 115 | ...state, 116 | searchContext: action.searchContext, 117 | }; 118 | case SET_ACTIVE_COMPONENT: 119 | return { 120 | ...state, 121 | activeComponent: action.activeComponent, 122 | }; 123 | case RESET: 124 | return initialState; 125 | default: 126 | return state; 127 | } 128 | } 129 | 130 | export function loadSearch() { 131 | return (dispatch, getState) => { 132 | const { 133 | auth: {user: {subdomain}, token}, 134 | search: {searchContext, selectedOrganization, searchText}, 135 | passwords: {password}, 136 | } = getState(); 137 | 138 | const searchOptions = {subdomain, token, searchText}; 139 | 140 | if (searchContext === 'organization') { 141 | searchOptions.orgId = (selectedOrganization && selectedOrganization.id) || password.attributes['organization-id']; 142 | searchOptions.kind = 'passwords'; 143 | } else { 144 | searchOptions.kind = 'organizations,passwords'; 145 | } 146 | 147 | return dispatch({ 148 | types: [LOAD_SEARCH, LOAD_SEARCH_SUCCESS, LOAD_SEARCH_FAIL], 149 | promise: getSearch(searchOptions) 150 | .then(results => { 151 | dispatch(showSearchResult()); 152 | return results; 153 | }), 154 | }); 155 | }; 156 | } 157 | 158 | export function setSearchText(searchText) { 159 | return { 160 | type: SET_SEARCH_TEXT, 161 | searchText, 162 | }; 163 | } 164 | 165 | export function showSearchResult() { 166 | return { 167 | type: SHOW_SEARCH_RESULT, 168 | }; 169 | } 170 | 171 | export function hideSearchResult() { 172 | return { 173 | type: HIDE_SEARCH_RESULT, 174 | }; 175 | } 176 | 177 | export function selectPassword(password) { 178 | return { 179 | type: SELECT_PASSWORD, 180 | password, 181 | }; 182 | } 183 | 184 | export function deselectPassword() { 185 | return (dispatch, getState) => { 186 | return dispatch({ 187 | type: DESELECT_PASSWORD, 188 | }); 189 | }; 190 | } 191 | 192 | export function selectOrganization(organization) { 193 | return (dispatch, getState) => { 194 | dispatch({ 195 | type: SELECT_ORGANIZATION, 196 | organization, 197 | }); 198 | dispatch(setActiveComponent('organization')); 199 | dispatch(setSearchContext({searchContext: 'organization'})); 200 | dispatch(setSearchText('')); 201 | dispatch(clearSearchResults()); 202 | 203 | }; 204 | } 205 | 206 | export function deselectOrganization() { 207 | return (dispatch) => { 208 | dispatch(passwordDeselect()); 209 | dispatch({ 210 | type: DESELECT_ORGANIZATION, 211 | }); 212 | }; 213 | } 214 | 215 | export function clearSearchResults() { 216 | return { 217 | type: CLEAR_RESULTS, 218 | }; 219 | } 220 | 221 | export function setSearchContext({searchContext}) { 222 | return (dispatch) => { 223 | dispatch({ 224 | type: SET_SEARCH_CONTEXT, 225 | searchContext, 226 | }); 227 | dispatch(loadSearch()); 228 | }; 229 | } 230 | 231 | export function toggleSearchContext() { 232 | return (dispatch, getState) => { 233 | // @TODO finish this 234 | const {selectedType, searchContext} = getState().search; 235 | if (selectedType) { 236 | return dispatch(setSearchContext({searchContext: searchContext === 'global' ? 'organization' : 'global'})); 237 | } 238 | return dispatch(setSearchContext({searchContext: 'global'})); 239 | }; 240 | } 241 | 242 | export function handleNavigation() { 243 | return (dispatch, getState) => { 244 | const {search, activeComponent: currentComponent} = getState().search; 245 | 246 | if (currentComponent === 'utils') { 247 | dispatch(setActiveComponent('header')); 248 | } else if (currentComponent === 'header') { 249 | dispatch(setActiveComponent('utils')); 250 | } else if (currentComponent === 'organization') { 251 | // on back from org, show global results 252 | dispatch(setActiveComponent('header')); 253 | dispatch(setSearchContext({searchContext: 'global'})); 254 | dispatch(deselectOrganization()); 255 | } else if (currentComponent === 'password') { 256 | // on back from password, show org results 257 | dispatch(setActiveComponent('organization')); 258 | dispatch(setSearchContext({searchContext: 'organization'})); 259 | dispatch(showSearchResult()); 260 | dispatch(deselectPassword()); 261 | } 262 | }; 263 | } 264 | 265 | export function setActiveComponent(activeComponent) { 266 | return { 267 | type: SET_ACTIVE_COMPONENT, 268 | activeComponent, 269 | }; 270 | } 271 | 272 | export function reset() { 273 | return { 274 | type: RESET, 275 | }; 276 | } 277 | -------------------------------------------------------------------------------- /src/strings.js: -------------------------------------------------------------------------------- 1 | export const SETTING_TOKEN = '__SC_ITG__'; 2 | export const SEND_CREDENTIALS = '__SC_SEND_CREDENTIALS__'; 3 | export const SETTING_REDUX = '__SC_REDUX__'; 4 | export const CORS_ANYWHERE = 'https://cwc-cors.herokuapp.com/'; 5 | -------------------------------------------------------------------------------- /webpack-dev.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | 5 | const babelOptions = require('./babelrc'); 6 | 7 | babelOptions.plugins.push('react-hot-loader/babel'); 8 | 9 | module.exports = { 10 | mode: 'development', 11 | entry: path.resolve('./src/app.js'), 12 | plugins: [ 13 | new HtmlWebpackPlugin({ 14 | inlineSource: '.(js|css)$', 15 | template: path.resolve('./src/index.html'), 16 | }), 17 | new webpack.DefinePlugin({ 18 | __DEV__: true, 19 | __CORS_ANYWHERE__: true, 20 | __DEBUG__: false, 21 | }), 22 | ], 23 | module: { 24 | rules: [{ 25 | test: /\.(js|jsx)$/, 26 | exclude: /node_modules/, 27 | use: [{loader: 'react-hot-loader/webpack'}, {loader: 'babel-loader', options: babelOptions}, {loader: 'eslint-loader'}], 28 | }], 29 | }, 30 | target: 'web', 31 | }; 32 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin'); 4 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 5 | const {CleanWebpackPlugin} = require('clean-webpack-plugin'); 6 | const webpack = require('webpack'); 7 | 8 | const babelOptions = require('./babelrc'); 9 | 10 | module.exports = { 11 | mode: 'production', 12 | entry: path.resolve('./src/app.js'), 13 | plugins: [ 14 | new CleanWebpackPlugin({ 15 | verbose: true, 16 | }), 17 | new webpack.DefinePlugin({ 18 | __DEV__: false, 19 | __CORS_ANYWHERE__: false, // disable for deployment 20 | __DEBUG__: false, 21 | }), 22 | new HtmlWebpackPlugin({ 23 | inlineSource: '.(js)$', 24 | filename: 'ITGlue.html', 25 | template: path.resolve('./src/index.html'), 26 | }), 27 | new HtmlWebpackInlineSourcePlugin(), 28 | ], 29 | module: { 30 | rules: [{ 31 | test: /\.(js|jsx)$/, 32 | exclude: /node_modules/, 33 | use: [{ 34 | loader: 'babel-loader', 35 | options: babelOptions, 36 | }], 37 | }], 38 | }, 39 | target: 'web', 40 | optimization: { 41 | minimize: true, 42 | minimizer: [ 43 | new UglifyJsPlugin(), 44 | ], 45 | }, 46 | }; 47 | --------------------------------------------------------------------------------