# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 # # Permission is hereby granted, free of charge, to any person obtaining a copy of this # software and associated documentation files (the "Software"), to deal in the Software # without restriction, including without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. openapi: "3.0.1" info: title: "URL Shortener API" version: "1.0.0" x-amazon-apigateway-request-validators: all: validateRequestBody: true validateRequestParameters: true params: validateRequestBody: false validateRequestParameters: true body: validateRequestBody: true validateRequestParameters: false paths: /{linkId}: ## URL redirector get: summary: Get a url by ID and redirect x-amazon-apigateway-request-validator: params parameters: - in: path name: linkId schema: type: string required: true description: Short link ID for full URL responses: "301": description: "301 redirect" headers: Location: type: "string" Cache-Control: type: "string" ## API Gateway Integration x-amazon-apigateway-integration: credentials: Fn::GetAtt: [ DDBReadRole, Arn ] uri: {"Fn::Sub":"arn:aws:apigateway:${AWS::Region}:dynamodb:action/GetItem"} httpMethod: "POST" requestTemplates: application/json: {"Fn::Sub": "{\"Key\": {\"id\": {\"S\": \"$input.params().path.linkId\"}}, \"TableName\": \"${LinkTable}\"}"} passthroughBehavior: "when_no_templates" responses: "200": statusCode: "301" responseParameters: method.response.header.Location: {"Fn::Sub": ["'master.${ampDomain}?error=url_not_found'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]} method.response.header.Cache-Control: "'max-age=300'" responseTemplates: application/json: "#set($inputRoot = $input.path('$')) \ #if($inputRoot.toString().contains(\"Item\")) \ #set($context.responseOverride.header.Location = $inputRoot.Item.url.S) \ #end" type: "aws" /app: ## Get all links for user get: summary: Fetch all links for authenticated user security: - UserAuthorizer: [] parameters: - $ref: '#/components/parameters/authHeader' responses: "200": description: "200 response" headers: Access-Control-Allow-Origin: type: "string" Cache-Control: type: "string" ## API Gateway Integration x-amazon-apigateway-integration: credentials: Fn::GetAtt: [ DDBReadRole, Arn ] uri: {"Fn::Sub":"arn:aws:apigateway:${AWS::Region}:dynamodb:action/Query"} httpMethod: "POST" requestTemplates: application/json: { "Fn::Sub": "{\"TableName\": \"${LinkTable}\", \ \"IndexName\":\"OwnerIndex\",\"KeyConditionExpression\": \"#n_owner = :v_owner\", \ \"ExpressionAttributeValues\": \ {\":v_owner\": {\"S\": \"$context.authorizer.claims.email\"}},\"ExpressionAttributeNames\": {\"#n_owner\": \"owner\"}}"} passthroughBehavior: "when_no_templates" responses: "200": statusCode: "200" responseParameters: method.response.header.Cache-Control: "'no-cache, no-store'" method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} responseTemplates: application/json: "#set($inputRoot = $input.path('$'))[ \ #foreach($elem in $inputRoot.Items) { \ \"id\":\"$elem.id.S\", \ \"url\": \"$elem.url.S\", \ \"timestamp\": \"$elem.timestamp.S\", \ \"owner\": \"$elem.owner.S\"} \ #if($foreach.hasNext),#end \ #end]" type: "AWS" ## Create a new link post: summary: Create new url security: - UserAuthorizer: [] x-amazon-apigateway-request-validator: body parameters: - $ref: '#/components/parameters/authHeader' requestBody: description: Optional description in *Markdown* required: true content: application/json: schema: $ref: '#/components/schemas/PostBody' responses: "200": description: "200 response" headers: Access-Control-Allow-Origin: type: "string" "400": description: "400 response" headers: Access-Control-Allow-Origin: type: "string" ## API Gateway integration x-amazon-apigateway-integration: credentials: Fn::GetAtt: [ DDBCrudRole, Arn ] uri: { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:dynamodb:action/UpdateItem" } httpMethod: "POST" requestTemplates: application/json: { "Fn::Sub": "{\"TableName\": \"${LinkTable}\",\ \"ConditionExpression\":\"attribute_not_exists(id)\", \ \"Key\": {\"id\": {\"S\": $input.json('$.id')}}, \ \"ExpressionAttributeNames\": {\"#u\": \"url\",\"#o\": \"owner\",\"#ts\": \"timestamp\"}, \ \"ExpressionAttributeValues\":{\":u\": {\"S\": $input.json('$.url')},\":o\": {\"S\": \"$context.authorizer.claims.email\"},\":ts\": {\"S\": \"$context.requestTime\"}}, \ \"UpdateExpression\": \"SET #u = :u, #o = :o, #ts = :ts\", \ \"ReturnValues\": \"ALL_NEW\"}" } passthroughBehavior: "when_no_templates" responses: "200": statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} responseTemplates: application/json: "#set($inputRoot = $input.path('$')) \ {\"id\":\"$inputRoot.Attributes.id.S\", \ \"url\":\"$inputRoot.Attributes.url.S\", \"timestamp\":\"$inputRoot.Attributes.timestamp.S\", \ \"owner\":\"$inputRoot.Attributes.owner.S\"}" "400": statusCode: "400" responseParameters: method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} responseTemplates: application/json: "#set($inputRoot = $input.path('$')) \ #if($inputRoot.toString().contains(\"ConditionalCheckFailedException\")) \ #set($context.responseOverride.status = 200) {\"error\": true,\"message\": \"URL link already exists\"} \ #end" type: "aws" ## Options for get and post that do not have a linkId options: responses: "200": description: "200 response" headers: Access-Control-Allow-Origin: schema: type: "string" Access-Control-Allow-Methods: schema: type: "string" Access-Control-Allow-Headers: schema: type: "string" x-amazon-apigateway-integration: requestTemplates: application/json: "{\"statusCode\": 200}" passthroughBehavior: "when_no_match" responses: default: statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Methods: "'POST, GET, OPTIONS'" method.response.header.Access-Control-Allow-Headers: "'authorization, content-type'" method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} type: "mock" /app/{linkId}: ## Delete link delete: summary: Delete url security: - UserAuthorizer: [] x-amazon-apigateway-request-validator: params parameters: - $ref: '#/components/parameters/authHeader' - $ref: '#/components/parameters/linkIdHeader' responses: "200": description: "200 response" headers: Access-Control-Allow-Origin: type: "string" "400": description: "400 response" ## AOI gateway integration x-amazon-apigateway-integration: credentials: Fn::GetAtt: [ DDBCrudRole, Arn ] uri: { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:dynamodb:action/DeleteItem" } httpMethod: "POST" requestTemplates: application/json: { "Fn::Sub": "{\"Key\": {\"id\": {\"S\": \"$input.params().path.linkId\"}}, \ \"TableName\": \"${LinkTable}\", \ \"ConditionExpression\": \"#owner = :owner\", \ \"ExpressionAttributeValues\":{\":owner\": {\"S\": \"$context.authorizer.claims.email\"}}, \ \"ExpressionAttributeNames\": {\"#owner\": \"owner\"}}" } passthroughBehavior: "when_no_templates" responses: "200": statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} "400": statusCode: "400" responseTemplates: application/json: "{\"error\": {\"message\":\"Either you do not have permission to do this operation or a parameter is missing\"}}" type: "aws" ## Update links put: summary: Update specific URL security: - UserAuthorizer: [] x-amazon-apigateway-request-validator: body requestBody: description: Optional description in *Markdown* required: true content: application/json: schema: $ref: '#/components/schemas/PutBody' parameters: - $ref: '#/components/parameters/authHeader' - $ref: '#/components/parameters/linkIdHeader' responses: "200": description: "301 response" headers: Access-Control-Allow-Origin: type: "string" "400": description: "400 response" headers: Access-Control-Allow-Origin: type: "string" ## API gateway integration x-amazon-apigateway-integration: credentials: Fn::GetAtt: [ DDBCrudRole, Arn ] uri: { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:dynamodb:action/UpdateItem" } httpMethod: "POST" requestTemplates: application/json: { "Fn::Sub": "{\"TableName\": \"${LinkTable}\",\ \"Key\": {\"id\": {\"S\": $input.json('$.id')}}, \ \"ExpressionAttributeNames\": {\"#u\": \"url\", \"#owner\": \"owner\", \"#id\":\"id\"}, \ \"ExpressionAttributeValues\":{\":u\": {\"S\": $input.json('$.url')}, \":owner\": {\"S\": \"$context.authorizer.claims.email\"}, \":linkId\":{\"S\":\"$input.params().path.linkId\"}}, \ \"UpdateExpression\": \"SET #u = :u\", \ \"ReturnValues\": \"ALL_NEW\", \ \"ConditionExpression\": \"#owner = :owner AND #id = :linkId\"}" } passthroughBehavior: "when_no_templates" responses: "200": statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} responseTemplates: application/json: "#set($inputRoot = $input.path('$')) \ {\"id\":\"$inputRoot.Attributes.id.S\", \ \"url\":\"$inputRoot.Attributes.url.S\", \"timestamp\":\"$inputRoot.Attributes.timestamp.S\", \ \"owner\":\"$inputRoot.Attributes.owner.S\"}" "400": statusCode: "400" responseParameters: method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} application/json: "{\"error\": {\"message\":\"Either you do not have permission to do this operation or a parameter is missing\"}}" type: "aws" options: responses: "200": description: "200 response" headers: Access-Control-Allow-Origin: schema: type: "string" Access-Control-Allow-Methods: schema: type: "string" Access-Control-Allow-Headers: schema: type: "string" x-amazon-apigateway-integration: requestTemplates: application/json: "{\"statusCode\" : 200}" passthroughBehavior: "when_no_match" responses: default: statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Methods: "'PUT, DELETE, OPTIONS'" method.response.header.Access-Control-Allow-Headers: "'authorization, content-type'" method.response.header.Access-Control-Allow-Origin: {"Fn::If": ["IsLocal", "'*'", {"Fn::Sub": ["'https://master.${ampDomain}'", { "ampDomain": {"Fn::GetAtt":["AmplifyApp", "DefaultDomain"]} }]}]} type: "mock" ## Validation models components: schemas: PostBody: type: object properties: id: type: string url: type: string pattern: ^https?://[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*) required: - id - url PutBody: type: object properties: id: type: string url: type: string pattern: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/ timestamp: type: string owner: type: string required: - id - url - timestamp - owner parameters: authHeader: in: header name: Authorization required: true description: Contains authorization token schema: type: string linkIdHeader: in: path name: linkId required: true description: Short link ID for full URL schema: type: string ## Authorizer definition securityDefinitions: UserAuthorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "cognito_user_pools" x-amazon-apigateway-authorizer: providerARNs: - Fn::GetAtt: [ UserPool, Arn ] type: "cognito_user_pools"