├── .gitignore ├── README.md ├── functions ├── auth │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── user-management.js ├── images │ ├── aws-es-lambda.js │ └── index.js ├── index │ └── index.js └── tags │ ├── aws-es-lambda.js │ ├── image-recognition.js │ ├── index.js │ ├── package-lock.json │ └── package.json ├── package-lock.json ├── package.json ├── template.yaml ├── test.jpg └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | packaged.yaml 2 | node_modules/ 3 | api-url 4 | 5 | # Created by https://www.gitignore.io/api/osx,node,linux,windows 6 | 7 | ### Linux ### 8 | *~ 9 | 10 | # temporary files which can be created if a process still has a handle open of a deleted file 11 | .fuse_hidden* 12 | 13 | # KDE directory preferences 14 | .directory 15 | 16 | # Linux trash folder which might appear on any partition or disk 17 | .Trash-* 18 | 19 | # .nfs files are created when an open file is removed but is still being accessed 20 | .nfs* 21 | 22 | ### Node ### 23 | # Logs 24 | logs 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | *.pid.lock 35 | 36 | # Directory for instrumented libs generated by jscoverage/JSCover 37 | lib-cov 38 | 39 | # Coverage directory used by tools like istanbul 40 | coverage 41 | 42 | # nyc test coverage 43 | .nyc_output 44 | 45 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 46 | .grunt 47 | 48 | # Bower dependency directory (https://bower.io/) 49 | bower_components 50 | 51 | # node-waf configuration 52 | .lock-wscript 53 | 54 | # Compiled binary addons (http://nodejs.org/api/addons.html) 55 | build/Release 56 | 57 | # Dependency directories 58 | node_modules/ 59 | jspm_packages/ 60 | 61 | # Typescript v1 declaration files 62 | typings/ 63 | 64 | # Optional npm cache directory 65 | .npm 66 | 67 | # Optional eslint cache 68 | .eslintcache 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variables file 80 | .env 81 | 82 | 83 | ### OSX ### 84 | *.DS_Store 85 | .AppleDouble 86 | .LSOverride 87 | 88 | # Icon must end with two \r 89 | Icon 90 | 91 | # Thumbnails 92 | ._* 93 | 94 | # Files that might appear in the root of a volume 95 | .DocumentRevisions-V100 96 | .fseventsd 97 | .Spotlight-V100 98 | .TemporaryItems 99 | .Trashes 100 | .VolumeIcon.icns 101 | .com.apple.timemachine.donotpresent 102 | 103 | # Directories potentially created on remote AFP share 104 | .AppleDB 105 | .AppleDesktop 106 | Network Trash Folder 107 | Temporary Items 108 | .apdisk 109 | 110 | ### Windows ### 111 | # Windows thumbnail cache files 112 | Thumbs.db 113 | ehthumbs.db 114 | ehthumbs_vista.db 115 | 116 | # Folder config file 117 | Desktop.ini 118 | 119 | # Recycle Bin used on file shares 120 | $RECYCLE.BIN/ 121 | 122 | # Windows Installer files 123 | *.cab 124 | *.msi 125 | *.msm 126 | *.msp 127 | 128 | # Windows shortcuts 129 | *.lnk 130 | 131 | 132 | # End of https://www.gitignore.io/api/osx,node,linux,windows -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless API Example 2 | 3 | Example API implementation for [this article](#). 4 | 5 | ## Uses 6 | 7 | - AWS SDK 8 | - AWS CLI 9 | - [AWS SAM](https://github.com/awslabs/serverless-application-model) 10 | - AWS SAM CLI 11 | - AWS CloudFormation (via CLI) 12 | - AWS Lambda 13 | - AWS API-Gateway 14 | - AWS S3 15 | - AWS Elasticsearch Service 16 | - AWS Cognito 17 | - AWS Systems Manager Parameter Store 18 | - [Clarifai](https://www.clarifai.com/) 19 | 20 | ## Requirements 21 | 22 | - Node.js >8.10 23 | - AWS CLI 24 | - AWS SAM CLI 25 | 26 | ## Setup 27 | 28 | npm i 29 | npm run createDeployBucket -- s3:// 30 | npm run setup 31 | npm run store-api-key -- 32 | 33 | ## Test 34 | 35 | npm run test 36 | -------------------------------------------------------------------------------- /functions/auth/index.js: -------------------------------------------------------------------------------- 1 | const users = require("./user-management"); 2 | 3 | exports.handler = async event => { 4 | const body = JSON.parse(event.body); 5 | if (event.path === "/signup") return signUp(body); 6 | return signIn(body); 7 | }; 8 | 9 | const signUp = async ({ username, password }) => { 10 | try { 11 | await users.signUp(username, password); 12 | return createResponse({ message: "Created" }, 201); 13 | } catch (e) { 14 | console.log(e); 15 | return createResponse({ message: e.message }, 400); 16 | } 17 | }; 18 | 19 | const signIn = async ({ username, password }) => { 20 | try { 21 | const token = await users.signIn(username, password); 22 | return createResponse({ token }, 201); 23 | } catch (e) { 24 | console.log(e); 25 | return createResponse({ message: e.message }, 400); 26 | } 27 | }; 28 | 29 | const createResponse = (data = { message: "OK" }, statusCode = 200) => ({ 30 | statusCode, 31 | body: JSON.stringify(data), 32 | headers: { "Access-Control-Allow-Origin": "*" } 33 | }); -------------------------------------------------------------------------------- /functions/auth/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-api-auth", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "amazon-cognito-identity-js": { 8 | "version": "3.0.13", 9 | "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-3.0.13.tgz", 10 | "integrity": "sha512-E9s0gc5nidRq7Kfv4Tf6qQpwq8gFNWJQ6rl7yN4PaMayPc14vT52rcu3ER4PoKgzxM311lDuBkDvQZ4UawaO7w==", 11 | "requires": { 12 | "buffer": "4.9.1", 13 | "crypto-js": "^3.1.9-1", 14 | "js-cookie": "^2.1.4" 15 | } 16 | }, 17 | "base64-js": { 18 | "version": "1.3.0", 19 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", 20 | "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" 21 | }, 22 | "buffer": { 23 | "version": "4.9.1", 24 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", 25 | "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", 26 | "requires": { 27 | "base64-js": "^1.0.2", 28 | "ieee754": "^1.1.4", 29 | "isarray": "^1.0.0" 30 | } 31 | }, 32 | "crypto-js": { 33 | "version": "3.1.9-1", 34 | "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", 35 | "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" 36 | }, 37 | "ieee754": { 38 | "version": "1.1.13", 39 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 40 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 41 | }, 42 | "isarray": { 43 | "version": "1.0.0", 44 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 45 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 46 | }, 47 | "js-cookie": { 48 | "version": "2.2.0", 49 | "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.0.tgz", 50 | "integrity": "sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s=" 51 | }, 52 | "node-fetch": { 53 | "version": "2.6.0", 54 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", 55 | "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /functions/auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-api-auth", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Kay Plößer", 10 | "license": "GPL", 11 | "dependencies": { 12 | "amazon-cognito-identity-js": "^3.0.13", 13 | "node-fetch": "^2.6.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /functions/auth/user-management.js: -------------------------------------------------------------------------------- 1 | global.fetch = require("node-fetch"); 2 | const Cognito = require("amazon-cognito-identity-js"); 3 | 4 | const userPool = new Cognito.CognitoUserPool({ 5 | UserPoolId: process.env.USER_POOL_ID, 6 | ClientId: process.env.USER_POOL_CLIENT_ID 7 | }); 8 | 9 | exports.signUp = (username, password) => 10 | new Promise((resolve, reject) => 11 | userPool.signUp(username, password, null, null, (error, result) => 12 | error ? reject(error) : resolve(result) 13 | ) 14 | ); 15 | 16 | exports.signIn = (username, password) => 17 | new Promise((resolve, reject) => { 18 | const authenticationDetails = new Cognito.AuthenticationDetails({ 19 | Username: username, 20 | Password: password 21 | }); 22 | const cognitoUser = new Cognito.CognitoUser({ 23 | Username: username, 24 | Pool: userPool 25 | }); 26 | cognitoUser.authenticateUser(authenticationDetails, { 27 | onSuccess: result => resolve(result.getIdToken().getJwtToken()), 28 | onFailure: reject 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /functions/images/aws-es-lambda.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const AWS = require("aws-sdk"); 3 | 4 | // Basic AWS Elasticsearch util functions copied from an AWS example 5 | 6 | const httpClient = new AWS.HttpClient(); 7 | const credentials = new AWS.EnvironmentCredentials("AWS"); 8 | const endpoint = new AWS.Endpoint(process.env.ELASTICSEARCH_URL); 9 | 10 | const sendRequest = ({ httpMethod, requestPath, payload }) => 11 | new Promise((resolve, reject) => { 12 | const request = new AWS.HttpRequest(endpoint, process.env.REGION); 13 | 14 | request.method = httpMethod; 15 | request.path = path.join(request.path, requestPath); 16 | request.body = payload; 17 | request.headers["Content-Type"] = "application/json"; 18 | request.headers.Host = process.env.ELASTICSEARCH_URL; 19 | 20 | const signer = new AWS.Signers.V4(request, "es"); 21 | signer.addAuthorization(credentials, new Date()); 22 | 23 | httpClient.handleRequest( 24 | request, 25 | null, 26 | response => { 27 | const { statusCode, statusMessage, headers } = response; 28 | let body = ""; 29 | response.on("data", chunk => { 30 | body += chunk; 31 | }); 32 | response.on("end", () => { 33 | const data = { statusCode, statusMessage, headers }; 34 | if (body) data.body = JSON.parse(body); 35 | resolve(data); 36 | }); 37 | }, 38 | reject 39 | ); 40 | }); 41 | 42 | exports.query = (index, body) => 43 | sendRequest({ 44 | httpMethod: "POST", 45 | requestPath: index + "/_search", 46 | payload: JSON.stringify(body) 47 | }); 48 | 49 | exports.delete = (index, id) => 50 | sendRequest({ 51 | httpMethod: "DELETE", 52 | requestPath: index + "/_doc/" + id, 53 | payload: "" 54 | }); 55 | -------------------------------------------------------------------------------- /functions/images/index.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | const elasticsearch = require("./aws-es-lambda"); 3 | 4 | exports.handler = async event => { 5 | switch (event.httpMethod.toLowerCase()) { 6 | case "post": 7 | return createImage(event); 8 | case "delete": 9 | return deleteImage(event); 10 | default: 11 | return listImages(event); 12 | } 13 | }; 14 | 15 | // Called with API-GW event 16 | const createImage = async event => { 17 | const userName = extractUserName(event); 18 | const fileName = "" + Math.random() + Date.now() + "+" + userName; 19 | const { url, fields } = await createPresignedUploadCredentials(fileName); 20 | return response({ 21 | formConfig: { 22 | uploadUrl: url, 23 | formFields: fields 24 | } 25 | }, 201); 26 | }; 27 | 28 | // Called with API-GW event 29 | const deleteImage = async event => { 30 | const { imageId } = event.pathParameters; 31 | await elasticsearch.delete("images", imageId); 32 | return response({ message: "Deleted image: " + imageId }); 33 | }; 34 | 35 | // Called with API-GW event 36 | const listImages = async event => { 37 | const userName = extractUserName(event); 38 | 39 | // Bad Elasticsearch query to retrieve all images of user with a specific tag 40 | // Please improve :D 41 | const queries = [] 42 | 43 | const params = { 44 | query: { 45 | bool: { 46 | must: [{ wildcard: { fileName: "*" + userName } }] 47 | } 48 | } 49 | }; 50 | 51 | 52 | if (event.queryStringParameters && event.queryStringParameters.tags) { 53 | let tags = event.queryStringParameters.tags.split(","); 54 | if (tags.length === 1) tags = tags[0]; 55 | params.query.bool.filter = [{ match: { tags } }]; 56 | } 57 | 58 | console.log(JSON.stringify(params)); 59 | const result = await elasticsearch.query("images", params); 60 | console.log(JSON.stringify(result)); 61 | const images = result.body.hits.hits.map(hit => ({ 62 | id: hit._id, 63 | tags: hit._source.tags, 64 | fileUrl: `https://${process.env.IMAGE_BUCKET_NAME}.s3.amazonaws.com/${hit._source.fileName}` 65 | })); 66 | return response({ images }); 67 | }; 68 | 69 | // ============================== HELPERS ============================== 70 | 71 | const extractUserName = event => event.requestContext.authorizer.claims["cognito:username"]; 72 | 73 | const response = (data, statusCode = 200) => ({ 74 | statusCode, 75 | body: JSON.stringify(data), 76 | headers: { "Access-Control-Allow-Origin": "*" } 77 | }); 78 | 79 | const s3Client = new AWS.S3(); 80 | const createPresignedUploadCredentials = fileName => { 81 | const params = { 82 | Bucket: process.env.IMAGE_BUCKET_NAME, 83 | Fields: { Key: fileName } 84 | }; 85 | 86 | return new Promise((resolve, reject) => 87 | s3Client.createPresignedPost(params, (error, result) => 88 | error ? reject(error) : resolve(result) 89 | ) 90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /functions/index/index.js: -------------------------------------------------------------------------------- 1 | exports.handler = async () => ({ 2 | statusCode: 200, 3 | headers: { "Access-Control-Allow-Origin": "*" }, 4 | body: JSON.stringify({ 5 | endpoints: [ 6 | { 7 | title: "Sign Up", 8 | method: "POST", 9 | url: "/Prod/singup", 10 | statusCode: 201, 11 | requestBody: { 12 | username: "string", 13 | password: "string" 14 | } 15 | }, 16 | { 17 | title: "Sign In", 18 | method: "POST", 19 | url: "/Prod/singin", 20 | statusCode: 201, 21 | responseBody: { 22 | username: "string", 23 | password: "string" 24 | }, 25 | responseBody: { 26 | token: "string" 27 | } 28 | }, 29 | { 30 | title: "Create Image", 31 | method: "POST", 32 | url: "/Prod/images", 33 | statusCode: 201, 34 | responseBody: { 35 | formConfig: { 36 | uploadUrl: "string", 37 | formFields: { 38 | Key: "string", 39 | bucket: "string", 40 | Policy: "string", 41 | "X-Amz-Algorithm": "string", 42 | "X-Amz-Credential": "string", 43 | "X-Amz-Date": "string", 44 | "X-Amz-Security-Token": "string", 45 | "X-Amz-Signature": "string" 46 | } 47 | } 48 | } 49 | }, 50 | { 51 | title: "List Images", 52 | method: "GET", 53 | url: "/Prod/images", 54 | responseBody: { 55 | images: [ 56 | { 57 | id: "string", 58 | fileUrl: "string", 59 | tags: ["string"] 60 | } 61 | ] 62 | } 63 | }, 64 | { 65 | title: "Delete Image", 66 | method: "DELETE", 67 | url: "/Prod/images/{imageId}" 68 | }, 69 | { 70 | title: "List Tags", 71 | method: "GET", 72 | url: "/Prod/tags", 73 | responseBody: { 74 | tags: ["string"] 75 | } 76 | }, 77 | { 78 | title: "List Images by Tag", 79 | method: "GET", 80 | url: "/Prod/tags/{tagName}/images", 81 | statusCode: 308 82 | }, 83 | ] 84 | }) 85 | }); -------------------------------------------------------------------------------- /functions/tags/aws-es-lambda.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const AWS = require("aws-sdk"); 3 | 4 | // Basic AWS Elasticsearch util functions copied from an AWS example 5 | 6 | const httpClient = new AWS.HttpClient(); 7 | const credentials = new AWS.EnvironmentCredentials("AWS"); 8 | const endpoint = new AWS.Endpoint(process.env.ELASTICSEARCH_URL); 9 | 10 | const sendRequest = ({ httpMethod, requestPath, payload }) => 11 | new Promise((resolve, reject) => { 12 | const request = new AWS.HttpRequest(endpoint, process.env.REGION); 13 | 14 | request.method = httpMethod; 15 | request.path = path.join(request.path, requestPath); 16 | request.body = payload; 17 | request.headers["Content-Type"] = "application/json"; 18 | request.headers.Host = process.env.ELASTICSEARCH_URL; 19 | 20 | const signer = new AWS.Signers.V4(request, "es"); 21 | signer.addAuthorization(credentials, new Date()); 22 | 23 | httpClient.handleRequest( 24 | request, 25 | null, 26 | response => { 27 | const { statusCode, statusMessage, headers } = response; 28 | let body = ""; 29 | response.on("data", chunk => (body += chunk)); 30 | response.on("end", () => { 31 | const data = { statusCode, statusMessage, headers }; 32 | if (body) data.body = JSON.parse(body); 33 | resolve(data); 34 | }); 35 | }, 36 | reject 37 | ); 38 | }); 39 | 40 | exports.query = (index, data) => 41 | sendRequest({ 42 | httpMethod: "POST", 43 | requestPath: index + "/_search", 44 | payload: JSON.stringify(data) 45 | }); 46 | 47 | exports.post = (index, data) => 48 | sendRequest({ 49 | httpMethod: "POST", 50 | requestPath: index, 51 | payload: JSON.stringify(data) 52 | }); 53 | -------------------------------------------------------------------------------- /functions/tags/image-recognition.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk") 2 | const Clarifai = require("clarifai"); 3 | 4 | exports.predict = async imageUrl => { 5 | const clarifaiApp = await initClarifaiApp(); 6 | const model = await clarifaiApp.models.initModel({ 7 | version: "aa7f35c01e0642fda5cf400f543e7c40", 8 | id: Clarifai.GENERAL_MODEL 9 | }); 10 | const clarifaiResult = await model.predict(imageUrl); 11 | const tags = clarifaiResult.outputs[0].data.concepts 12 | .filter(concept => concept.value > 0.9) 13 | .map(concept => concept.name); 14 | return tags; 15 | }; 16 | 17 | let clarifaiApp = null; 18 | const ssm = new AWS.SSM(); 19 | const initClarifaiApp = async () => { 20 | // The Lambda will only init the API client after a cold-start 21 | if (!clarifaiApp) { 22 | // Clarifai API needs an API key which is stored in SSM Parameter Store 23 | // The Parameter Store key is inside an environment variable 24 | const result = await ssm.getParameter({ 25 | Name: process.env.PARAMETER_STORE_CLARIFAI_API_KEY, 26 | WithDecryption: true 27 | }).promise(); 28 | clarifaiApp = new Clarifai.App({ apiKey: result.Parameter.Value }); 29 | } 30 | return clarifaiApp; 31 | } -------------------------------------------------------------------------------- /functions/tags/index.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | const imageRecognition = require("./image-recognition"); 3 | const elasticsearch = require("aws-es-lambda"); 4 | 5 | exports.handler = async event => { 6 | if (event.path === "/tags") return listTags(event); 7 | if (!!event.Records) return extractTags(event); 8 | return listImages(event); 9 | }; 10 | 11 | // Called with S3 event 12 | const extractTags = async event => { 13 | const record = event.Records[0]; 14 | const bucketName = record.s3.bucket.name; 15 | const fileName = record.s3.object.key; 16 | const tags = await imageRecognition.predict(`https://${bucketName}.s3.amazonaws.com/${fileName}`); 17 | await elasticsearch.post("images/image", { fileName, tags }); 18 | }; 19 | 20 | // Called with API-GW event 21 | const listTags = async event => { 22 | const userName = extractUserName(event); 23 | // Bad Elasticsearch query to get all tags of all images of current user, 24 | // please improve :D 25 | const params = { 26 | query: { wildcard: { fileName: "*" + userName } }, 27 | aggs: { tags_count: { terms: { field: "tags" } } } 28 | }; 29 | const result = await elasticsearch.query("images", params); 30 | const tags = result.body.aggregations.tags_count.buckets.map(tag => tag.key) 31 | return createResponse({ tags }); 32 | }; 33 | 34 | // Called with API-GW event 35 | const listImages = async event => ({ 36 | statusCode: 308, // Permanent Redirect 37 | headers: { 38 | "Access-Control-Allow-Origin": "*", 39 | Location: "/Prod/images?tags=" + event.pathParameters.tagId 40 | } 41 | }); 42 | 43 | // ============================== HELPERS ============================== 44 | 45 | const createResponse = (data = { message: "OK" }, statusCode = 200) => ({ 46 | statusCode, 47 | body: JSON.stringify(data), 48 | headers: { "Access-Control-Allow-Origin": "*" } 49 | }); 50 | 51 | const extractUserName = event => event.requestContext.authorizer.claims["cognito:username"]; 52 | -------------------------------------------------------------------------------- /functions/tags/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tags", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "asap": { 8 | "version": "2.0.6", 9 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", 10 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" 11 | }, 12 | "aws4": { 13 | "version": "1.8.0", 14 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 15 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" 16 | }, 17 | "axios": { 18 | "version": "0.24.0", 19 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", 20 | "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", 21 | "requires": { 22 | "follow-redirects": "^1.14.4" 23 | } 24 | }, 25 | "clarifai": { 26 | "version": "2.9.1", 27 | "resolved": "https://registry.npmjs.org/clarifai/-/clarifai-2.9.1.tgz", 28 | "integrity": "sha512-xUxl0bNhBTRn93BBjzYG3nQ/BRZI5VcAZOn1hsukTEFgE31grtegztMT26AbFdmWkCJin1dM6TaC4APSHYs/Ug==", 29 | "requires": { 30 | "axios": ">=0.11.1 <2", 31 | "promise": "^7.1.1", 32 | "valid-url": "^1.0.9" 33 | } 34 | }, 35 | "follow-redirects": { 36 | "version": "1.14.7", 37 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", 38 | "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==" 39 | }, 40 | "promise": { 41 | "version": "7.3.1", 42 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", 43 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", 44 | "requires": { 45 | "asap": "~2.0.3" 46 | } 47 | }, 48 | "valid-url": { 49 | "version": "1.0.9", 50 | "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", 51 | "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /functions/tags/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tags", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Kay Plößer", 10 | "license": "ISC", 11 | "dependencies": { 12 | "aws4": "^1.8.0", 13 | "clarifai": "^2.9.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-api", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ajv": { 8 | "version": "6.10.0", 9 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", 10 | "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", 11 | "requires": { 12 | "fast-deep-equal": "^2.0.1", 13 | "fast-json-stable-stringify": "^2.0.0", 14 | "json-schema-traverse": "^0.4.1", 15 | "uri-js": "^4.2.2" 16 | } 17 | }, 18 | "asn1": { 19 | "version": "0.2.4", 20 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 21 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 22 | "requires": { 23 | "safer-buffer": "~2.1.0" 24 | } 25 | }, 26 | "assert-plus": { 27 | "version": "1.0.0", 28 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 29 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 30 | }, 31 | "asynckit": { 32 | "version": "0.4.0", 33 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 34 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 35 | }, 36 | "aws-sign2": { 37 | "version": "0.7.0", 38 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 39 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 40 | }, 41 | "aws4": { 42 | "version": "1.8.0", 43 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 44 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" 45 | }, 46 | "bcrypt-pbkdf": { 47 | "version": "1.0.2", 48 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 49 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 50 | "requires": { 51 | "tweetnacl": "^0.14.3" 52 | } 53 | }, 54 | "caseless": { 55 | "version": "0.12.0", 56 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 57 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 58 | }, 59 | "combined-stream": { 60 | "version": "1.0.8", 61 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 62 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 63 | "requires": { 64 | "delayed-stream": "~1.0.0" 65 | } 66 | }, 67 | "core-util-is": { 68 | "version": "1.0.2", 69 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 70 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 71 | }, 72 | "dashdash": { 73 | "version": "1.14.1", 74 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 75 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 76 | "requires": { 77 | "assert-plus": "^1.0.0" 78 | } 79 | }, 80 | "delayed-stream": { 81 | "version": "1.0.0", 82 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 83 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 84 | }, 85 | "ecc-jsbn": { 86 | "version": "0.1.2", 87 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 88 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 89 | "requires": { 90 | "jsbn": "~0.1.0", 91 | "safer-buffer": "^2.1.0" 92 | } 93 | }, 94 | "extend": { 95 | "version": "3.0.2", 96 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 97 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 98 | }, 99 | "extsprintf": { 100 | "version": "1.3.0", 101 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 102 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 103 | }, 104 | "fast-deep-equal": { 105 | "version": "2.0.1", 106 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 107 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" 108 | }, 109 | "fast-json-stable-stringify": { 110 | "version": "2.0.0", 111 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 112 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 113 | }, 114 | "forever-agent": { 115 | "version": "0.6.1", 116 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 117 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 118 | }, 119 | "form-data": { 120 | "version": "2.3.3", 121 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 122 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 123 | "requires": { 124 | "asynckit": "^0.4.0", 125 | "combined-stream": "^1.0.6", 126 | "mime-types": "^2.1.12" 127 | } 128 | }, 129 | "getpass": { 130 | "version": "0.1.7", 131 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 132 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 133 | "requires": { 134 | "assert-plus": "^1.0.0" 135 | } 136 | }, 137 | "har-schema": { 138 | "version": "2.0.0", 139 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 140 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 141 | }, 142 | "har-validator": { 143 | "version": "5.1.3", 144 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 145 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 146 | "requires": { 147 | "ajv": "^6.5.5", 148 | "har-schema": "^2.0.0" 149 | } 150 | }, 151 | "http-signature": { 152 | "version": "1.2.0", 153 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 154 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 155 | "requires": { 156 | "assert-plus": "^1.0.0", 157 | "jsprim": "^1.2.2", 158 | "sshpk": "^1.7.0" 159 | } 160 | }, 161 | "is-typedarray": { 162 | "version": "1.0.0", 163 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 164 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 165 | }, 166 | "isstream": { 167 | "version": "0.1.2", 168 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 169 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 170 | }, 171 | "jsbn": { 172 | "version": "0.1.1", 173 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 174 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 175 | }, 176 | "json-schema": { 177 | "version": "0.2.3", 178 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 179 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 180 | }, 181 | "json-schema-traverse": { 182 | "version": "0.4.1", 183 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 184 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 185 | }, 186 | "json-stringify-safe": { 187 | "version": "5.0.1", 188 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 189 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 190 | }, 191 | "jsprim": { 192 | "version": "1.4.1", 193 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 194 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 195 | "requires": { 196 | "assert-plus": "1.0.0", 197 | "extsprintf": "1.3.0", 198 | "json-schema": "0.2.3", 199 | "verror": "1.10.0" 200 | } 201 | }, 202 | "mime-db": { 203 | "version": "1.40.0", 204 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 205 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 206 | }, 207 | "mime-types": { 208 | "version": "2.1.24", 209 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 210 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 211 | "requires": { 212 | "mime-db": "1.40.0" 213 | } 214 | }, 215 | "oauth-sign": { 216 | "version": "0.9.0", 217 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 218 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 219 | }, 220 | "performance-now": { 221 | "version": "2.1.0", 222 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 223 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 224 | }, 225 | "psl": { 226 | "version": "1.1.33", 227 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.33.tgz", 228 | "integrity": "sha512-LTDP2uSrsc7XCb5lO7A8BI1qYxRe/8EqlRvMeEl6rsnYAqDOl8xHR+8lSAIVfrNaSAlTPTNOCgNjWcoUL3AZsw==" 229 | }, 230 | "punycode": { 231 | "version": "2.1.1", 232 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 233 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 234 | }, 235 | "qs": { 236 | "version": "6.5.2", 237 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 238 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 239 | }, 240 | "request": { 241 | "version": "2.88.0", 242 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 243 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 244 | "requires": { 245 | "aws-sign2": "~0.7.0", 246 | "aws4": "^1.8.0", 247 | "caseless": "~0.12.0", 248 | "combined-stream": "~1.0.6", 249 | "extend": "~3.0.2", 250 | "forever-agent": "~0.6.1", 251 | "form-data": "~2.3.2", 252 | "har-validator": "~5.1.0", 253 | "http-signature": "~1.2.0", 254 | "is-typedarray": "~1.0.0", 255 | "isstream": "~0.1.2", 256 | "json-stringify-safe": "~5.0.1", 257 | "mime-types": "~2.1.19", 258 | "oauth-sign": "~0.9.0", 259 | "performance-now": "^2.1.0", 260 | "qs": "~6.5.2", 261 | "safe-buffer": "^5.1.2", 262 | "tough-cookie": "~2.4.3", 263 | "tunnel-agent": "^0.6.0", 264 | "uuid": "^3.3.2" 265 | } 266 | }, 267 | "safe-buffer": { 268 | "version": "5.1.2", 269 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 270 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 271 | }, 272 | "safer-buffer": { 273 | "version": "2.1.2", 274 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 275 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 276 | }, 277 | "sshpk": { 278 | "version": "1.16.1", 279 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 280 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 281 | "requires": { 282 | "asn1": "~0.2.3", 283 | "assert-plus": "^1.0.0", 284 | "bcrypt-pbkdf": "^1.0.0", 285 | "dashdash": "^1.12.0", 286 | "ecc-jsbn": "~0.1.1", 287 | "getpass": "^0.1.1", 288 | "jsbn": "~0.1.0", 289 | "safer-buffer": "^2.0.2", 290 | "tweetnacl": "~0.14.0" 291 | } 292 | }, 293 | "tough-cookie": { 294 | "version": "2.4.3", 295 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 296 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 297 | "requires": { 298 | "psl": "^1.1.24", 299 | "punycode": "^1.4.1" 300 | }, 301 | "dependencies": { 302 | "punycode": { 303 | "version": "1.4.1", 304 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 305 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 306 | } 307 | } 308 | }, 309 | "tunnel-agent": { 310 | "version": "0.6.0", 311 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 312 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 313 | "requires": { 314 | "safe-buffer": "^5.0.1" 315 | } 316 | }, 317 | "tweetnacl": { 318 | "version": "0.14.5", 319 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 320 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 321 | }, 322 | "uri-js": { 323 | "version": "4.2.2", 324 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 325 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 326 | "requires": { 327 | "punycode": "^2.1.0" 328 | } 329 | }, 330 | "uuid": { 331 | "version": "3.3.2", 332 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 333 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 334 | }, 335 | "verror": { 336 | "version": "1.10.0", 337 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 338 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 339 | "requires": { 340 | "assert-plus": "^1.0.0", 341 | "core-util-is": "1.0.2", 342 | "extsprintf": "^1.2.0" 343 | } 344 | } 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-api", 3 | "version": "0.0.1", 4 | "description": "", 5 | "scripts": { 6 | "createDeployBucket": "aws s3 mb ", 7 | "package": "sam package --template-file template.yaml --s3-bucket serverless-api-123456789 --output-template-file packaged.yaml", 8 | "deploy": "sam deploy --template-file packaged.yaml --stack-name serverless-api --capabilities CAPABILITY_IAM", 9 | "getApiUrl": "aws cloudformation describe-stacks --stack-name serverless-api --query 'Stacks[0].Outputs[?OutputKey==`ApiUrl`].OutputValue' --output text >> api-url", 10 | "setup": "npm run package && npm run deploy || npm run getApiUrl", 11 | "store-api-key": "aws ssm put-parameter --name "/serverless-api/CLARIFAI_API_KEY" --type "SecureString" --value ", 12 | "test": "node ./test" 13 | }, 14 | "author": "Kay Plößer", 15 | "license": "GPL", 16 | "dependencies": { 17 | "request": "^2.88.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: "AWS::Serverless-2016-10-31" 3 | Description: "A example REST API build with serverless technology" 4 | 5 | Globals: 6 | 7 | # Configuration defaults for all Lambda functions 8 | Function: 9 | Runtime: nodejs8.10 10 | Handler: index.handler 11 | Timeout: 30 12 | Tags: 13 | Application: Serverless API 14 | 15 | Resources: 16 | 17 | # Explicit API resource needed to configure an authorizer 18 | ServerlessApi: 19 | Type: AWS::Serverless::Api 20 | Properties: 21 | StageName: Prod 22 | Cors: "'*'" 23 | Auth: 24 | DefaultAuthorizer: CognitoAuthorizer 25 | Authorizers: 26 | CognitoAuthorizer: 27 | UserPoolArn: !GetAtt UserPool.Arn 28 | GatewayResponses: 29 | # API-Gateway responses are missing CORS headers 30 | # They need to be defined in Lambda functions or here 31 | UNAUTHORIZED: 32 | StatusCode: 401 33 | ResponseParameters: 34 | Headers: 35 | Access-Control-Expose-Headers: "'WWW-Authenticate'" 36 | Access-Control-Allow-Origin: "'*'" 37 | WWW-Authenticate: >- 38 | 'Bearer realm="admin"' 39 | 40 | # A simple function that just returns URLs to other endpoints 41 | IndexFunction: 42 | Type: AWS::Serverless::Function 43 | Properties: 44 | CodeUri: functions/index/ 45 | Events: 46 | Home: 47 | Type: Api 48 | Properties: 49 | RestApiId: !Ref ServerlessApi 50 | Path: / 51 | Method: GET 52 | # The following lines disable auth for this event 53 | Auth: 54 | Authorizer: NONE 55 | 56 | # Lambda function that handles signin and signup with the help of Cognito 57 | AuthFunction: 58 | Type: AWS::Serverless::Function 59 | Properties: 60 | CodeUri: functions/auth/ 61 | # The configuration needed for Cognito usage via AWS SDK 62 | Environment: 63 | Variables: 64 | USER_POOL_ID: !Ref UserPool 65 | USER_POOL_CLIENT_ID: !Ref UserPoolClient 66 | Events: 67 | Signup: 68 | Type: Api 69 | Properties: 70 | RestApiId: !Ref ServerlessApi 71 | Path: /signup 72 | Method: POST 73 | # Endpoint available for unauthorized users 74 | Auth: 75 | Authorizer: NONE 76 | Signin: 77 | Type: Api 78 | Properties: 79 | RestApiId: !Ref ServerlessApi 80 | Path: /signin 81 | Method: POST 82 | # Endpoint available for unauthorized users 83 | Auth: 84 | Authorizer: NONE 85 | 86 | # Lambda function that prevents the emission of confirmation emails 87 | PreSignupFunction: 88 | Type: AWS::Serverless::Function 89 | Properties: 90 | InlineCode: | 91 | exports.handler = async event => { 92 | event.response = { autoConfirmUser: true }; 93 | return event; 94 | }; 95 | 96 | # The Cognito user pool that stores our user accounts 97 | UserPool: 98 | Type: AWS::Cognito::UserPool 99 | Properties: 100 | UserPoolName: ApiUserPool 101 | LambdaConfig: 102 | PreSignUp: !GetAtt PreSignupFunction.Arn 103 | Policies: 104 | PasswordPolicy: 105 | MinimumLength: 6 106 | 107 | # The Cognito user pool client that is used to connect to our user pool 108 | UserPoolClient: 109 | Type: AWS::Cognito::UserPoolClient 110 | Properties: 111 | UserPoolId: !Ref UserPool 112 | ClientName: ApiUserPoolClient 113 | GenerateSecret: no 114 | 115 | # Premission for Cognito to execute our pre-signup Lambda function 116 | LambdaCognitoUserPoolExecutionPermission: 117 | Type: AWS::Lambda::Permission 118 | Properties: 119 | Action: lambda:InvokeFunction 120 | FunctionName: !GetAtt PreSignupFunction.Arn 121 | Principal: cognito-idp.amazonaws.com 122 | SourceArn: !Sub 'arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool}' 123 | 124 | # Elasticsearch is used to store the extracted image tags 125 | ElasticsearchDomain: 126 | Type: AWS::Elasticsearch::Domain 127 | Properties: 128 | ElasticsearchVersion: "2.3" 129 | ElasticsearchClusterConfig: 130 | InstanceCount: 1 131 | InstanceType: t2.micro.elasticsearch 132 | EBSOptions: 133 | EBSEnabled: true 134 | Iops: 0 135 | VolumeSize: 10 136 | VolumeType: gp2 137 | 138 | # S3 bucket is used to store the images 139 | ImageBucket: 140 | Type: AWS::S3::Bucket 141 | Properties: 142 | # Configured to be public accessible 143 | # So users can directly upload from the browser and 144 | # Clarifai can read directly via HTTP 145 | AccessControl: PublicRead 146 | CorsConfiguration: 147 | CorsRules: 148 | - AllowedHeaders: 149 | - "*" 150 | AllowedMethods: 151 | - HEAD 152 | - GET 153 | - PUT 154 | - POST 155 | AllowedOrigins: 156 | - "*" 157 | 158 | # Public read policy for S3 159 | ImageBucketPublicReadPolicy: 160 | Type: AWS::S3::BucketPolicy 161 | Properties: 162 | Bucket: !Ref ImageBucket 163 | PolicyDocument: 164 | Statement: 165 | - Action: s3:GetObject 166 | Effect: Allow 167 | Principal: "*" 168 | Resource: !Join ["", ["arn:aws:s3:::", !Ref "ImageBucket", "/*" ]] 169 | 170 | # Lambda function to handle image URL listing, deletion and the creation 171 | # of pre-signed POST upload URLs for direct browser uploads 172 | ImageFunction: 173 | Type: AWS::Serverless::Function 174 | Properties: 175 | CodeUri: functions/images/ 176 | Policies: 177 | - AmazonS3FullAccess # AWS managed policy 178 | - ElasticsearchHttpPostPolicy: # AWS SAM policity template 179 | DomainName: !Ref ElasticsearchDomain 180 | # Config variables for Elasticsearch and S3 access 181 | Environment: 182 | Variables: 183 | IMAGE_BUCKET_NAME: !Ref ImageBucket 184 | ELASTICSEARCH_URL: !GetAtt ElasticsearchDomain.DomainEndpoint 185 | REGION: !Ref AWS::Region 186 | Events: 187 | # API-Gateway event for a JSON response of all images 188 | ListImages: 189 | Type: Api 190 | Properties: 191 | RestApiId: !Ref ServerlessApi 192 | Path: /images 193 | Method: GET 194 | # API-Gateway event to delete one image from Elasticsearch 195 | DeleteImage: 196 | Type: Api 197 | Properties: 198 | RestApiId: !Ref ServerlessApi 199 | Path: /images/{imageId} 200 | Method: DELETE 201 | # API-Gateway event to create a pre-signed POST URL so the users 202 | # can directly upload to the S3 bucket 203 | CreateImage: 204 | Type: Api 205 | Properties: 206 | RestApiId: !Ref ServerlessApi 207 | Path: /images 208 | Method: POST 209 | 210 | # Lambda function to handle image tagging via the Clarifai API 211 | TagsFunction: 212 | Type: AWS::Serverless::Function 213 | Properties: 214 | CodeUri: functions/tags/ 215 | Environment: 216 | Variables: 217 | # The Parameter Store API is used to retrieve the encrypted Clarifai API key 218 | # The key here is the Parameter Store key and not the API key 219 | PARAMETER_STORE_CLARIFAI_API_KEY: /serverless-api/CLARIFAI_API_KEY_ENC 220 | ELASTICSEARCH_URL: !GetAtt ElasticsearchDomain.DomainEndpoint 221 | REGION: !Ref AWS::Region 222 | Policies: 223 | - AmazonS3ReadOnlyAccess # Managed policy 224 | - ElasticsearchHttpPostPolicy: # SAM policy template 225 | DomainName: !Ref ElasticsearchDomain 226 | - Statement: # Inline policy document 227 | - Action: [ 'ssm:GetParameter' ] 228 | Effect: Allow 229 | Resource: '*' 230 | Events: 231 | # API-Gateway event to list all tags stored in Elasticsearch 232 | ListTags: 233 | Type: Api 234 | Properties: 235 | RestApiId: !Ref ServerlessApi 236 | Path: /tags 237 | Method: GET 238 | # API-Gateway event to list all images with one tag 239 | # Redirects permanently to /images?tags={tagId} 240 | ListImages: 241 | Type: Api 242 | Properties: 243 | RestApiId: !Ref ServerlessApi 244 | Path: /tags/{tagId}/images 245 | Method: GET 246 | ExtractTags: 247 | Type: S3 248 | Properties: 249 | Bucket: !Ref ImageBucket 250 | Events: s3:ObjectCreated:* 251 | 252 | Outputs: 253 | 254 | # When the package.json scripts are used, this output will be stored 255 | # inside a api-url file 256 | ApiUrl: 257 | Description: The target URL of the created API 258 | Value: !Sub "https://${ServerlessApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" 259 | Export: 260 | Name: ApiUrl -------------------------------------------------------------------------------- /test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kay-is/serverless-api-example/7bab3a64759bed099a096805885b7ce9009a0c7f/test.jpg -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { promisify } = require("util"); 3 | const request = promisify(require("request")); 4 | 5 | // This file tests some basic image upload and tagging as CLI 6 | // it gets the API URL from an api-url file 7 | 8 | const baseUrl = fs.readFileSync(__dirname + "/api-url", 'utf8').trim(); 9 | 10 | console.log("Using API base URL: " + baseUrl); 11 | 12 | const run = async () => { 13 | let result; 14 | 15 | console.log("Sign up as user 'test'"); 16 | result = await sendRequest({ 17 | method: "POST", 18 | url: baseUrl + "signup", 19 | body: { 20 | username: "test", 21 | password: "Abc123!" 22 | } 23 | }); 24 | console.log(result); 25 | 26 | console.log("Sign in as user 'test'"); 27 | result = await sendRequest({ 28 | method: "POST", 29 | url: baseUrl + "signin", 30 | body: { 31 | username: "test", 32 | password: "Abc123!" 33 | } 34 | }); 35 | console.log(result); 36 | const {token} = result; 37 | 38 | console.log("Get signed S3 image URL"); 39 | result = await sendRequest({ 40 | method: "POST", 41 | url: baseUrl + "images", 42 | headers: { 43 | Authorization: "Bearer " + token 44 | } 45 | }); 46 | console.log(result); 47 | 48 | const imagePath = __dirname + '/test.jpg' 49 | const knownLength = fs.statSync(imagePath).size; 50 | 51 | console.log("Upload image to S3"); 52 | result = await sendRequest({ 53 | method: "POST", 54 | url: result.formConfig.uploadUrl, 55 | formData: { 56 | ...result.formConfig.formFields, 57 | file: { // S3 expects file in the "file" field 58 | value: fs.createReadStream(imagePath), 59 | options: { 60 | filename: result.formConfig.formFields.Key, 61 | contentType: 'image/jpeg', 62 | knownLength 63 | } 64 | } 65 | } 66 | }); 67 | console.log(result); 68 | 69 | await sleep(1000); 70 | 71 | console.log("Get all tags"); 72 | result = await sendRequest({ 73 | method: "GET", 74 | url: baseUrl + "tags", 75 | headers: { 76 | Authorization: "Bearer " + token 77 | } 78 | }); 79 | console.log(result); 80 | 81 | const tag = result.tags[0]; 82 | 83 | console.log("Get images by tag: " + tag); 84 | result = await sendRequest({ 85 | method: "GET", 86 | url: baseUrl + `tags/${tag}/images`, 87 | headers: { 88 | Authorization: "Bearer " + token 89 | } 90 | }); 91 | console.log(JSON.stringify(result, null, 2)); 92 | }; 93 | 94 | const sendRequest = async options => { 95 | if (options.body) options.body = JSON.stringify(options.body); 96 | const result = await request(options); 97 | try { 98 | return JSON.parse(result.body); 99 | } catch(e) { 100 | return result.body 101 | } 102 | }; 103 | 104 | const sleep = ms => new Promise(r => setTimeout(r, ms)); 105 | 106 | run(); --------------------------------------------------------------------------------