Skip to content

Commit

Permalink
feat: move events to api gateway v2 (#38)
Browse files Browse the repository at this point in the history
- add headers to response
- update authorizer to v2 response
  • Loading branch information
mimurawil authored Nov 27, 2023
1 parent 81c0ce4 commit 931cce1
Show file tree
Hide file tree
Showing 11 changed files with 16,200 additions and 16,195 deletions.
32,234 changes: 16,116 additions & 16,118 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"test": "npm run codegen && jest",
"lint": "eslint . --ext .ts",
"deploy": "sls deploy",
"teardown": "sls remove",
"format": "prettier --write \"**/*.{ts,js,json,md}\"",
"ts:check": "tsc --noEmit",
"codegen": "graphql-codegen --config codegen.ts",
Expand All @@ -25,6 +26,7 @@
"dependencies": {
"@apollo/server": "4.3.0",
"@as-integrations/aws-lambda": "1.2.1",
"@graphql-tools/merge": "8.3.15",
"aws-sdk": "2.1306.0",
"contentful": "^10.6.9",
"contentful-management": "10.26.0",
Expand All @@ -40,7 +42,6 @@
"@graphql-codegen/typescript": "2.8.7",
"@graphql-codegen/typescript-resolvers": "2.7.12",
"@graphql-tools/executor": "0.0.12",
"@graphql-tools/merge": "8.3.15",
"@types/aws-lambda": "8.10.109",
"@types/jest": "29.2.5",
"@types/node": "18.11.18",
Expand Down
63 changes: 36 additions & 27 deletions serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,90 +21,99 @@ plugins:
provider:
name: aws
profile: moviesApi
deploymentMethod: direct
runtime: nodejs18.x
environment:
CONTENTFUL_SPACE_ID: ${env:CONTENTFUL_SPACE_ID}
CONTENTFUL_DELIVERY_API_TOKEN: ${env:CONTENTFUL_DELIVERY_API_TOKEN}
httpApi:
cors: true
authorizers:
simpleAuthorizerFunc:
type: request
identitySource:
- $request.header.Authorization
functionName: customAuthorizer
iam:
role:
statements:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: arn:aws:lambda:*:*:*

# The `functions` block defines what code to deploy
functions:
# auth functions & APIs
simpleAuthorizerFunc:
customAuthorizer:
handler: src/handlers/auth/authorizer.handler

getValidToken:
handler: src/handlers/auth/getValidToken.handler
events:
- http:
path: auth/token
- httpApi:
path: /auth/token
method: get
cors: true

# healthcheck - check status of the app
healthcheck:
handler: src/handlers/healthcheck.handler
events:
- http:
path: healthcheck
- httpApi:
path: /healthcheck
method: get

# graphql
graphql:
handler: src/handlers/graphql.server
events:
- http:
path: graphql
- httpApi:
path: /graphql
method: post
cors: true
- http:
path: graphql
- httpApi:
path: /graphql
method: get
cors: true
environment:
SLS_STAGE: ${sls:stage}

# RESTful APIs
moviesByGenre:
handler: src/handlers/genres/moviesByGenre.handler
events:
- http:
path: genres/movies
- httpApi:
path: /genres/movies
method: get
cors: true
authorizer:
name: simpleAuthorizerFunc
movies:
handler: src/handlers/movies/getMovies.handler
events:
- http:
path: movies
- httpApi:
path: /movies
method: get
cors: true
authorizer:
name: simpleAuthorizerFunc
movieById:
handler: src/handlers/movies/movieById.handler
events:
- http:
path: movies/{id}
- httpApi:
path: /movies/{id}
method: get
cors: true
authorizer:
name: simpleAuthorizerFunc
movieTitles:
handler: src/handlers/movies/getMovieTitles.handler
events:
- http:
path: movies/titles
- httpApi:
path: /movies/titles
method: get
cors: true
authorizer:
name: simpleAuthorizerFunc
genreById:
handler: src/handlers/genres/genreById.handler
events:
- http:
path: movies/genres/{id}
- httpApi:
path: /movies/genres/{id}
method: get
cors: true
authorizer:
name: simpleAuthorizerFunc
19 changes: 5 additions & 14 deletions src/handlers/auth/authorizer.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
import { APIGatewayRequestAuthorizerHandler } from 'aws-lambda';
import { isTokenValid } from '@utils/api/apiAuth';
import { APIGatewayRequestSimpleAuthorizerHandlerV2 } from 'aws-lambda';

export const handler: APIGatewayRequestAuthorizerHandler = async (event) => {
const authorization = event.headers?.['Authorization'] || '';
export const handler: APIGatewayRequestSimpleAuthorizerHandlerV2 = async (event) => {
const authorization = event.headers?.['authorization'] || '';

// removing the initial "Bearer "
const token = authorization.substring(7);
const isAuthorized = isTokenValid(token);

// ref: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html#http-api-lambda-authorizer.payload-format-response
return {
principalId: 'simpleAuthorizerPrincipal', // principal user identification associated with the token sent by the client - let's hardcode for simplicity
policyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: isAuthorized ? 'Allow' : 'Deny',
Action: ['execute-api:Invoke'],
Resource: ['*'], // all resources in this project - for this purpose this is fine
},
],
},
isAuthorized: isAuthorized,
context: {},
};
};
11 changes: 6 additions & 5 deletions src/handlers/genres/genreById.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import getGenreById from '@models/Genre/getById';
import { GenreResponse } from '@customTypes/apiResponse';
import { notFoundResponse, serverErrorResponse } from '@utils/api/apiResponses';
import {
mountSuccessResponse,
notFoundResponse,
serverErrorResponse,
} from '@utils/api/apiResponses';

