├── .eslintrc.json ├── .gitignore ├── README.md ├── package.json ├── src ├── lib │ ├── authenticator.js │ ├── channel-subscribe-widget.js │ ├── generate-signed-websocket-url.js │ ├── get-region-from-gateway-name.js │ ├── main.js │ ├── message-form-widget.js │ ├── message-list-widget.js │ ├── mqtt-client.js │ ├── observable.js │ ├── sigv4url.js │ └── status-widget.js ├── policies │ └── unauthenticated-mqtt.json ├── style │ └── main.css ├── util │ ├── package-html.js │ └── post-message.js └── web │ └── index.html └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "crockford", 3 | "env": { 4 | "es6": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 6 8 | }, 9 | "rules": { 10 | "strict": ["error", "function"], 11 | "semi": ["error", "always"], 12 | "no-const-assign": "error", 13 | "indent": ["error", "tab", { "SwitchCase": 1 } ], 14 | "no-plusplus": "off", 15 | "quotes": ["error", "single", {"avoidEscape": true, "allowTemplateLiterals": true}], 16 | "new-cap": ["error", { "capIsNewExceptions": ["Deferred", "Event"] }], 17 | "no-unused-vars": "error", 18 | "no-const-assign": "error", 19 | "prefer-const": "error", 20 | "one-var": "off", 21 | "no-var": "error" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | site 2 | env 3 | node_modules 4 | npm-debug.log 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless chat 2 | 3 | This is an example project showing how to abuse AWS IOT Gateway to create a massively-scalable online chat system using a static HTML page. 4 | 5 | IOT Gateway supports websockets, which can be used to connect browsers directly to a message queue, and send/receive messages connected to hierarchical topics. In this case, we're allowing anonymous users to subscribe to any topic starting with `/chat/` (check the [unauthenticated policy](src/policies/unauthenticated-mqtt.json) for more information. The security is enforced using normal AWS IAM policies, and provided through AWS Cognito authentication, which allows us to assign IAM policies to unauthenticated users. 6 | 7 | The result is that chat allows anonymous users to access exchange messages through hierarchical chat topics, without any active server components we need to maintain. 8 | 9 | ## Prerequisites 10 | 11 | ### Find your aws gateway name: 12 | 13 | ```bash 14 | aws iot describe-endpoint --query endpointAddress --output text 15 | ``` 16 | #### Create a Cognito Identity Pool for Federated Identities (not a Cognito User Pool). 17 | 18 | For unauthenticated access, do the following when creating the identity pool: 19 | 20 | * enable access to unauthenticated identities 21 | * no need to attach authentication providers 22 | * on 'Your Cognito identities require access to your resources' screen open up the 'Show details' dropdown and adjust role names if you want 23 | * go to IAM, then add the [unauthenticated policy](src/policies/unauthenticated-mqtt.json) to your unauthenticated access role 24 | 25 | ## Configuring 26 | 27 | 1. create `./env/.json` for your environment, with 28 | 29 | ```js 30 | { 31 | "iotGatewayName": "", 32 | "cognitoIdentityPoolId": "" 33 | } 34 | ``` 35 | ## Building for development usagw 36 | 37 | 1. create `dev.json` in `./env` as described in the Configuring section 38 | 2. `npm run rebuild` 39 | 3. `npm run serve-dev` 40 | 41 | ## Building for production usage 42 | 43 | 1. create `production.json` in `./env` 44 | 2. `npm run rebuild --serverless-chat:buildenv=production` 45 | 3. upload the `site` folder somewhere 46 | 47 | ## Posting an update directly to the gateway 48 | 49 | Check out the [`src/util/post-message.js`](src/util/post-message.js) to see how you can also post messages directly to chat channels (eg a system notification, or replying to messages from a Lambda function. 50 | 51 | ### TODO 52 | 53 | 1. Add sender info 54 | 2. authenticated access 55 | 3. automated config 56 | 4. Connection keep-alive/reconnect 57 | 58 | # More info 59 | 60 | * [Paho MQTT Client for JavaScript](https://eclipse.org/paho/clients/js/) - used to connect to the IoT Gateway 61 | * [AWS IOT Platform](https://aws.amazon.com/iot-platform/how-it-works/) 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-chat", 3 | "version": "1.0.0", 4 | "description": "An example how to build a serverless chat system with AWS IOT Gateway", 5 | "private": true, 6 | "scripts": { 7 | "clean": "rm -rf site", 8 | "build-site": "mkdir -p site && node src/util/package-html.js", 9 | "build-scripts": "webpack", 10 | "build-styles": "mkdir -p site && cleancss -o site/$npm_package_version/main.css src/style/main.css", 11 | "serve-dev": "http-server site -c-1", 12 | "rebuild": "npm run clean && npm run build-site --$npm_package_name:buildenv=$npm_package_config_buildenv && webpack && npm run build-styles", 13 | "env": "env" 14 | }, 15 | "config": { 16 | "buildenv": "dev" 17 | }, 18 | "author": "", 19 | "contributors": [ 20 | "Alexander Simovic (https://github.com/simalexan)", 21 | "Slobodan Stojanovic (http://slobodan.me/)" 22 | ], 23 | "license": "MIT", 24 | "devDependencies": { 25 | "babili-webpack-plugin": "0.0.10", 26 | "clean-css": "^4.0.7", 27 | "clean-css-cli": "^4.0.7", 28 | "crypto-js": "^3.1.8", 29 | "eslint": "^3.15.0", 30 | "eslint-config-crockford": "^1.0.0", 31 | "eslint-config-defaults": "^9.0.0", 32 | "exports-loader": "^0.6.3", 33 | "handlebars": "^4.0.6", 34 | "http-server": "^0.9.0", 35 | "imports-loader": "^0.7.0", 36 | "webpack": "^2.2.1" 37 | }, 38 | "dependencies": { 39 | "aws-sdk": "^2.13.0", 40 | "bootstrap": "^3.3.7", 41 | "jquery": "^3.1.1", 42 | "paho-mqtt": "^1.0.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/authenticator.js: -------------------------------------------------------------------------------- 1 | /*global module, require */ 2 | const AWS = require('aws-sdk'); 3 | module.exports = function Authenticator(cognitoIdentityPoolId) { 4 | 'use strict'; 5 | const self = this; 6 | self.getAnonymousCredentials = function () { 7 | AWS.config.region = cognitoIdentityPoolId.split(':')[0]; 8 | return new Promise(function (resolve, reject) { 9 | const credentials = new AWS.CognitoIdentityCredentials({ 10 | IdentityPoolId: cognitoIdentityPoolId 11 | }); 12 | credentials.get(function (err) { 13 | if (err) { // TODO: Promisify 14 | return reject(err); 15 | } 16 | resolve(credentials); 17 | }); 18 | }); 19 | }; 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /src/lib/channel-subscribe-widget.js: -------------------------------------------------------------------------------- 1 | /*global require */ 2 | const jQuery = require('jquery'); 3 | jQuery.fn.channelSubscribeWidget = function (client) { 4 | 'use strict'; 5 | const container = jQuery(this), 6 | trySubscribing = function (channelName) { 7 | client.subscribe('chat/' + channelName); 8 | }; 9 | container.find('[role=channel-selector]').on('click', function () { 10 | trySubscribing(jQuery(this).text()); 11 | }); 12 | container.find('[role=channel-creator]').on('click', function () { 13 | const topicField = jQuery('#topic'); 14 | if (topicField.val()) { 15 | trySubscribing(topicField.val()); 16 | } else { 17 | container.find('#create-group').addClass('has-error'); 18 | } 19 | }); 20 | return container; 21 | }; 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/lib/generate-signed-websocket-url.js: -------------------------------------------------------------------------------- 1 | /*global module, require */ 2 | const getRegionFromGatewayName = require('./get-region-from-gateway-name'), 3 | sigV4Url = require('./sigv4url'); 4 | module.exports = function generateSignedWebsocketURL(credentials, gatewayName) { 5 | 'use strict'; 6 | return sigV4Url( 7 | 'wss', 8 | gatewayName, 9 | '/mqtt', 10 | 'iotdevicegateway', 11 | getRegionFromGatewayName(gatewayName), 12 | credentials.accessKeyId, 13 | credentials.secretAccessKey, 14 | credentials.sessionToken 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/get-region-from-gateway-name.js: -------------------------------------------------------------------------------- 1 | /*global module */ 2 | module.exports = function getRegionFromGatewayName(gatewayName) { 3 | 'use strict'; 4 | return gatewayName.split('.')[2]; 5 | }; 6 | -------------------------------------------------------------------------------- /src/lib/main.js: -------------------------------------------------------------------------------- 1 | /*global document, require */ 2 | 3 | require('./status-widget'); 4 | require('./message-form-widget'); 5 | require('./message-list-widget'); 6 | require('./channel-subscribe-widget'); 7 | const config = require('config'), 8 | Authenticator = require('./authenticator'), 9 | MQTTClient = require('./mqtt-client'), 10 | client = new MQTTClient({log: true}), 11 | jQuery = require('jquery'), 12 | authenticator = new Authenticator(config.cognitoIdentityPoolId), 13 | showError = function (e) { 14 | 'use strict'; 15 | const errorMessage = e && (typeof e === 'string' ? e : (e.stack || e.message || JSON.stringify(e))); 16 | jQuery('').alert().appendTo(jQuery('[role=error-panel]')); 20 | }, 21 | initPage = function () { 22 | 'use strict'; 23 | const connectButton = document.getElementById('connect'); 24 | 25 | jQuery('form').on('submit', evt => evt.preventDefault()); 26 | jQuery('#status').statusWidget(client); 27 | jQuery('#messageForm').messageFormWidget(client); 28 | jQuery('#messageList').messageListWidget(client); 29 | jQuery('#channels').channelSubscribeWidget(client); 30 | 31 | 32 | connectButton.addEventListener('click', () => { 33 | authenticator 34 | .getAnonymousCredentials() 35 | .then(credentials => client.init(credentials, config.iotGatewayName)) 36 | .catch(showError); 37 | }); 38 | 39 | client.addEventListener('error', showError); 40 | 41 | jQuery('[role=tab-button]').on('click', function (e) { 42 | e.preventDefault(); 43 | jQuery(this).tab('show'); 44 | }); 45 | 46 | client.addEventListener('subscribed', function () { 47 | jQuery('#tab-messages').tab('show'); 48 | }); 49 | client.addEventListener('connected', function () { 50 | jQuery('#channel-select').tab('show'); 51 | }); 52 | 53 | client.addEventListener('connected', (credentials) => { 54 | jQuery('[role=account-id]').text(credentials.identityId); 55 | }); 56 | 57 | client.addEventListener('subscribed subscribing', function (channelName) { 58 | jQuery('[role=channel-name]').text(channelName); 59 | }); 60 | 61 | client.addEventListener('disconnected', () => { 62 | jQuery('#sign-out').tab('show'); 63 | showError('You got disconnected'); 64 | }); 65 | 66 | }; 67 | 68 | require('bootstrap'); 69 | 70 | document.addEventListener('DOMContentLoaded', initPage); 71 | -------------------------------------------------------------------------------- /src/lib/message-form-widget.js: -------------------------------------------------------------------------------- 1 | /*global require */ 2 | const jQuery = require('jquery'); 3 | jQuery.fn.messageFormWidget = function (client) { 4 | 'use strict'; 5 | const messageForm = jQuery(this), 6 | messageField = messageForm.find('[role=messageText]'); 7 | messageForm.on('submit', () => { 8 | client.post(messageField.val()); 9 | }); 10 | client.addEventListener('sent', function () { 11 | messageField.val(''); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/message-list-widget.js: -------------------------------------------------------------------------------- 1 | /*global require */ 2 | const jQuery = require('jquery'); 3 | jQuery.fn.messageListWidget = function (client) { 4 | 'use strict'; 5 | const messageList = jQuery(this), 6 | appendMessage = function (message) { 7 | jQuery('
  • ').text(message).appendTo(messageList); 8 | }; 9 | 10 | client.addEventListener('received', appendMessage); 11 | client.addEventListener('subscribed', () => messageList.empty()); 12 | return messageList; 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/mqtt-client.js: -------------------------------------------------------------------------------- 1 | /*global module, console, require */ 2 | const observable = require('./observable'), 3 | Paho = require('exports-loader?Paho!paho-mqtt'), 4 | generateSignedWebsocketURL = require('./generate-signed-websocket-url'); 5 | module.exports = function MQTTClient(options) { 6 | 'use strict'; 7 | observable(this); 8 | let userCredentials, 9 | pahoClient, 10 | currentTopic; 11 | const self = this, 12 | shouldLog = options && options.log, 13 | log = function (message, arg) { 14 | if (!shouldLog) { 15 | return; 16 | } 17 | console.log(message, arg); 18 | }, 19 | onConnect = function (args) { 20 | log('connected', args); 21 | self.dispatchEvent('connected', userCredentials); 22 | }, 23 | onFailure = function (err) { 24 | log('error ===>', err); 25 | self.dispatchEvent('error', err); 26 | }, 27 | onMessage = function (message) { 28 | log('message ===>', JSON.stringify(message.payloadString)); 29 | self.dispatchEvent('received', message.payloadString); 30 | }, 31 | onDisconnect = function (err) { 32 | log('disconnected', err); 33 | currentTopic = undefined; 34 | self.dispatchEvent('disconnected', err); 35 | }, 36 | onMessageDelivered = function (deliveredMessage) { 37 | log('sent', deliveredMessage); 38 | self.dispatchEvent('internal:delivered', deliveredMessage); 39 | self.dispatchEvent('sent', deliveredMessage.payloadString); 40 | }; 41 | 42 | self.init = function (credentials, iotGatewayName) { 43 | const requestUrl = generateSignedWebsocketURL(credentials, iotGatewayName), 44 | clientId = credentials.identityId.replace('.', ''), 45 | connectOptions = { 46 | onSuccess: onConnect, 47 | useSSL: true, 48 | timeout: 3, 49 | mqttVersion: 4, 50 | onFailure: onFailure 51 | }; 52 | 53 | pahoClient = new Paho.MQTT.Client(requestUrl, clientId); 54 | userCredentials = credentials; 55 | self.dispatchEvent('connecting'); 56 | pahoClient.connect(connectOptions); 57 | pahoClient.onMessageArrived = onMessage; 58 | pahoClient.onConnectionLost = onDisconnect; 59 | pahoClient.onMessageDelivered = onMessageDelivered; 60 | }; 61 | self.subscribe = function (topicName) { 62 | self.dispatchEvent('subscribing', topicName); 63 | return new Promise(function (resolve, reject) { 64 | if (!pahoClient) { 65 | reject('not connected'); 66 | }; 67 | if (!topicName) { 68 | reject('cannot subscribe to empty topic'); 69 | }; 70 | const onSubscribeSuccess = function () { 71 | currentTopic = topicName; 72 | log('subscribed to ' + currentTopic); 73 | self.dispatchEvent('subscribed', currentTopic); 74 | resolve(topicName); 75 | }, 76 | onSubscribeFail = function (error) { 77 | log('failed to subscribe to ' + topicName); 78 | onFailure(error); 79 | reject(error.errorCode || error); 80 | }; 81 | if (currentTopic) { 82 | pahoClient.unsubscribe(currentTopic); 83 | } 84 | pahoClient.subscribe(topicName, { 85 | onSuccess: onSubscribeSuccess, 86 | onFailure: onSubscribeFail 87 | }); 88 | }).then(function () { 89 | return self.post(userCredentials.identityId + ' joined the chat'); 90 | }); 91 | }; 92 | self.post = function (messageText) { 93 | return new Promise(function (resolve, reject) { 94 | if (!pahoClient) { 95 | throw 'not connected'; 96 | }; 97 | if (!currentTopic) { 98 | throw 'not subscribed to a channel'; 99 | }; 100 | 101 | const messageSent = function () { 102 | unsubscribe(); //eslint-disable-line no-use-before-define 103 | resolve(); 104 | }, 105 | messageError = function (e) { 106 | unsubscribe(); //eslint-disable-line no-use-before-define 107 | reject(e); 108 | }, 109 | subscribe = function () { 110 | self.addEventListener('error', messageError); 111 | self.addEventListener('internal:delivered', messageSent); 112 | }, 113 | unsubscribe = function () { 114 | self.removeEventListener('error', messageError); 115 | self.removeEventListener('internal:delivered', messageSent); 116 | }; 117 | 118 | subscribe(); 119 | const message = new Paho.MQTT.Message(messageText); 120 | message.destinationName = currentTopic; 121 | pahoClient.send(message); 122 | }); 123 | }; 124 | 125 | }; 126 | -------------------------------------------------------------------------------- /src/lib/observable.js: -------------------------------------------------------------------------------- 1 | /*global module, console*/ 2 | module.exports = function observable(base) { 3 | 'use strict'; 4 | let listeners = []; 5 | base.addEventListener = function (types, listener, priority) { 6 | types.split(' ').forEach(function (type) { 7 | if (type) { 8 | listeners.push({ 9 | type: type, 10 | listener: listener, 11 | priority: priority || 0 12 | }); 13 | } 14 | }); 15 | }; 16 | base.listeners = function (type) { 17 | return listeners.filter(function (listenerDetails) { 18 | return listenerDetails.type === type; 19 | }).map(function (listenerDetails) { 20 | return listenerDetails.listener; 21 | }); 22 | }; 23 | base.removeEventListener = function (type, listener) { 24 | listeners = listeners.filter(function (details) { 25 | return details.listener !== listener; 26 | }); 27 | }; 28 | base.dispatchEvent = function (type) { 29 | const args = Array.prototype.slice.call(arguments, 1); 30 | listeners 31 | .filter(function (listenerDetails) { 32 | return listenerDetails.type === type; 33 | }) 34 | .sort(function (firstListenerDetails, secondListenerDetails) { 35 | return secondListenerDetails.priority - firstListenerDetails.priority; 36 | }) 37 | .some(function (listenerDetails) { 38 | try { 39 | return listenerDetails.listener.apply(undefined, args) === false; 40 | } catch (e) { 41 | console.log('dispatchEvent failed', e, listenerDetails); 42 | } 43 | 44 | }); 45 | }; 46 | return base; 47 | }; 48 | -------------------------------------------------------------------------------- /src/lib/sigv4url.js: -------------------------------------------------------------------------------- 1 | /*global module, require */ 2 | /*eslint-disable new-cap */ 3 | 4 | const CryptoJS = require('crypto-js'); 5 | 6 | const sign = function (key, msg) { 7 | 'use strict'; 8 | const hash = CryptoJS.HmacSHA256(msg, key); 9 | return hash.toString(CryptoJS.enc.Hex); 10 | }, 11 | sha256 = function (msg) { 12 | 'use strict'; 13 | const hash = CryptoJS.SHA256(msg); 14 | return hash.toString(CryptoJS.enc.Hex); 15 | }, 16 | getSignatureKey = function (key, dateStamp, regionName, serviceName) { 17 | 'use strict'; 18 | const kDate = CryptoJS.HmacSHA256(dateStamp, 'AWS4' + key), 19 | kRegion = CryptoJS.HmacSHA256(regionName, kDate), 20 | kService = CryptoJS.HmacSHA256(serviceName, kRegion), 21 | kSigning = CryptoJS.HmacSHA256('aws4_request', kService); 22 | return kSigning; 23 | }; 24 | 25 | 26 | module.exports = function sigV4Url(protocol, host, uri, service, region, accessKey, secretKey, sessionToken) { 27 | 'use strict'; 28 | const time = new Date().toISOString().split('T'), 29 | dateStamp = time[0].replace(/\-/g, ''), 30 | amzdate = dateStamp + 'T' + time[1].substring(0, 8).replace(/:/g, '') + 'Z', 31 | algorithm = 'AWS4-HMAC-SHA256', 32 | method = 'GET', 33 | credentialScope = dateStamp + '/' + region + '/' + service + '/' + 'aws4_request', 34 | canonicalQuerystring = //TODO: use querystring 35 | 'X-Amz-Algorithm=AWS4-HMAC-SHA256' + 36 | '&X-Amz-Credential=' + encodeURIComponent(accessKey + '/' + credentialScope) + 37 | '&X-Amz-Date=' + amzdate + 38 | '&X-Amz-SignedHeaders=host', 39 | canonicalHeaders = 'host:' + host + '\n', 40 | payloadHash = sha256(''), 41 | canonicalRequest = method + '\n' + uri + '\n' + canonicalQuerystring + '\n' + canonicalHeaders + '\nhost\n' + payloadHash, 42 | stringToSign = algorithm + '\n' + amzdate + '\n' + credentialScope + '\n' + sha256(canonicalRequest), 43 | signingKey = getSignatureKey(secretKey, dateStamp, region, service), 44 | signature = sign(signingKey, stringToSign); 45 | 46 | let signedQueryString = canonicalQuerystring + '&X-Amz-Signature=' + signature; 47 | if (sessionToken) { 48 | signedQueryString = signedQueryString + '&X-Amz-Security-Token=' + encodeURIComponent(sessionToken); 49 | } 50 | 51 | return protocol + '://' + host + uri + '?' + signedQueryString; 52 | }; 53 | -------------------------------------------------------------------------------- /src/lib/status-widget.js: -------------------------------------------------------------------------------- 1 | /*global require */ 2 | const jQuery = require('jquery'); 3 | jQuery.fn.statusWidget = function (client) { 4 | 'use strict'; 5 | const elements = jQuery(this), 6 | showStatus = function (string) { 7 | elements.text(string); 8 | }, 9 | showError = function (e) { 10 | const errorMessage = e && (typeof e === 'string' ? e : (e.stack || e.message || JSON.stringify(e))); 11 | showStatus('Error: ' + errorMessage); 12 | }; 13 | 14 | client.addEventListener('connected', () => showStatus('connected')); 15 | client.addEventListener('connecting', () => showStatus('connecting...')); 16 | client.addEventListener('disconnected', () => showStatus('disconnected')); 17 | client.addEventListener('error', showError); 18 | return elements; 19 | }; 20 | -------------------------------------------------------------------------------- /src/policies/unauthenticated-mqtt.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "iot:Connect" 8 | ], 9 | "Resource": "*" 10 | }, 11 | { 12 | "Effect": "Allow", 13 | "Action": "iot:Receive", 14 | "Resource": "*" 15 | }, 16 | { 17 | "Effect": "Allow", 18 | "Action": "iot:Subscribe", 19 | "Resource": "arn:aws:iot:*:*:topicfilter/chat/*" 20 | }, 21 | { 22 | "Effect": "Allow", 23 | "Action": "iot:Publish", 24 | "Resource": "*" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/style/main.css: -------------------------------------------------------------------------------- 1 | @import url('../../node_modules/bootstrap/dist/css/bootstrap.min.css'); 2 | -------------------------------------------------------------------------------- /src/util/package-html.js: -------------------------------------------------------------------------------- 1 | /*global require, __dirname, process, console*/ 2 | /*eslint strict: ["error", "global"]*/ 3 | 'use strict'; 4 | const fs = require('fs'), 5 | path = require('path'), 6 | Handlebars = require('handlebars'), 7 | version = process.env.npm_package_version, 8 | packageName = process.env.npm_package_name, 9 | environment = process.env.npm_package_config_buildenv, 10 | rootPath = path.resolve(__dirname, '..', '..'), 11 | webSourcePath = path.resolve(rootPath, 'src', 'web'), 12 | readTemplateValues = function () { 13 | const envPath = path.resolve(rootPath, 'env', environment + '.json'); 14 | try { 15 | return { 16 | version: version, 17 | environment: environment, 18 | config: JSON.parse(fs.readFileSync(envPath, 'utf8')) 19 | }; 20 | } catch (e) { 21 | console.error('Error reading config file', envPath); 22 | console.error(`Please create that file or re-run with --${packageName}:buildenv=`); 23 | process.exit(1); 24 | } 25 | }, 26 | processTemplate = function (fileName, templateValues) { 27 | console.log('building', fileName); 28 | const filePath = path.join(webSourcePath, fileName), 29 | source = fs.readFileSync(filePath, 'utf8'), 30 | template = Handlebars.compile(source), 31 | result = template(templateValues); 32 | fs.writeFileSync(path.resolve(rootPath, 'site', fileName), result, 'utf8'); 33 | }, 34 | isHtmlFile = function (fileName) { 35 | const filePath = path.join(webSourcePath, fileName), 36 | stat = fs.lstatSync(filePath); 37 | return stat.isFile() && path.extname(filePath) === '.html'; 38 | }; 39 | 40 | 41 | 42 | console.log('======> BUILDING with', environment); 43 | const templateValues = readTemplateValues(); 44 | fs.readdirSync(webSourcePath) 45 | .filter(isHtmlFile) 46 | .forEach(f => processTemplate(f, templateValues)); 47 | -------------------------------------------------------------------------------- /src/util/post-message.js: -------------------------------------------------------------------------------- 1 | /*global require, console */ 2 | /*eslint strict: ["error", "global"]*/ 3 | 'use strict'; 4 | const AWS = require('aws-sdk'), 5 | postToEndpoint = function (endpoint, topic, message) { 6 | const iotdata = new AWS.IotData({endpoint: endpoint}); 7 | return iotdata.publish({ 8 | topic: topic, 9 | payload: message 10 | }).promise(); 11 | }, 12 | postToDefaultEndpoint = function (topic, message) { 13 | const iot = new AWS.Iot(); 14 | return iot.describeEndpoint().promise().then(data => postToEndpoint(data.endpointAddress, topic, message)); 15 | }; 16 | 17 | AWS.config.update({region: 'us-east-1'}); 18 | postToDefaultEndpoint('chat/Serverless', 'this is from a server') 19 | .then(console.log, e => console.error('error posting', e)); 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Serverless Chat 10 | 11 | 12 | 13 | 14 | 15 |
    16 |
    17 | 18 | not signed in 19 |
    20 |
    21 | 22 | 23 |
    24 |
    25 | 26 |
    27 |

    Serverless Chat

    28 |
    29 |
    30 |
    31 |

    Welcome to the serverless chat. This is a multi-user online messaging system working 32 | completely from a static HTML page, using AWS IOT Gateway web sockets. Check out the GitHub project for the source code 33 | and installation instructions.

    34 |

    Log in as a guest

    35 |
    36 |
    37 | 40 |
    41 | Cancel 42 |
    43 |
    44 |
    45 | 49 |

    Select a chat channel

    50 |
    51 | 52 | 53 | 54 |
    55 |

    ... or join a new one

    56 |
    57 |
    58 |
    59 |
    Create new:
    60 | 61 |
    62 |
    63 | 64 |
    65 |

    66 |
    67 | Sign out 68 |
    69 |
    70 |
    71 | 72 |
    73 | Cancel 74 | View Messages 75 |
    76 |
    77 | 78 |
    79 | 82 | 83 |
    84 |
      85 |
      86 | 87 |
      88 |
      89 | 90 | 91 |
      92 | 93 | 94 |
      95 |
      96 |
      97 |
      98 |
      99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /*global require, module, __dirname, process, console */ 2 | const path = require('path'), 3 | webpack = require('webpack'), 4 | env = process.env.npm_package_config_buildenv, 5 | packageName = process.env.npm_package_name, 6 | BabiliPlugin = require('babili-webpack-plugin'), 7 | plugins = [ 8 | new webpack.ProvidePlugin({ 9 | $: 'jquery', 10 | jQuery: 'jquery' 11 | }) 12 | ]; 13 | 14 | if (!env) { 15 | throw `package buildenv is not defined, aborting. Please run from NPM and optionally specify the build environment using --${packageName}:buildenv=`; 16 | } 17 | console.log('=====> webpack building for ' + env); 18 | //build minimised if prod/staging 19 | if (env !== 'dev') { 20 | plugins.push(new BabiliPlugin()); 21 | } 22 | module.exports = { 23 | entry: path.resolve(__dirname, 'src', 'lib', 'main.js'), 24 | devtool: 'source-map', 25 | output: { 26 | filename: '[name].js', 27 | path: path.resolve(__dirname, 'site', process.env.npm_package_version) 28 | }, 29 | plugins: plugins, 30 | resolve: { 31 | alias: { 32 | config: path.join(__dirname, 'env', env + '.json') 33 | } 34 | } 35 | }; 36 | --------------------------------------------------------------------------------