├── src
├── config.js
├── index.css
├── setupProxy.js
├── App.css
├── components
│ ├── Error.js
│ ├── Loading.js
│ ├── View.js
│ ├── AuthWizard.js
│ ├── auth
│ │ ├── Auth.js
│ │ ├── Register.js
│ │ ├── Login.js
│ │ ├── LoginForm.js
│ │ └── RegisterForm.js
│ ├── DeleteDialog.js
│ ├── ConfigWizard.js
│ ├── Nav.js
│ └── Instance.js
├── index.js
├── WithTheme.js
├── lib
│ ├── authWindow.js
│ └── configWindow.js
├── Router.js
├── api
│ ├── me.js
│ └── solutions.js
├── views
│ ├── demo.css
│ ├── Account.js
│ ├── SolutionsMine.js
│ ├── SolutionsDiscover.js
│ ├── Demo.js
│ └── Authentications.js
├── logo.svg
└── registerServiceWorker.js
├── public
├── app.ico
├── app.icns
├── favicon.ico
├── favicon-32.png
├── favicon-57.png
├── favicon-72.png
├── favicon-96.png
├── favicon-120.png
├── favicon-128.png
├── favicon-144.png
├── favicon-152.png
├── favicon-195.png
├── favicon-228.png
├── manifest.json
└── index.html
├── .images
└── getting-token.png
├── Dockerfile
├── .graphqlconfig
├── server
├── logging.js
├── server.js
├── domain
│ ├── login.js
│ └── registration.js
├── db.js
├── gqlclient.js
├── configuration.js
├── auth.js
├── graphql.js
└── api.js
├── .gitignore
├── graphql.config.json
├── package.json
└── README.md
/src/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | username: 'a',
3 | password: 'a',
4 | };
5 |
--------------------------------------------------------------------------------
/public/app.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/public/app.ico
--------------------------------------------------------------------------------
/public/app.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/public/app.icns
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/public/favicon-32.png
--------------------------------------------------------------------------------
/public/favicon-57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/public/favicon-57.png
--------------------------------------------------------------------------------
/public/favicon-72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/public/favicon-72.png
--------------------------------------------------------------------------------
/public/favicon-96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/public/favicon-96.png
--------------------------------------------------------------------------------
/public/favicon-120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/public/favicon-120.png
--------------------------------------------------------------------------------
/public/favicon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/public/favicon-128.png
--------------------------------------------------------------------------------
/public/favicon-144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/public/favicon-144.png
--------------------------------------------------------------------------------
/public/favicon-152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/public/favicon-152.png
--------------------------------------------------------------------------------
/public/favicon-195.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/public/favicon-195.png
--------------------------------------------------------------------------------
/public/favicon-228.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/public/favicon-228.png
--------------------------------------------------------------------------------
/.images/getting-token.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trayio/embedded-edition-sample-app/HEAD/.images/getting-token.png
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: "Roboto", "Helvetica", "Arial", sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:6-alpine
2 |
3 | COPY ./ /oem
4 | WORKDIR /oem
5 |
6 | RUN npm install
7 |
8 | ENTRYPOINT ["/usr/local/bin/npm"]
9 | CMD ["start"]
10 |
11 |
--------------------------------------------------------------------------------
/src/setupProxy.js:
--------------------------------------------------------------------------------
1 | const proxy = require('http-proxy-middleware');
2 |
3 | module.exports = function(app) {
4 | app.use(proxy('/api', { target: 'http://localhost:3001' }));
5 | };
6 |
--------------------------------------------------------------------------------
/.graphqlconfig:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "TrayOEM": {
4 | "schemaPath": "graphql.schema.json",
5 | "extensions": {
6 | "endpoints": {
7 | "default": "https://staging.tray.io/graphql"
8 | }
9 | }
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/server/logging.js:
--------------------------------------------------------------------------------
1 |
2 | export const log = ({object, message}) => {
3 | if (!process.env.quiet) {
4 | console.log('------------------------------');
5 | if (message) {
6 | console.log(message);
7 | }
8 | if (object) {
9 | console.log(JSON.stringify(object, null, 4));
10 | }
11 | console.log('------------------------------');
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | font-family: "Roboto", "Helvetica", "Arial", sans-serif;
4 | }
5 |
6 | .App-logo {
7 | animation: App-logo-spin infinite 20s linear;
8 | height: 80px;
9 | }
10 |
11 | .App-header {
12 | background-color: #222;
13 | height: 150px;
14 | padding: 20px;
15 | color: white;
16 | }
17 |
18 | .App-title {
19 | font-size: 1.5em;
20 | }
21 |
22 | .App-intro {
23 | font-size: large;
24 | }
25 |
26 | @keyframes App-logo-spin {
27 | from { transform: rotate(0deg); }
28 | to { transform: rotate(360deg); }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Error.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {withTheme} from '@material-ui/core/styles';
3 |
4 | const styles = {
5 | container: {
6 | width: "100%",
7 | height: "100%",
8 | display: "flex",
9 | alignItems: "center",
10 | justifyContent: "center",
11 | color: "crimson",
12 | padding: '0 20px',
13 | }
14 | };
15 |
16 | const Error = ({msg}) => (
17 |
18 |
{JSON.stringify(msg, null, 4)}
19 |
20 | );
21 |
22 | export default withTheme()(Error);
--------------------------------------------------------------------------------
/src/components/Loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CircularProgress from '@material-ui/core/CircularProgress';
3 |
4 | class Loading extends React.Component {
5 | render() {
6 | const styles = {
7 | container: {
8 | width: "100%",
9 | height: "100%",
10 | display: "flex",
11 | alignItems: "center",
12 | justifyContent: "center",
13 | }
14 | };
15 |
16 | const spinner = (
17 |
18 |
19 |
20 | );
21 |
22 | return this.props.loading ? spinner : this.props.children;
23 | }
24 | }
25 |
26 | export default Loading;
27 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter } from 'react-router-dom'
4 | import './index.css';
5 | import App from './Router';
6 | import registerServiceWorker from './registerServiceWorker';
7 | import blue from '@material-ui/core/colors/blue';
8 | import { createMuiTheme, MuiThemeProvider } from '@material-ui/core/styles';
9 |
10 | const theme = createMuiTheme({
11 | palette: {
12 | type: 'light',
13 | primary: blue,
14 | },
15 | });
16 |
17 | ReactDOM.render((
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | ), document.getElementById('root'));
26 |
27 | registerServiceWorker();
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 | # See https://help.github.com/ignore-files/ for more about ignoring files.
23 |
24 | # dependencies
25 | /node_modules
26 |
27 | # testing
28 | /coverage
29 |
30 | # production
31 | /build
32 |
33 | # misc
34 | .DS_Store
35 | .env.local
36 | .env.development.local
37 | .env.test.local
38 | .env.production.local
39 |
40 | npm-debug.log*
41 | yarn-debug.log*
42 | yarn-error.log*
43 | .idea/OEMReactSample.iml
44 | .idea/inspectionProfiles/
45 | .idea/misc.xml
46 | .idea/modules.xml
47 | .idea/workspace.xml
48 | .idea/vcs.xml
49 | .idea/
50 |
51 | .env
52 | .token
53 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const app = express();
3 |
4 | let bodyParser = require('body-parser');
5 |
6 | // support json encoded bodies
7 | app.use(bodyParser.json());
8 | // support encoded bodies
9 | app.use(bodyParser.urlencoded({extended: true}));
10 |
11 | // Set CORS headers
12 | app.use(function (req, res, next) {
13 | res.header("Access-Control-Allow-Origin", ["localhost"]);
14 | res.header("Access-Control-Allow-Credentials", true);
15 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
16 | next();
17 | });
18 |
19 | // Configure Express application:
20 | app.use(require('morgan')('tiny'));
21 |
22 | require('./configuration').setEnvironment();
23 |
24 | // Authentication and Authorization Middleware:
25 | require('./auth')(app);
26 |
27 | // Setup API router:
28 | require('./api')(app);
29 |
30 | app.listen(process.env.PORT || 3001, () => {
31 | console.log(`Express started on port ${process.env.PORT || 3001} with Graphql endpoint ${process.env.TRAY_ENDPOINT}`);
32 | });
33 |
--------------------------------------------------------------------------------
/server/domain/login.js:
--------------------------------------------------------------------------------
1 | /** @module domain/login */
2 |
3 | import {get} from 'lodash';
4 | import {mutations} from '../graphql';
5 | import {retrieveUserFromMockDB} from '../db';
6 |
7 | /**
8 | * Attempt to retrieve a user from the DB:
9 | * @param {Request}
10 | * @return {User | undefined}
11 | */
12 | export const attemptLogin = req => {
13 | const user = retrieveUserFromMockDB(req.body);
14 |
15 | if (user) {
16 | req.session.user = user;
17 | req.session.admin = true;
18 | }
19 |
20 | return user;
21 | };
22 |
23 | /**
24 | * Attempt to generate access token for a given user:
25 | * @param {Request}
26 | * @param {Response}
27 | * @param {User}
28 | * @return {Promise} Promise that wraps authorization mutation.
29 | */
30 | export const generateUserAccessToken = (req, res, user) =>
31 | mutations.authorize(user.trayId)
32 | .then(authorizeResponse => {
33 | req.session.token = get(
34 | authorizeResponse,
35 | 'data.authorize.accessToken'
36 | );
37 |
38 | return authorizeResponse;
39 | });
40 |
--------------------------------------------------------------------------------
/src/WithTheme.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Typography from '@material-ui/core/Typography';
3 | import { withTheme } from '@material-ui/core/styles';
4 |
5 | function WithTheme(props) {
6 | const { theme } = props;
7 | const primaryText = theme.palette.text.primary;
8 | const primaryColor = theme.palette.primary.main;
9 |
10 | const styles = {
11 | primaryText: {
12 | backgroundColor: theme.palette.background.default,
13 | padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`,
14 | color: primaryText,
15 | },
16 | primaryColor: {
17 | backgroundColor: primaryColor,
18 | padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`,
19 | color: theme.palette.common.white,
20 | },
21 | };
22 |
23 | return (
24 |
25 | {`Primary color ${primaryColor}`}
26 | {`Primary text ${primaryText}`}
27 |
28 | );
29 | }
30 |
31 | export default withTheme()(WithTheme);
32 |
--------------------------------------------------------------------------------
/server/db.js:
--------------------------------------------------------------------------------
1 | // In-memory users instead of a DB:
2 | const mockUserDB = [];
3 |
4 | /**
5 | * Retreive user from the Mock DB:
6 | * @param {User} input - {username: 'myname', password: 'mypass'}
7 | * @returns {User | undefined}
8 | */
9 | export const retrieveUserFromMockDB = input => {
10 | const matches = mockUserDB.filter(
11 | user =>
12 | user.username === input.username &&
13 | user.password === input.password
14 | );
15 |
16 | return matches[0];
17 | };
18 |
19 | /**
20 | * Check user exists in Mock DB:
21 | * @param {User} input
22 | * @returns {Boolean}
23 | */
24 | export const userExistsInMockDB = input => {
25 | const matches = mockUserDB.filter(user => user.username === input.username);
26 | return matches.length > 0;
27 | };
28 |
29 | /**
30 | * Insert user into the Mock DB:
31 | * @param {User} input
32 | *
33 | * @returns {Void}
34 | */
35 | export const insertUserToMockDB = input => {
36 | mockUserDB.push({
37 | name: input.body.name,
38 | uuid: input.uuid,
39 | trayId: input.trayId,
40 | username: input.body.username,
41 | password: input.body.password,
42 | });
43 | };
44 |
--------------------------------------------------------------------------------
/src/components/View.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Nav from './Nav';
3 | import { withTheme } from '@material-ui/core/styles';
4 |
5 | class View extends React.Component {
6 | render() {
7 | const styles = {
8 | header: {
9 | backgroundColor: "#2196F3",
10 | padding: "12px 10px",
11 | color: "white",
12 | fontWeight: 500,
13 | fontSize: "1.3rem",
14 | },
15 | container: {
16 | backgroundColor: "#F5F5F5",
17 | display: "flex",
18 | minHeight: 500,
19 | paddingBottom: 40,
20 | },
21 | content: {width: "100%"},
22 | };
23 |
24 | return (
25 |
26 |
OEM Demo Application
27 |
28 |
29 |
30 | {this.props.children}
31 |
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | export default withTheme()(View);
39 |
--------------------------------------------------------------------------------
/src/lib/authWindow.js:
--------------------------------------------------------------------------------
1 | export const openAuthWindow = (url) => {
2 | // Must open window from user interaction code otherwise it is likely
3 | // to be blocked by a popup blocker:
4 | const authWindow = window.open(
5 | undefined,
6 | '_blank',
7 | 'width=500,height=500,scrollbars=no',
8 | );
9 |
10 | const onmessage = e => {
11 | console.log('message', e.data.type, e.data);
12 |
13 | if (e.data.type === 'tray.authPopup.error') {
14 | // Handle popup error message
15 | alert(`Error: ${e.data.error}`);
16 | authWindow.close();
17 | }
18 | if (e.data.type === 'tray.authpopup.close' || e.data.type === 'tray.authpopup.finish') {
19 | authWindow.close();
20 | }
21 | };
22 | window.addEventListener('message', onmessage);
23 |
24 | // Check if popup window has been closed
25 | const CHECK_TIMEOUT = 1000;
26 | const checkClosedWindow = () => {
27 | if (authWindow.closed) {
28 | window.removeEventListener('message', onmessage);
29 | } else {
30 | setTimeout(checkClosedWindow, CHECK_TIMEOUT);
31 | }
32 | }
33 |
34 | checkClosedWindow();
35 | authWindow.location = url;
36 | };
37 |
--------------------------------------------------------------------------------
/src/Router.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
3 | import Login from "./components/auth/Login";
4 | import Register from "./components/auth/Register";
5 | import { PrivateRoute, RedirectMain } from "./components/auth/Auth";
6 |
7 | import Demo from "./views/Demo";
8 | import Account from "./views/Account";
9 | import SolutionsMine from "./views/SolutionsMine";
10 | import SolutionsDiscover from "./views/SolutionsDiscover";
11 | import Authentications from "./views/Authentications";
12 |
13 | const App = () => (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 |
30 | export default App;
31 |
--------------------------------------------------------------------------------
/server/gqlclient.js:
--------------------------------------------------------------------------------
1 | import fetch from "node-fetch";
2 | import { HttpLink } from 'apollo-link-http';
3 | import { setContext } from 'apollo-link-context';
4 | import { ApolloClient } from 'apollo-client';
5 | import { InMemoryCache } from 'apollo-cache-inmemory';
6 |
7 | const gqlEndpoint = process.env.TRAY_ENDPOINT;
8 | const masterToken = process.env.TRAY_MASTER_TOKEN;
9 |
10 | const defaultOptions = {
11 | watchQuery: {
12 | fetchPolicy: 'network-only',
13 | errorPolicy: 'ignore',
14 | },
15 | query: {
16 | fetchPolicy: 'network-only',
17 | errorPolicy: 'all',
18 | },
19 | };
20 |
21 | // Create a Apollo Client Context for a given auth token:
22 | const authLink = token =>
23 | setContext((_, {headers}) => ({
24 | headers: {
25 | ...headers,
26 | authorization: `Bearer ${token}`,
27 | },
28 | }));
29 |
30 | // Generate an Apollo Client for a given auth token:
31 | const generateClient = token =>
32 | new ApolloClient({
33 | link: authLink(token).concat(new HttpLink({uri: gqlEndpoint, fetch})),
34 | cache: new InMemoryCache(),
35 | defaultOptions,
36 | });
37 |
38 | module.exports = {
39 | generateClient,
40 | masterClient: generateClient(masterToken),
41 | };
42 |
--------------------------------------------------------------------------------
/src/api/me.js:
--------------------------------------------------------------------------------
1 | export const me = () =>
2 | fetch('/api/me', {credentials: 'include'})
3 | .then(async res => ({
4 | ok: res.ok,
5 | body: await res.json(),
6 | statusText: res.statusText,
7 | }))
8 |
9 | export const listAuths = () =>
10 | fetch('/api/auths', {credentials: 'include'})
11 | .then(async res => ({
12 | ok: res.ok,
13 | body: await res.json(),
14 | }))
15 |
16 | export const getAuthEditUrl = (authId) =>
17 | fetch('/api/auth', {
18 | body: JSON.stringify({
19 | authId
20 | }),
21 | headers: {
22 | 'content-type': 'application/json',
23 | },
24 | method: 'POST',
25 | credentials: 'include'
26 | })
27 | .then(async res => ({
28 | ok: res.ok,
29 | body: await res.json(),
30 | }))
31 |
32 | export const getAuthCreateUrl = (solutionInstanceId, externalAuthId) =>
33 | fetch('/api/auth/create', {
34 | body: JSON.stringify({
35 | solutionInstanceId,
36 | externalAuthId
37 | }),
38 | headers: {
39 | 'content-type': 'application/json',
40 | },
41 | method: 'POST',
42 | credentials: 'include'
43 | })
44 | .then(async res => ({
45 | ok: res.ok,
46 | body: await res.json(),
47 | }))
48 |
--------------------------------------------------------------------------------
/src/views/demo.css:
--------------------------------------------------------------------------------
1 |
2 | .header {
3 | color: rgb(21, 27, 38);
4 | font-size: 13px;
5 | font-family: "Helvetica", sans-serif;
6 | margin-bottom: 0;
7 | }
8 |
9 | button {
10 | background: transparent;
11 | border: 0;
12 | padding: 0;
13 | text-decoration: none;
14 | font-size: 13px;
15 | color: #14aaf5;
16 | }
17 |
18 | button:hover {
19 | color: #32c1ff;
20 | text-decoration: underline;
21 | cursor: pointer;
22 | }
23 |
24 | .integration-name {
25 | font-weight: 600;
26 | }
27 |
28 | .activate {
29 | float: right;
30 | color: rgb(183, 191, 198);
31 | font-size: 13px;
32 | }
33 |
34 | .activate:hover {
35 | cursor: pointer;
36 | text-decoration: underline;
37 | }
38 |
39 | p {
40 | font-size: 13px;
41 | line-height: 20px;
42 | margin: 0;
43 | }
44 |
45 | .footer {
46 | margin-top: 20px;
47 | padding-top: 10px;
48 | border-top: 1px solid #e0e6e8;
49 | }
50 |
51 | .integration-container {
52 | padding: 5px 0;
53 | }
54 |
55 | .deactivate {
56 | float: right;
57 | }
58 |
59 | .reconfigure {
60 | margin-right: 5px;
61 | float: right;
62 | }
63 |
64 | .reconfigure:hover, .deactivate:hover {
65 | cursor: pointer;
66 | }
67 |
68 | .workflow-header {
69 | color: #848f99;
70 | font-size: 11px;
71 | border-bottom: 1px solid #e0e6e8;
72 | width: 100%;
73 | margin-bottom: 5px;
74 | }
75 |
76 | .app-name {
77 | margin-bottom: 5px;
78 | }
79 |
80 | .demo-container {
81 | padding: 20px;
82 | background: white;
83 | }
84 |
--------------------------------------------------------------------------------
/graphql.config.json:
--------------------------------------------------------------------------------
1 | {
2 |
3 | "README_schema" : "Specifies how to load the GraphQL schema that completion, error highlighting, and documentation is based on in the IDE",
4 | "schema": {
5 |
6 | "README_file" : "Remove 'file' to use request url below. A relative or absolute path to the JSON from a schema introspection query, e.g. '{ data: ... }' or a .graphql/.graphqls file describing the schema using GraphQL Schema Language. Changes to the file are watched.",
7 | "file": "graphql.schema.json",
8 |
9 | "README_request" : "To request the schema from a url instead, remove the 'file' JSON property above (and optionally delete the default graphql.schema.json file).",
10 | "request": {
11 | "url" : "https://tray.io/graphql",
12 | "method" : "POST",
13 | "README_postIntrospectionQuery" : "Whether to POST an introspectionQuery to the url. If the url always returns the schema JSON, set to false and consider using GET",
14 | "postIntrospectionQuery" : true,
15 | "README_options" : "See the 'Options' section at https://github.com/then/then-request",
16 | "options" : {
17 | "headers": {
18 | "user-agent" : "JS GraphQL"
19 | }
20 | }
21 | }
22 |
23 | },
24 |
25 | "README_endpoints": "A list of GraphQL endpoints that can be queried from '.graphql' files in the IDE",
26 | "endpoints" : [
27 | {
28 | "name": "TrayOEM",
29 | "url": "https://tray.io/graphql",
30 | "options" : {
31 | "headers": {
32 | "user-agent" : "JS GraphQL"
33 | }
34 | }
35 | }
36 | ]
37 |
38 | }
--------------------------------------------------------------------------------
/src/components/AuthWizard.js:
--------------------------------------------------------------------------------
1 | import { withTheme } from "@material-ui/core/styles/index";
2 | import React from "react";
3 |
4 | export class AuthWizard extends React.PureComponent {
5 | state = {};
6 |
7 | constructor(props) {
8 | super(props);
9 | this.iframe = React.createRef();
10 | }
11 |
12 | componentDidMount() {
13 | window.addEventListener("message", this.handleIframeEvents);
14 | }
15 |
16 | componentWillUnmount() {
17 | window.removeEventListener("message", this.handleIframeEvents);
18 | }
19 |
20 | handleIframeEvents = (e) => {
21 | console.log(`${e.data.type} event received`);
22 | // Here we should handle all event types
23 | if (e.data.type === "tray.authPopup.error") {
24 | alert(`Error: ${e.data.error}`);
25 | }
26 | if (e.data.type === "tray.authpopup.close") {
27 | this.props.onClose();
28 | }
29 | if (e.data.type === "tray.authpopup.finish") {
30 | this.props.onClose();
31 | }
32 | };
33 |
34 | render() {
35 | const styles = {
36 | container: {
37 | flex: 1,
38 | position: "relative",
39 | },
40 | iframe: {
41 | width: "100%",
42 | height: "100%",
43 | minHeight: "500px",
44 | border: "1px solid #2196f3",
45 | borderRadius: "4px",
46 | boxSizing: "border-box",
47 | },
48 | };
49 |
50 | return (
51 |
52 |
53 |
54 | );
55 | }
56 | }
57 |
58 | export default withTheme()(AuthWizard);
59 |
--------------------------------------------------------------------------------
/src/components/auth/Auth.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Redirect, Route } from 'react-router-dom';
3 |
4 | export const auth = {
5 | isAuthenticated: false,
6 |
7 | authenticate(cb) {
8 | this.isAuthenticated = true
9 | if (typeof cb === 'function') {
10 | cb(true);
11 | }
12 | },
13 |
14 | signout(cb) {
15 | fetch('/api/logout', {
16 | method: 'POST',
17 | credentials: 'include',
18 | })
19 | .then((res) => {
20 | this.isAuthenticated = false;
21 | if (typeof cb === 'function') {
22 | // user was logged out
23 | cb(true);
24 | }
25 | })
26 | .catch((err) => {
27 | console.log('Error logging out user.');
28 | if (typeof cb === 'function') {
29 | // user was not logged out
30 | cb(false);
31 | }
32 | });
33 | }
34 | };
35 |
36 | export const PrivateRoute = ({component: Component, ...rest}) => (
37 | (
38 | auth.isAuthenticated ? (
39 |
40 | ) : (
41 |
45 | )
46 | )}/>
47 | );
48 |
49 | export const RedirectMain = (props) => (
50 | auth.isAuthenticated ? (
51 |
52 | ) : (
53 |
54 | )
55 | );
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oemreactsample",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^1.2.1",
7 | "@material-ui/icons": "^1.1.0",
8 | "apollo-boost": "^0.1.9",
9 | "apollo-cache-inmemory": "^1.2.5",
10 | "apollo-client": "^2.3.5",
11 | "apollo-link": "^1.2.2",
12 | "apollo-link-context": "^1.0.8",
13 | "apollo-link-error": "^1.1.0",
14 | "apollo-link-http": "^1.5.4",
15 | "body-parser": "^1.17.2",
16 | "dotenv": "^10.0.0",
17 | "express": "^4.16.3",
18 | "express-session": "^1.15.6",
19 | "graphql": "^0.13.2",
20 | "graphql-tag": "^2.9.2",
21 | "lodash": "^4.17.13",
22 | "morgan": "^1.9.0",
23 | "node-fetch": "^2.1.2",
24 | "react": "^16.4.0",
25 | "react-apollo": "^2.1.5",
26 | "react-dom": "^16.4.0",
27 | "react-router-dom": "^4.3.1",
28 | "react-scripts": "3.1.2",
29 | "uuid": "^3.2.1"
30 | },
31 | "scripts": {
32 | "start": "concurrently \"npm run api\" \"npm run start-client\"",
33 | "start-client": "HTTPS=true react-scripts start",
34 | "api": "BABEL_DISABLE_CACHE=1 nodemon --exec babel-node -r dotenv/config server/server.js --presets @babel/preset-env",
35 | "build": "react-scripts build",
36 | "test": "react-scripts test --env=jsdom",
37 | "eject": "react-scripts eject"
38 | },
39 | "devDependencies": {
40 | "@babel/core": "^7.14.5",
41 | "@babel/node": "^7.14.5",
42 | "@babel/preset-env": "^7.14.5",
43 | "concurrently": "^6.2.0",
44 | "nodemon": "^1.19.4",
45 | "request-debug": "^0.2.0"
46 | },
47 | "browserslist": [
48 | ">0.2%",
49 | "not dead",
50 | "not ie <= 11",
51 | "not op_mini all"
52 | ]
53 | }
54 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | OEM integration demo application
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/DeleteDialog.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Loading from './Loading';
4 | import Button from '@material-ui/core/Button';
5 | import Dialog from '@material-ui/core/Dialog';
6 | import DialogActions from '@material-ui/core/DialogActions';
7 | import DialogContent from '@material-ui/core/DialogContent';
8 | import DialogContentText from '@material-ui/core/DialogContentText';
9 |
10 | import { deleteWorkflow } from '../api/workflows';
11 |
12 | export default class DeleteDialog extends React.PureComponent {
13 |
14 | onCloseDialog = () => {
15 | this.props.onCloseDialog();
16 | }
17 |
18 | onDeleteWorkflow = () => {
19 | deleteWorkflow(this.props.id)
20 | .then(() => {
21 | return this.props.reload();
22 | })
23 | }
24 |
25 | render() {
26 | return (
27 |
33 |
34 |
35 | Are you sure you want to delete this workflow?
36 |
37 |
38 |
39 |
40 |
44 | Yes
45 |
46 |
51 | No
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/server/domain/registration.js:
--------------------------------------------------------------------------------
1 | /** @module domain/registration */
2 |
3 | import uuidv1 from 'uuid/v1';
4 |
5 | import {isNil} from 'lodash';
6 | import {mutations} from "../graphql";
7 | import {insertUserToMockDB, retrieveUserFromMockDB, userExistsInMockDB,} from '../db';
8 |
9 | /**
10 | * Validate user object:
11 | * @param {User} user - The user input given
12 | * @return {Array} List of missing input fields
13 | */
14 | const validateNewUser = user => {
15 | const errors = [];
16 | const fields = ['username', 'password', 'name'];
17 |
18 | fields.forEach(f => {
19 | if (isNil(user[f]) || user[f] === '') {
20 | errors.push(f);
21 | }
22 | });
23 |
24 | return errors;
25 | };
26 |
27 | /**
28 | * Check if a given user already exists:
29 | * @param {Request} req
30 | * @return {Boolean}
31 | */
32 | export const checkUserExists = req =>
33 | userExistsInMockDB(req.body);
34 |
35 | /**
36 | * Validate user object from a request:
37 | * @param {Request} req
38 | * @return {Validation} Has a valid field and a list of errors if not valud
39 | */
40 | export const validateRequest = req => {
41 | const validationErrors = validateNewUser(req.body);
42 |
43 | if (validationErrors.length) {
44 | return {
45 | valid: false,
46 | errors: validationErrors,
47 | };
48 | }
49 |
50 | return { valid: true };
51 | };
52 |
53 | /**
54 | * Generate a new user:
55 | * @param {Request} req
56 | * @return {User} The new user that was created
57 | */
58 | export const generateNewUser = req => {
59 | // Generate UUID for user:
60 | const uuid = uuidv1();
61 |
62 | // Generate a tray user for this account:
63 | return mutations.createExternalUser(uuid, req.body.name)
64 | .then(createRes => {
65 | // Add user to internal DB:
66 | insertUserToMockDB(
67 | {
68 | uuid: uuid,
69 | body: req.body,
70 | trayId: createRes.data.createExternalUser.userId,
71 | },
72 | );
73 |
74 | return retrieveUserFromMockDB(req.body);
75 | });
76 | };
77 |
--------------------------------------------------------------------------------
/src/api/solutions.js:
--------------------------------------------------------------------------------
1 | export const listSolutions = () =>
2 | fetch('/api/solutions', {credentials: 'include'})
3 | .then(async res => ({
4 | ok: res.ok,
5 | body: await res.json(),
6 | }));
7 |
8 | export const listSolutionInstances = () =>
9 | fetch('/api/solutionInstances', {credentials: 'include'})
10 | .then(async res => ({
11 | ok: res.ok,
12 | body: await res.json(),
13 | }));
14 |
15 | export const createSolutionInstance = (id, name) =>
16 | fetch('/api/solutionInstances', {
17 | body: JSON.stringify({
18 | id: id,
19 | name: name,
20 | }),
21 | headers: {
22 | 'content-type': 'application/json',
23 | },
24 | method: 'POST',
25 | credentials: 'include',
26 | }).then(async res => ({
27 | ok: res.ok,
28 | body: await res.json(),
29 | }));
30 |
31 | export const updateSolutionInstance = (solutionInstanceId, enabled) =>
32 | fetch(`/api/solutionInstance/${solutionInstanceId}`, {
33 | method: 'PATCH',
34 | credentials: 'include',
35 | headers: {
36 | 'content-type': 'application/json',
37 | },
38 | body: JSON.stringify({
39 | enabled: enabled,
40 | }),
41 | }).then(async res => ({
42 | ok: res.ok,
43 | }));
44 |
45 | export const updateSolutionInstanceConfig = solutionInstanceId =>
46 | fetch(`/api/solutionInstance/${solutionInstanceId}/config`, {
47 | method: 'PATCH',
48 | credentials: 'include',
49 | }).then(async res => ({
50 | ok: res.ok,
51 | body: await res.json(),
52 | }));
53 |
54 | export const getSolutionInstance = id =>
55 | fetch(`/api/solutionInstance/${id}`, {credentials: 'include'})
56 | .then(async res => ({
57 | ok: res.ok,
58 | body: await res.json(),
59 | }));
60 |
61 | export const deleteSolutionInstance = id =>
62 | fetch(`/api/solutionInstance/${id}`, {
63 | method: 'DELETE',
64 | credentials: 'include',
65 | })
66 | .then(async res => ({
67 | ok: res.ok,
68 | }));
69 |
--------------------------------------------------------------------------------
/src/components/auth/Register.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import RegisterForm from './RegisterForm'
3 |
4 | export default class Register extends React.Component {
5 | state = {
6 | redirectToReferrer: false,
7 | error: false,
8 | success: false,
9 | loading: false,
10 | }
11 |
12 | showError = () => this.setState({
13 | error: true,
14 | loading: false
15 | });
16 |
17 | register = (data) => {
18 | this.setState({
19 | loading: true
20 | });
21 | fetch('/api/register', {
22 | method: 'POST',
23 | body: JSON.stringify(data),
24 | headers: {
25 | 'Content-Type': 'application/json'
26 | },
27 | credentials: 'include'
28 | })
29 | .then((response) => {
30 | if (response.ok) {
31 |
32 | this.setState({
33 | success: true,
34 | loading: false,
35 | });
36 |
37 | setTimeout(() => window.location = '/login', 1000);
38 | } else {
39 | this.showError();
40 | }
41 | })
42 | .catch((err) => {
43 | this.showError();
44 | });
45 | }
46 |
47 | explain = 'This will create a new in-memory user account in the local Express backend that will persist until the backend is restarted.';
48 |
49 | render() {
50 | return (
51 |
52 |
Register a New User
53 |
{this.explain}
59 |
60 | {this.state.error ?
Registration failed : ""}
61 | {this.state.success ?
Registration success : ""}
62 |
63 |
64 | )
65 | }
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/src/views/Account.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import View from '../components/View';
3 | import Error from '../components/Error';
4 | import Typography from '@material-ui/core/Typography';
5 | import Loading from '../components/Loading';
6 |
7 | import { me } from '../api/me';
8 |
9 | export class Account extends React.PureComponent {
10 |
11 | state = {
12 | loading: true,
13 | error: false,
14 | email: '',
15 | username: '',
16 | userInfo: {},
17 | }
18 |
19 | componentDidMount() {
20 | me().then(({ok, body, statusText}) => {
21 | if (ok) {
22 | this.setState({
23 | username: body.username,
24 | email: body.email,
25 | loading: false,
26 | });
27 | } else {
28 | this.setState({
29 | error: statusText,
30 | loading: false,
31 | });
32 | }
33 | });
34 | }
35 |
36 | render() {
37 | const style = {
38 | bold: {
39 | fontWeight: 'bold'
40 | },
41 | };
42 |
43 | return (
44 |
45 |
46 | {this.state.error ?
47 | :
48 |
49 |
50 | Your Tray account
51 |
52 |
53 |
54 | Tray username:
55 | {this.state.username}
56 |
57 |
58 | Tray email:
59 | {this.state.email}
60 |
61 |
62 |
63 | }
64 |
65 |
66 | );
67 | }
68 | }
69 |
70 | export default Account;
71 |
--------------------------------------------------------------------------------
/src/components/auth/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LoginForm from './LoginForm'
3 | import { Redirect } from 'react-router-dom';
4 | import Loading from "../Loading";
5 | import { auth } from './Auth';
6 |
7 | export default class Login extends React.Component {
8 | state = {
9 | loading: false,
10 | redirectToReferrer: false
11 | }
12 |
13 | login = (data) => {
14 | console.log('Logging in ' + data.username);
15 | this.setState({
16 | loading: true
17 | })
18 | fetch('/api/login', {
19 | method: 'POST',
20 | body: JSON.stringify(data),
21 | credentials: 'include',
22 | headers: {
23 | 'Content-Type': 'application/json'
24 | },
25 | })
26 | .then(res => {
27 | if (res.ok) {
28 | auth.authenticate(() => {
29 | this.setState(
30 | {
31 | redirectToReferrer: true,
32 | loading: false
33 | }
34 | )
35 | });
36 | } else {
37 | res.json().then(body => {
38 | alert(`Unable to login: ${body.error}`);
39 | this.setState(
40 | {
41 | loading: false
42 | }
43 | )
44 | });
45 | }
46 | })
47 | .catch((err) => {
48 | console.error('Error logging in.', err);
49 | });
50 | }
51 |
52 | render() {
53 | const style = {
54 | container: {
55 | height: "300px",
56 | },
57 | warning: {
58 | textAlign: "center",
59 | border: "none",
60 | },
61 | };
62 |
63 | const {from} = this.props.location.state || {from: {pathname: '/'}};
64 | const {redirectToReferrer} = this.state;
65 |
66 | if (redirectToReferrer) {
67 | return (
68 |
69 | );
70 | }
71 |
72 | return (
73 |
74 |
75 |
76 |
77 |
78 | );
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/ConfigWizard.js:
--------------------------------------------------------------------------------
1 | import {withTheme} from "@material-ui/core/styles/index";
2 | import React from 'react';
3 |
4 | export class ConfigWizard extends React.PureComponent {
5 | state = {
6 | ready: false,
7 | };
8 |
9 | constructor(props) {
10 | super(props);
11 | this.iframe = React.createRef();
12 | }
13 |
14 | componentDidMount() {
15 | window.addEventListener("message", this.handleIframeEvents);
16 | }
17 |
18 | componentWillUnmount() {
19 | window.removeEventListener("message", this.handleIframeEvents);
20 | }
21 |
22 | handleIframeEvents = (e) => {
23 | console.log(`${e.data.type} event received`);
24 | // Here we should handle all event types
25 | if (e.data.type === 'tray.configPopup.error') {
26 | alert(`Error: ${e.data.err}`);
27 | }
28 | if (e.data.type === 'tray.configPopup.cancel') {
29 | this.props.onClose();
30 | }
31 | if (e.data.type === 'tray.configPopup.ready') {
32 | this.setState({ ready: true });
33 | }
34 | if (e.data.type === 'tray.configPopup.finish') {
35 | this.props.onClose();
36 | }
37 | };
38 |
39 | render() {
40 | const styles = {
41 | container: {
42 | flex: 1,
43 | position: 'relative',
44 | },
45 | iframe: {
46 | width: '100%',
47 | height: '100%',
48 | minHeight: '500px',
49 | border: '1px solid #2196f3',
50 | borderRadius: '4px',
51 | boxSizing: 'border-box',
52 | },
53 | loadingScreen: {
54 | top: 0,
55 | left: 0,
56 | right: 0,
57 | bottom: 0,
58 | position: 'absolute',
59 | background: 'white',
60 | border: '1px solid #2196f3',
61 | borderRadius: '4px',
62 | }
63 | };
64 |
65 | return (
66 |
67 |
73 | {!this.state.ready &&
Loading...
}
74 |
75 | )
76 | }
77 |
78 | }
79 |
80 | export default withTheme()(ConfigWizard);
81 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/views/SolutionsMine.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import View from '../components/View';
3 | import Error from '../components/Error';
4 | import Typography from '@material-ui/core/Typography';
5 | import { withTheme } from "@material-ui/core/styles/index";
6 | import Loading from '../components/Loading';
7 | import Instance from '../components/Instance';
8 |
9 | import { listSolutionInstances } from '../api/solutions';
10 |
11 | export class SolutionsMine extends React.PureComponent {
12 |
13 | styles = {
14 | list: {
15 | maxWidth: "1000px",
16 | margin: 'auto',
17 | marginBottom: '30px',
18 | fontFamily: "Roboto, Helvetica, Arial, sans-serif"
19 | }
20 | }
21 |
22 | state = {
23 | loading: true,
24 | error: false,
25 | solutionInstances: [],
26 | }
27 |
28 | componentDidMount() {
29 | this.loadAllSolutionInstances();
30 | }
31 |
32 | loadAllSolutionInstances = () => {
33 | listSolutionInstances()
34 | .then(({ok, body}) => {
35 | if (ok) {
36 | this.setState({
37 | solutionInstances: body.data,
38 | loading: false,
39 | });
40 | } else {
41 | this.setState({
42 | error: body,
43 | loading: false,
44 | });
45 | }
46 | });
47 | }
48 |
49 | buildList(solutionInstances) {
50 | return (
51 |
52 |
53 |
54 | My Solution Instances
55 |
56 | {
57 | solutionInstances.map(({id, name, enabled}) => (
58 |
65 | ))
66 | }
67 |
68 |
69 | );
70 | }
71 |
72 | render() {
73 | return (
74 |
75 |
76 | {this.state.error ?
77 | :
78 | this.buildList(this.state.solutionInstances)
79 | }
80 |
81 |
82 | );
83 | }
84 |
85 | }
86 |
87 | export default withTheme()(SolutionsMine);
88 |
--------------------------------------------------------------------------------
/server/configuration.js:
--------------------------------------------------------------------------------
1 | export const setEnvironment = () => {
2 | const productionGraphqlEndpoint = 'https://tray.io/graphql';
3 | const productionEuGraphqlEndpoint = "https://eu1.tray.io/graphql";
4 | const productionApGraphqlEndpoint = "https://ap1.tray.io/graphql";
5 | const stagingGraphqlEndpoint = 'https://staging.tray.io/graphql';
6 | const frontendStagingGraphqlEndpoint = 'https://frontend-staging.tray.io/graphql';
7 |
8 | const appUrlProd = 'https://embedded.tray.io';
9 | const appEuUrlProd = "https://embedded.eu1.tray.io";
10 | const appApUrlProd = "https://embedded.ap1.tray.io";
11 | const appUrlStaging = 'https://embedded.staging.tray.io';
12 | const appUrlFrontendStaging = 'https://embedded.frontend-staging.tray.io';
13 |
14 | switch (process.env.TRAY_ENDPOINT) {
15 | case 'stg':
16 | case 'staging':
17 | console.log(`ENDPOINT passed as staging`);
18 | process.env.TRAY_ENDPOINT = stagingGraphqlEndpoint;
19 | process.env.TRAY_APP_URL = appUrlStaging;
20 | break;
21 | case 'prod':
22 | case 'production':
23 | console.log(`ENDPOINT passed as production`);
24 | process.env.TRAY_ENDPOINT = productionGraphqlEndpoint;
25 | process.env.TRAY_APP_URL = appUrlProd;
26 | break;
27 | case 'eu1-prod':
28 | case 'eu1-production':
29 | process.env.TRAY_ENDPOINT = productionEuGraphqlEndpoint;
30 | process.env.TRAY_APP_URL = appEuUrlProd;
31 | break;
32 | case 'ap1-prod':
33 | case 'ap1-production':
34 | process.env.TRAY_ENDPOINT = productionApGraphqlEndpoint;
35 | process.env.TRAY_APP_URL = appApUrlProd;
36 | break;
37 | case 'fe-stg':
38 | case 'frontend-staging':
39 | console.log(`ENDPOINT passed as frontend-staging`);
40 | process.env.TRAY_ENDPOINT = frontendStagingGraphqlEndpoint;
41 | process.env.TRAY_APP_URL = appUrlFrontendStaging;
42 | break;
43 | default:
44 | console.log(`No valid ENDPOINT was passed. Defaulting to production ${productionGraphqlEndpoint}`);
45 | process.env.TRAY_ENDPOINT = productionGraphqlEndpoint;
46 | process.env.TRAY_APP_URL = appUrlProd;
47 | break;
48 | }
49 |
50 | //Make sure user has passed all required ENV variables before we start server
51 | if (!process.env.TRAY_MASTER_TOKEN || !process.env.TRAY_PARTNER) {
52 | console.error('\x1b[35m',
53 | `\nOne or both of following required env parameters are missing:
54 | TRAY_MASTER_TOKEN (Partner Master Key)
55 | TRAY_PARTNER (Partner NAME)
56 | Make sure they are defined as env variables and start API again.
57 | NOTE: Make sure the names use the TRAY_ prefix e.g. TRAY_MASTER_TOKEN as opposed to MASTER_TOKEN\n`
58 | );
59 | process.exit(-1);
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/src/lib/configWindow.js:
--------------------------------------------------------------------------------
1 | export const openConfigWindow = () => {
2 | // Must open window from user interaction code otherwise it is likely
3 | // to be blocked by a popup blocker:
4 | const configWindow = window.open(
5 | undefined,
6 | '_blank',
7 | 'width=500,height=500,scrollbars=no',
8 | );
9 |
10 | // Listen to popup messages
11 | let configFinished = false;
12 | const onmessage = e => {
13 | console.log('message', e.data.type, e.data);
14 |
15 | if (e.data.type === 'tray.configPopup.error') {
16 | // Handle popup error message
17 | alert(`Error: ${e.data.err}`);
18 | configWindow.close();
19 | }
20 | if (e.data.type === 'tray.configPopup.cancel') {
21 | configWindow.close();
22 | }
23 | if (e.data.type === 'tray.configPopup.finish') {
24 | // Handle popup finish message
25 | configFinished = true;
26 | configWindow.close();
27 | }
28 | if (e.data.type === 'tray.configPopup.validate') {
29 | // Return validation in progress
30 | configWindow.postMessage({
31 | type: 'tray.configPopup.client.validation',
32 | data: {
33 | inProgress: true,
34 | }
35 | }, '*');
36 |
37 | setTimeout(() => {
38 | // Add errors to all inputs
39 | const errors = e.data.data.visibleValues.reduce(
40 | (errors, externalId) => {
41 | console.log(`Visible ${externalId} value:`, e.data.data.configValues[externalId]);
42 | // Uncomment next line to set an error message
43 | // errors[externalId] = 'Custom error message';
44 | return errors;
45 | },
46 | {}
47 | );
48 |
49 | // Return validation
50 | configWindow.postMessage({
51 | type: 'tray.configPopup.client.validation',
52 | data: {
53 | inProgress: false,
54 | errors: errors,
55 | }
56 | }, '*');
57 | },
58 | 2000
59 | );
60 | }
61 | };
62 | window.addEventListener('message', onmessage);
63 |
64 | // Check if popup window has been closed before finishing the configuration.
65 | // We use a polling function due to the fact that some browsers may not
66 | // display prompts created in the beforeunload event handler.
67 | const CHECK_TIMEOUT = 1000;
68 | const checkWindow = () => {
69 | if (configWindow.closed) {
70 | // Handle popup closing
71 | if (!configFinished) {
72 | alert('Configuration not finished');
73 | } else {
74 | alert(
75 | 'Configuration finished. You can enable the new ' +
76 | 'solution instance from the "Solutions > My Instances" section'
77 | );
78 | console.log('Configuration finished');
79 | }
80 | window.removeEventListener('message', onmessage);
81 | } else {
82 | setTimeout(checkWindow, CHECK_TIMEOUT);
83 | }
84 | }
85 |
86 | checkWindow();
87 |
88 | return configWindow;
89 | };
90 |
--------------------------------------------------------------------------------
/server/auth.js:
--------------------------------------------------------------------------------
1 | import { log } from './logging';
2 |
3 | import { attemptLogin, generateUserAccessToken } from './domain/login';
4 |
5 | import { checkUserExists, generateNewUser, validateRequest } from './domain/registration';
6 |
7 | module.exports = function (app) {
8 |
9 | app.use(require('express-session')({
10 | secret: '2C44-4D44-WppQ38S',
11 | resave: true,
12 | saveUninitialized: true
13 | }));
14 |
15 | /*
16 | * /api/login:
17 | * Attempt to retrieve user from DB, if not found respond with 401 status.
18 | * Otherwise attempt to generate a tray access token and set user info onto
19 | * session.
20 | */
21 | app.post('/api/login', function (req, res) {
22 | const user = attemptLogin(req);
23 |
24 | if (user && (!user.uuid || !user.trayId)) {
25 | res.status(500).send({
26 | error: `Unable to login. User "${user.username}" found locally is missing one or more of following required fields: uuid, trayId`
27 | });
28 | } else if (user) {
29 | log({
30 | message: 'Logged in with:',
31 | object: user
32 | });
33 |
34 | // Attempt to generate the external user token and save to session:
35 | generateUserAccessToken(req, res, user)
36 | .then(_ => res.sendStatus(200))
37 | .catch(err => {
38 | log({message: 'Failed to generate user access token:', object: err});
39 | res.status(500).send(err);
40 | });
41 | } else {
42 | log({message: 'Login failed for user:', object: req.body});
43 | res.status(401).send({error: 'User not found. Keep in mind OEM Demo app stores new users in-memory and they are lost on server restart.'});
44 | }
45 | });
46 |
47 | /*
48 | * /api/register:
49 | * Check if user already exists, if so respond with 409 status.
50 | * Validate request body, if not valid respond with 400 status.
51 | * Otherwise attempt to generate a tray user and insert new user object into
52 | * the DB.
53 | */
54 | app.post('/api/register', function (req, res) {
55 | if (checkUserExists(req)) {
56 | log({message: 'Failed to create user, already exists:', object: req.body});
57 | return res.status(409).send(`User name ${req.body.username} already exists`);
58 | }
59 |
60 | const validation = validateRequest(req);
61 |
62 | if (!validation.valid) {
63 | const errorMsg = `The following params missing in user object, [${validation.errors.join(', ')}]`;
64 | log({message: errorMsg});
65 | return res.status(400).send(errorMsg);
66 | }
67 |
68 | generateNewUser(req)
69 | .then(user => {
70 | log({message: `successfully created user ${req.body.username}`, object: user});
71 | return res.status(200).send(user);
72 | })
73 | .catch(err => {
74 | log({message: 'There was an error creating the external Tray user:', object: err});
75 | res.status(500).send('There was an error creating the external Tray user:');
76 | });
77 | });
78 |
79 | /*
80 | * /api/logout:
81 | * Remove session data.
82 | */
83 | app.post('/api/logout', function (req, res) {
84 | req.session.destroy();
85 | res.sendStatus(200);
86 | });
87 |
88 | // Authenticate all endpoints except the auth endpoints defined in this module
89 | app.use(function (req, res, next) {
90 | if (req.session && req.session.admin) {
91 | return next();
92 | } else {
93 | return res.sendStatus(401);
94 | }
95 | });
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/src/components/auth/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Paper from '@material-ui/core/Paper';
3 | import Button from '@material-ui/core/Button';
4 | import { white } from '@material-ui/core/colors/';
5 | import PersonAdd from '@material-ui/icons/PersonAdd';
6 | import Input from '@material-ui/core/Input';
7 | import Typography from '@material-ui/core/Typography';
8 |
9 | class LoginForm extends React.Component {
10 | render() {
11 | const {onLogin} = this.props;
12 | const styles = {
13 | field: {marginTop: 10},
14 | btnSpan: {marginLeft: 5},
15 | loginContainer: {
16 | backgroundColor: white,
17 | minWidth: 320,
18 | maxWidth: 400,
19 | height: 'auto',
20 | position: 'absolute',
21 | top: '20%',
22 | left: 0,
23 | right: 0,
24 | margin: 'auto',
25 | },
26 | paper: {
27 | padding: 20,
28 | overflow: 'auto'
29 | },
30 | buttonsDiv: {
31 | textAlign: 'center',
32 | padding: 10
33 | },
34 | loginBtn: {
35 | marginTop: 20,
36 | float: 'right'
37 | },
38 | loginHeader: {
39 | textAlign: "center",
40 | marginBottom: 15,
41 | }
42 | };
43 |
44 | return (
45 |
46 |
47 |
48 |
52 | Login to OEM demo app
53 |
54 |
89 |
90 |
91 |
92 | }
96 | color="secondary"
97 | >
98 | Register New Account
99 |
100 |
101 |
102 |
103 |
104 | );
105 | }
106 | }
107 |
108 | export default LoginForm;
109 |
--------------------------------------------------------------------------------
/src/components/auth/RegisterForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Paper from '@material-ui/core/Paper';
3 | import Button from '@material-ui/core/Button';
4 | import { white } from '@material-ui/core/colors/';
5 | import Input from '@material-ui/core/Input';
6 | import CircularProgress from '@material-ui/core/CircularProgress';
7 |
8 | class RegisterForm extends React.Component {
9 |
10 | render() {
11 | const {onRegister} = this.props;
12 | const styles = {
13 | loginContainer: {
14 | backgroundColor: white,
15 | minWidth: 320,
16 | maxWidth: 400,
17 | height: 'auto',
18 | position: 'absolute',
19 | left: 0,
20 | right: 0,
21 | margin: '30px auto'
22 | },
23 | paper: {
24 | padding: 20,
25 | overflow: 'auto'
26 | },
27 | buttonsDiv: {
28 | textAlign: 'center',
29 | padding: 10
30 | },
31 | field: {
32 | marginTop: 10,
33 | },
34 | loginBtn: {
35 | marginTop: 20,
36 | float: 'right'
37 | },
38 | btnSpan: {
39 | marginLeft: 5
40 | },
41 | };
42 |
43 | return (
44 |
45 |
46 |
47 | {this.props.loading ?
48 |
49 |
50 |
:
51 | }
104 |
105 |
106 |
107 |
108 |
109 | );
110 | }
111 | }
112 |
113 | export default RegisterForm
--------------------------------------------------------------------------------
/src/components/Nav.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withStyles } from '@material-ui/core/styles';
3 | import List from '@material-ui/core/List';
4 | import ListItem from '@material-ui/core/ListItem';
5 | import ListItemIcon from '@material-ui/core/ListItemIcon';
6 | import ListItemText from '@material-ui/core/ListItemText';
7 | import Collapse from '@material-ui/core/Collapse';
8 | import AccountIcon from '@material-ui/icons/AccountCircle';
9 | import AccountBox from '@material-ui/icons/AccountBox';
10 | import PlugIcon from '@material-ui/icons/SettingsInputComponent';
11 | import ExpandLess from '@material-ui/icons/ExpandLess';
12 | import ExpandMore from '@material-ui/icons/ExpandMore';
13 | import CircleIcon from '@material-ui/icons/FiberManualRecord';
14 | import { Link } from 'react-router-dom';
15 |
16 | const styles = theme => ({
17 | root: {
18 | width: '100%',
19 | height: '100%',
20 | maxWidth: 250,
21 | backgroundColor: theme.palette.background.paper,
22 | paddingBottom: 40,
23 | },
24 | nested: {
25 | paddingLeft: theme.spacing.unit * 4,
26 | },
27 | link: {
28 | textDecoration: "none",
29 | }
30 | });
31 |
32 | class Nav extends React.PureComponent {
33 | state = {
34 | solutionsOpen: true,
35 | };
36 |
37 | handleSolutionsClick = () => {
38 | this.setState({solutionsOpen: !this.state.solutionsOpen});
39 | };
40 |
41 | render() {
42 | const {classes} = this.props;
43 |
44 | return (
45 |
46 |
47 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | {this.state.solutionsOpen ? : }
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | );
105 | }
106 | }
107 |
108 | export default withStyles(styles)(Nav);
109 |
--------------------------------------------------------------------------------
/src/views/SolutionsDiscover.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import View from '../components/View';
3 | import Error from '../components/Error';
4 | import List from '@material-ui/core/List';
5 | import ListItem from '@material-ui/core/ListItem';
6 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
7 | import ListItemText from '@material-ui/core/ListItemText';
8 | import Grid from '@material-ui/core/Grid';
9 | import Paper from '@material-ui/core/Paper';
10 | import Typography from '@material-ui/core/Typography';
11 | import Button from '@material-ui/core/Button';
12 | import Loading from '../components/Loading';
13 |
14 | import { openConfigWindow } from '../lib/configWindow';
15 | import { listSolutions, createSolutionInstance } from '../api/solutions';
16 |
17 | export class SolutionsDiscover extends React.PureComponent {
18 |
19 | state = {
20 | loading: true,
21 | error: false,
22 | solutions: [],
23 | }
24 |
25 | componentDidMount() {
26 | listSolutions()
27 | .then(({ok, body}) => {
28 | if (ok) {
29 | this.setState({
30 | solutions: body.data,
31 | loading: false,
32 | });
33 | } else {
34 | this.setState({
35 | error: body,
36 | loading: false,
37 | });
38 | }
39 | });
40 | }
41 |
42 | onUseWorkflowClick(id, name) {
43 | const configWindow = openConfigWindow();
44 |
45 | createSolutionInstance(id, name).then(({body}) => {
46 | // After we generate the popup URL, set it to the previously opened
47 | // window:
48 | configWindow.location = body.data.popupUrl;
49 | });
50 | }
51 |
52 | buildList(solutions) {
53 | const styles = {
54 | controls: {marginLeft: "20px"},
55 | button: {width: "100%"},
56 | text: {fontWeight: "bold"},
57 | grid: {
58 | maxWidth: "900px",
59 | margin: "20px auto",
60 | },
61 | header: {margin: "20px"},
62 | list: {
63 | margin: "10px",
64 | maxWidth: "1000px",
65 | backgroundColor: "white",
66 | },
67 | };
68 |
69 | return (
70 |
71 |
75 | Discover solutions
76 |
77 |
78 |
79 | {
80 | solutions.map(({title, id}, index) =>
81 |
85 |
90 | this.onUseWorkflowClick(id, title)}
92 | >
93 |
98 | Use
99 |
100 |
101 |
102 | )
103 | }
104 |
105 |
106 |
107 | );
108 | }
109 |
110 | render() {
111 | return (
112 |
113 |
114 | {
115 | this.state.error ?
116 | :
117 | this.buildList(this.state.solutions)
118 | }
119 |
120 |
121 | );
122 | }
123 |
124 | }
125 |
126 | export default SolutionsDiscover;
127 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/server/graphql.js:
--------------------------------------------------------------------------------
1 | // Module with all graphql queries and mutations:
2 |
3 | import gql from 'graphql-tag';
4 |
5 | import { generateClient, masterClient } from './gqlclient';
6 |
7 | export const queries = {
8 | me: token => {
9 | const query = gql`
10 | {
11 | viewer {
12 | details {
13 | username
14 | email
15 | }
16 | }
17 | }
18 | `;
19 |
20 | return generateClient(token).query({query});
21 | },
22 |
23 | auths: token => {
24 | const query = gql`
25 | {
26 | viewer {
27 | authentications {
28 | edges {
29 | node {
30 | id
31 | name
32 | }
33 | }
34 | }
35 | }
36 | }
37 | `;
38 |
39 | return generateClient(token).query({query});
40 | },
41 |
42 | solutions: () => {
43 | const query = gql`
44 | {
45 | viewer {
46 | solutions {
47 | edges {
48 | node {
49 | id
50 | title
51 | }
52 | }
53 | }
54 | }
55 | }
56 | `;
57 |
58 | return masterClient.query({query});
59 | },
60 |
61 | solutionInstances: token => {
62 | const query = gql`
63 | {
64 | viewer {
65 | solutionInstances {
66 | edges {
67 | node {
68 | id
69 | name
70 | enabled
71 | }
72 | }
73 | }
74 | }
75 | }
76 | `;
77 |
78 | return generateClient(token).query({query});
79 | },
80 |
81 | solutionInstance: (id, token) => {
82 | const query = gql`
83 | {
84 | viewer {
85 | solutionInstances(criteria: {ids: "${id}"}) {
86 | edges {
87 | node {
88 | id
89 | name
90 | enabled
91 | }
92 | }
93 | }
94 | }
95 | }
96 | `;
97 |
98 |
99 | return generateClient(token).query({query});
100 | },
101 |
102 | trayUsername: uuid => {
103 | const query = gql`
104 | {
105 | users(criteria: {externalUserId: "${uuid}"}) {
106 | edges {
107 | node {
108 | id
109 | }
110 | }
111 | }
112 | }
113 | `;
114 |
115 | return masterClient.query({query});
116 | }
117 | };
118 |
119 | export const mutations = {
120 | authorize: trayId => {
121 | const mutation = gql`
122 | mutation {
123 | authorize(input: {userId: "${trayId}"}) {
124 | accessToken
125 | }
126 | }
127 | `;
128 |
129 | return masterClient.mutate({mutation});
130 | },
131 |
132 | createSolutionInstance: (userToken, solutionId, name) => {
133 | const mutation = gql`
134 | mutation {
135 | createSolutionInstance(input: {solutionId: "${solutionId}", instanceName: "${name}", authValues: [], configValues: []}) {
136 | solutionInstance {
137 | id
138 | }
139 | }
140 | }
141 | `;
142 |
143 | return generateClient(userToken).mutate({mutation});
144 | },
145 |
146 | updateSolutionInstance: (userToken, solutionInstanceId, enabled ) => {
147 | const mutation = gql`
148 | mutation {
149 | updateSolutionInstance(input: {solutionInstanceId: "${solutionInstanceId}", enabled: ${enabled}}) {
150 | clientMutationId
151 | }
152 | }
153 | `;
154 |
155 | return generateClient(userToken).mutate({mutation});
156 | },
157 |
158 | createExternalUser: (uuid, name) => {
159 | const mutation = gql`
160 | mutation {
161 | createExternalUser(input : {externalUserId: "${uuid}", name: "${name}"}) {
162 | userId
163 | }
164 | }
165 | `;
166 |
167 | return masterClient.mutate({mutation})
168 | },
169 |
170 | getGrantTokenForUser: (trayId, workflowId) => {
171 | const mutation = gql`
172 | mutation {
173 | generateAuthorizationCode(input: {userId: "${trayId}"}) {
174 | authorizationCode
175 | }
176 | }
177 | `;
178 |
179 | return masterClient.mutate({mutation})
180 | .then(payload => {
181 | return {
182 | payload,
183 | workflowId,
184 | };
185 | });
186 | },
187 |
188 | deleteSolutionInstance: (userToken, solutionInstanceId) => {
189 | const mutation = gql`
190 | mutation {
191 | removeSolutionInstance(input: {solutionInstanceId: "${solutionInstanceId}"}) {
192 | clientMutationId
193 | }
194 | }
195 | `;
196 |
197 | return generateClient(userToken).mutate({mutation});
198 | },
199 | };
200 |
--------------------------------------------------------------------------------
/server/api.js:
--------------------------------------------------------------------------------
1 | import { mutations, queries } from './graphql';
2 | import { get, map, values } from 'lodash';
3 |
4 | // Get nodes for a given path from graphQL response:
5 | function getNodesAt (results, path) {
6 | return map(
7 | values(get(results, path)),
8 | x => x.node,
9 | );
10 | }
11 |
12 | const solutionPath = `${process.env.TRAY_APP_URL}/external/solutions/${process.env.TRAY_PARTNER}`;
13 | const editAuthPath = `${process.env.TRAY_APP_URL}/external/auth/edit/${process.env.TRAY_PARTNER}`;
14 | const createAuthPath = `${process.env.TRAY_APP_URL}/external/auth/create/${process.env.TRAY_PARTNER}`;
15 |
16 | module.exports = function (app) {
17 |
18 | // GET Account:
19 | app.get('/api/me', (req, res) => {
20 | queries.me(req.session.token)
21 | .then((results) => res.status(200).send(results.data.viewer.details))
22 | .catch(err => res.status(500).send(err));
23 | });
24 |
25 | // GET user auths:
26 | app.get('/api/auths', (req, res) => {
27 |
28 | queries.auths(req.session.token)
29 | .then((results) => {
30 | res.status(200)
31 | .send({
32 | data: getNodesAt(results, 'data.viewer.authentications.edges')
33 | })
34 | }
35 | )
36 | .catch(err => res.status(500).send(err));
37 | });
38 |
39 | // GET auth url:
40 | app.post('/api/auth', (req, res) => {
41 | mutations.getGrantTokenForUser(req.session.user.trayId)
42 | .then(({payload}) => {
43 | const authorizationCode = payload.data.generateAuthorizationCode.authorizationCode;
44 | res.status(200).send({
45 | data: {
46 | popupUrl: `${editAuthPath}/${req.body.authId}?code=${authorizationCode}`
47 | }
48 | });
49 | })
50 | .catch(err => res.status(500).send(err));
51 | });
52 |
53 | // GET auth create url:
54 | app.post('/api/auth/create', (req, res) => {
55 | mutations.getGrantTokenForUser(req.session.user.trayId)
56 | .then(({payload}) => {
57 | const authorizationCode = payload.data.generateAuthorizationCode.authorizationCode;
58 | const popupUrl = req.body.solutionInstanceId && req.body.externalAuthId ?
59 | `${createAuthPath}/${req.body.solutionInstanceId}/${req.body.externalAuthId}?code=${authorizationCode}` :
60 | `${createAuthPath}?code=${authorizationCode}`;
61 |
62 | res.status(200).send({
63 | data: {
64 | popupUrl
65 | }
66 | });
67 | })
68 | .catch(err => res.status(500).send(err));
69 | });
70 |
71 | // GET Solutions:
72 | app.get('/api/solutions', (req, res) => {
73 | queries.solutions()
74 | .then((results) => {
75 | res.status(200).send({
76 | data: getNodesAt(results, 'data.viewer.solutions.edges')
77 | });
78 | })
79 | .catch(err => res.status(500).send(err));
80 | });
81 |
82 | // GET Solution Instances:
83 | app.get('/api/solutionInstances', (req, res) => {
84 | const externalUserToken = req.session.token;
85 |
86 | if (!externalUserToken) {
87 | res.status(500).send('Missing external user auth');
88 | }
89 |
90 | queries.solutionInstances(externalUserToken)
91 | .then(results => {
92 | res.status(200).send({
93 | data: getNodesAt(results, 'data.viewer.solutionInstances.edges'),
94 | });
95 | })
96 | .catch(err => res.status(500).send(err));
97 | });
98 |
99 | // POST Solution Instances
100 | app.post('/api/solutionInstances', (req, res) => {
101 | mutations.createSolutionInstance(
102 | req.session.token,
103 | req.body.id,
104 | req.body.name,
105 | )
106 | .then(solutionInstance => {
107 | return mutations.getGrantTokenForUser(
108 | req.session.user.trayId,
109 | ).then(({payload}) => {
110 | const solutionInstanceId = solutionInstance.data.createSolutionInstance.solutionInstance.id;
111 | const authorizationCode = payload.data.generateAuthorizationCode.authorizationCode;
112 | res.status(200).send({
113 | data: {
114 | popupUrl: `${solutionPath}/configure/${solutionInstanceId}?code=${authorizationCode}`
115 | }
116 | });
117 | })
118 | })
119 | .catch(err => {
120 | res.status(500).send(err)
121 | });
122 | });
123 |
124 | // PATCH solution instance:
125 | app.patch('/api/solutionInstance/:solutionInstanceId', (req, res) => {
126 | mutations.updateSolutionInstance(
127 | req.session.token,
128 | req.params.solutionInstanceId,
129 | req.body.enabled
130 | )
131 | .then(() => res.sendStatus(200))
132 | .catch(err => res.status(500).send({err}));
133 | });
134 |
135 |
136 | // PATCH Solution Instance configuration:
137 | app.patch('/api/solutionInstance/:solutionInstanceId/config', (req, res) => {
138 | mutations.getGrantTokenForUser(
139 | req.session.user.trayId,
140 | req.params.solutionInstanceId,
141 | )
142 | .then(({payload}) => {
143 | const authorizationCode = payload.data.generateAuthorizationCode.authorizationCode;
144 | res.status(200).send({
145 | data: {
146 | popupUrl: `${solutionPath}/configure/${req.params.solutionInstanceId}?code=${authorizationCode}`
147 | }
148 | });
149 | })
150 | .catch(err => res.status(500).send({err}));
151 | });
152 |
153 | // DELETE Solution Instance:
154 | app.delete('/api/solutionInstance/:solutionInstanceId', (req, res) => {
155 | mutations.deleteSolutionInstance(
156 | req.session.token,
157 | req.params.solutionInstanceId,
158 | )
159 | .then(() => res.sendStatus(200))
160 | .catch(err => res.status(500).send({err}));
161 | });
162 |
163 | };
164 |
--------------------------------------------------------------------------------
/src/views/Demo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Loading from '../components/Loading';
3 |
4 | // Update to use Solutions
5 | import { openConfigWindow } from '../lib/configWindow';
6 | // Add ability to remove solution instance
7 | import {
8 | listSolutions,
9 | listSolutionInstances,
10 | createSolutionInstance,
11 | deleteSolutionInstance,
12 | updateSolutionInstanceConfig,
13 | } from '../api/solutions';
14 |
15 | import './demo.css';
16 |
17 | import config from '../config.js';
18 |
19 | export class Demo extends React.PureComponent {
20 | state = {
21 | solutions: null,
22 | instances: null,
23 | loadinginstances: true,
24 | loadingSolutions: true,
25 | }
26 |
27 | componentDidMount() {
28 | fetch('/api/login', {
29 | method: 'POST',
30 | body: JSON.stringify({
31 | username: config.username,
32 | password: config.password,
33 | }),
34 | credentials: 'include',
35 | headers: {
36 | 'Content-Type': 'application/json'
37 | },
38 | }).then(() => {
39 | this.listSolutions();
40 | this.listInstances();
41 | });
42 | }
43 |
44 | calculateInstancesSize() {
45 | if (!this.state.instances || !this.state.instances.length) {
46 | return 0;
47 | }
48 |
49 | return 40 + this.state.instances.length * 18;
50 | }
51 |
52 | calculateSolutionSize() {
53 | if (!this.state.solutions || !this.state.solutions.length) {
54 | return 0;
55 | }
56 |
57 | return this.state.solutions.length * 30;
58 | }
59 |
60 | calculateSize() {
61 | const standardContentHeight = 139;
62 | return standardContentHeight + this.calculateInstancesSize() + this.calculateSolutionSize();
63 | }
64 |
65 | listSolutions = () => {
66 | listSolutions().then(({body}) => {
67 | this.setState({solutions: body.data, loadingSolutions: false});
68 | });
69 | }
70 |
71 | listInstances = () => {
72 | listSolutionInstances().then(({body}) => {
73 | this.setState({instances: body.data, loadinginstances: false});
74 | });
75 | }
76 |
77 | onClickActivateIntegration = (id, title) => {
78 | const configWindow = openConfigWindow();
79 |
80 | createSolutionInstance(id, title).then(({body}) => {
81 | // After we generate the popup URL, set it to the previously opened
82 | // window:
83 | configWindow.location = body.data.popupUrl;
84 | this.listInstances();
85 | });
86 | }
87 |
88 | onClickDeactivateIntegration = id => {
89 | deleteSolutionInstance(id).then(this.listInstances);
90 | }
91 |
92 | onReconfigureIntegration = id => {
93 | const configWindow = openConfigWindow();
94 |
95 | updateSolutionInstanceConfig(id).then(({body}) => {
96 | // After we generate the popup URL, set it to the previously opened
97 | // window:
98 | configWindow.location = body.data.popupUrl;
99 | });
100 | }
101 |
102 | renderSolutions() {
103 | return this.state.solutions && this.state.solutions.map(i => {
104 | return (
105 |
106 | {i.title}
107 | this.onClickActivateIntegration(i.id, i.title)}
110 | >
111 | Activate
112 |
113 |
114 | );
115 | });
116 | }
117 |
118 | renderInstances() {
119 | if (!this.state.instances || !this.state.instances.length) return null;
120 |
121 | return (
122 |
123 |
124 |
App Name
125 |
126 |
127 | {this.state.instances.map(w => {
128 | return (
129 |
130 |
131 | {w.name}
132 |
133 | this.onClickDeactivateIntegration(w.id)}
136 | >
137 | Delete
138 |
139 | this.onReconfigureIntegration(w.id)}
142 | >
143 | Reconfigure
144 |
145 |
146 | );
147 | })}
148 |
149 | );
150 | }
151 |
152 | render() {
153 | window.parent.postMessage({
154 | type: 'tray_demo_size',
155 | height: this.calculateSize() + 'px',
156 | }, '*');
157 |
158 | return (
159 |
160 |
161 |
162 |
Available Integrations
163 | {this.renderSolutions()}
164 |
165 |
166 |
Active Integrations
167 |
168 | {this.state.instances && this.state.instances.length ?
169 |
You have authorized the following applications with Asana Connect .
:
170 |
Applications you authorize with Asana Connect will appear here.
171 | }
172 |
173 | {this.renderInstances()}
174 |
175 |
176 | Manage Developer Apps
177 |
178 |
179 |
180 | );
181 | }
182 | }
183 |
184 | export default Demo;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tray.io Embedded Edition sample application
2 |
3 | ## Table of Contents
4 |
5 | - [Intro](#trayio-embedded-edition-sample-application)
6 | - [Important concepts in Tray.io Embedded Edition](#important-concepts-in-trayio-embedded-edition)
7 | - [Setting up and running](#setting-up-and-running-the-sample-application)
8 |
9 | ## Intro
10 |
11 | In this repo is a sample webapp which runs on top of the Tray.io Embedded Edition API - this is an application which simply allows you to create new external users linked to your Tray.io partner account, and allow them to create and configure copies of your Solutions that exist on your Tray.io partner account.
12 |
13 | ## Important concepts in Tray.io Embedded Edition
14 |
15 | There are a few key things we should define to understand how to integrate Embedded Edition.
16 |
17 | #### Your Partner Account
18 |
19 | This is the Tray.io account we will provide for the purposes of setting up your integration to Tray.io. You will have to create any Solutions that you would like your users to use on this account. When you sign up an external user to Tray.io through your system, they will be considered to be a user linked to this account's team.
20 |
21 | #### Your Partner Accounts Workflows
22 |
23 | Workflows allow you to build automation within Tray by linking a series of steps. Each step will have a connector that can authenticate and run API calls against a certain service, or transform some data existing from previous steps in the workflow.
24 |
25 | #### Your Partner Accounts Projects
26 |
27 | Projects allow you to package one or more workflows together, in order to be able to provide a solution in your application.
28 |
29 | #### Your Partner Accounts Solutions
30 |
31 | Your Solutions will be available to list and edit through the Tray.io GraphQL API for usage in your application. These are built from Projects, and will be what your External Users configure to get a version of your Project that is linked to their own API Authentications and custom configuration values for the workflows used.
32 |
33 | #### Your Partner Accounts External Users
34 |
35 | In order for your users to take advantage of the Tray.io platform, they must have a Tray account. We have set up a system to provision Tray.io accounts which will be linked to the team of your Partner user. This will generate the associated tray accounts for the end users but the end users would not be aware of this.
36 |
37 | #### Your External Users Solution Instances
38 |
39 | When an external user configures a Solution, a copy of that Solution will be created in that accounts Solution instance list. Their Solution Instance must then be configured and enabled with your users application Authentications for the services used within that Solution.
40 |
41 | ### Integration details:
42 |
43 | - [Embedded Edition GraphQL API](https://tray.io/docs/article/partner-api-intro)
44 | - [Using the Tray.io configurator and authentication UIs from within your application](https://tray.io/docs/article/embedded-external-configuration)
45 | - [Authenticating your external users](https://github.com/trayio/embedded-edition-sample-app#authenticating-your-external-users)
46 |
47 | ## Setting up and running the sample application
48 |
49 | The application will require the following information to run:
50 |
51 | ```
52 | TRAY_ENDPOINT => prod / eu1-prod / fe-stg / stg
53 |
54 | TRAY_MASTER_TOKEN =>
55 |
56 | TRAY_PARTNER =>
57 | ```
58 |
59 | #### Getting the master token
60 |
61 | You can retrieve the token for any environment by visiting the Tray app instance for that environment i.e.
62 |
63 | prod -> https://app.tray.io
64 | eu1-prod -> https://app.eu1.tray.io
65 | stg -> https://app.staging.tray.io
66 | fe-stg -> https://app.frontend-staging.tray.io
67 |
68 | You will then need to log on as a embedded user and visit `Settings & people` -> `Tokens`
69 |
70 | 
71 |
72 | The app will bring the environment variables it needs from a `.env` file at the root of the repository.
73 |
74 | ### Setup configuration
75 |
76 | The required configuration for the application to run needs to be stored in a `.env` file at the root of the application. An example on how to do that:
77 |
78 | ```
79 | touch .env
80 | cat <> .env
81 | # choose from "prod", "stg" or "fe-stg"
82 | TRAY_ENDPOINT=prod
83 | # ensure master token matches the environment chosen in "TRAY_ENDPOINT"
84 | TRAY_MASTER_TOKEN=
85 | # can be any partner "asana", "tray.io" etc
86 | TRAY_PARTNER=tray.io
87 | EOT
88 | ```
89 |
90 | ### Running the application
91 |
92 | To set up and run the sample application first you must have Node LTS v10 or greater and then install the packages:
93 |
94 | ```
95 | npm install
96 | ```
97 |
98 | The application needs both an API and client instance. You can easily run both concurrently with the `start script`:
99 |
100 | ```
101 | npm run start
102 | ```
103 |
104 | ## Implementation details
105 |
106 | #### Making queries and executing mutations on the GQL API
107 |
108 | You can see the query + mutation definitions in the file `server/graphql.js`. For example the Solutions listing query for a partner account is defined as the code below:
109 |
110 | ```
111 | listSolutions: () => {
112 | const query = gql`
113 | {
114 | viewer {
115 | solutions {
116 | edges {
117 | node {
118 | id
119 | title
120 | }
121 | }
122 | }
123 | }
124 | }
125 | `;
126 |
127 | return masterClient.query({query});
128 | }
129 | ```
130 |
131 | This query fetches all Solutions for the given master token and provides the id and title fields. In order to make this query you must pass your Tray.io Partner Accounts API master token, we have some middleware which is using the Apollo Relay client imported from the `server/gqlclient.js`.
132 |
133 | To create side effects through the GraphQL API you must run a mutation. For example to create a Solution Instance from a Solution for a given external user, the mutation is defined as the code below:
134 |
135 | ```
136 | createSolutionInstance: (userToken, solutionId, name) => {
137 | const mutation = gql`
138 | mutation {
139 | createSolutionInstance(
140 | input: {
141 | solutionId: "${solutionId}",
142 | instanceName: "${name}",
143 | }
144 | ) {
145 | solutionInstance {
146 | id
147 | }
148 | }
149 | }
150 | `;
151 |
152 | return generateClient(userToken).mutate({mutation});
153 | },
154 | ```
155 |
156 | This code runs the createSolutionInstance mutation with the `solutionId` template variable passed in to determine which Solution to copy over to the External User account. It's run using a client that is generated from the user token, which is the user that will receive the new Solution Instance.
157 |
158 | #### Authenticating your external users
159 |
160 | In order to run user mutations or queries you will have to generate a user access token, so before running the mutation above you would have to run the `authorize` mutation with the required users trayId:
161 |
162 | ```
163 | authorize: trayId => {
164 | const mutation = gql`
165 | mutation {
166 | authorize(input: {userId: "${trayId}"}) {
167 | accessToken
168 | }
169 | }
170 | `;
171 |
172 | return masterClient.mutate({mutation});
173 | },
174 | ```
175 |
--------------------------------------------------------------------------------
/src/views/Authentications.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import View from '../components/View';
3 | import Error from '../components/Error';
4 | import Typography from '@material-ui/core/Typography';
5 | import Paper from '@material-ui/core/Paper';
6 | import Button from '@material-ui/core/Button';
7 | import Checkbox from '@material-ui/core/Checkbox';
8 | import FormControlLabel from '@material-ui/core/FormControlLabel';
9 | import { withTheme } from "@material-ui/core/styles/index";
10 | import Loading from '../components/Loading';
11 | import { listAuths, getAuthEditUrl } from '../api/me';
12 | import { openAuthWindow } from "../lib/authWindow";
13 | import { getAuthCreateUrl } from "../api/me";
14 | import List from "@material-ui/core/List";
15 | import ListItem from "@material-ui/core/ListItem";
16 | import TextField from "@material-ui/core/TextField";
17 | import ListItemText from "@material-ui/core/ListItemText";
18 | import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction";
19 | import Grid from "@material-ui/core/Grid";
20 | import RefreshIcon from '@material-ui/icons/Refresh';
21 | import { AuthWizard } from '../components/AuthWizard';
22 |
23 | export class Authentications extends React.PureComponent {
24 |
25 | styles = {
26 | controls: {marginLeft: "20px"},
27 | button: {width: "100%"},
28 | text: {fontWeight: "bold"},
29 | grid: {
30 | maxWidth: "900px",
31 | margin: "20px auto",
32 | },
33 | header: {margin: "20px"},
34 | headerOptions: { display: 'flex', margin: "0 20px 20px 20px" },
35 | headerOption: { marginRight: "20px" },
36 | list: {
37 | margin: "10px",
38 | maxWidth: "1000px",
39 | backgroundColor: "white",
40 | },
41 | advancedInput: {
42 | marginRight: "20px",
43 | flex: 1,
44 | }
45 | };
46 |
47 | state = {
48 | loading: true,
49 | error: false,
50 | auths: [],
51 | params: '',
52 | shouldOpenInFrame: false,
53 | iframeURL: undefined
54 | }
55 |
56 | componentDidMount() {
57 | this.loadAuths();
58 | }
59 |
60 | loadAuths = () => {
61 | this.setState({
62 | loading: true
63 | }, () => {
64 | listAuths()
65 | .then(({ok, body}) => {
66 | if (ok) {
67 | this.setState({
68 | auths: body.data || [],
69 | loading: false,
70 | });
71 | } else {
72 | this.setState({
73 | error: body,
74 | loading: false,
75 | });
76 | }
77 | });
78 | });
79 | }
80 |
81 | openAuthWizard = (url) => {
82 | if (this.state.shouldOpenInFrame) {
83 | this.setState({ iframeURL: url });
84 | } else {
85 | openAuthWindow(url);
86 | }
87 | }
88 |
89 | closeIframe = () => {
90 | this.setState({ iframeURL: undefined });
91 | this.loadAuths();
92 | }
93 |
94 | onCreateAuth = () => {
95 | getAuthCreateUrl()
96 | .then(({body}) => {
97 | this.openAuthWizard(`${body.data.popupUrl}&${this.state.params}`);
98 | })
99 | };
100 |
101 | buildList() {
102 | return (
103 |
104 |
105 |
106 | {
107 | this.state.auths.map(({id, name}, index) =>
108 |
112 |
117 |
118 |
123 | Edit
124 |
125 |
126 |
127 | )
128 | }
129 |
130 |
131 |
132 | );
133 | }
134 |
135 | showAuthWindow = (id) => () => {
136 | getAuthEditUrl(id)
137 | .then(({body}) => {
138 | this.openAuthWizard(`${body.data.popupUrl}&${this.state.params}`);
139 | })
140 | };
141 |
142 | handleChange = name => event => {
143 | this.setState({
144 | [name]: event.target.value,
145 | });
146 | };
147 |
148 | render() {
149 | return (
150 |
151 |
152 |
156 |
161 | Authentications
162 |
163 |
164 |
171 | New
172 |
173 |
180 | Refresh
181 |
182 |
183 |
193 | {
199 | this.setState({ shouldOpenInFrame: checked });
200 | }}
201 | />
202 | }
203 | label="Open in iframe"
204 | />
205 |
206 |
207 | {this.state.error ? (
208 |
209 | ) : (
210 | <>
211 | {this.state.iframeURL && (
212 |
216 | )}
217 | {this.buildList()}
218 | >
219 | )}
220 |
221 |
222 | );
223 | }
224 |
225 | }
226 |
227 | export default withTheme()(Authentications);
228 |
--------------------------------------------------------------------------------
/src/components/Instance.js:
--------------------------------------------------------------------------------
1 | import Typography from '@material-ui/core/Typography';
2 | import ExpansionPanel from '@material-ui/core/ExpansionPanel';
3 | import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
4 | import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
5 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
6 | import Button from '@material-ui/core/Button';
7 | import { withTheme } from "@material-ui/core/styles/index";
8 | import React from 'react';
9 | import Loading from './Loading';
10 | import { get } from 'lodash';
11 |
12 | import { openConfigWindow } from '../lib/configWindow';
13 |
14 | import {
15 | updateSolutionInstance,
16 | updateSolutionInstanceConfig,
17 | deleteSolutionInstance,
18 | } from '../api/solutions';
19 | import {ConfigWizard} from "./ConfigWizard";
20 | import TextField from "@material-ui/core/TextField";
21 | import {getAuthCreateUrl} from "../api/me";
22 | import {openAuthWindow} from "../lib/authWindow";
23 |
24 | export class Instance extends React.PureComponent {
25 | state = {
26 | error: false,
27 | loading: false,
28 | instanceState: undefined,
29 | configWizardSrc: undefined,
30 | authExternalId: undefined,
31 | authUrlParams: ''
32 | };
33 |
34 | openWizard = (openInIframe, addCustomValidation = false) => {
35 | updateSolutionInstanceConfig(this.props.id).then(({body}) => {
36 | const url = addCustomValidation ? `${body.data.popupUrl}&customValidation=true` : body.data.popupUrl;
37 |
38 | if (!openInIframe) {
39 | const configWindow = openConfigWindow();
40 | configWindow.location = url;
41 | } else {
42 | this.setState({
43 | configWizardSrc: url
44 | })
45 | }
46 | });
47 | };
48 |
49 | onClickConfigure = () => {
50 | this.openWizard(false,false);
51 | };
52 |
53 | onClickConfigureWithValidation = () => {
54 | this.openWizard(false,true);
55 | };
56 |
57 | onClickConfigureInIframe = () => {
58 | this.openWizard(true, false);
59 | };
60 |
61 | onClickEnable = () => {
62 | const enabled = get(this.state, 'instanceState', this.props.enabled);
63 | updateSolutionInstance(this.props.id, !enabled).then(()=>{
64 | this.setState({instanceState: !enabled});
65 | });
66 | };
67 |
68 | onClickDelete = () => {
69 | deleteSolutionInstance(this.props.id).then(this.props.loadAllSolutionInstances);
70 | }
71 |
72 | closeIframe = () => {
73 | this.setState({
74 | configWizardSrc: undefined
75 | })
76 | };
77 |
78 | onCreateAuth = () => {
79 | getAuthCreateUrl(this.props.id, this.state.authExternalId)
80 | .then(({body}) => {
81 | openAuthWindow(`${body.data.popupUrl}&${this.state.authUrlParams}`);
82 | })
83 | };
84 |
85 | handleChange = name => event => {
86 | this.setState({
87 | [name]: event.target.value,
88 | });
89 | };
90 |
91 | render() {
92 | const {id, name} = this.props;
93 | const {configWizardSrc} = this.state;
94 |
95 | const enabled = get(this.state, 'instanceState', this.props.enabled);
96 |
97 | const styles = {
98 | controls: {
99 | margin: "10px",
100 | float: "right",
101 | maxWidth: '400px'
102 | },
103 | pill: {
104 | backgroundColor: enabled ? "#7ebc54" : "#df5252",
105 | borderRadius: "4px",
106 | marginRight: "10px",
107 | color: "white",
108 | padding: "3px 5px",
109 | },
110 | item: {
111 | width: '100%',
112 | border: 'none',
113 | },
114 | name: {
115 | marginTop: '2px'
116 | },
117 | button: {
118 | width: "100%",
119 | marginBottom: "10px"
120 | },
121 | textFields: {
122 | width: "100%",
123 | margin: "10px 0",
124 | }
125 | };
126 |
127 | return (
128 |
129 |
133 | }>
134 |
135 | {enabled ? "enabled" : "disabled"}
136 |
137 |
138 | {name}
139 |
140 |
141 |
142 |
143 |
144 |
150 | {enabled ? 'Disable' : 'Enable'}
151 |
152 |
158 | Configure
159 |
160 |
166 | Configure with custom validation
167 |
168 |
174 | Configure in iframe
175 |
176 |
182 | Delete
183 |
184 |
185 | Create auth
186 |
187 |
196 |
205 |
212 | Create auth
213 |
214 |
215 | {configWizardSrc && }
216 |
217 |
218 |
219 |
220 | );
221 | }
222 |
223 | }
224 |
225 | export default withTheme()(Instance);
226 |
--------------------------------------------------------------------------------