├── client ├── .env ├── postcss.config.js ├── babel.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── assets │ │ └── main.scss │ ├── postcss.config.js │ ├── router │ │ └── index.js │ ├── main.js │ ├── store │ │ └── index.js │ ├── App.vue │ └── views │ │ └── Dashboard.vue ├── package.json └── README.md ├── assets ├── pat.png └── client.png ├── templates ├── app │ ├── linkId │ │ ├── delete-400-response.json │ │ ├── put-response-200.json │ │ ├── delete-request.json │ │ └── put-request.json │ ├── post-response-400.json │ ├── post-response-200.json │ ├── get-response.json │ ├── get-request.json │ └── post-request.json └── linkiId │ ├── get-request.json │ └── get-response.json ├── CODE_OF_CONDUCT.md ├── .gitignore ├── LICENSE ├── CONTRIBUTING.md ├── README.md ├── api.yaml └── template.yaml /client/.env: -------------------------------------------------------------------------------- 1 | VUE_APP_NAME= 2 | VUE_APP_API_ROOT= 3 | VUE_APP_AUTH_DOMAIN= 4 | VUE_APP_CLIENT_ID= -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /assets/pat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-api-gateway-url-shortener/HEAD/assets/pat.png -------------------------------------------------------------------------------- /assets/client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-api-gateway-url-shortener/HEAD/assets/client.png -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-api-gateway-url-shortener/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/assets/main.scss: -------------------------------------------------------------------------------- 1 | @import "~bulma/bulma"; 2 | 3 | .text-clip{ 4 | overflow:hidden; 5 | text-overflow: ellipsis; 6 | white-space: nowrap; 7 | } -------------------------------------------------------------------------------- /templates/app/linkId/delete-400-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "message": "Either you do not have permission to do this operation or a parameter is missing" 4 | } 5 | } -------------------------------------------------------------------------------- /templates/linkiId/get-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "Key": { 3 | "id": { 4 | "S": "$input.params().path.linkId" 5 | } 6 | }, 7 | "TableName": "SlipLink-LinkTable-A2SD9C321JCE" 8 | } -------------------------------------------------------------------------------- /templates/linkiId/get-response.json: -------------------------------------------------------------------------------- 1 | #set($inputRoot = $input.path('$')) 2 | #if ($inputRoot.toString().contains("Item")) 3 | #set($context.responseOverride.header.Location = $inputRoot.Item.url.S) 4 | #end -------------------------------------------------------------------------------- /templates/app/post-response-400.json: -------------------------------------------------------------------------------- 1 | #set($inputRoot = $input.path('$')) 2 | #if($inputRoot.toString().contains("ConditionalCheckFailedException")) 3 | #set($context.responseOverride.status = 200) 4 | {"error": true,"message": "URL link already exists"} 5 | #end -------------------------------------------------------------------------------- /templates/app/post-response-200.json: -------------------------------------------------------------------------------- 1 | #set($inputRoot = $input.path('$')) 2 | { 3 | "id": "$inputRoot.Attributes.id.S", 4 | "url": "$inputRoot.Attributes.url.S", 5 | "timestamp": "$inputRoot.Attributes.timestamp.S", 6 | "owner": "$inputRoot.Attributes.owner.S" 7 | } -------------------------------------------------------------------------------- /templates/app/linkId/put-response-200.json: -------------------------------------------------------------------------------- 1 | #set($inputRoot = $input.path('$')) 2 | { 3 | "id": "$inputRoot.Attributes.id.S", 4 | "url": "$inputRoot.Attributes.url.S", 5 | "timestamp": "$inputRoot.Attributes.timestamp.S", 6 | "owner": "$inputRoot.Attributes.owner.S" 7 | } -------------------------------------------------------------------------------- /templates/app/get-response.json: -------------------------------------------------------------------------------- 1 | #set($inputRoot = $input.path('$')) 2 | [ 3 | #foreach($elem in $inputRoot.Items) { 4 | "id": "$elem.id.S", 5 | "url": "$elem.url.S", 6 | "timestamp": "$elem.timestamp.S", 7 | "owner": "$elem.owner.S" 8 | }#if ($foreach.hasNext),#end 9 | #end 10 | ] -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | */**/node_modules 3 | */**/dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | *.toml -------------------------------------------------------------------------------- /templates/app/get-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "TableName": "SlipLink-LinkTable-A2SD9C321JCE", 3 | "IndexName": "OwnerIndex", 4 | "KeyConditionExpression": "#n_owner = :v_owner", 5 | "ExpressionAttributeValues": { 6 | ":v_owner": { 7 | "S": "$context.authorizer.claims.email" 8 | } 9 | }, 10 | "ExpressionAttributeNames": { 11 | "#n_owner": "owner" 12 | } 13 | } -------------------------------------------------------------------------------- /templates/app/linkId/delete-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "Key": { 3 | "id": { 4 | "S": "$input.params().path.linkId" 5 | } 6 | }, 7 | "TableName": "SlipLink-LinkTable-A2SD9C321JCE", 8 | "ConditionExpression": "#owner = :owner", 9 | "ExpressionAttributeValues": { 10 | ":owner": { 11 | "S": "$context.authorizer.claims.email" 12 | } 13 | }, 14 | "ExpressionAttributeNames": { 15 | "#owner": "owner" 16 | } 17 | } -------------------------------------------------------------------------------- /templates/app/linkId/put-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "TableName": "Shortie", 3 | "Key": { 4 | "id": { 5 | "S": $input.json('$.id') + "_" + $context.authorizer.claims.email 6 | } 7 | }, 8 | "ExpressionAttributeNames": { 9 | "#u": "url", 10 | "#owner": "owner", 11 | "#id":"id" 12 | }, 13 | "ExpressionAttributeValues": { 14 | ":u": { 15 | "S": $input.json('$.url') 16 | }, 17 | ":owner": { 18 | "S": "$context.authorizer.claims.email" 19 | }, 20 | ":linkId": { 21 | "S": "$input.params().path.linkId" 22 | } 23 | }, 24 | "UpdateExpression": "SET #u = :u", 25 | "ReturnValues": "ALL_NEW", 26 | "ConditionExpression": "#owner = :owner AND #id = :linkId" 27 | } 28 | -------------------------------------------------------------------------------- /templates/app/post-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "TableName": "SlipLink-LinkTable-A2SD9C321JCE", 3 | "ConditionExpression": "attribute_not_exists(id)", 4 | "Key": { 5 | "id": { 6 | "S": $input.json('$.id') + "_" + $context.authorizer.claims.email 7 | } 8 | }, 9 | "ExpressionAttributeNames": { 10 | "#u": "url", 11 | "#o": "owner", 12 | "#ts": "timestamp" 13 | }, 14 | "ExpressionAttributeValues": { 15 | ":u": { 16 | "S": $input.json('$.url') 17 | }, 18 | ":o": { 19 | "S": "$context.authorizer.claims.email" 20 | }, 21 | ":ts": { 22 | "S": "$context.requestTime" 23 | } 24 | }, 25 | "UpdateExpression": "SET #u = :u, #o = :o, #ts = :ts", 26 | "ReturnValues": "ALL_NEW" 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /client/src/postcss.config.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | // software and associated documentation files (the "Software"), to deal in the Software 6 | // without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so. 9 | // 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | module.exports = { 18 | plugins: { 19 | autoprefixer: {} 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^1.8.2", 12 | "bulma": "^1.0.3", 13 | "core-js": "^3.41.0", 14 | "moment": "^2.30.1", 15 | "vue": "^3.5.13", 16 | "vue-router": "^4.5.0", 17 | "vuex": "^4.1.0" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "^5.0.8", 21 | "@vue/cli-plugin-eslint": "^5.0.8", 22 | "@vue/cli-plugin-router": "^5.0.8", 23 | "@vue/cli-plugin-vuex": "^5.0.8", 24 | "@vue/cli-service": "^5.0.8", 25 | "babel-eslint": "^10.1.0", 26 | "eslint": "9.22.0", 27 | "eslint-plugin-vue": "^10.0.0", 28 | "sass": "^1.85.1", 29 | "sass-loader": "^16.0.5", 30 | "vue-template-compiler": "^2.7.16" 31 | }, 32 | "eslintConfig": { 33 | "root": true, 34 | "env": { 35 | "node": true 36 | }, 37 | "extends": [ 38 | "plugin:vue/essential", 39 | "eslint:recommended" 40 | ], 41 | "rules": {}, 42 | "parserOptions": { 43 | "parser": "babel-eslint" 44 | } 45 | }, 46 | "browserslist": [ 47 | "> 1%", 48 | "last 2 versions" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /client/src/router/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | // software and associated documentation files (the "Software"), to deal in the Software 6 | // without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so. 9 | // 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | import Vue from 'vue' 18 | import VueRouter from 'vue-router' 19 | import Dashboard from '../views/Dashboard.vue' 20 | 21 | Vue.use(VueRouter) 22 | 23 | const routes = [ 24 | { 25 | path: '/', 26 | name: 'dashboard', 27 | component: Dashboard 28 | } 29 | ] 30 | 31 | const router = new VueRouter({ 32 | mode: 'history', 33 | base: process.env.BASE_URL, 34 | routes 35 | }) 36 | 37 | export default router 38 | -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | // software and associated documentation files (the "Software"), to deal in the Software 6 | // without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so. 9 | // 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | import Vue from 'vue' 18 | import App from './App.vue' 19 | import router from './router' 20 | import store from './store' 21 | import moment from 'moment' 22 | require('./assets/main.scss') 23 | 24 | Vue.config.productionTip = false 25 | 26 | // filters 27 | Vue.filter('formatDate', function (value) { 28 | return moment(value, 'DD/MMM/YYYY:HH:mm:ss Z').format("YYYY-MM-DD HH:mm:ss A") 29 | }) 30 | 31 | new Vue({ 32 | router, 33 | store, 34 | render: h => h(App) 35 | }).$mount('#app') 36 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Functionless URL Shortner 25 | 26 | 27 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | // software and associated documentation files (the "Software"), to deal in the Software 6 | // without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so. 9 | // 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | import Vue from 'vue' 18 | import Vuex from 'vuex' 19 | 20 | Vue.use(Vuex) 21 | 22 | export default new Vuex.Store({ 23 | state: { 24 | authorized: false, 25 | links: [] 26 | }, 27 | mutations: { 28 | authorize(state){ 29 | state.authorized = true; 30 | }, 31 | deAuthorize(state) { 32 | state.authorized = false; 33 | }, 34 | hydrateLinks(state, links) { 35 | state.links = links; 36 | }, 37 | drainLinks(state){ 38 | state.links.length = 0; 39 | }, 40 | addLink(state, link){ 41 | state.links.push(link); 42 | }, 43 | removeLink(state, ind){ 44 | state.links.splice(ind, 1); 45 | }, 46 | updateLink(state, link, ind){ 47 | state.links[ind] = link; 48 | } 49 | }, 50 | getters: { 51 | }, 52 | actions: { 53 | }, 54 | modules: { 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | # URL Shortener Client 18 | This application is a simple Vuejs application that interacts with the functionless url shortener. 19 | 20 | ![Personal access token scopes](../assets/client.png) 21 | 22 | ## Requirements 23 | * [Node](https://nodejs.org) 24 | * [Vue CLI](https://cli.vuejs.org/) 25 | 26 | If Node with NPM is already installed simply run. 27 | ``` 28 | npm install -g @vue/cli 29 | ``` 30 | 31 | ## Setup 32 | 33 | **In order for the local client to work for testing, be sure and set the *UseLocalClient* option when launching the backend stack** 34 | 35 | **The following commands need to be run from within the `client` folder.** 36 | 37 | ### 1. Update the environment variables in the `.env` file. 38 | The client needs some information about the backend. These values were output when you deployed the backend. If you need them again, simply run in your terminal: 39 | ``` 40 | aws cloudformation describe-stacks --stack-name URLShortener 41 | ``` 42 | Update and save the `.env` file. when you are done it should look "something" like this. 43 | 44 | ``` 45 | VUE_APP_NAME=shortener 46 | VUE_APP_API_ROOT=https://fd7c8be3rg.execute-api.us-west-2.amazonaws.com/Prod 47 | VUE_APP_AUTH_DOMAIN=https://shortener.auth.us-west-2.amazoncognito.com 48 | VUE_APP_CLIENT_ID=432p7npp8tf7a8pnb0hg5cbegl 49 | ``` 50 | 51 | ### 2. Install NPM dependencies 52 | 53 | ``` 54 | npm i 55 | ``` 56 | 57 | ### 3. Start the local server 58 | ``` 59 | npm run serve 60 | ``` 61 | 62 | ### 4. Open the webpage at [http://localhost:8080](http://localhost:8080) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 89 | 90 | 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | # Functionless URL Shortener 18 | This app creates a URL shortener without using any compute. All business logic is handled at the Amazon API Gateway level. The basic app will create an API Gateway instance utilizing Cognito for authentication and authorization. It will also create an Amazon DynamoDB table for data storage. It will also create a simple Vuejs application as a demo client. 19 | 20 | Read the blog series about this application: 21 | 1. [Building a serverless URL shortener app without AWS Lambda – part 1](https://aws.amazon.com/blogs/compute/building-a-serverless-url-shortener-app-without-lambda-part-1) 22 | 1. [Building a serverless URL shortener app without AWS Lambda – part 2](https://aws.amazon.com/blogs/compute/building-a-serverless-url-shortener-app-without-lambda-part-2) 23 | 1. [Building a serverless URL shortener app without AWS Lambda – part 3](https://aws.amazon.com/blogs/compute/building-a-serverless-url-shortener-app-without-lambda-part-3) 24 | 25 | ## The Backend 26 | 27 | ### Services Used 28 | * Amazon API Gateway 29 | * Amazon Cognito 30 | * Amazon DynamoDB 31 | * AWS Amplify Console 32 | * Amazon CloudFront *Will cause a lengthy deployment time. See note under **Deploying** 33 | * Amazon S3 34 | 35 | 36 | ### Requirements for deployment 37 | * AWS CLI 38 | * AWS SAM CLI v0.37.0+ 39 | * Forked copy of this repository. Instructions for forking a GitHib repository can be found here 40 | * A GitHub personal access token with the *repo* scope as shown below. Instructions for creating a personal access token can be found here 41 | 42 | ![Personal access token scopes](./assets/pat.png) 43 | 44 | **Be sure and store you new token in a place that you can find it.** 45 | 46 | ### Deploying 47 | 48 | ***Note: This stack includes an Amazon CloudFront distribution which can take around 30 minutes to create. Don't be alarmed if the deploy seems to hang for a long time.*** 49 | In the terminal, use the SAM CLI guided deployment the first time you deploy 50 | ```bash 51 | sam deploy -g 52 | ``` 53 | 54 | #### Choose options 55 | You can choose the default for all options except *GithubRepository* and ** 56 | 57 | ```bash 58 | ## The name of the CloudFormation stack 59 | Stack Name [URLShortener]: 60 | 61 | ## The region you want to deploy in 62 | AWS Region [us-west-2]: 63 | 64 | ## The name of the application (lowercase no spaces). This must be globally unique 65 | Parameter AppName [shortener]: 66 | 67 | ## Enables public client and local client for testing. (Less secure) 68 | Parameter UseLocalClient [false]: 69 | 70 | ## GitHub forked repository URL 71 | Parameter GithubRepository []: 72 | 73 | ## Github Personal access token 74 | Parameter PersonalAccessToken: 75 | 76 | ## Shows you resources changes to be deployed and requires a 'Y' to initiate deploy 77 | Confirm changes before deploy [y/N]: 78 | 79 | ## SAM needs permission to be able to create roles to connect to the resources in your template 80 | Allow SAM CLI IAM role creation [Y/n]: 81 | 82 | ## Save your choice for later deployments 83 | Save arguments to samconfig.toml [Y/n]: 84 | ``` 85 | 86 | SAM will then deploy the AWS CloudFormation stack to your AWS account and provide required outputs for the included client. 87 | 88 | After the first deploy you may re-deploy using `sam deploy` or redeploy with different options using `sam deploy -g`. 89 | 90 | ## The Client 91 | 92 | *The client can also be run locally for debugging. Instructions can be found [here](./client/README.md).* 93 | 94 | The client is a Vue.js application that interfaces with the backend and allows you to authenticate and manage URL links. The client is hosted using Amplify Console. To avoid circular dependencies, we need to provide some information for the client after stack is built. The information needed is provided at the end of the deploy process. If you do not have the information you can run the following: 95 | 96 | ```bash 97 | aws cloudformation describe-stacks --stack-name URLShortener 98 | ``` 99 | 100 | We need to add this information to the environment variables for the Amplify Console app. There are two options for adding the variables. 101 | 102 | #### Option 1: using the AWS CLI (Update the *\* to reflect the information returned from the deployment.) 103 | 104 | ```bash 105 | aws amplify update-app --app-id --environment-variables \ 106 | VUE_APP_NAME=\ 107 | ,VUE_APP_CLIENT_ID=\ 108 | ,VUE_APP_API_ROOT=\ 109 | ,VUE_APP_AUTH_DOMAIN= 110 | ``` 111 | 112 | *Also available in the stack output as **AmplifyEnvironmentUpdateCommand*** 113 | 114 | #### Option 2: Amplify Console page 115 | 1. Open the [Amplify Console page](https://us-west-2.console.aws.amazon.com/amplify/home) 116 | 1. On the left side, under **All apps**, choose *Url-Shortner-Client* 117 | 1. Under **App settings** choose *Environment variables* 118 | 1. Choose the *manage variables* button 119 | 1. Choose *add variable* 120 | 1. Fill in the *variable* and it's corresponding *Value* 121 | 1. Leave defaults for *Branches* and *Actions* 122 | 1. Repeat for all four variables 123 | 1. Choose save 124 | 125 | ### Starting the first deployment 126 | After deploying the CloudFormation template, you need to go into the Amplify Console and trigger a build. The CloudFormation template can provision the resources, but can’t trigger a build since it creates resources but cannot trigger actions. This can be done via the AWS CLI. 127 | 128 | #### Option 1: Using the AWS CLI (Update the *\* to reflect the information returned from the deployment.) 129 | 130 | ```bash 131 | aws amplify start-job --app-id --branch-name master --job-type RELEASE 132 | ``` 133 | *Also available in the stack output as **AmplifyDeployCommand*** 134 | 135 | To check on the status, you can view it on the AWS Amplify Console or run: 136 | ```bash 137 | aws amplify get-job --app-id --branch-name master --job-id 138 | ``` 139 | 140 | #### Option 2: Amplify Console page 141 | 1. Open the Amplify Console page 142 | 1. On the left side, under **All apps**, choose *Url-Shortner-Client* 143 | 1. Click *Run build* 144 | 145 | *Note: this is only required for the first build subsequent client builds will be triggered when updates are committed to your forked repository. 146 | 147 | ## Cleanup 148 | 1. Open the CloudFormation console 149 | 1. Locate a stack named *URLShortener* 150 | 1. Select the radio option next to it 151 | 1. Select **Delete** 152 | 1. Select **Delete stack** to confirm 153 | 154 | *Note: If you opted to have access logs (on by default), you may have to delete the S3 bucket manually. 155 | -------------------------------------------------------------------------------- /client/src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 145 | 146 | 263 | -------------------------------------------------------------------------------- /api.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | openapi: "3.0.1" 18 | info: 19 | title: "URL Shortener API" 20 | version: "1.0.0" 21 | 22 | x-amazon-apigateway-request-validators: 23 | all: 24 | validateRequestBody: true 25 | validateRequestParameters: true 26 | params: 27 | validateRequestBody: false 28 | validateRequestParameters: true 29 | body: 30 | validateRequestBody: true 31 | validateRequestParameters: false 32 | 33 | paths: 34 | /{linkId}: 35 | ## URL redirector 36 | get: 37 | summary: Get a url by ID and redirect 38 | x-amazon-apigateway-request-validator: params 39 | parameters: 40 | - in: path 41 | name: linkId 42 | schema: 43 | type: string 44 | required: true 45 | description: Short link ID for full URL 46 | responses: 47 | "301": 48 | description: "301 redirect" 49 | headers: 50 | Location: 51 | type: "string" 52 | Cache-Control: 53 | type: "string" 54 | 55 | ## API Gateway Integration 56 | x-amazon-apigateway-integration: 57 | credentials: 58 | Fn::GetAtt: [ DDBReadRole, Arn ] 59 | uri: {"Fn::Sub":"arn:aws:apigateway:${AWS::Region}:dynamodb:action/GetItem"} 60 | httpMethod: "POST" 61 | requestTemplates: 62 | application/json: {"Fn::Sub": "{\"Key\": {\"id\": {\"S\": \"$input.params().path.linkId\"}}, \"TableName\": \"${LinkTable}\"}"} 63 | passthroughBehavior: "when_no_templates" 64 | responses: 65 | "200": 66 | statusCode: "301" 67 | responseParameters: 68 | method.response.header.Location: {"Fn::Sub": ["'master.${ampDomain}?error=url_not_found'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]} 69 | method.response.header.Cache-Control: "'max-age=300'" 70 | responseTemplates: 71 | application/json: "#set($inputRoot = $input.path('$')) \ 72 | #if($inputRoot.toString().contains(\"Item\")) \ 73 | #set($context.responseOverride.header.Location = $inputRoot.Item.url.S) \ 74 | #end" 75 | type: "aws" 76 | 77 | /app: 78 | ## Get all links for user 79 | get: 80 | summary: Fetch all links for authenticated user 81 | security: 82 | - UserAuthorizer: [] 83 | parameters: 84 | - $ref: '#/components/parameters/authHeader' 85 | responses: 86 | "200": 87 | description: "200 response" 88 | headers: 89 | Access-Control-Allow-Origin: 90 | type: "string" 91 | Cache-Control: 92 | type: "string" 93 | 94 | 95 | ## API Gateway Integration 96 | x-amazon-apigateway-integration: 97 | credentials: 98 | Fn::GetAtt: [ DDBReadRole, Arn ] 99 | uri: {"Fn::Sub":"arn:aws:apigateway:${AWS::Region}:dynamodb:action/Query"} 100 | httpMethod: "POST" 101 | requestTemplates: 102 | application/json: { "Fn::Sub": "{\"TableName\": \"${LinkTable}\", \ 103 | \"IndexName\":\"OwnerIndex\",\"KeyConditionExpression\": \"#n_owner = :v_owner\", \ 104 | \"ExpressionAttributeValues\": \ 105 | {\":v_owner\": {\"S\": \"$context.authorizer.claims.email\"}},\"ExpressionAttributeNames\": {\"#n_owner\": \"owner\"}}"} 106 | passthroughBehavior: "when_no_templates" 107 | responses: 108 | "200": 109 | statusCode: "200" 110 | responseParameters: 111 | method.response.header.Cache-Control: "'no-cache, no-store'" 112 | method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} 113 | responseTemplates: 114 | application/json: "#set($inputRoot = $input.path('$'))[ \ 115 | #foreach($elem in $inputRoot.Items) { \ 116 | \"id\":\"$elem.id.S\", \ 117 | \"url\": \"$elem.url.S\", \ 118 | \"timestamp\": \"$elem.timestamp.S\", \ 119 | \"owner\": \"$elem.owner.S\"} \ 120 | #if($foreach.hasNext),#end \ 121 | #end]" 122 | type: "AWS" 123 | 124 | ## Create a new link 125 | post: 126 | summary: Create new url 127 | security: 128 | - UserAuthorizer: [] 129 | x-amazon-apigateway-request-validator: body 130 | parameters: 131 | - $ref: '#/components/parameters/authHeader' 132 | requestBody: 133 | description: Optional description in *Markdown* 134 | required: true 135 | content: 136 | application/json: 137 | schema: 138 | $ref: '#/components/schemas/PostBody' 139 | responses: 140 | "200": 141 | description: "200 response" 142 | headers: 143 | Access-Control-Allow-Origin: 144 | type: "string" 145 | "400": 146 | description: "400 response" 147 | headers: 148 | Access-Control-Allow-Origin: 149 | type: "string" 150 | 151 | ## API Gateway integration 152 | x-amazon-apigateway-integration: 153 | credentials: 154 | Fn::GetAtt: [ DDBCrudRole, Arn ] 155 | uri: { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:dynamodb:action/UpdateItem" } 156 | httpMethod: "POST" 157 | requestTemplates: 158 | application/json: { "Fn::Sub": "{\"TableName\": \"${LinkTable}\",\ 159 | \"ConditionExpression\":\"attribute_not_exists(id)\", \ 160 | \"Key\": {\"id\": {\"S\": $input.json('$.id')}}, \ 161 | \"ExpressionAttributeNames\": {\"#u\": \"url\",\"#o\": \"owner\",\"#ts\": \"timestamp\"}, \ 162 | \"ExpressionAttributeValues\":{\":u\": {\"S\": $input.json('$.url')},\":o\": {\"S\": \"$context.authorizer.claims.email\"},\":ts\": {\"S\": \"$context.requestTime\"}}, \ 163 | \"UpdateExpression\": \"SET #u = :u, #o = :o, #ts = :ts\", \ 164 | \"ReturnValues\": \"ALL_NEW\"}" } 165 | passthroughBehavior: "when_no_templates" 166 | responses: 167 | "200": 168 | statusCode: "200" 169 | responseParameters: 170 | method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} 171 | responseTemplates: 172 | application/json: "#set($inputRoot = $input.path('$')) \ 173 | {\"id\":\"$inputRoot.Attributes.id.S\", \ 174 | \"url\":\"$inputRoot.Attributes.url.S\", 175 | \"timestamp\":\"$inputRoot.Attributes.timestamp.S\", \ 176 | \"owner\":\"$inputRoot.Attributes.owner.S\"}" 177 | "400": 178 | statusCode: "400" 179 | responseParameters: 180 | method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} 181 | responseTemplates: 182 | application/json: "#set($inputRoot = $input.path('$')) \ 183 | #if($inputRoot.toString().contains(\"ConditionalCheckFailedException\")) \ 184 | #set($context.responseOverride.status = 200) 185 | {\"error\": true,\"message\": \"URL link already exists\"} \ 186 | #end" 187 | type: "aws" 188 | 189 | ## Options for get and post that do not have a linkId 190 | options: 191 | responses: 192 | "200": 193 | description: "200 response" 194 | headers: 195 | Access-Control-Allow-Origin: 196 | schema: 197 | type: "string" 198 | Access-Control-Allow-Methods: 199 | schema: 200 | type: "string" 201 | Access-Control-Allow-Headers: 202 | schema: 203 | type: "string" 204 | x-amazon-apigateway-integration: 205 | requestTemplates: 206 | application/json: "{\"statusCode\": 200}" 207 | passthroughBehavior: "when_no_match" 208 | responses: 209 | default: 210 | statusCode: "200" 211 | responseParameters: 212 | method.response.header.Access-Control-Allow-Methods: "'POST, GET, OPTIONS'" 213 | method.response.header.Access-Control-Allow-Headers: "'authorization, content-type'" 214 | method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} 215 | type: "mock" 216 | 217 | /app/{linkId}: 218 | ## Delete link 219 | delete: 220 | summary: Delete url 221 | security: 222 | - UserAuthorizer: [] 223 | x-amazon-apigateway-request-validator: params 224 | parameters: 225 | - $ref: '#/components/parameters/authHeader' 226 | - $ref: '#/components/parameters/linkIdHeader' 227 | responses: 228 | "200": 229 | description: "200 response" 230 | headers: 231 | Access-Control-Allow-Origin: 232 | type: "string" 233 | "400": 234 | description: "400 response" 235 | 236 | ## AOI gateway integration 237 | x-amazon-apigateway-integration: 238 | credentials: 239 | Fn::GetAtt: [ DDBCrudRole, Arn ] 240 | uri: { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:dynamodb:action/DeleteItem" } 241 | httpMethod: "POST" 242 | requestTemplates: 243 | application/json: { "Fn::Sub": "{\"Key\": {\"id\": {\"S\": \"$input.params().path.linkId\"}}, \ 244 | \"TableName\": \"${LinkTable}\", \ 245 | \"ConditionExpression\": \"#owner = :owner\", \ 246 | \"ExpressionAttributeValues\":{\":owner\": {\"S\": \"$context.authorizer.claims.email\"}}, \ 247 | \"ExpressionAttributeNames\": {\"#owner\": \"owner\"}}" } 248 | passthroughBehavior: "when_no_templates" 249 | responses: 250 | "200": 251 | statusCode: "200" 252 | responseParameters: 253 | method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} 254 | "400": 255 | statusCode: "400" 256 | responseTemplates: 257 | application/json: "{\"error\": {\"message\":\"Either you do not have permission to do this operation or a parameter is missing\"}}" 258 | type: "aws" 259 | ## Update links 260 | put: 261 | summary: Update specific URL 262 | security: 263 | - UserAuthorizer: [] 264 | x-amazon-apigateway-request-validator: body 265 | requestBody: 266 | description: Optional description in *Markdown* 267 | required: true 268 | content: 269 | application/json: 270 | schema: 271 | $ref: '#/components/schemas/PutBody' 272 | parameters: 273 | - $ref: '#/components/parameters/authHeader' 274 | - $ref: '#/components/parameters/linkIdHeader' 275 | responses: 276 | "200": 277 | description: "301 response" 278 | headers: 279 | Access-Control-Allow-Origin: 280 | type: "string" 281 | "400": 282 | description: "400 response" 283 | headers: 284 | Access-Control-Allow-Origin: 285 | type: "string" 286 | 287 | ## API gateway integration 288 | x-amazon-apigateway-integration: 289 | credentials: 290 | Fn::GetAtt: [ DDBCrudRole, Arn ] 291 | uri: { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:dynamodb:action/UpdateItem" } 292 | httpMethod: "POST" 293 | requestTemplates: 294 | application/json: { "Fn::Sub": "{\"TableName\": \"${LinkTable}\",\ 295 | \"Key\": {\"id\": {\"S\": $input.json('$.id')}}, \ 296 | \"ExpressionAttributeNames\": {\"#u\": \"url\", \"#owner\": \"owner\", \"#id\":\"id\"}, \ 297 | \"ExpressionAttributeValues\":{\":u\": {\"S\": $input.json('$.url')}, \":owner\": {\"S\": \"$context.authorizer.claims.email\"}, \":linkId\":{\"S\":\"$input.params().path.linkId\"}}, \ 298 | \"UpdateExpression\": \"SET #u = :u\", \ 299 | \"ReturnValues\": \"ALL_NEW\", \ 300 | \"ConditionExpression\": \"#owner = :owner AND #id = :linkId\"}" } 301 | passthroughBehavior: "when_no_templates" 302 | responses: 303 | "200": 304 | statusCode: "200" 305 | responseParameters: 306 | method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} 307 | responseTemplates: 308 | application/json: "#set($inputRoot = $input.path('$')) \ 309 | {\"id\":\"$inputRoot.Attributes.id.S\", \ 310 | \"url\":\"$inputRoot.Attributes.url.S\", 311 | \"timestamp\":\"$inputRoot.Attributes.timestamp.S\", \ 312 | \"owner\":\"$inputRoot.Attributes.owner.S\"}" 313 | "400": 314 | statusCode: "400" 315 | responseParameters: 316 | method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} 317 | application/json: "{\"error\": {\"message\":\"Either you do not have permission to do this operation or a parameter is missing\"}}" 318 | type: "aws" 319 | options: 320 | responses: 321 | "200": 322 | description: "200 response" 323 | headers: 324 | Access-Control-Allow-Origin: 325 | schema: 326 | type: "string" 327 | Access-Control-Allow-Methods: 328 | schema: 329 | type: "string" 330 | Access-Control-Allow-Headers: 331 | schema: 332 | type: "string" 333 | x-amazon-apigateway-integration: 334 | requestTemplates: 335 | application/json: "{\"statusCode\" : 200}" 336 | passthroughBehavior: "when_no_match" 337 | responses: 338 | default: 339 | statusCode: "200" 340 | responseParameters: 341 | method.response.header.Access-Control-Allow-Methods: "'PUT, DELETE, OPTIONS'" 342 | method.response.header.Access-Control-Allow-Headers: "'authorization, content-type'" 343 | method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} 344 | type: "mock" 345 | 346 | ## Validation models 347 | components: 348 | schemas: 349 | PostBody: 350 | type: object 351 | properties: 352 | id: 353 | type: string 354 | url: 355 | type: string 356 | pattern: ^https?://[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*) 357 | required: 358 | - id 359 | - url 360 | PutBody: 361 | type: object 362 | properties: 363 | id: 364 | type: string 365 | url: 366 | type: string 367 | pattern: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/ 368 | timestamp: 369 | type: string 370 | owner: 371 | type: string 372 | required: 373 | - id 374 | - url 375 | - timestamp 376 | - owner 377 | parameters: 378 | authHeader: 379 | in: header 380 | name: Authorization 381 | required: true 382 | description: Contains authorization token 383 | schema: 384 | type: string 385 | linkIdHeader: 386 | in: path 387 | name: linkId 388 | required: true 389 | description: Short link ID for full URL 390 | schema: 391 | type: string 392 | 393 | ## Authorizer definition 394 | securityDefinitions: 395 | UserAuthorizer: 396 | type: "apiKey" 397 | name: "Authorization" 398 | in: "header" 399 | x-amazon-apigateway-authtype: "cognito_user_pools" 400 | x-amazon-apigateway-authorizer: 401 | providerARNs: 402 | - Fn::GetAtt: [ UserPool, Arn ] 403 | type: "cognito_user_pools" 404 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | AWSTemplateFormatVersion: '2010-09-09' 18 | Transform: AWS::Serverless-2016-10-31 19 | Description: Functionless URL Shortner 20 | 21 | ################################################################################################### 22 | ## Template Parameters ## 23 | ################################################################################################### 24 | Parameters: 25 | AppName: 26 | Type: String 27 | Description: Name of application (no spaces). Value must be globally unique 28 | Default: shortener 29 | UseLocalClient: 30 | Type: String 31 | Description: Enables public client and local client for testing. (Less secure) 32 | Default: 'false' 33 | GithubRepository: 34 | Type: String 35 | Description: Forked GitHub repository URL 36 | PersonalAcessToken: 37 | Type: String 38 | Description: Github personal access token 39 | NoEcho: true 40 | CustomDomain: 41 | Type: String 42 | Description: Cstom domain added to client # only configures cognito for now. Manually handle domain on amplify console for client 43 | Default: none 44 | 45 | ################################################################################################### 46 | ## Template Conditions ## 47 | ################################################################################################### 48 | Conditions: 49 | IsLocal: !Equals [!Ref UseLocalClient, 'true'] 50 | HasCustomDomain: !Not [!Equals [!Ref CustomDomain, 'none']] 51 | 52 | ################################################################################################### 53 | ## Template Resources ## 54 | ################################################################################################### 55 | Resources: 56 | ## API Gateway 57 | SiteAPI: 58 | Type: AWS::Serverless::Api 59 | Properties: 60 | StageName: Prod 61 | EndpointConfiguration: REGIONAL 62 | TracingEnabled: true 63 | MethodSettings: 64 | - HttpMethod: "*" 65 | ResourcePath: "/*" 66 | LoggingLevel: INFO 67 | DataTraceEnabled: true 68 | MetricsEnabled: true 69 | ThrottlingRateLimit: 2000 70 | ThrottlingBurstLimit: 1000 71 | - HttpMethod: "GET" 72 | ResourcePath: "/{linkId}" 73 | ThrottlingRateLimit: 10000 74 | ThrottlingBurstLimit: 4000 75 | DefinitionBody: 76 | 'Fn::Transform': 77 | Name: 'AWS::Include' 78 | Parameters: 79 | Location: './api.yaml' 80 | 81 | ## URL DynamoDB Table 82 | LinkTable: 83 | Type: AWS::DynamoDB::Table 84 | Properties: 85 | BillingMode: PAY_PER_REQUEST 86 | KeySchema: 87 | - AttributeName: id 88 | KeyType: HASH 89 | AttributeDefinitions: 90 | - AttributeName: id 91 | AttributeType: S 92 | - AttributeName: owner 93 | AttributeType: S 94 | GlobalSecondaryIndexes: 95 | - IndexName: OwnerIndex 96 | KeySchema: 97 | - AttributeName: owner 98 | KeyType: HASH 99 | Projection: 100 | ProjectionType: ALL 101 | 102 | ## Cognito user pool 103 | UserPool: 104 | Type: AWS::Cognito::UserPool 105 | Properties: 106 | UserPoolName: !Sub ${AppName}-UserPool 107 | Policies: 108 | PasswordPolicy: 109 | MinimumLength: 8 110 | AutoVerifiedAttributes: 111 | - email 112 | UsernameAttributes: 113 | - email 114 | Schema: 115 | - AttributeDataType: String 116 | Name: email 117 | Required: false 118 | 119 | ## Cognito user pool domain 120 | UserPoolDomain: 121 | Type: AWS::Cognito::UserPoolDomain 122 | Properties: 123 | Domain: !Sub ${AppName}-${AWS::AccountId} 124 | UserPoolId: !Ref UserPool 125 | 126 | ## Cognito user pool client 127 | UserPoolClient: 128 | Type: AWS::Cognito::UserPoolClient 129 | Properties: 130 | UserPoolId: !Ref UserPool 131 | ClientName: !Sub ${AppName}-UserPoolClient 132 | GenerateSecret: false 133 | SupportedIdentityProviders: 134 | - COGNITO 135 | CallbackURLs: 136 | - !Join [ ".", [ https://master, !GetAtt AmplifyApp.DefaultDomain ]] 137 | - !If [IsLocal, http://localhost:8080, !Ref "AWS::NoValue"] 138 | - !If [HasCustomDomain, !Ref CustomDomain, !Ref "AWS::NoValue"] 139 | LogoutURLs: 140 | - !Join [ ".", [ https://master, !GetAtt AmplifyApp.DefaultDomain ]] 141 | - !If [IsLocal, http://localhost:8080, !Ref "AWS::NoValue"] 142 | - !If [HasCustomDomain, !Ref CustomDomain, !Ref "AWS::NoValue"] 143 | AllowedOAuthFlowsUserPoolClient: true 144 | AllowedOAuthFlows: 145 | - code 146 | AllowedOAuthScopes: 147 | - email 148 | - openid 149 | 150 | ## CloudFront distribution 151 | CloudFrontDistro: 152 | Type: AWS::CloudFront::Distribution 153 | Properties: 154 | DistributionConfig: 155 | Comment: URL Shortener CDN 156 | DefaultCacheBehavior: 157 | AllowedMethods: ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] 158 | CachedMethods: ["GET", "HEAD", "OPTIONS"] 159 | Compress: true 160 | DefaultTTL: 0 161 | ForwardedValues: 162 | Headers: 163 | - Access-Control-Request-Headers 164 | - Access-Control-Request-Method 165 | - Origin 166 | - Authorization 167 | QueryString: false 168 | TargetOriginId: "URLShortenerAPIGW" 169 | ViewerProtocolPolicy: redirect-to-https 170 | CustomErrorResponses: 171 | - ErrorCachingMinTTL: 0 172 | ErrorCode: 400 173 | - ErrorCachingMinTTL: 1 174 | ErrorCode: 403 175 | - ErrorCachingMinTTL: 5 176 | ErrorCode: 500 177 | Logging: 178 | Bucket: !GetAtt CloudFrontAccessLogsBucket.DomainName 179 | Enabled: true 180 | Origins: 181 | - CustomOriginConfig: 182 | OriginProtocolPolicy: https-only 183 | DomainName: !Sub ${SiteAPI}.execute-api.${AWS::Region}.amazonaws.com 184 | Id: "URLShortenerAPIGW" 185 | OriginPath: /Prod 186 | 187 | ## CloudFront access logs storage 188 | CloudFrontAccessLogsBucket: 189 | Type: AWS::S3::Bucket 190 | 191 | ## Amplify Application for hosting 192 | AmplifyApp: 193 | Type: AWS::Amplify::App 194 | Properties: 195 | Name: Url-Shortener-Client 196 | Description: Basic client for URL Shortner 197 | Repository: !Ref GithubRepository 198 | AccessToken: !Ref PersonalAcessToken 199 | BuildSpec: |- 200 | version: 0.1 201 | frontend: 202 | phases: 203 | preBuild: 204 | commands: 205 | - cd client 206 | - npm ci 207 | build: 208 | commands: 209 | - npm run build 210 | artifacts: 211 | baseDirectory: client/dist 212 | files: 213 | - '**/*' 214 | cache: 215 | paths: 216 | - node_modules/**/* 217 | IAMServiceRole: !GetAtt AmplifyRole.Arn 218 | 219 | ## Amplify Branch for hosting 220 | AmplifyBranch: 221 | Type: AWS::Amplify::Branch 222 | Properties: 223 | BranchName: master 224 | AppId: !GetAtt AmplifyApp.AppId 225 | Description: Master Branch 226 | EnableAutoBuild: true 227 | 228 | ################################################################################################### 229 | ## IAM Roles ## 230 | ################################################################################################### 231 | 232 | ## Dynamo DB Read Role 233 | DDBReadRole: 234 | Type: "AWS::IAM::Role" 235 | Properties: 236 | AssumeRolePolicyDocument: 237 | Version: "2012-10-17" 238 | Statement: 239 | - 240 | Effect: "Allow" 241 | Principal: 242 | Service: "apigateway.amazonaws.com" 243 | Action: 244 | - "sts:AssumeRole" 245 | Policies: 246 | - PolicyName: DDBReadPolicy 247 | PolicyDocument: 248 | Version: '2012-10-17' 249 | Statement: 250 | Action: 251 | - dynamodb:GetItem 252 | - dynamodb:Scan 253 | - dynamodb:Query 254 | Effect: Allow 255 | Resource: 256 | - !GetAtt LinkTable.Arn 257 | - !Sub 258 | - ${TableArn}/index/* 259 | - {TableArn: !GetAtt LinkTable.Arn} 260 | 261 | ## Dynamo DB Read/Write Role 262 | DDBCrudRole: 263 | Type: "AWS::IAM::Role" 264 | Properties: 265 | AssumeRolePolicyDocument: 266 | Version: "2012-10-17" 267 | Statement: 268 | - 269 | Effect: "Allow" 270 | Principal: 271 | Service: "apigateway.amazonaws.com" 272 | Action: 273 | - "sts:AssumeRole" 274 | Policies: 275 | - PolicyName: DDBCrudPolicy 276 | PolicyDocument: 277 | Version: '2012-10-17' 278 | Statement: 279 | Action: 280 | - dynamodb:DeleteItem 281 | - dynamodb:UpdateItem 282 | Effect: Allow 283 | Resource: !GetAtt LinkTable.Arn 284 | 285 | ## Amplify Hosting Role 286 | AmplifyRole: 287 | Type: AWS::IAM::Role 288 | Properties: 289 | AssumeRolePolicyDocument: 290 | Version: 2012-10-17 291 | Statement: 292 | - Effect: Allow 293 | Principal: 294 | Service: 295 | - amplify.amazonaws.com 296 | Action: 297 | - sts:AssumeRole 298 | Policies: 299 | - PolicyName: Amplify 300 | PolicyDocument: 301 | Version: 2012-10-17 302 | Statement: 303 | - Effect: Allow 304 | Action: "amplify:*" 305 | Resource: "*" 306 | 307 | ## CloudWatchRole for aws gateway account 308 | Account: 309 | Type: 'AWS::ApiGateway::Account' 310 | Properties: 311 | CloudWatchRoleArn: !GetAtt CloudWatchRole.Arn 312 | 313 | CloudWatchRole: 314 | Type: 'AWS::IAM::Role' 315 | Properties: 316 | AssumeRolePolicyDocument: 317 | Version: 2012-10-17 318 | Statement: 319 | - Effect: Allow 320 | Principal: 321 | Service: 322 | - apigateway.amazonaws.com 323 | Action: 'sts:AssumeRole' 324 | Path: / 325 | ManagedPolicyArns: 326 | - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs 327 | 328 | ################################################################################################### 329 | ## Metrics outputs ## 330 | ################################################################################################### 331 | 332 | NotifyTopic: 333 | Type: AWS::SNS::Topic 334 | 335 | APIGateway4xxAlarm: 336 | Type: AWS::CloudWatch::Alarm 337 | Properties: 338 | AlarmName: "URL Shortener API 4xx Alarm" 339 | AlarmDescription: "4xx monitor" 340 | MetricName: "4XXError" 341 | Namespace: "AWS/ApiGateway" 342 | Dimensions: 343 | - Name: "ApiName" 344 | Value: "URL Shortener API" 345 | Statistic: "Average" 346 | Period: 60 347 | EvaluationPeriods: 1 348 | Threshold: .01 349 | ComparisonOperator: "GreaterThanThreshold" 350 | AlarmActions: 351 | - !Ref NotifyTopic 352 | 353 | APIGateway5xxAlarm: 354 | Type: AWS::CloudWatch::Alarm 355 | Properties: 356 | AlarmName: "URL Shortener API 5xx Alarm" 357 | AlarmDescription: "5xx monitor" 358 | MetricName: "5XXError" 359 | Namespace: "AWS/ApiGateway" 360 | Dimensions: 361 | - Name: "ApiName" 362 | Value: "URL Shortener API" 363 | Statistic: "Average" 364 | Period: 60 365 | EvaluationPeriods: 1 366 | Threshold: .01 367 | ComparisonOperator: "GreaterThanThreshold" 368 | AlarmActions: 369 | - !Ref NotifyTopic 370 | 371 | APIGatewayLatencyAlarm: 372 | Type: AWS::CloudWatch::Alarm 373 | Properties: 374 | AlarmName: "URL Shortener API Latency Alarm" 375 | AlarmDescription: "Latency monitor" 376 | MetricName: "Latency" 377 | Namespace: "AWS/ApiGateway" 378 | Dimensions: 379 | - Name: "ApiName" 380 | Value: "URL Shortener API" 381 | ExtendedStatistic: "p99" 382 | Period: 300 383 | EvaluationPeriods: 1 384 | Threshold: 75 385 | ComparisonOperator: "GreaterThanThreshold" 386 | AlarmActions: 387 | - !Ref NotifyTopic 388 | 389 | DDB5xxAlarm: 390 | Type: AWS::CloudWatch::Alarm 391 | Properties: 392 | AlarmName: "URL Shortener DDB 5xx Alarm" 393 | AlarmDescription: "System monitor" 394 | MetricName: "SystemErrors" 395 | Namespace: "AWS/DynamoDB" 396 | Dimensions: 397 | - Name: "TableName" 398 | Value: !Ref LinkTable 399 | Statistic: "Average" 400 | Period: 60 401 | EvaluationPeriods: 1 402 | Threshold: .01 403 | ComparisonOperator: "GreaterThanThreshold" 404 | AlarmActions: 405 | - !Ref NotifyTopic 406 | 407 | DDB4xxAlarm: 408 | Type: AWS::CloudWatch::Alarm 409 | Properties: 410 | AlarmName: "URL Shortener DDB 4xx Alarm" 411 | AlarmDescription: "User monitor" 412 | MetricName: "UserErrors" 413 | Namespace: "AWS/DynamoDB" 414 | Dimensions: 415 | - Name: "TableName" 416 | Value: !Ref LinkTable 417 | Statistic: "Average" 418 | Period: 60 419 | EvaluationPeriods: 1 420 | Threshold: .10 421 | ComparisonOperator: "GreaterThanThreshold" 422 | AlarmActions: 423 | - !Ref NotifyTopic 424 | 425 | CloudFrontTotalErrorRateAlarm: 426 | Type: AWS::CloudWatch::Alarm 427 | Properties: 428 | AlarmName: "Url Shortener CloudFront Errors" 429 | AlarmDescription: "CDN error monitor" 430 | MetricName: TotalErrorRate 431 | Namespace: AWS/CloudFront 432 | Dimensions: 433 | - Name: DistributionId 434 | Value: !Ref CloudFrontDistro 435 | Statistic: Sum 436 | Period: 60 437 | EvaluationPeriods: 1 438 | ComparisonOperator: GreaterThanOrEqualToThreshold 439 | Threshold: 5 440 | AlarmActions: 441 | - !Ref NotifyTopic 442 | 443 | CloudFrontTotalCacheHitRateAlarm: 444 | Type: AWS::CloudWatch::Alarm 445 | Properties: 446 | AlarmName: "Url Shortener CloudFront Cache Hit Rate" 447 | AlarmDescription: "CDN eache monitor" 448 | MetricName: CacheHitRate 449 | Namespace: AWS/CloudFront 450 | Dimensions: 451 | - Name: DistributionId 452 | Value: !Ref CloudFrontDistro 453 | Statistic: Average 454 | Period: 300 455 | EvaluationPeriods: 1 456 | ComparisonOperator: LessThanOrEqualToThreshold 457 | Threshold: .80 458 | AlarmActions: 459 | - !Ref NotifyTopic 460 | 461 | 462 | ################################################################################################### 463 | ## Template outputs ## 464 | ################################################################################################### 465 | 466 | Outputs: 467 | VueAppName: 468 | Description: Name of your application 469 | Value: !Ref AppName 470 | 471 | VueAppAPIRoot: 472 | Description: API Gateway endpoint URL for linker 473 | Value: !Sub "https://${CloudFrontDistro.DomainName}" 474 | 475 | VueAppAuthDomain: 476 | Description: Domain used for authentication 477 | Value: !Sub https://${AppName}-${AWS::AccountId}.auth.${AWS::Region}.amazoncognito.com 478 | 479 | VueAppClientId: 480 | Description: Cognito User Pool Client Id 481 | Value: !Ref UserPoolClient 482 | 483 | ClientDomainAddress: 484 | Description: Domain for client 485 | Value: !Join [ ".", [ https://master, !GetAtt AmplifyApp.DefaultDomain ]] 486 | 487 | AmplifyAppId: 488 | Description: Amplify application ID 489 | Value: !GetAtt AmplifyApp.AppId 490 | 491 | AmplifyEnvironmentUpdateCommand: 492 | Description: Command to add environment variables to the Amplify application 493 | Value: !Sub 494 | - aws amplify update-app --app-id ${AmplifyID} --environment-variables VUE_APP_NAME=${AppName},VUE_APP_CLIENT_ID=${UserPoolClient},VUE_APP_API_ROOT=${APIRoot},VUE_APP_AUTH_DOMAIN=${APIAuthDomain} 495 | - AmplifyID: !GetAtt AmplifyApp.AppId 496 | APIRoot: !Join ["", [ "https://", !GetAtt CloudFrontDistro.DomainName]] 497 | APIAuthDomain: !Sub https://${AppName}-${AWS::AccountId}.auth.${AWS::Region}.amazoncognito.com 498 | 499 | AmplifyDeployCommand: 500 | Description: Command to deploy the Amplify application 501 | Value: !Sub 502 | - aws amplify start-job --app-id ${AmplifyID} --branch-name master --job-type RELEASE 503 | - AmplifyID: !GetAtt AmplifyApp.AppId 504 | --------------------------------------------------------------------------------