From f68dfed1fca9d700e1164894983b997693c49da8 Mon Sep 17 00:00:00 2001 From: Tony Zhu Date: Thu, 17 May 2018 14:50:36 -0700 Subject: [PATCH] Seed the project with deploy to Azure botton and basic action response --- .gitignore | 86 +++++++++------------------- .gitmodules | 3 + README.md | 4 ++ action/ActionProcessor.js | 65 +++++++++++++++++++++ action/function.json | 16 ++++++ action/index.js | 20 +++++++ azuredeploy.json | 116 ++++++++++++++++++++++++++++++++++++++ host.json | 1 + package.json | 15 +++++ 9 files changed, 266 insertions(+), 60 deletions(-) create mode 100644 .gitmodules create mode 100644 action/ActionProcessor.js create mode 100644 action/function.json create mode 100644 action/index.js create mode 100644 azuredeploy.json create mode 100644 host.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index ad46b30..5f8f074 100644 --- a/.gitignore +++ b/.gitignore @@ -1,61 +1,27 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# next.js build output -.next +bin +obj +csx +.vs +edge +Publish +.vscode + +*.user +*.suo +*.cscfg +*.Cache +project.lock.json + +/packages +/node_modules +/TestResults + +/tools/NuGet.exe +/App_Data +/secrets +/data +.secrets +appsettings.json +local.settings.json +npm-debug.log diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9b617c7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "outlook-actionable-messages-node-token-validation"] + path = outlook-actionable-messages-node-token-validation + url = git@github.com:OfficeDev/outlook-actionable-messages-node-token-validation.git diff --git a/README.md b/README.md index 72f1506..a1db3b1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ +# HelloActionableMessages + +[![Deploy to Azure](http://azuredeploy.net/deploybutton.png)](https://azuredeploy.net/) + # Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a diff --git a/action/ActionProcessor.js b/action/ActionProcessor.js new file mode 100644 index 0000000..2abe170 --- /dev/null +++ b/action/ActionProcessor.js @@ -0,0 +1,65 @@ +var validation = require('../outlook-actionable-messages-node-token-validation/ActionableMessageTokenValidator'); + +class ActionProcessor{ + static process(headers, body, cb, logFun = null){ + var auth = headers['authorization'].trim().split(' '); + if (auth.length == 2 && auth[0].toLowerCase() == 'bearer') { + var token = auth[1]; + }else{ + cb({ + status: 401, + body: "Invalid auth header" + }); + return; + } + + var validator = new validation.ActionableMessageTokenValidator(); + + // Set targetUrl with your service domain URL. + // For example, if the service URL is https://api.xyz.com/finance/expense?id=1234, + // then set it to https://api.xyz.com + var targetUrl = null; + + // validateToken will verify the following + // 1. The token is issued by Microsoft and its digital signature is valid. + // 2. The token has not expired. + // 3. The audience claim matches the service domain URL. + validator.validateToken( + token, + null, + function (err, result) { + if (err) { + logFun('error: ' + err.message); + cb({ + status: 401, + body: err.message + }); + return; + } + // We have a valid token. We will verify the sender and the action performer. + // You should replace the code below with your own validation logic. + // In this example, we verify that the email is sent by expense@contoso.com + // and the action performer is someone with a @contoso.com email address. + // + // You should also return the CARD-ACTION-STATUS header in the response. + // The value of the header will be displayed to the user. + + var card = { + "@type": "MessageCard", + "@context": "http://schema.org/extensions", + "summary": "Hello " + result.actionPerformer + ". Your action email was from " + result.sender, + "title": "Hello " + result.actionPerformer, + "themeColor": "009F9C" + } + + cb({ + status: 200, + headers: {'CARD-UPDATE-IN-BODY': 'true'}, + body: card + }) + }); + + } +} + +exports.ActionProcessor = ActionProcessor; \ No newline at end of file diff --git a/action/function.json b/action/function.json new file mode 100644 index 0000000..0694816 --- /dev/null +++ b/action/function.json @@ -0,0 +1,16 @@ +{ + "disabled": false, + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} \ No newline at end of file diff --git a/action/index.js b/action/index.js new file mode 100644 index 0000000..68b9e20 --- /dev/null +++ b/action/index.js @@ -0,0 +1,20 @@ +var processor = require('./ActionProcessor'); + +module.exports = function (context, req) { + context.log('JavaScript HTTP trigger function processed a request.'); + + if (req.body) { + + processor.ActionProcessor.process(req.headers, req.body, function(res){ + context.res = res; + context.done(); + }, context.log) + } + else { + context.res = { + status: 400, + body: "Invalid request" + }; + context.done(); + } +}; \ No newline at end of file diff --git a/azuredeploy.json b/azuredeploy.json new file mode 100644 index 0000000..9980b30 --- /dev/null +++ b/azuredeploy.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appName": { + "type": "string", + "metadata": { + "description": "The name of the function app that you wish to create." + } + }, + "storageAccountType": { + "type": "string", + "defaultValue": "Standard_LRS", + "allowedValues": [ + "Standard_LRS", + "Standard_GRS", + "Standard_RAGRS" + ], + "metadata": { + "description": "Storage Account type" + } + }, + "repoUrl": { + "type": "string" + }, + "branch": { + "type": "string" + } + }, + "variables": { + "functionAppName": "[parameters('appName')]", + "hostingPlanName": "[parameters('appName')]", + "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'azfunctions')]", + "storageAccountid": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "apiVersion": "2016-12-01", + "location": "[resourceGroup().location]", + "kind": "Storage", + "sku": { + "name": "[parameters('storageAccountType')]" + } + }, + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2015-04-01", + "name": "[variables('hostingPlanName')]", + "location": "[resourceGroup().location]", + "properties": { + "name": "[variables('hostingPlanName')]", + "computeMode": "Dynamic", + "sku": "Dynamic" + } + }, + { + "apiVersion": "2015-08-01", + "type": "Microsoft.Web/sites", + "name": "[variables('functionAppName')]", + "location": "[resourceGroup().location]", + "kind": "functionapp", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ], + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "siteConfig": { + "appSettings": [ + { + "name": "AzureWebJobsDashboard", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" + }, + { + "name": "AzureWebJobsStorage", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" + }, + { + "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" + }, + { + "name": "WEBSITE_CONTENTSHARE", + "value": "[toLower(variables('functionAppName'))]" + }, + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~1" + }, + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "6.5.0" + } + ] + } + }, + "resources": [ + { + "apiVersion": "2015-08-01", + "name": "web", + "type": "sourcecontrols", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', parameters('appName'))]" + ], + "properties": { + "RepoUrl": "[parameters('repoUrl')]", + "branch": "[parameters('branch')]", + "IsManualIntegration": "true" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/host.json b/host.json new file mode 100644 index 0000000..6f31cf5 --- /dev/null +++ b/host.json @@ -0,0 +1 @@ +{ } \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..5dc184c --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "helloactionablemessages", + "version": "1.0.0", + "description": "Actionable Message Sample", + "scripts": { + "code": "func host start --debug vscode" + }, + "dependencies": { + "jsonwebtoken": "7.x", + "request": "2.x", + "rsa-pem-from-mod-exp": "^0.8.4", + "base64url": "^1.0.6" + }, + "license": "MIT" +}