├── .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 |
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 |
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 | }
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 |
--------------------------------------------------------------------------------