diff --git a/.gitignore b/.gitignore index 2c4ae48..305ac9f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ jspm_packages # Serverless directories .serverless +*-function.json .DS_Store \ No newline at end of file diff --git a/package.json b/package.json index 6ca7d9a..789f2c8 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,15 @@ "name": "codingcoach-api", "version": "1.0.0", "description": "Azure Functions sample for the Serverless framework", - "main": "handler.js", "keywords": [ "azure", "serverless" ], + "dependencies": { + "express-jwt": "^5.3.1", + "node-fetch": "^2.3.0" + }, "devDependencies": { "serverless-azure-functions": "*" } -} \ No newline at end of file +} diff --git a/serverless.yml b/serverless.yml new file mode 100644 index 0000000..10e6f7a --- /dev/null +++ b/serverless.yml @@ -0,0 +1,28 @@ + +service: codingcoach-api + +# You can pin your service to only deploy with a specific Serverless version +# Check out our docs for more details +# frameworkVersion: "=X.X.X" + +provider: + name: azure + location: West US + +plugins: + - serverless-azure-functions + +# you can add packaging information here +#package: +# include: +# - include-me.js +# - include-me-dir/** +# exclude: +# - exclude-me.js +# - exclude-me-dir/** + +functions: + users: ${file(src/handlers/users/config.yml):users} + users-add: ${file(src/handlers/users/config.yml):usersadd} + hello: ${file(src/handlers/hello/config.yml):hello} + diff --git a/services/hello/hello-function.json b/services/hello/hello-function.json deleted file mode 100644 index f348993..0000000 --- a/services/hello/hello-function.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "disabled": false, - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "http", - "direction": "out", - "name": "res" - } - ], - "entryPoint": "hello", - "scriptFile": "handler.js" -} \ No newline at end of file diff --git a/services/hello/serverless.yml b/services/hello/serverless.yml deleted file mode 100644 index cf31553..0000000 --- a/services/hello/serverless.yml +++ /dev/null @@ -1,57 +0,0 @@ -# Welcome to Serverless! -# -# This file is the main config file for your service. -# It's very minimal at this point and uses default values. -# You can always add more config options for more control. -# We've included some commented out config examples here. -# Just uncomment any of them to get that config option. -# -# For full config options, check the docs: -# docs.serverless.com -# -# Happy Coding! - -service: codingcoach-hello # NOTE: update this with your service name - -# You can pin your service to only deploy with a specific Serverless version -# Check out our docs for more details -# frameworkVersion: "=X.X.X" - -provider: - name: azure - location: West US - -plugins: - - serverless-azure-functions - -# you can add packaging information here -#package: -# include: -# - include-me.js -# - include-me-dir/** -# exclude: -# - exclude-me.js -# - exclude-me-dir/** - -functions: - hello: - handler: handler.hello - events: - - http: true - x-azure-settings: - authLevel : anonymous - - http: true - x-azure-settings: - direction: out - name: res - -# The following are a few examples of other events you can configure: -# -# events: -# - queue: YourQueueName -# x-azure-settings: -# connection : StorageAppSettingName -# - blob: -# x-azure-settings: -# name: bindingName -# direction: in diff --git a/services/users/serverless.yml b/services/users/serverless.yml deleted file mode 100644 index a211752..0000000 --- a/services/users/serverless.yml +++ /dev/null @@ -1,67 +0,0 @@ -# Welcome to Serverless! -# -# This file is the main config file for your service. -# It's very minimal at this point and uses default values. -# You can always add more config options for more control. -# We've included some commented out config examples here. -# Just uncomment any of them to get that config option. -# -# For full config options, check the docs: -# docs.serverless.com -# -# Happy Coding! - -service: codingcoach-users # NOTE: update this with your service name - -# You can pin your service to only deploy with a specific Serverless version -# Check out our docs for more details -# frameworkVersion: "=X.X.X" - -provider: - name: azure - location: West US - -plugins: - - serverless-azure-functions - -# you can add packaging information here -#package: -# include: -# - include-me.js -# - include-me-dir/** -# exclude: -# - exclude-me.js -# - exclude-me-dir/** - -functions: - list: - handler: list.list - events: - - http: true - x-azure-settings: - authLevel : anonymous - - http: true - x-azure-settings: - direction: out - name: res - store: - handler: store.store - events: - - http: true - x-azure-settings: - authLevel : anonymous - - http: true - x-azure-settings: - direction: out - name: res - -# The following are a few examples of other events you can configure: -# -# events: -# - queue: YourQueueName -# x-azure-settings: -# connection : StorageAppSettingName -# - blob: -# x-azure-settings: -# name: bindingName -# direction: in diff --git a/services/users/store.js b/services/users/store.js deleted file mode 100644 index e519aa4..0000000 --- a/services/users/store.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -module.exports.store = function (context) { - const json = { - success: true, - message: 'Succesfully saved' - }; - - context.res = { - status: 200, - body: json, - headers: { - 'Content-Type': 'application/json' - } - }; - - context.done(); -}; diff --git a/services/users/users-function.json b/services/users/users-function.json deleted file mode 100644 index eed3aba..0000000 --- a/services/users/users-function.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "disabled": false, - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "http", - "direction": "out", - "name": "res" - } - ], - "entryPoint": "list", - "scriptFile": "list.js" -} \ No newline at end of file diff --git a/src/config/constants.js b/src/config/constants.js new file mode 100644 index 0000000..fa4d8c5 --- /dev/null +++ b/src/config/constants.js @@ -0,0 +1,9 @@ + +module.exports = { + auth0: { + DOMAIN: process.env.AUTH0_DOMAIN, + CLIENT_ID: process.env.AUTH0_CLIENT_ID, + CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET, + CERTIFICATE: process.env.AUTH0_CERTIFICATE, + }, +}; diff --git a/src/handlers/hello/config.yml b/src/handlers/hello/config.yml new file mode 100644 index 0000000..f06de91 --- /dev/null +++ b/src/handlers/hello/config.yml @@ -0,0 +1,10 @@ +hello: + handler: src/handlers/hello/handler.hello + events: + - http: true + x-azure-settings: + authLevel : anonymous + - http: true + x-azure-settings: + direction: out + name: res \ No newline at end of file diff --git a/services/hello/handler.js b/src/handlers/hello/handler.js similarity index 100% rename from services/hello/handler.js rename to src/handlers/hello/handler.js diff --git a/src/handlers/users/config.yml b/src/handlers/users/config.yml new file mode 100644 index 0000000..7b41c15 --- /dev/null +++ b/src/handlers/users/config.yml @@ -0,0 +1,20 @@ +users: + handler: src/handlers/users/list.list + events: + - http: true + x-azure-settings: + authLevel : anonymous + - http: true + x-azure-settings: + direction: out + name: res +usersadd: + handler: src/handlers/users/store.add + events: + - http: true + x-azure-settings: + authLevel : anonymous + - http: true + x-azure-settings: + direction: out + name: res \ No newline at end of file diff --git a/services/users/list.js b/src/handlers/users/list.js similarity index 56% rename from services/users/list.js rename to src/handlers/users/list.js index 9a398d8..659cb39 100644 --- a/services/users/list.js +++ b/src/handlers/users/list.js @@ -1,8 +1,9 @@ 'use strict'; -/* eslint-disable no-param-reassign */ +const authMiddleware = require('../../middlewares/auth0.js'); -module.exports.list = function (context) { +module.exports.list = authMiddleware((context) => { + // TODO: get this data from the database const users = [{ id: 1, name: 'Emma Wedekind' @@ -16,11 +17,14 @@ module.exports.list = function (context) { context.res = { status: 200, - body: users, + body: { + success: true, + users, + }, headers: { 'Content-Type': 'application/json' } }; context.done(); -}; +}); diff --git a/src/handlers/users/store.js b/src/handlers/users/store.js new file mode 100644 index 0000000..c45d48d --- /dev/null +++ b/src/handlers/users/store.js @@ -0,0 +1,69 @@ +'use strict'; + +const fetch = require('node-fetch'); +const config = require('../../config/constants.js'); +const authMiddleware = require('../../middlewares/auth0.js'); + +// Get an access token for the Auth0 Admin API +function getAdminAccessToken() { + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + client_id: config.auth0.CLIENT_ID, + client_secret: config.auth0.CLIENT_SECRET, + audience: `${config.auth0.DOMAIN}/api/v2/`, + grant_type: 'client_credentials', + }), + }; + + return fetch(`${config.auth0.DOMAIN}/oauth/token`, options) + .then((response) => response.json()); +} + + +// Get the user's profile from auth0 +function getUserProfile(accessToken, userID) { + const options = { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + } + return fetch(`${config.auth0.DOMAIN}/api/v2/users/${userID}`, options) + .then(response => response.json()); +} + +module.exports.add = authMiddleware(async (context, request) => { + let res = {}; + try { + const data = await getAdminAccessToken(); + const user = await getUserProfile(data.access_token, request.user.sub); + + // @TODO: check if the current user already exist in the database, + // if not, we need to add the new record. + + res = { + body: { + success: true, + message: 'User successfully saved', + user, + }, + }; + } catch (error) { + res = { + status: 500, + body: { + success: false, + error, + }, + }; + } + + context.res = { + ...res, + headers: { + 'Content-Type': 'application/json' + }, + }; + context.done() +}); diff --git a/src/middlewares/auth0.js b/src/middlewares/auth0.js new file mode 100644 index 0000000..d3289d0 --- /dev/null +++ b/src/middlewares/auth0.js @@ -0,0 +1,36 @@ +const jwt = require('express-jwt'); +const config = require('../config/constants.js'); + +/** + * We use this middleware to check if the user is authenticated. + */ + +const middleware = jwt({ + secret: config.auth0.CERTIFICATE, + aud: `${config.auth0.DOMAIN}/api/v2/`, + issuer: `${config.auth0.DOMAIN}/`, + algorithms: ['RS256'], +}); + +module.exports = (next) => { + return (context, req) => { + middleware(req, null, (err) => { + if (err) { + context.res = { + status: err.status || 500, + headers: { + 'Content-Type': 'application/json' + }, + body: { + success: false, + message: err.message, + }, + }; + + return context.done(); + } + + return next(context, req); + }); + }; +}; diff --git a/yarn.lock b/yarn.lock index 7c52e0e..5f25857 100644 --- a/yarn.lock +++ b/yarn.lock @@ -198,6 +198,10 @@ async@>=0.6.0, async@^2.0.0, async@^2.1.2: dependencies: lodash "^4.17.11" +async@^1.5.0: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -296,8 +300,8 @@ bl@^1.0.0: safe-buffer "^5.1.1" bluebird@^3.4.6, bluebird@^3.5.0: - version "3.5.3" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" + version "3.5.4" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.4.tgz#d6cc661595de30d5b3af5fcedd3c0b3ef6ec5714" bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" @@ -724,12 +728,18 @@ date-utils@*: version "1.2.21" resolved "https://registry.yarnpkg.com/date-utils/-/date-utils-1.2.21.tgz#61fb16cdc1274b3c9acaaffe9fc69df8720a2b64" -debug@^2.1.2, debug@^2.2.0, debug@^2.3.3: +debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: ms "2.0.0" +debug@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + dependencies: + ms "^2.1.1" + decamelize@^1.0.0, decamelize@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -1022,6 +1032,19 @@ expand-template@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" +express-jwt@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/express-jwt/-/express-jwt-5.3.1.tgz#66f05c7dddb5409c037346a98b88965bb10ea4ae" + dependencies: + async "^1.5.0" + express-unless "^0.3.0" + jsonwebtoken "^8.1.0" + lodash.set "^4.0.0" + +express-unless@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/express-unless/-/express-unless-0.3.1.tgz#2557c146e75beb903e2d247f9b5ba01452696e20" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -1645,6 +1668,21 @@ jsonpath@^0.2.11: static-eval "0.2.3" underscore "1.7.0" +jsonwebtoken@^8.1.0: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -1662,7 +1700,7 @@ jwa@^1.4.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jws@3.x.x: +jws@3.x.x, jws@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" dependencies: @@ -1744,6 +1782,38 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + +lodash.set@^4.0.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" + lodash@^4.14.0, lodash@^4.16.6, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.8.0: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" @@ -1917,6 +1987,10 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -1950,10 +2024,10 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508" needle@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e" + version "2.3.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.3.0.tgz#ce3fea21197267bacb310705a7bbe24f2a3a3492" dependencies: - debug "^2.1.2" + debug "^4.1.0" iconv-lite "^0.4.4" sax "^1.2.4" @@ -1971,6 +2045,10 @@ node-abi@^2.7.0: dependencies: semver "^5.4.1" +node-fetch@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5" + node-libs-browser@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.0.tgz#c72f60d9d46de08a940dedbb25f3ffa2f9bbaa77" @@ -2578,7 +2656,7 @@ sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.6.0: version "5.7.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" @@ -2756,8 +2834,8 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e" + version "3.0.4" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz#75ecd1a88de8c184ef015eafb51b5b48bfd11bb1" split-string@^3.0.1, split-string@^3.0.2: version "3.1.0"