Skip to content

Commit 0d5b6d6

Browse files
wip
1 parent 813c409 commit 0d5b6d6

File tree

8 files changed

+456
-164
lines changed

8 files changed

+456
-164
lines changed

infrastructure/terraform/components/api/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ No requirements.
3434
|------|--------|---------|
3535
| <a name="module_authorizer_lambda"></a> [authorizer\_lambda](#module\_authorizer\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip | n/a |
3636
| <a name="module_domain_truststore"></a> [domain\_truststore](#module\_domain\_truststore) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.zip | n/a |
37+
| <a name="module_get_letter_data"></a> [get\_letter\_data](#module\_get\_letter\_data) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip | n/a |
3738
| <a name="module_get_letters"></a> [get\_letters](#module\_get\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip | n/a |
3839
| <a name="module_kms"></a> [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-kms.zip | n/a |
3940
| <a name="module_logging_bucket"></a> [logging\_bucket](#module\_logging\_bucket) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.zip | n/a |

infrastructure/terraform/components/api/locals.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ locals {
99
AWS_REGION = var.region
1010
AUTHORIZER_LAMBDA_ARN = module.authorizer_lambda.function_arn
1111
GET_LETTERS_LAMBDA_ARN = module.get_letters.function_arn
12+
GET_LETTER_DATA_LAMBDA_ARN = module.get_letters.function_arn
1213
PATCH_LETTER_LAMBDA_ARN = module.patch_letter.function_arn
1314
})
1415

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
module "get_letter_data" {
2+
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip"
3+
4+
function_name = "get_letter_data"
5+
description = "Get the letter data"
6+
7+
aws_account_id = var.aws_account_id
8+
component = var.component
9+
environment = var.environment
10+
project = var.project
11+
region = var.region
12+
group = var.group
13+
14+
log_retention_in_days = var.log_retention_in_days
15+
kms_key_arn = module.kms.key_arn
16+
17+
iam_policy_document = {
18+
body = data.aws_iam_policy_document.get_letter_data_lambda.json
19+
}
20+
21+
function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
22+
function_code_base_path = local.aws_lambda_functions_dir_path
23+
function_code_dir = "api-handler/dist"
24+
function_include_common = true
25+
handler_function_name = "getLetterData"
26+
runtime = "nodejs22.x"
27+
memory = 128
28+
timeout = 5
29+
log_level = var.log_level
30+
31+
force_lambda_code_deploy = var.force_lambda_code_deploy
32+
enable_lambda_insights = false
33+
34+
send_to_firehose = true
35+
log_destination_arn = local.destination_arn
36+
log_subscription_role_arn = local.acct.log_subscription_role_arn
37+
38+
lambda_env_vars = merge(local.common_lambda_env_vars, {})
39+
}
40+
41+
data "aws_iam_policy_document" "get_letter_data_lambda" {
42+
statement {
43+
sid = "KMSPermissions"
44+
effect = "Allow"
45+
46+
actions = [
47+
"kms:Decrypt",
48+
"kms:GenerateDataKey",
49+
]
50+
51+
resources = [
52+
module.kms.key_arn, ## Requires shared kms module
53+
]
54+
}
55+
56+
statement {
57+
sid = "AllowDynamoDBAccess"
58+
effect = "Allow"
59+
60+
actions = [
61+
"dynamodb:BatchGetItem",
62+
"dynamodb:GetItem",
63+
"dynamodb:Query",
64+
"dynamodb:Scan",
65+
]
66+
67+
resources = [
68+
aws_dynamodb_table.letters.arn,
69+
"${aws_dynamodb_table.letters.arn}/index/supplierStatus-index"
70+
]
71+
}
72+
}

infrastructure/terraform/components/api/resources/spec.tmpl.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,34 @@
5454
}
5555
},
5656
"/letters/{id}": {
57+
"get": {
58+
"description": "Returns 302 with pre-signed URL to the letter data",
59+
"responses": {
60+
"302": {
61+
"description": "Found"
62+
}
63+
},
64+
"security": [
65+
{
66+
"LambdaAuthorizer": []
67+
}
68+
],
69+
"summary": "Get letter data",
70+
"x-amazon-apigateway-integration": {
71+
"contentHandling": "CONVERT_TO_TEXT",
72+
"credentials": "${APIG_EXECUTION_ROLE_ARN}",
73+
"httpMethod": "POST",
74+
"passthroughBehavior": "WHEN_NO_TEMPLATES",
75+
"responses": {
76+
".*": {
77+
"statusCode": "200"
78+
}
79+
},
80+
"timeoutInMillis": 29000,
81+
"type": "AWS_PROXY",
82+
"uri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${GET_LETTER_DATA_LAMBDA_ARN}/invocations"
83+
}
84+
},
5785
"parameters": [
5886
{
5987
"description": "Unique identifier of this resource",

lambdas/api-handler/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"esbuild": "^0.24.0"
44
},
55
"devDependencies": {
6+
"@aws-sdk/s3-request-presigner": "^3.901.0",
67
"@tsconfig/node22": "^22.0.2",
78
"@types/aws-lambda": "^8.10.148",
89
"@types/jest": "^29.5.14",

lambdas/api-handler/src/handlers/get-letter-data.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,13 @@ import { createLetterRepository } from "../infrastructure/letter-repo-factory";
33
import { assertNotEmpty, lowerCaseKeys } from "../utils/validation";
44
import { ApiErrorDetail } from '../contracts/errors';
55
import { lambdaConfig } from "../config/lambda-config";
6-
import pino from 'pino';
76
import { mapErrorToResponse } from "../mappers/error-mapper";
87
import { ValidationError } from "../errors";
8+
import { getLetterDataUrl } from "../services/letter-operations";
99

1010
const letterRepo = createLetterRepository();
1111

12-
const log = pino();
13-
14-
// The endpoint should only return pending letters for now
15-
const status = "PENDING";
16-
17-
export const getLetters: APIGatewayProxyHandler = async (event) => {
12+
export const getLetterData: APIGatewayProxyHandler = async (event) => {
1813

1914
let correlationId;
2015

@@ -23,16 +18,12 @@ export const getLetters: APIGatewayProxyHandler = async (event) => {
2318
const lowerCasedHeaders = lowerCaseKeys(event.headers);
2419
correlationId = assertNotEmpty(lowerCasedHeaders[lambdaConfig.APIM_CORRELATION_HEADER], new Error("The request headers don't contain the APIM correlation id"));
2520
const supplierId = assertNotEmpty(lowerCasedHeaders[lambdaConfig.SUPPLIER_ID_HEADER], new ValidationError(ApiErrorDetail.InvalidRequestMissingSupplierId));
26-
27-
// assert if letter exists and retrieve
28-
// call service
29-
30-
31-
// map response
21+
const letterId = assertNotEmpty( event.pathParameters?.id, new ValidationError(ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter));
3222

3323
return {
34-
statusCode: 200,
35-
body: JSON.stringify({}, null, 2),
24+
statusCode: 302,
25+
Location: getLetterDataUrl(supplierId, letterId, letterRepo),
26+
body: ''
3627
};
3728
}
3829
catch (error) {

lambdas/api-handler/src/services/letter-operations.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { NotFoundError, ValidationError } from '../errors';
33
import { LetterDto, PatchLetterResponse } from '../contracts/letters';
44
import { mapToPatchLetterResponse } from '../mappers/letter-mapper';
55
import { ApiErrorDetail } from '../contracts/errors';
6+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
7+
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
68

79

810
export const getLettersForSupplier = async (supplierId: string, status: string, limit: number, letterRepo: LetterRepository): Promise<LetterBase[]> => {
@@ -30,7 +32,7 @@ export const patchLetterStatus = async (letterToUpdate: LetterDto, letterId: str
3032
return mapToPatchLetterResponse(updatedLetter);
3133
}
3234

33-
export const getLetterData = async (supplierId: string, letterId: string, letterRepo: LetterRepository): Promise<LetterBase> => {
35+
export const getLetterDataUrl = async (supplierId: string, letterId: string, letterRepo: LetterRepository): Promise<string> => {
3436

3537
let letter;
3638

@@ -43,5 +45,20 @@ export const getLetterData = async (supplierId: string, letterId: string, letter
4345
throw error;
4446
}
4547

48+
return getPresignedUrl(letter.url);
49+
}
50+
51+
async function getPresignedUrl(s3Uri: string) {
52+
const client = new S3Client();
53+
54+
const url = new URL(s3Uri); // works for s3:// URIs
55+
const bucket = url.hostname;
56+
const key = url.pathname.slice(1); // remove leading '/'
57+
58+
const command = new GetObjectCommand({
59+
Bucket: bucket,
60+
Key: key,
61+
});
4662

63+
return await getSignedUrl(client, command, { expiresIn: 3600 });
4764
}

0 commit comments

Comments
 (0)