mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-09-30 22:53:13 +00:00
Phase 0 of proposal-system: complete project setup including: - CDK infrastructure (3 stacks: foundation, compute, frontend) - .NET 8 solution with Clean Architecture (Domain, Application, Infrastructure, Api) - EF Core data model (PostgreSQL) with all entities - Python Lambda placeholders (pdf-extract, pdf-generate, library-ingest) - React 19 web frontend scaffold (Vite + MUI) - React Native mobile placeholder - Shared TypeScript API contracts - GitHub Actions CI/CD (ci.yaml + deploy.yaml) - OIDC deploy role (githubdeploy-proposal-system) - Dependabot configuration - Cognito User Pool with Google OAuth, PKCE clients, groups
199 lines
7 KiB
TypeScript
199 lines
7 KiB
TypeScript
import * as cdk from 'aws-cdk-lib';
|
|
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2';
|
|
import * as apigatewayv2Integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
|
|
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
import * as sqs from 'aws-cdk-lib/aws-sqs';
|
|
import * as cognito from 'aws-cdk-lib/aws-cognito';
|
|
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
|
|
import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources';
|
|
import { Construct } from 'constructs';
|
|
|
|
export interface ComputeStackProps extends cdk.StackProps {
|
|
vpc: ec2.IVpc;
|
|
lambdaSecurityGroup: ec2.ISecurityGroup;
|
|
dbSecret: secretsmanager.ISecret;
|
|
uploadsBucket: s3.IBucket;
|
|
generatedBucket: s3.IBucket;
|
|
libraryBucket: s3.IBucket;
|
|
jobsQueue: sqs.IQueue;
|
|
userPool: cognito.IUserPool;
|
|
}
|
|
|
|
export class ComputeStack extends cdk.Stack {
|
|
constructor(scope: Construct, id: string, props: ComputeStackProps) {
|
|
super(scope, id, props);
|
|
|
|
const privateSubnets = { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS };
|
|
|
|
// .NET 8 API Lambda
|
|
const apiFunction = new lambda.Function(this, 'ApiFunction', {
|
|
functionName: 'proposal-system-api',
|
|
runtime: lambda.Runtime.DOTNET_8,
|
|
architecture: lambda.Architecture.ARM_64,
|
|
handler: 'ProposalSystem.Api',
|
|
code: lambda.Code.fromAsset('../api/src/ProposalSystem.Api/bin/Release/net8.0/linux-arm64/publish'),
|
|
memorySize: 1024,
|
|
timeout: cdk.Duration.seconds(30),
|
|
vpc: props.vpc,
|
|
vpcSubnets: privateSubnets,
|
|
securityGroups: [props.lambdaSecurityGroup],
|
|
environment: {
|
|
ASPNETCORE_ENVIRONMENT: 'Production',
|
|
DB_SECRET_ARN: props.dbSecret.secretArn,
|
|
UPLOADS_BUCKET: props.uploadsBucket.bucketName,
|
|
GENERATED_BUCKET: props.generatedBucket.bucketName,
|
|
LIBRARY_BUCKET: props.libraryBucket.bucketName,
|
|
JOBS_QUEUE_URL: props.jobsQueue.queueUrl,
|
|
},
|
|
tracing: lambda.Tracing.ACTIVE,
|
|
});
|
|
|
|
// API Lambda permissions
|
|
props.dbSecret.grantRead(apiFunction);
|
|
props.uploadsBucket.grantReadWrite(apiFunction);
|
|
props.generatedBucket.grantRead(apiFunction);
|
|
props.jobsQueue.grantSendMessages(apiFunction);
|
|
|
|
apiFunction.addToRolePolicy(new iam.PolicyStatement({
|
|
actions: ['cognito-idp:AdminGetUser', 'cognito-idp:AdminListGroupsForUser'],
|
|
resources: [props.userPool.userPoolArn],
|
|
}));
|
|
|
|
// API Gateway HTTP API
|
|
const httpApi = new apigatewayv2.HttpApi(this, 'HttpApi', {
|
|
apiName: 'proposal-system-gateway',
|
|
corsPreflight: {
|
|
allowOrigins: [
|
|
'https://proposals.seahaven.com',
|
|
'http://localhost:5173',
|
|
],
|
|
allowMethods: [
|
|
apigatewayv2.CorsHttpMethod.GET,
|
|
apigatewayv2.CorsHttpMethod.POST,
|
|
apigatewayv2.CorsHttpMethod.PUT,
|
|
apigatewayv2.CorsHttpMethod.DELETE,
|
|
apigatewayv2.CorsHttpMethod.OPTIONS,
|
|
],
|
|
allowHeaders: ['Authorization', 'Content-Type', 'X-Requested-With'],
|
|
maxAge: cdk.Duration.hours(1),
|
|
},
|
|
});
|
|
|
|
const apiIntegration = new apigatewayv2Integrations.HttpLambdaIntegration(
|
|
'ApiIntegration',
|
|
apiFunction
|
|
);
|
|
|
|
httpApi.addRoutes({
|
|
path: '/{proxy+}',
|
|
methods: [apigatewayv2.HttpMethod.ANY],
|
|
integration: apiIntegration,
|
|
});
|
|
|
|
// Python Lambda: PDF Extract
|
|
const pdfExtractFunction = new lambda.Function(this, 'PdfExtractFunction', {
|
|
functionName: 'proposal-system-pdf-extract',
|
|
runtime: lambda.Runtime.PYTHON_3_12,
|
|
architecture: lambda.Architecture.ARM_64,
|
|
handler: 'app.handler',
|
|
code: lambda.Code.fromAsset('../lambdas/pdf-extract'),
|
|
memorySize: 1024,
|
|
timeout: cdk.Duration.seconds(120),
|
|
vpc: props.vpc,
|
|
vpcSubnets: privateSubnets,
|
|
securityGroups: [props.lambdaSecurityGroup],
|
|
environment: {
|
|
UPLOADS_BUCKET: props.uploadsBucket.bucketName,
|
|
API_BASE_URL: httpApi.apiEndpoint,
|
|
},
|
|
});
|
|
|
|
props.uploadsBucket.grantRead(pdfExtractFunction);
|
|
pdfExtractFunction.addToRolePolicy(new iam.PolicyStatement({
|
|
actions: ['bedrock:InvokeModel'],
|
|
resources: ['*'],
|
|
}));
|
|
|
|
// Python Lambda: PDF Generate
|
|
const pdfGenerateFunction = new lambda.Function(this, 'PdfGenerateFunction', {
|
|
functionName: 'proposal-system-pdf-generate',
|
|
runtime: lambda.Runtime.PYTHON_3_12,
|
|
architecture: lambda.Architecture.ARM_64,
|
|
handler: 'app.handler',
|
|
code: lambda.Code.fromAsset('../lambdas/pdf-generate'),
|
|
memorySize: 512,
|
|
timeout: cdk.Duration.seconds(30),
|
|
vpc: props.vpc,
|
|
vpcSubnets: privateSubnets,
|
|
securityGroups: [props.lambdaSecurityGroup],
|
|
environment: {
|
|
GENERATED_BUCKET: props.generatedBucket.bucketName,
|
|
API_BASE_URL: httpApi.apiEndpoint,
|
|
},
|
|
});
|
|
|
|
props.generatedBucket.grantWrite(pdfGenerateFunction);
|
|
|
|
// Python Lambda: Library Ingest
|
|
const libraryIngestFunction = new lambda.Function(this, 'LibraryIngestFunction', {
|
|
functionName: 'proposal-system-library-ingest',
|
|
runtime: lambda.Runtime.PYTHON_3_12,
|
|
architecture: lambda.Architecture.ARM_64,
|
|
handler: 'app.handler',
|
|
code: lambda.Code.fromAsset('../lambdas/library-ingest'),
|
|
memorySize: 512,
|
|
timeout: cdk.Duration.seconds(60),
|
|
vpc: props.vpc,
|
|
vpcSubnets: privateSubnets,
|
|
securityGroups: [props.lambdaSecurityGroup],
|
|
environment: {
|
|
LIBRARY_BUCKET: props.libraryBucket.bucketName,
|
|
API_BASE_URL: httpApi.apiEndpoint,
|
|
},
|
|
});
|
|
|
|
props.libraryBucket.grantWrite(libraryIngestFunction);
|
|
libraryIngestFunction.addToRolePolicy(new iam.PolicyStatement({
|
|
actions: ['bedrock:StartIngestionJob'],
|
|
resources: ['*'],
|
|
}));
|
|
|
|
// SQS Event Source for Python Lambdas
|
|
// All three consume from the same queue, routed by message attributes
|
|
// For now, use a single consumer that routes internally
|
|
// TODO: Phase 4 will refine this to use message filtering or separate queues per function
|
|
pdfExtractFunction.addEventSource(new lambdaEventSources.SqsEventSource(props.jobsQueue, {
|
|
batchSize: 1,
|
|
filters: [
|
|
lambda.FilterCriteria.filter({
|
|
body: { jobType: lambda.FilterRule.isEqual('pdf-extract') },
|
|
}),
|
|
],
|
|
}));
|
|
|
|
pdfGenerateFunction.addEventSource(new lambdaEventSources.SqsEventSource(props.jobsQueue, {
|
|
batchSize: 1,
|
|
filters: [
|
|
lambda.FilterCriteria.filter({
|
|
body: { jobType: lambda.FilterRule.isEqual('pdf-generate') },
|
|
}),
|
|
],
|
|
}));
|
|
|
|
libraryIngestFunction.addEventSource(new lambdaEventSources.SqsEventSource(props.jobsQueue, {
|
|
batchSize: 1,
|
|
filters: [
|
|
lambda.FilterCriteria.filter({
|
|
body: { jobType: lambda.FilterRule.isEqual('library-ingest') },
|
|
}),
|
|
],
|
|
}));
|
|
|
|
// Outputs
|
|
new cdk.CfnOutput(this, 'ApiEndpoint', { value: httpApi.apiEndpoint });
|
|
new cdk.CfnOutput(this, 'ApiFunctionArn', { value: apiFunction.functionArn });
|
|
}
|
|
}
|