Updates and added SES client.

This commit is contained in:
Jesse Brault 2026-01-11 13:22:48 -06:00
parent ae4c215e5e
commit a4a4f55d08
7 changed files with 1457 additions and 33 deletions

View File

@ -3,7 +3,7 @@ import * as cdk from 'aws-cdk-lib/core';
import { JbApiAwsStack } from '../lib/jb-api-aws-stack';
const app = new cdk.App();
new JbApiAwsStack(app, 'JbApiAwsStack', {
new JbApiAwsStack(app, 'JbApi', {
/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,

View File

@ -1,10 +1,19 @@
import * as cdk from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { ContactConstruct } from './lambda/contact/ContactConstruct';
import { DomainName } from 'aws-cdk-lib/aws-apigateway';
export class JbApiAwsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new ContactConstruct(this, id);
const apiDomainName = DomainName.fromDomainNameAttributes(this, id, {
domainName: 'api.jessebrault.com',
domainNameAliasTarget:
'd-fax16c4l5l.execute-api.us-east-2.amazonaws.com',
domainNameAliasHostedZoneId: 'ZOJJZC49E0EPZ'
});
new ContactConstruct(this, 'ContactConstruct', { apiDomainName });
}
}

View File

@ -0,0 +1,132 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context
} from 'aws-lambda';
import {
SendEmailCommand,
SESClient,
SESClientConfig
} from '@aws-sdk/client-ses';
interface ContactRequest {
name: string;
email: string;
institution: string;
message: string;
}
interface ValidationError {
field: keyof ContactRequest;
message: string;
}
interface ValidationErrorsResponse {
errors: ValidationError[];
}
const sesClient = new SESClient({
region: 'us-east-2'
} satisfies SESClientConfig);
export async function handler(
event: APIGatewayProxyEvent,
context: Context
): Promise<APIGatewayProxyResult> {
if (event.body === null) {
return {
statusCode: 400,
body: JSON.stringify({
message: 'ContactRequest body required.'
})
};
}
const { name, email, institution, message } = JSON.parse(
event.body
) as ContactRequest;
const errors: ValidationError[] = [];
// name
if (!name) {
errors.push({
field: 'name',
message: 'Name is required.'
});
} else if (name.trim().length === 0) {
errors.push({
field: 'name',
message: 'Name may not be blank.'
});
}
// email
if (!email) {
errors.push({
field: 'email',
message: 'Email is required.'
});
} else if (email.trim().length === 0) {
errors.push({
field: 'email',
message: 'Email may not be blank.'
});
}
// message
if (!message) {
errors.push({
field: 'message',
message: 'Message is required.'
});
} else if (message.trim().length === 0) {
errors.push({
field: 'message',
message: 'Message may not be blank.'
});
}
if (errors.length) {
return {
statusCode: 400,
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({ errors } satisfies ValidationErrorsResponse)
};
} else {
const sendEmailCommand = new SendEmailCommand({
Source: 'noreply@api.jessebrault.com',
Destination: {
ToAddresses: ['jesse@jessebrault.com']
},
Message: {
Subject: {
Charset: 'UTF-8',
Data: 'Contact request'
},
Body: {
Text: {
Charset: 'utf-8',
Data: `
Contact Request from jessebrault.com
From ${name}
Email ${email}
Institution ${institution ?? '<none>'}
--- Message ---
${message}
`.trim()
}
}
}
});
await sesClient.send(sendEmailCommand);
return {
statusCode: 200,
body: JSON.stringify({
message: 'Success'
})
};
}
}

View File

@ -1,16 +0,0 @@
import { APIGatewayEvent, Context, APIGatewayProxyResult } from 'aws-lambda';
export async function handler(
event: APIGatewayEvent,
context: Context
): Promise<APIGatewayProxyResult> {
return {
statusCode: 200,
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({
message: 'Hello, World!'
})
};
}

View File

@ -1,22 +1,38 @@
import { Construct } from 'constructs';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import {
BasePathMapping,
IDomainName,
LambdaIntegration,
RestApi
} from 'aws-cdk-lib/aws-apigateway';
export interface ContactConstructProps {
apiDomainName: IDomainName;
}
export class ContactConstruct extends Construct {
constructor(scope: Construct, id: string) {
constructor(scope: Construct, id: string, props: ContactConstructProps) {
super(scope, id);
const contactLambda = new nodejs.NodejsFunction(this, 'contact', {
const contactServiceRestApi = new RestApi(this, 'ContactService', {
description: 'The ContactService rest api.'
});
new BasePathMapping(this, 'ContactServiceMapping', {
domainName: props.apiDomainName,
restApi: contactServiceRestApi,
basePath: 'contact'
});
// contact endpoint
const contactLambda = new NodejsFunction(this, 'Contact', {
description: 'The lambda for the contact endpoint.',
runtime: lambda.Runtime.NODEJS_24_X
});
const api = new apigateway.LambdaRestApi(this, 'ContactApi', {
handler: contactLambda,
proxy: false
});
const contactResource = api.root.addResource('contact');
contactResource.addMethod('GET');
const contactIntegration = new LambdaIntegration(contactLambda);
const contactResource =
contactServiceRestApi.root.addResource('contact');
contactResource.addMethod('POST', contactIntegration);
}
}

1288
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@
"typescript": "~5.9.3"
},
"dependencies": {
"@aws-sdk/client-ses": "^3.966.0",
"aws-cdk-lib": "^2.232.2",
"constructs": "^10.0.0"
}