export const handler: APIGatewayProxyHandler = async (event) => {
const genreId = event?.pathParameters?.id;
Expand All @@ -25,10 +29,7 @@ export const handler: APIGatewayProxyHandler = async (event) => {
totalMovies: movies?.length || 0,
};

return {
statusCode: 200,
body: JSON.stringify(result),
};
return mountSuccessResponse(result);
} catch (err) {
return serverErrorResponse;
}
Expand Down
7 changes: 2 additions & 5 deletions src/handlers/genres/moviesByGenre.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import getAllGenre from '@models/Genre/getAll';
import { serverErrorResponse } from '@utils/api/apiResponses';
import { mountSuccessResponse, serverErrorResponse } from '@utils/api/apiResponses';
import { DEFAULT_CONTENTFUL_LIMIT } from '@utils/contentful';

export const handler: APIGatewayProxyHandler = async (event) => {
Expand All @@ -21,10 +21,7 @@ export const handler: APIGatewayProxyHandler = async (event) => {
try {
const result = await getAllGenre({ page, limit });

return {
statusCode: 200,
body: JSON.stringify(result),
};
return mountSuccessResponse(result);
} catch (err) {
return serverErrorResponse;
}
Expand Down
6 changes: 3 additions & 3 deletions src/handlers/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ export const apolloServer = new ApolloServer<MyContext>({
message = error.message;
}

if (process.env.SLS_STAGE !== 'prod') {
if (process.env.SLS_STAGE !== 'production') {
return { ...formattedError, message };
}

return { message: message.message || message };
},
introspection: process.env.SLS_STAGE !== 'prod',
introspection: true,
});

export const server = startServerAndCreateLambdaHandler<MyContext>(apolloServer, {
context: async ({ event }) => {
// ref: https://www.apollographql.com/docs/apollo-server/security/authentication/#api-wide-authorization
const authorization = event.headers?.['Authorization'] || '';
const authorization = event.headers?.['authorization'] || '';

if (!authorization) {
throw new GraphQLError('User is not authorized to access this resource', {
Expand Down
7 changes: 2 additions & 5 deletions src/handlers/movies/getMovieTitles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import getAllMovies from '@models/Movie/getAll';
import { serverErrorResponse } from '@utils/api/apiResponses';
import { mountSuccessResponse, serverErrorResponse } from '@utils/api/apiResponses';
import { DEFAULT_CONTENTFUL_LIMIT } from '@utils/contentful';

export const handler: APIGatewayProxyHandler = async (event) => {
Expand All @@ -21,10 +21,7 @@ export const handler: APIGatewayProxyHandler = async (event) => {
try {
const result = await getAllMovies({ page, limit, select: ['sys.id', 'fields.title'] });

return {
statusCode: 200,
body: JSON.stringify(result),
};
return mountSuccessResponse(result);
} catch (err) {
return serverErrorResponse;
}
Expand Down
19 changes: 7 additions & 12 deletions src/handlers/movies/getMovies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import getAllGenres from '@models/Genre/getAll';
import { Movie } from '@customTypes/movie';
import { DEFAULT_CONTENTFUL_LIMIT } from '@utils/contentful';
import { CONTENTFUL_INCLUDE, ContentfulIncludeOptions } from '@customTypes/contentful';
import { notFoundResponse, serverErrorResponse } from '@utils/api/apiResponses';
import {
mountSuccessResponse,
notFoundResponse,
serverErrorResponse,
} from '@utils/api/apiResponses';

type SearchFilters = {
page?: number;
Expand Down Expand Up @@ -58,21 +62,12 @@ export const handler: APIGatewayProxyHandler = async (event) => {
return notFoundResponse;
}

return {
statusCode: 200,
body: JSON.stringify({
data: response.result,
totalPages: response.totalPages,
}),
};
return mountSuccessResponse({ data: response?.result, totalPages: response?.totalPages });
}

const entries = await getAllMovies(searchFilters);

return {
statusCode: 200,
body: JSON.stringify(entries),
};
return mountSuccessResponse(entries);
} catch (error: unknown) {
console.error('Error fetching movies:', error);
return serverErrorResponse;
Expand Down
15 changes: 10 additions & 5 deletions src/handlers/movies/movieById.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import { notFoundResponse, serverErrorResponse } from '@utils/api/apiResponses';
import {
mountSuccessResponse,
notFoundResponse,
serverErrorResponse,
} from '@utils/api/apiResponses';
import getMovieById from '@models/Movie/getById';
import { CONTENTFUL_INCLUDE } from '@customTypes/contentful';
import { isCustomContentfulError } from '@utils/api/utils';
Expand All @@ -16,10 +20,11 @@ export const handler: APIGatewayProxyHandler = async (event) => {
include: CONTENTFUL_INCLUDE.genres,
});

return {
statusCode: 200,
body: JSON.stringify(movie),
};
if (!movie) {
return notFoundResponse;
}

return mountSuccessResponse(movie);
} catch (error: unknown) {
console.error('Error fetching movies:', error);

Expand Down
11 changes: 11 additions & 0 deletions src/utils/api/apiResponses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,14 @@ export const serverErrorResponse: APIGatewayProxyResult = {
statusCode: 500,
body: JSON.stringify({ error: 'Internal Server Error' }),
};

export const mountSuccessResponse = (bodyObject: object): APIGatewayProxyResult => {
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true,
},
body: JSON.stringify(bodyObject),
};
};

0 comments on commit 931cce1

Please sign in to comment.