From 0e91eb5bae0d9cd1c89bd44b63ee6fb6cb5f9a55 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Mon, 17 Mar 2025 10:49:21 -0400 Subject: [PATCH 01/24] Fix error on mock auth.middleware.spec.ts --- backend/src/auth/auth.middleware.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/auth/auth.middleware.spec.ts b/backend/src/auth/auth.middleware.spec.ts index 180c3e97..61950b7b 100644 --- a/backend/src/auth/auth.middleware.spec.ts +++ b/backend/src/auth/auth.middleware.spec.ts @@ -94,7 +94,8 @@ describe('AuthMiddleware', () => { }); it('should not set user when token verification fails', () => { - jwtService.verify.mockImplementation(() => { + // Cast the verify method to any to allow mockImplementation + (jwtService.verify as any).mockImplementation(() => { throw new Error('Invalid token'); }); From 1f0f0a317cbb7cd2942add5d4fa2bbc5e641dbd9 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Tue, 18 Mar 2025 08:18:09 -0400 Subject: [PATCH 02/24] Add cursor rule --- .cursor/rules/general.mdc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.cursor/rules/general.mdc b/.cursor/rules/general.mdc index ffaf7bbd..91789219 100644 --- a/.cursor/rules/general.mdc +++ b/.cursor/rules/general.mdc @@ -1,7 +1,6 @@ --- description: Follow this rules for every request globs: -alwaysApply: true --- - Project Proposal Overview: This project proposes an AI-powered medical report translator that simplifies complex medical documents for patients and caregivers. By leveraging AI-driven text extraction and natural language processing (NLP), the system translates medical jargon into plain language, helping users understand their health conditions, diagnoses, and test results without relying on unreliable online searches. @@ -122,4 +121,8 @@ AWS architecture: [aws architecture.pdf](mdc:docs/assets/aws architecture.pdf) ``` ``` +# Typescript rules + +- Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator. + This rule provides clear guidelines on what units to use, how to convert between units, and why it's important for your project. You can add this to your general rules to ensure consistency across the codebase. From cf425c1acf230f02e777e0958ae11bd7ee010eb1 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Tue, 18 Mar 2025 10:42:25 -0400 Subject: [PATCH 03/24] Fixing tthe backend stack --- backend/bin/backend.ts | 12 ---------- backend/cdk.json | 2 +- backend/infrastructure/reports-table.ts | 30 ------------------------- backend/src/iac/backend-stack.ts | 29 ++++++++++++++++++++++++ backend/src/iac/cdk.index.ts | 26 +++++++++++++++++++++ backend/src/index.ts | 1 - 6 files changed, 56 insertions(+), 44 deletions(-) delete mode 100644 backend/bin/backend.ts delete mode 100644 backend/infrastructure/reports-table.ts create mode 100644 backend/src/iac/cdk.index.ts delete mode 100644 backend/src/index.ts diff --git a/backend/bin/backend.ts b/backend/bin/backend.ts deleted file mode 100644 index 846dd8cf..00000000 --- a/backend/bin/backend.ts +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env node -import 'source-map-support/register'; -import * as cdk from 'aws-cdk-lib'; -import { BackendStack } from '../src'; - -const app = new cdk.App(); -new BackendStack(app, 'MedicalReportsBackendStack', { - env: { - account: process.env.CDK_DEFAULT_ACCOUNT, - region: process.env.CDK_DEFAULT_REGION || 'us-east-1', - }, -}); diff --git a/backend/cdk.json b/backend/cdk.json index bb2d4f4e..eb7abb27 100644 --- a/backend/cdk.json +++ b/backend/cdk.json @@ -1,5 +1,5 @@ { - "app": "npx ts-node --prefer-ts-exts bin/backend.ts", + "app": "npx ts-node --prefer-ts-exts src/iac/cdk.index.ts", "watch": { "include": [ "**" diff --git a/backend/infrastructure/reports-table.ts b/backend/infrastructure/reports-table.ts deleted file mode 100644 index 5788f9a7..00000000 --- a/backend/infrastructure/reports-table.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Construct } from 'constructs'; -import { RemovalPolicy } from 'aws-cdk-lib'; -import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; - -export function createReportsTable(scope: Construct, id: string): Table { - const table = new Table(scope, id, { - tableName: 'reports', - partitionKey: { - name: 'id', - type: AttributeType.STRING, - }, - billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.RETAIN, - }); - - // Add a GSI for querying by userId - table.addGlobalSecondaryIndex({ - indexName: 'userIdIndex', - partitionKey: { - name: 'userId', - type: AttributeType.STRING, - }, - sortKey: { - name: 'createdAt', - type: AttributeType.STRING, - }, - }); - - return table; -} diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index adcd0adf..9a6d50c1 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -6,6 +6,8 @@ import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import * as cognito from 'aws-cdk-lib/aws-cognito'; import * as elbv2_actions from 'aws-cdk-lib/aws-elasticloadbalancingv2-actions'; import { Construct } from 'constructs'; +import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; +import { RemovalPolicy } from 'aws-cdk-lib'; interface BackendStackProps extends cdk.StackProps { environment: string; @@ -170,6 +172,33 @@ export class BackendStack extends cdk.Stack { }), }); + const table = new Table(scope, id, { + tableName: `${appName}ReportsTable`, + partitionKey: { + name: 'userId', + type: AttributeType.STRING, + }, + sortKey: { + name: 'id', + type: AttributeType.STRING, + }, + billingMode: BillingMode.PAY_PER_REQUEST, + removalPolicy: RemovalPolicy.RETAIN, + }); + + // Add a GSI for querying by date (most recent first) + table.addGlobalSecondaryIndex({ + indexName: 'userIdDateIndex', + partitionKey: { + name: 'userId', + type: AttributeType.STRING, + }, + sortKey: { + name: 'date', + type: AttributeType.STRING, + }, + }); + // Add output for Cognito domain new cdk.CfnOutput(this, 'CognitoDomain', { value: `https://${userPoolDomain.domainName}.auth.${this.region}.amazoncognito.com`, diff --git a/backend/src/iac/cdk.index.ts b/backend/src/iac/cdk.index.ts new file mode 100644 index 00000000..7fedf3e2 --- /dev/null +++ b/backend/src/iac/cdk.index.ts @@ -0,0 +1,26 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { BackendStack } from './backend-stack'; + +export * from './backend-stack'; + +// This function can be called to create the stack +export function main() { + const app = new cdk.App(); + + new BackendStack(app, 'MedicalReportsBackendStack', { + environment: 'development', + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION || 'us-east-1', + }, + }); + + return app; +} + +// If this file is run directly, create the stack +if (require.main === module) { + main(); +} diff --git a/backend/src/index.ts b/backend/src/index.ts deleted file mode 100644 index 75d00c9a..00000000 --- a/backend/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './iac/backend-stack'; From 3a88eab5de6ca3351656c03d3ff9719327047e75 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Tue, 18 Mar 2025 11:59:03 -0400 Subject: [PATCH 04/24] Fix DynamoDB table creation --- backend/src/iac/backend-stack.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 9a6d50c1..196dc68f 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -172,8 +172,9 @@ export class BackendStack extends cdk.Stack { }), }); - const table = new Table(scope, id, { - tableName: `${appName}ReportsTable`, + // Create DynamoDB table for reports + const reportsTable = new Table(this, `${appName}ReportsTable`, { + tableName: `${appName}ReportsTable${props.environment}`, partitionKey: { name: 'userId', type: AttributeType.STRING, @@ -183,11 +184,11 @@ export class BackendStack extends cdk.Stack { type: AttributeType.STRING, }, billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.RETAIN, + removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY, }); // Add a GSI for querying by date (most recent first) - table.addGlobalSecondaryIndex({ + reportsTable.addGlobalSecondaryIndex({ indexName: 'userIdDateIndex', partitionKey: { name: 'userId', @@ -199,6 +200,12 @@ export class BackendStack extends cdk.Stack { }, }); + // Add output for the table name + new cdk.CfnOutput(this, 'ReportsTableName', { + value: reportsTable.tableName, + description: 'DynamoDB Reports Table Name', + }); + // Add output for Cognito domain new cdk.CfnOutput(this, 'CognitoDomain', { value: `https://${userPoolDomain.domainName}.auth.${this.region}.amazoncognito.com`, From c0bddd43449c6bd0277aaf6b9671f303ba06dc42 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Tue, 18 Mar 2025 20:08:01 -0400 Subject: [PATCH 05/24] Implement HTTPS --- backend/src/iac/backend-stack.ts | 176 ++++++++++++++++++++----------- 1 file changed, 117 insertions(+), 59 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 196dc68f..e51bb92a 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -5,12 +5,17 @@ import * as logs from 'aws-cdk-lib/aws-logs'; import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import * as cognito from 'aws-cdk-lib/aws-cognito'; import * as elbv2_actions from 'aws-cdk-lib/aws-elasticloadbalancingv2-actions'; +import * as acm from 'aws-cdk-lib/aws-certificatemanager'; +import * as route53 from 'aws-cdk-lib/aws-route53'; +import * as targets from 'aws-cdk-lib/aws-route53-targets'; import { Construct } from 'constructs'; import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; import { RemovalPolicy } from 'aws-cdk-lib'; interface BackendStackProps extends cdk.StackProps { environment: string; + domainName?: string; // Optional domain name for certificate + hostedZoneId?: string; // Optional hosted zone ID for domain } export class BackendStack extends cdk.Stack { @@ -21,23 +26,14 @@ export class BackendStack extends cdk.Stack { const appName = 'AIMedicalReport'; // Look up existing VPC or create a new one - let vpc: ec2.IVpc; - try { - vpc = ec2.Vpc.fromLookup(this, `${appName}VPC`, { - isDefault: false, - vpcName: `${appName}VPC`, - }); - } catch { - vpc = new ec2.Vpc(this, `${appName}VPC`, { - vpcName: `${appName}VPC`, - maxAzs: isProd ? 3 : 2, - }); - } + const vpc: ec2.IVpc = new ec2.Vpc(this, `${appName}VPC`, { + vpcName: `${appName}VPC-${props.environment}`, + maxAzs: 2, + }); - // Look up existing ECS Cluster or create a new one const cluster = new ecs.Cluster(this, `${appName}Cluster`, { vpc, - clusterName: `${appName}Cluster`, + clusterName: `${appName}Cluster-${props.environment}`, containerInsights: true, }); @@ -49,13 +45,17 @@ export class BackendStack extends cdk.Stack { }); // Task Definition - const taskDefinition = new ecs.FargateTaskDefinition(this, `${appName}TaskDef`, { - memoryLimitMiB: isProd ? 1024 : 512, - cpu: isProd ? 512 : 256, - }); + const taskDefinition = new ecs.FargateTaskDefinition( + this, + `${appName}TaskDef-${props.environment}`, + { + memoryLimitMiB: isProd ? 1024 : 512, + cpu: isProd ? 512 : 256, + }, + ); // Container - const container = taskDefinition.addContainer(`${appName}Container`, { + const container = taskDefinition.addContainer(`${appName}Container-${props.environment}`, { image: ecs.ContainerImage.fromAsset('../backend/', { file: 'Dockerfile.prod', buildArgs: { @@ -83,48 +83,87 @@ export class BackendStack extends cdk.Stack { const userPool = cognito.UserPool.fromUserPoolId( this, `${appName}UserPool`, - 'ai-cognito-medical-reports-user-pool', + 'us-east-1_PszlvSmWc', ); // Create a Cognito domain if it doesn't exist - const userPoolDomain = new cognito.UserPoolDomain(this, `${appName}UserPoolDomain`, { - userPool, - cognitoDomain: { - domainPrefix: `${appName.toLowerCase()}-auth`, - }, - }); - - // Create a Cognito User Pool Client for the ALB - const userPoolClient = new cognito.UserPoolClient(this, `${appName}UserPoolClient`, { - userPool, - generateSecret: true, - authFlows: { - userPassword: true, - userSrp: true, - }, - oAuth: { - flows: { - authorizationCodeGrant: true, + const userPoolDomain = new cognito.UserPoolDomain( + this, + `${appName}UserPoolDomain-${props.environment}`, + { + userPool, + cognitoDomain: { + domainPrefix: `${appName.toLowerCase()}-auth-${props.environment}-modus`, }, - callbackUrls: [`http://${appName.toLowerCase()}.example.com/oauth2/idpresponse`], // Update with your actual domain }, - }); + ); // Create ALB - const alb = new elbv2.ApplicationLoadBalancer(this, `${appName}ALB`, { + const alb = new elbv2.ApplicationLoadBalancer(this, `${appName}ALB-${props.environment}`, { vpc, internetFacing: true, loadBalancerName: `${appName}-${props.environment}`, }); + // HTTPS IMPLEMENTATION - CERTIFICATE + let certificate; + if (props.domainName && props.hostedZoneId) { + // If domain name is provided, create or import certificate + const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', { + hostedZoneId: props.hostedZoneId, + zoneName: props.domainName, + }); + + certificate = new acm.Certificate(this, `${appName}Certificate-${props.environment}`, { + domainName: props.domainName, + validation: acm.CertificateValidation.fromDns(hostedZone), + }); + + // Create DNS record for ALB + new route53.ARecord(this, `${appName}AliasRecord-${props.environment}`, { + zone: hostedZone, + recordName: props.domainName, + target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(alb)), + }); + } else { + // For development or when no domain is provided, generate a self-signed certificate + certificate = new acm.Certificate(this, `${appName}SelfSignedCert-${props.environment}`, { + domainName: alb.loadBalancerDnsName, + validation: acm.CertificateValidation.fromDns(), + }); + } + + // Create a Cognito User Pool Client for the ALB + const userPoolClient = new cognito.UserPoolClient( + this, + `${appName}UserPoolClient-${props.environment}`, + { + userPool, + generateSecret: true, + authFlows: { + userPassword: true, + userSrp: true, + }, + oAuth: { + flows: { + authorizationCodeGrant: true, + }, + // Update callback URLs to use HTTPS + callbackUrls: props.domainName + ? [`https://${props.domainName}/oauth2/idpresponse`] + : [`https://${alb.loadBalancerDnsName}/oauth2/idpresponse`], + }, + }, + ); + // Create Fargate Service - const fargateService = new ecs.FargateService(this, `${appName}Service`, { + const fargateService = new ecs.FargateService(this, `${appName}Service-${props.environment}`, { cluster, taskDefinition, desiredCount: isProd ? 2 : 1, assignPublicIp: false, securityGroups: [ - new ec2.SecurityGroup(this, `${appName}ServiceSG`, { + new ec2.SecurityGroup(this, `${appName}ServiceSG-${props.environment}`, { vpc, allowAllOutbound: true, }), @@ -146,23 +185,31 @@ export class BackendStack extends cdk.Stack { } // Create ALB Target Group - const targetGroup = new elbv2.ApplicationTargetGroup(this, `${appName}TargetGroup`, { - vpc, - port: 3000, - protocol: elbv2.ApplicationProtocol.HTTP, - targetType: elbv2.TargetType.IP, - healthCheck: { - path: '/health', - interval: cdk.Duration.seconds(30), - timeout: cdk.Duration.seconds(5), + const targetGroup = new elbv2.ApplicationTargetGroup( + this, + `${appName}TargetGroup-${props.environment}`, + { + vpc, + port: 3000, + protocol: elbv2.ApplicationProtocol.HTTP, + targetType: elbv2.TargetType.IP, + healthCheck: { + path: '/health', + interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(5), + }, + targets: [fargateService], }, - targets: [fargateService], - }); + ); - // Create HTTP Listener - alb.addListener(`${appName}HttpListener`, { - port: 80, - protocol: elbv2.ApplicationProtocol.HTTP, + // HTTPS IMPLEMENTATION - LISTENERS + + // Create HTTPS Listener + alb.addListener(`${appName}HttpsListener-${props.environment}`, { + port: 443, + protocol: elbv2.ApplicationProtocol.HTTPS, + certificates: [certificate], + sslPolicy: elbv2.SslPolicy.RECOMMENDED, defaultAction: new elbv2_actions.AuthenticateCognitoAction({ userPool, userPoolClient, @@ -172,8 +219,19 @@ export class BackendStack extends cdk.Stack { }), }); + // Create HTTP Listener that redirects to HTTPS + alb.addListener(`${appName}HttpListener-${props.environment}`, { + port: 80, + protocol: elbv2.ApplicationProtocol.HTTP, + defaultAction: elbv2.ListenerAction.redirect({ + protocol: elbv2.ApplicationProtocol.HTTPS, + port: '443', + permanent: true, + }), + }); + // Create DynamoDB table for reports - const reportsTable = new Table(this, `${appName}ReportsTable`, { + const reportsTable = new Table(this, `${appName}ReportsTable-${props.environment}`, { tableName: `${appName}ReportsTable${props.environment}`, partitionKey: { name: 'userId', From 8788e0aa1bdc09bd6e43fc5bdf619451c1476f98 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Tue, 18 Mar 2025 20:18:56 -0400 Subject: [PATCH 06/24] Change stack name and cognito prefix --- backend/src/iac/backend-stack.ts | 2 +- backend/src/iac/cdk.index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index e51bb92a..e0910e5f 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -93,7 +93,7 @@ export class BackendStack extends cdk.Stack { { userPool, cognitoDomain: { - domainPrefix: `${appName.toLowerCase()}-auth-${props.environment}-modus`, + domainPrefix: `modus-ai-${props.environment}`, }, }, ); diff --git a/backend/src/iac/cdk.index.ts b/backend/src/iac/cdk.index.ts index 7fedf3e2..ce325479 100644 --- a/backend/src/iac/cdk.index.ts +++ b/backend/src/iac/cdk.index.ts @@ -9,7 +9,7 @@ export * from './backend-stack'; export function main() { const app = new cdk.App(); - new BackendStack(app, 'MedicalReportsBackendStack', { + new BackendStack(app, 'ai-medical-reports-backend-stack', { environment: 'development', env: { account: process.env.CDK_DEFAULT_ACCOUNT, From 1be125bbebb8235ac28aa910b6b2abe51906eeac Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Wed, 19 Mar 2025 10:42:14 -0400 Subject: [PATCH 07/24] Use existing Cognito domain name --- backend/src/iac/backend-stack.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index e0910e5f..559664ee 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -87,15 +87,10 @@ export class BackendStack extends cdk.Stack { ); // Create a Cognito domain if it doesn't exist - const userPoolDomain = new cognito.UserPoolDomain( + const userPoolDomain = cognito.UserPoolDomain.fromDomainName( this, - `${appName}UserPoolDomain-${props.environment}`, - { - userPool, - cognitoDomain: { - domainPrefix: `modus-ai-${props.environment}`, - }, - }, + `${appName}ExistingDomain-${props.environment}`, + 'us-east-1pszlvsmwc' // The domain prefix without the .auth.region.amazoncognito.com part ); // Create ALB From 93b38baadbe9c509a394ee6a37dd969652abbd06 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 09:57:30 -0400 Subject: [PATCH 08/24] Fix warning --- backend/Dockerfile.prod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Dockerfile.prod b/backend/Dockerfile.prod index 0178e183..b61602ec 100644 --- a/backend/Dockerfile.prod +++ b/backend/Dockerfile.prod @@ -1,4 +1,4 @@ -FROM node:20-slim as builder +FROM node:20-slim AS builder ARG NODE_ENV=production ENV NODE_ENV=${NODE_ENV} From 425441fcf5287d1ccdcf334460526e448510d137 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 16:22:46 -0400 Subject: [PATCH 09/24] Remove HTTPS --- backend/src/iac/backend-stack.ts | 133 ++++++++++--------------------- 1 file changed, 42 insertions(+), 91 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 559664ee..e6c6b99d 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -4,10 +4,6 @@ import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import * as cognito from 'aws-cdk-lib/aws-cognito'; -import * as elbv2_actions from 'aws-cdk-lib/aws-elasticloadbalancingv2-actions'; -import * as acm from 'aws-cdk-lib/aws-certificatemanager'; -import * as route53 from 'aws-cdk-lib/aws-route53'; -import * as targets from 'aws-cdk-lib/aws-route53-targets'; import { Construct } from 'constructs'; import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; import { RemovalPolicy } from 'aws-cdk-lib'; @@ -90,43 +86,58 @@ export class BackendStack extends cdk.Stack { const userPoolDomain = cognito.UserPoolDomain.fromDomainName( this, `${appName}ExistingDomain-${props.environment}`, - 'us-east-1pszlvsmwc' // The domain prefix without the .auth.region.amazoncognito.com part + 'us-east-1pszlvsmwc', // The domain prefix without the .auth.region.amazoncognito.com part ); - // Create ALB + // 1. Create ALB const alb = new elbv2.ApplicationLoadBalancer(this, `${appName}ALB-${props.environment}`, { vpc, internetFacing: true, loadBalancerName: `${appName}-${props.environment}`, }); - // HTTPS IMPLEMENTATION - CERTIFICATE - let certificate; - if (props.domainName && props.hostedZoneId) { - // If domain name is provided, create or import certificate - const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', { - hostedZoneId: props.hostedZoneId, - zoneName: props.domainName, - }); + // 2. Create ALB Target Group + const targetGroup = new elbv2.ApplicationTargetGroup( + this, + `${appName}TargetGroup-${props.environment}`, + { + vpc, + port: 3000, + protocol: elbv2.ApplicationProtocol.HTTP, + targetType: elbv2.TargetType.IP, + healthCheck: { + path: '/health', + interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(5), + }, + }, + ); - certificate = new acm.Certificate(this, `${appName}Certificate-${props.environment}`, { - domainName: props.domainName, - validation: acm.CertificateValidation.fromDns(hostedZone), - }); + // 3. HTTP 80 Listener + alb.addListener(`${appName}HttpListener-${props.environment}`, { + port: 80, + protocol: elbv2.ApplicationProtocol.HTTP, + defaultAction: elbv2.ListenerAction.redirect({ + protocol: elbv2.ApplicationProtocol.HTTPS, + }), + }); - // Create DNS record for ALB - new route53.ARecord(this, `${appName}AliasRecord-${props.environment}`, { - zone: hostedZone, - recordName: props.domainName, - target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(alb)), - }); - } else { - // For development or when no domain is provided, generate a self-signed certificate - certificate = new acm.Certificate(this, `${appName}SelfSignedCert-${props.environment}`, { - domainName: alb.loadBalancerDnsName, - validation: acm.CertificateValidation.fromDns(), - }); - } + // 4. Create Fargate Service + const fargateService = new ecs.FargateService(this, `${appName}Service-${props.environment}`, { + cluster, + taskDefinition, + desiredCount: isProd ? 2 : 1, + assignPublicIp: false, + securityGroups: [ + new ec2.SecurityGroup(this, `${appName}ServiceSG-${props.environment}`, { + vpc, + allowAllOutbound: true, + }), + ], + }); + + // 5. Register the service with the Target Group + targetGroup.addTarget(fargateService); // Create a Cognito User Pool Client for the ALB const userPoolClient = new cognito.UserPoolClient( @@ -151,20 +162,6 @@ export class BackendStack extends cdk.Stack { }, ); - // Create Fargate Service - const fargateService = new ecs.FargateService(this, `${appName}Service-${props.environment}`, { - cluster, - taskDefinition, - desiredCount: isProd ? 2 : 1, - assignPublicIp: false, - securityGroups: [ - new ec2.SecurityGroup(this, `${appName}ServiceSG-${props.environment}`, { - vpc, - allowAllOutbound: true, - }), - ], - }); - // Add autoscaling for production if (isProd) { const scaling = fargateService.autoScaleTaskCount({ @@ -179,52 +176,6 @@ export class BackendStack extends cdk.Stack { }); } - // Create ALB Target Group - const targetGroup = new elbv2.ApplicationTargetGroup( - this, - `${appName}TargetGroup-${props.environment}`, - { - vpc, - port: 3000, - protocol: elbv2.ApplicationProtocol.HTTP, - targetType: elbv2.TargetType.IP, - healthCheck: { - path: '/health', - interval: cdk.Duration.seconds(30), - timeout: cdk.Duration.seconds(5), - }, - targets: [fargateService], - }, - ); - - // HTTPS IMPLEMENTATION - LISTENERS - - // Create HTTPS Listener - alb.addListener(`${appName}HttpsListener-${props.environment}`, { - port: 443, - protocol: elbv2.ApplicationProtocol.HTTPS, - certificates: [certificate], - sslPolicy: elbv2.SslPolicy.RECOMMENDED, - defaultAction: new elbv2_actions.AuthenticateCognitoAction({ - userPool, - userPoolClient, - userPoolDomain: userPoolDomain, - next: elbv2.ListenerAction.forward([targetGroup]), - onUnauthenticatedRequest: elbv2.UnauthenticatedAction.AUTHENTICATE, - }), - }); - - // Create HTTP Listener that redirects to HTTPS - alb.addListener(`${appName}HttpListener-${props.environment}`, { - port: 80, - protocol: elbv2.ApplicationProtocol.HTTP, - defaultAction: elbv2.ListenerAction.redirect({ - protocol: elbv2.ApplicationProtocol.HTTPS, - port: '443', - permanent: true, - }), - }); - // Create DynamoDB table for reports const reportsTable = new Table(this, `${appName}ReportsTable-${props.environment}`, { tableName: `${appName}ReportsTable${props.environment}`, From b8e0bf0679d18d1f4706ee56b3308f38f4f27016 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 16:41:25 -0400 Subject: [PATCH 10/24] Fix in backend stack --- backend/src/iac/backend-stack.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index e6c6b99d..7f4d40a9 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -114,29 +114,31 @@ export class BackendStack extends cdk.Stack { ); // 3. HTTP 80 Listener - alb.addListener(`${appName}HttpListener-${props.environment}`, { + const httpListener = alb.addListener(`${appName}HttpListener-${props.environment}`, { port: 80, protocol: elbv2.ApplicationProtocol.HTTP, - defaultAction: elbv2.ListenerAction.redirect({ - protocol: elbv2.ApplicationProtocol.HTTPS, - }), + defaultAction: elbv2.ListenerAction.forward([targetGroup]), }); - // 4. Create Fargate Service + // 4. Create a security group for the Fargate service + const serviceSecurityGroup = new ec2.SecurityGroup(this, `${appName}ServiceSG-${props.environment}`, { + vpc, + allowAllOutbound: true, + }); + + // 5. Create the Fargate service WITHOUT registering it with the target group yet const fargateService = new ecs.FargateService(this, `${appName}Service-${props.environment}`, { cluster, taskDefinition, desiredCount: isProd ? 2 : 1, assignPublicIp: false, - securityGroups: [ - new ec2.SecurityGroup(this, `${appName}ServiceSG-${props.environment}`, { - vpc, - allowAllOutbound: true, - }), - ], + securityGroups: [serviceSecurityGroup], }); - // 5. Register the service with the Target Group + // 6. Add explicit dependency to ensure the listener exists before the service + fargateService.node.addDependency(httpListener); + + // 7. Now register the service with the target group targetGroup.addTarget(fargateService); // Create a Cognito User Pool Client for the ALB From 21a79942724d53da8cf8474be956e9be518cdd76 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 16:52:24 -0400 Subject: [PATCH 11/24] Remove authentication --- backend/src/app.module.spec.ts | 5 - backend/src/app.module.ts | 12 +- backend/src/auth/auth.middleware.spec.ts | 117 ---------------- backend/src/auth/auth.middleware.ts | 38 ------ backend/src/auth/auth.module.ts | 25 ---- backend/src/auth/get-user.decorator.spec.ts | 139 -------------------- backend/src/auth/get-user.decorator.ts | 15 --- backend/src/auth/jwt-auth.guard.spec.ts | 109 --------------- backend/src/auth/jwt-auth.guard.ts | 13 -- backend/src/auth/jwt.service.ts | 124 ----------------- backend/src/auth/jwt.strategy.ts | 25 ---- backend/src/auth/user.controller.ts | 37 ------ backend/src/auth/user.interface.ts | 23 ---- backend/src/iac/backend-stack.ts | 12 +- backend/src/reports/reports.controller.ts | 13 +- backend/src/user/user.controller.spec.ts | 46 +------ backend/src/user/user.controller.ts | 16 +-- backend/src/user/user.module.ts | 2 - 18 files changed, 17 insertions(+), 754 deletions(-) delete mode 100644 backend/src/auth/auth.middleware.spec.ts delete mode 100644 backend/src/auth/auth.middleware.ts delete mode 100644 backend/src/auth/auth.module.ts delete mode 100644 backend/src/auth/get-user.decorator.spec.ts delete mode 100644 backend/src/auth/get-user.decorator.ts delete mode 100644 backend/src/auth/jwt-auth.guard.spec.ts delete mode 100644 backend/src/auth/jwt-auth.guard.ts delete mode 100644 backend/src/auth/jwt.service.ts delete mode 100644 backend/src/auth/jwt.strategy.ts delete mode 100644 backend/src/auth/user.controller.ts delete mode 100644 backend/src/auth/user.interface.ts diff --git a/backend/src/app.module.spec.ts b/backend/src/app.module.spec.ts index 1f4e7ff2..5f4f6974 100644 --- a/backend/src/app.module.spec.ts +++ b/backend/src/app.module.spec.ts @@ -2,7 +2,6 @@ import { Test } from '@nestjs/testing'; import { AppModule } from './app.module'; import { ConfigModule } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; -import { JwtStrategy } from './auth/jwt.strategy'; import { ReportsService } from './reports/reports.service'; import { vi, describe, it, expect } from 'vitest'; @@ -20,10 +19,6 @@ describe('AppModule', () => { AppModule, ], }) - .overrideProvider(JwtStrategy) - .useValue({ - validate: vi.fn().mockImplementation(payload => payload), - }) .overrideProvider(ReportsService) .useValue({ findAll: vi.fn().mockResolvedValue([]), diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 2f739b8c..61dfc9e6 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,4 +1,4 @@ -import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; +import { Module, NestModule } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import configuration from './config/configuration'; import { AppController } from './app.controller'; @@ -6,10 +6,7 @@ import { AppService } from './app.service'; import { AwsSecretsService } from './services/aws-secrets.service'; import { PerplexityService } from './services/perplexity.service'; import { PerplexityController } from './controllers/perplexity/perplexity.controller'; -import { AuthModule } from './auth/auth.module'; import { UserController } from './user/user.controller'; -import { AuthMiddleware } from './auth/auth.middleware'; -import { UserModule } from './user/user.module'; import { ReportsModule } from './reports/reports.module'; @Module({ @@ -18,15 +15,14 @@ import { ReportsModule } from './reports/reports.module'; isGlobal: true, load: [configuration], }), - AuthModule, - UserModule, ReportsModule, ], controllers: [AppController, PerplexityController, UserController], providers: [AppService, AwsSecretsService, PerplexityService], }) export class AppModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer.apply(AuthMiddleware).forRoutes('*'); + configure() { + // Add your middleware configuration here if needed + // If you don't need middleware, you can leave this empty } } diff --git a/backend/src/auth/auth.middleware.spec.ts b/backend/src/auth/auth.middleware.spec.ts deleted file mode 100644 index 61950b7b..00000000 --- a/backend/src/auth/auth.middleware.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AuthMiddleware } from './auth.middleware'; -import { JwtService } from '@nestjs/jwt'; -import { ConfigService } from '@nestjs/config'; -import { vi, describe, it, expect, beforeEach } from 'vitest'; - -describe('AuthMiddleware', () => { - let middleware: AuthMiddleware; - let jwtService: JwtService; - - // Create a mock payload that will be returned by the verify method - const mockPayload = { - sub: 'user123', - username: 'testuser', - email: 'test@example.com', - groups: ['users'], - }; - - beforeEach(async () => { - // Create the testing module with real spies - const verifyMock = vi.fn().mockReturnValue(mockPayload); - const getMock = vi.fn().mockReturnValue('test-secret'); - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AuthMiddleware, - { - provide: JwtService, - useValue: { - verify: verifyMock, - }, - }, - { - provide: ConfigService, - useValue: { - get: getMock, - }, - }, - ], - }).compile(); - - middleware = module.get(AuthMiddleware); - jwtService = module.get(JwtService); - }); - - it('should be defined', () => { - expect(middleware).toBeDefined(); - }); - - it.skip('should set user on request when valid token is provided', () => { - // Create a mock request with a valid token - const mockRequest = { - headers: { - authorization: 'Bearer valid-token', - }, - user: undefined, - }; - const mockResponse = {}; - const mockNext = vi.fn(); - - // Call the middleware - middleware.use(mockRequest as any, mockResponse as any, mockNext); - - // Verify the JwtService.verify was called with the correct arguments - expect(jwtService.verify).toHaveBeenCalledWith('valid-token', { - secret: 'test-secret', - }); - - // Verify the user was set on the request - expect(mockRequest.user).toEqual({ - id: 'user123', - username: 'testuser', - email: 'test@example.com', - groups: ['users'], - }); - - // Verify next was called - expect(mockNext).toHaveBeenCalled(); - }); - - it('should not set user when no token is provided', () => { - const mockRequest = { - headers: {}, - user: undefined, - }; - - const mockResponse = {}; - const mockNext = vi.fn(); - - middleware.use(mockRequest as any, mockResponse as any, mockNext); - - expect(mockRequest.user).toBeUndefined(); - expect(mockNext).toHaveBeenCalled(); - }); - - it('should not set user when token verification fails', () => { - // Cast the verify method to any to allow mockImplementation - (jwtService.verify as any).mockImplementation(() => { - throw new Error('Invalid token'); - }); - - const mockRequest = { - headers: { - authorization: 'Bearer invalid-token', - }, - user: undefined, - }; - - const mockResponse = {}; - const mockNext = vi.fn(); - - middleware.use(mockRequest as any, mockResponse as any, mockNext); - - expect(mockRequest.user).toBeUndefined(); - expect(mockNext).toHaveBeenCalled(); - }); -}); diff --git a/backend/src/auth/auth.middleware.ts b/backend/src/auth/auth.middleware.ts deleted file mode 100644 index a9e15ec2..00000000 --- a/backend/src/auth/auth.middleware.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Injectable, NestMiddleware } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; -import { JwtService } from '@nestjs/jwt'; -import { ConfigService } from '@nestjs/config'; - -@Injectable() -export class AuthMiddleware implements NestMiddleware { - constructor( - private readonly jwtService: JwtService, - private readonly configService: ConfigService, - ) {} - - use(req: Request, res: Response, next: NextFunction) { - const authHeader = req.headers.authorization; - - if (authHeader && authHeader.startsWith('Bearer ')) { - try { - const token = authHeader.substring(7); - const payload = this.jwtService.verify(token, { - secret: this.configService.get('JWT_SECRET'), - }); - - req.user = { - id: payload.sub, - username: payload.username, - email: payload.email, - groups: payload.groups || [], - }; - } catch (error) { - // If token verification fails, we don't set the user - // but we also don't block the request - protected routes - // will be handled by JwtAuthGuard - } - } - - next(); - } -} diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts deleted file mode 100644 index 0427a5be..00000000 --- a/backend/src/auth/auth.module.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Module } from '@nestjs/common'; -import { JwtModule } from '@nestjs/jwt'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { JwtAuthGuard } from './jwt-auth.guard'; -import { JwtStrategy } from './jwt.strategy'; - -@Module({ - imports: [ - ConfigModule, - JwtModule.registerAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: async (configService: ConfigService) => ({ - secret: configService.get('JWT_SECRET'), - signOptions: { - expiresIn: configService.get('JWT_EXPIRES_IN', '1h'), - }, - }), - }), - ], - providers: [JwtAuthGuard, JwtStrategy], - exports: [JwtModule, JwtAuthGuard], - controllers: [], -}) -export class AuthModule {} diff --git a/backend/src/auth/get-user.decorator.spec.ts b/backend/src/auth/get-user.decorator.spec.ts deleted file mode 100644 index 2c538dda..00000000 --- a/backend/src/auth/get-user.decorator.spec.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { ExecutionContext } from '@nestjs/common'; -import { vi, describe, it, expect } from 'vitest'; - -// We need to mock the NestJS decorator factory -vi.mock('@nestjs/common', async () => { - const actual = await vi.importActual('@nestjs/common'); - return { - ...(actual as any), - createParamDecorator: (factory: (data: any, ctx: ExecutionContext) => any) => { - return (data?: string) => { - return { - factory, - data, - }; - }; - }, - }; -}); - -describe('GetUser Decorator', () => { - it('should extract user from request', () => { - // Create mock user - const user = { - id: 'user123', - email: 'test@example.com', - groups: ['users'], - }; - - // Create mock context - const mockExecutionContext = { - switchToHttp: vi.fn().mockReturnValue({ - getRequest: vi.fn().mockReturnValue({ - user, - }), - }), - } as unknown as ExecutionContext; - - // Create a mock factory function that matches the actual implementation - const extractUser = (data: string | undefined, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - const user = request.user; - return data ? user?.[data] : user; - }; - - // Call the function directly instead of trying to access decorator.factory - const result = extractUser(undefined, mockExecutionContext); - - // Verify the result - expect(result).toEqual(user); - expect(mockExecutionContext.switchToHttp).toHaveBeenCalled(); - expect(mockExecutionContext.switchToHttp().getRequest).toHaveBeenCalled(); - }); - - it('should return undefined if user is not in request', () => { - // Create mock context without user - const mockExecutionContext = { - switchToHttp: vi.fn().mockReturnValue({ - getRequest: vi.fn().mockReturnValue({}), - }), - } as unknown as ExecutionContext; - - // Create a mock factory function - const extractUser = (data: string | undefined, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - const user = request.user; - return data ? user?.[data] : user; - }; - - // Call the function directly - const result = extractUser(undefined, mockExecutionContext); - - // Verify the result - expect(result).toBeUndefined(); - }); - - it('should return specific property if data key is provided', () => { - // Create mock user with multiple properties - const user = { - id: 'user123', - email: 'test@example.com', - groups: ['users'], - preferences: { theme: 'dark' }, - }; - - // Create mock context - const mockExecutionContext = { - switchToHttp: vi.fn().mockReturnValue({ - getRequest: vi.fn().mockReturnValue({ - user, - }), - }), - } as unknown as ExecutionContext; - - // Create a mock implementation of the factory function that matches the actual implementation - const mockFactory = (data: string, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - const user = request.user; - - return data ? user?.[data] : user; - }; - - // Call the factory with the context and data key - const result = mockFactory('email', mockExecutionContext); - - // Verify the result is just the email - expect(result).toEqual('test@example.com'); - }); - - it('should return undefined if property does not exist', () => { - // Create mock user - const user = { - id: 'user123', - email: 'test@example.com', - }; - - // Create mock context - const mockExecutionContext = { - switchToHttp: vi.fn().mockReturnValue({ - getRequest: vi.fn().mockReturnValue({ - user, - }), - }), - } as unknown as ExecutionContext; - - // Create a mock implementation of the factory function that matches the actual implementation - const mockFactory = (data: string, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - const user = request.user; - - return data ? user?.[data] : user; - }; - - // Call the factory with the context - const result = mockFactory('nonExistentProperty', mockExecutionContext); - - // Verify the result is undefined - expect(result).toBeUndefined(); - }); -}); diff --git a/backend/src/auth/get-user.decorator.ts b/backend/src/auth/get-user.decorator.ts deleted file mode 100644 index 1187e8a4..00000000 --- a/backend/src/auth/get-user.decorator.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; -import { ApiParam } from '@nestjs/swagger'; - -export const GetUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - return request.user; -}); - -// You can create a helper function to use with ApiParam -export const ApiGetUser = () => - ApiParam({ - name: 'user', - description: 'User object extracted from JWT token', - type: 'object', - }); diff --git a/backend/src/auth/jwt-auth.guard.spec.ts b/backend/src/auth/jwt-auth.guard.spec.ts deleted file mode 100644 index 44054cd2..00000000 --- a/backend/src/auth/jwt-auth.guard.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { JwtAuthGuard } from './jwt-auth.guard'; -import { ExecutionContext } from '@nestjs/common'; -import { JwtModule } from '@nestjs/jwt'; -import { ConfigService } from '@nestjs/config'; -import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; - -// Mock the @nestjs/passport module with all required exports -vi.mock('@nestjs/passport', () => { - return { - AuthGuard: () => { - return class { - canActivate() { - return true; - } - }; - }, - PassportStrategy: () => { - return class {}; - }, - }; -}); - -// Also mock the JwtStrategy to avoid dependency on the mocked PassportStrategy -vi.mock('./jwt.strategy', () => { - return { - JwtStrategy: class { - constructor() {} - validate() { - return { userId: 1 }; - } - }, - }; -}); - -describe('JwtAuthGuard', () => { - let guard: JwtAuthGuard; - - // Mock the process.env.DISABLE_AUTH - const originalEnv = process.env.DISABLE_AUTH; - - beforeEach(async () => { - // Reset the environment variable - process.env.DISABLE_AUTH = 'false'; - - const module: TestingModule = await Test.createTestingModule({ - imports: [ - JwtModule.register({ - secret: 'test-secret', - signOptions: { expiresIn: '1h' }, - }), - ], - providers: [ - JwtAuthGuard, - { - provide: ConfigService, - useValue: { - get: vi.fn().mockReturnValue('test-secret'), - }, - }, - ], - }).compile(); - - guard = module.get(JwtAuthGuard); - }); - - afterEach(() => { - // Restore the original environment variable - process.env.DISABLE_AUTH = originalEnv; - }); - - it('should be defined', () => { - expect(guard).toBeDefined(); - }); - - it('should bypass authentication when DISABLE_AUTH is true', () => { - // Set the environment variable - process.env.DISABLE_AUTH = 'true'; - - const mockContext = {} as ExecutionContext; - - const result = guard.canActivate(mockContext); - - expect(result).toBe(true); - }); - - it('should call super.canActivate when DISABLE_AUTH is false', () => { - // Create a spy on the canActivate method - const superCanActivateSpy = vi.spyOn(guard, 'canActivate'); - - // Mock a complete execution context - const mockContext = { - switchToHttp: () => ({ - getRequest: () => ({ - headers: { - authorization: 'Bearer valid-token', - }, - }), - getResponse: () => ({}), - }), - } as ExecutionContext; - - // Call canActivate - guard.canActivate(mockContext); - - // Verify the spy was called - expect(superCanActivateSpy).toHaveBeenCalledWith(mockContext); - }); -}); diff --git a/backend/src/auth/jwt-auth.guard.ts b/backend/src/auth/jwt-auth.guard.ts deleted file mode 100644 index 15a914f8..00000000 --- a/backend/src/auth/jwt-auth.guard.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Injectable, ExecutionContext } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; - -@Injectable() -export class JwtAuthGuard extends AuthGuard('jwt') { - canActivate(context: ExecutionContext) { - if (process.env.DISABLE_AUTH === 'true') { - return true; - } - - return super.canActivate(context); - } -} diff --git a/backend/src/auth/jwt.service.ts b/backend/src/auth/jwt.service.ts deleted file mode 100644 index 667fe4ab..00000000 --- a/backend/src/auth/jwt.service.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import * as jwt from 'jsonwebtoken'; -import jwkToPem from 'jwk-to-pem'; -import axios from 'axios'; - -interface JWK { - alg: string; - e: string; - kid: string; - kty: 'RSA'; - n: string; - use: string; -} - -interface CognitoJWKS { - keys: JWK[]; -} - -interface DecodedToken { - sub: string; - email: string; - 'cognito:groups'?: string[]; - exp: number; - iat: number; - [key: string]: any; -} - -@Injectable() -export class JwtService { - private readonly logger = new Logger(JwtService.name); - private jwksCache: { [kid: string]: string } = {}; - private jwksCacheTime = 0; - private readonly JWKS_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours - - constructor(private configService: ConfigService) {} - - async verifyToken(token: string): Promise { - try { - // Decode the token without verification to get the key ID (kid) - const decodedToken = jwt.decode(token, { complete: true }); - - if ( - !decodedToken || - typeof decodedToken !== 'object' || - !decodedToken.header || - !decodedToken.header.kid - ) { - throw new Error('Invalid token format'); - } - - const kid = decodedToken.header.kid; - - // Get the JWKs - const jwks = await this.getJwks(); - const pem = jwks[kid]; - - if (!pem) { - throw new Error('Invalid token signature'); - } - - // Verify the token - const region = this.configService.get('AWS_REGION', 'us-east-1'); - const userPoolId = this.configService.get( - 'COGNITO_USER_POOL_ID', - 'ai-cognito-medical-reports-user-pool', - ); - - const verified = jwt.verify(token, pem, { - algorithms: ['RS256'], - issuer: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`, - }) as DecodedToken; - - return { - id: verified.sub, - email: verified.email, - groups: verified['cognito:groups'] || [], - }; - } catch (error: unknown) { - this.logger.error( - `Token verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - ); - throw error; - } - } - - private async getJwks(): Promise<{ [kid: string]: string }> { - const now = Date.now(); - - // Return cached JWKs if they're still valid - if ( - Object.keys(this.jwksCache).length > 0 && - now - this.jwksCacheTime < this.JWKS_CACHE_DURATION - ) { - return this.jwksCache; - } - - try { - const region = this.configService.get('AWS_REGION', 'us-east-1'); - const userPoolId = this.configService.get( - 'COGNITO_USER_POOL_ID', - 'ai-cognito-medical-reports-user-pool', - ); - const jwksUrl = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`; - - const response = await axios.get(jwksUrl); - - const jwks: { [kid: string]: string } = {}; - for (const key of response.data.keys) { - jwks[key.kid] = jwkToPem(key); - } - - this.jwksCache = jwks; - this.jwksCacheTime = now; - - return jwks; - } catch (error: unknown) { - this.logger.error( - `Error fetching JWKs: ${error instanceof Error ? error.message : 'Unknown error'}`, - ); - throw new Error('Failed to fetch JWKs'); - } - } -} diff --git a/backend/src/auth/jwt.strategy.ts b/backend/src/auth/jwt.strategy.ts deleted file mode 100644 index d0df0f40..00000000 --- a/backend/src/auth/jwt.strategy.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { ExtractJwt, Strategy } from 'passport-jwt'; -import { ConfigService } from '@nestjs/config'; -import { User } from './user.interface'; - -@Injectable() -export class JwtStrategy extends PassportStrategy(Strategy) { - constructor(private configService: ConfigService) { - super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - ignoreExpiration: false, - secretOrKey: configService.get('JWT_SECRET'), - }); - } - - async validate(payload: any): Promise { - return { - id: payload.sub, - username: payload.username, - email: payload.email, - groups: payload.groups || [], - }; - } -} diff --git a/backend/src/auth/user.controller.ts b/backend/src/auth/user.controller.ts deleted file mode 100644 index a4066009..00000000 --- a/backend/src/auth/user.controller.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Controller, Get, Param, NotFoundException } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { User } from './user.interface'; - -@ApiTags('users') -@Controller('users') -export class UserController { - // This is a mock implementation - in a real app, you'd inject a service - private users: User[] = [ - { id: '1', username: 'user1', email: 'user1@example.com', groups: ['users'] }, - { id: '2', username: 'user2', email: 'user2@example.com', groups: ['users', 'admin'] }, - ]; - - @ApiOperation({ summary: 'Get all users' }) - @ApiResponse({ status: 200, description: 'Return all users', type: [User] }) - @Get() - findAll(): Promise { - // Return mock data - in a real app, this would come from a service - return Promise.resolve(this.users); - } - - @ApiOperation({ summary: 'Get a user by ID' }) - @ApiResponse({ status: 200, description: 'Return a user by ID', type: User }) - @ApiResponse({ status: 404, description: 'User not found' }) - @Get(':id') - findOne(@Param('id') id: string): Promise { - const user = this.users.find(user => user.id === id); - - if (!user) { - throw new NotFoundException(`User with ID ${id} not found`); - } - - return Promise.resolve(user); - } - - // You can add more endpoints as needed -} diff --git a/backend/src/auth/user.interface.ts b/backend/src/auth/user.interface.ts deleted file mode 100644 index a3a2dcb9..00000000 --- a/backend/src/auth/user.interface.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class User { - @ApiProperty({ description: 'The unique identifier of the user' }) - id: string; - - @ApiProperty({ description: 'The username of the user' }) - username: string; - - @ApiProperty({ description: 'The email of the user' }) - email: string; - - @ApiProperty({ description: 'The groups the user belongs to' }) - groups: string[]; -} - -// Extend Express Request interface to include user property -// Using module augmentation instead of namespace -declare module 'express' { - interface Request { - user?: User; - } -} diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 7f4d40a9..d822b9fe 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -121,10 +121,14 @@ export class BackendStack extends cdk.Stack { }); // 4. Create a security group for the Fargate service - const serviceSecurityGroup = new ec2.SecurityGroup(this, `${appName}ServiceSG-${props.environment}`, { - vpc, - allowAllOutbound: true, - }); + const serviceSecurityGroup = new ec2.SecurityGroup( + this, + `${appName}ServiceSG-${props.environment}`, + { + vpc, + allowAllOutbound: true, + }, + ); // 5. Create the Fargate service WITHOUT registering it with the target group yet const fargateService = new ecs.FargateService(this, `${appName}Service-${props.environment}`, { diff --git a/backend/src/reports/reports.controller.ts b/backend/src/reports/reports.controller.ts index 35ea0db1..e7745ef6 100644 --- a/backend/src/reports/reports.controller.ts +++ b/backend/src/reports/reports.controller.ts @@ -1,13 +1,4 @@ -import { - Controller, - Get, - Patch, - Param, - Body, - Query, - UseGuards, - ValidationPipe, -} from '@nestjs/common'; +import { Controller, Get, Patch, Param, Body, Query, ValidationPipe } from '@nestjs/common'; import { ApiTags, ApiOperation, @@ -16,7 +7,6 @@ import { ApiParam, ApiQuery, } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { ReportsService } from './reports.service'; import { Report } from './models/report.model'; import { GetReportsQueryDto } from './dto/get-reports.dto'; @@ -24,7 +14,6 @@ import { UpdateReportStatusDto } from './dto/update-report-status.dto'; @ApiTags('reports') @Controller('reports') -@UseGuards(JwtAuthGuard) @ApiBearerAuth() export class ReportsController { constructor(private readonly reportsService: ReportsService) {} diff --git a/backend/src/user/user.controller.spec.ts b/backend/src/user/user.controller.spec.ts index af7b0236..9181e5f5 100644 --- a/backend/src/user/user.controller.spec.ts +++ b/backend/src/user/user.controller.spec.ts @@ -1,7 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UserController } from './user.controller'; -import { JwtAuthGuard } from '../auth/jwt-auth.guard'; -import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; describe('UserController', () => { let controller: UserController; @@ -9,14 +8,7 @@ describe('UserController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [UserController], - providers: [ - { - provide: JwtAuthGuard, - useValue: { - canActivate: vi.fn().mockReturnValue(true), - }, - }, - ], + providers: [], }).compile(); controller = module.get(UserController); @@ -25,38 +17,4 @@ describe('UserController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); - - describe('getProfile', () => { - it('should return user profile', () => { - const mockUser = { - id: '123', - username: 'testuser', - email: 'test@example.com', - groups: ['users'], - }; - - const result = controller.getProfile(mockUser); - - expect(result).toEqual({ - message: 'Authentication successful', - user: mockUser, - }); - }); - - it('should handle user with minimal information', () => { - const minimalUser = { - id: '456', - username: 'minimaluser', - email: 'minimal@example.com', - groups: [], - }; - - const result = controller.getProfile(minimalUser); - - expect(result).toEqual({ - message: 'Authentication successful', - user: minimalUser, - }); - }); - }); }); diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index 077bdd46..053e9509 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -1,16 +1,4 @@ -import { Controller, Get, UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '../auth/jwt-auth.guard'; -import { User } from '../auth/user.interface'; -import { GetUser } from '../auth/get-user.decorator'; +import { Controller } from '@nestjs/common'; @Controller('users') -export class UserController { - @UseGuards(JwtAuthGuard) - @Get('profile') - getProfile(@GetUser() user: User) { - return { - message: 'Authentication successful', - user, - }; - } -} +export class UserController {} diff --git a/backend/src/user/user.module.ts b/backend/src/user/user.module.ts index bb8b760a..fc6b0451 100644 --- a/backend/src/user/user.module.ts +++ b/backend/src/user/user.module.ts @@ -1,9 +1,7 @@ import { Module } from '@nestjs/common'; import { UserController } from './user.controller'; -import { AuthModule } from '../auth/auth.module'; @Module({ - imports: [AuthModule], controllers: [UserController], exports: [], }) From c087fbf07b8c3388afe172e11d7fde4d98b9daa1 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 17:15:00 -0400 Subject: [PATCH 12/24] Get env variables --- backend/src/iac/backend-stack.ts | 130 +++++++++++++------------------ 1 file changed, 55 insertions(+), 75 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index d822b9fe..1ad2ca79 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -40,6 +40,51 @@ export class BackendStack extends cdk.Stack { removalPolicy: isProd ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY, }); + // Create DynamoDB table for reports + const reportsTable = new Table(this, `${appName}ReportsTable-${props.environment}`, { + tableName: `${appName}ReportsTable${props.environment}`, + partitionKey: { + name: 'userId', + type: AttributeType.STRING, + }, + sortKey: { + name: 'id', + type: AttributeType.STRING, + }, + billingMode: BillingMode.PAY_PER_REQUEST, + removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY, + }); + + // Add a GSI for querying by date (most recent first) + reportsTable.addGlobalSecondaryIndex({ + indexName: 'userIdDateIndex', + partitionKey: { + name: 'userId', + type: AttributeType.STRING, + }, + sortKey: { + name: 'date', + type: AttributeType.STRING, + }, + }); + + // Look up existing Cognito User Pool + const userPoolId = process.env.AWS_COGNITO_CLIENT_ID || cognito.UserPool.fromUserPoolId( + this, + `${appName}UserPool`, + 'us-east-1_PszlvSmWc', + ).userPoolId; + + // Create a Cognito domain if it doesn't exist + const userPoolDomain = cognito.UserPoolDomain.fromDomainName( + this, + `${appName}ExistingDomain-${props.environment}`, + 'us-east-1pszlvsmwc', // The domain prefix without the .auth.region.amazoncognito.com part + ); + + // Replace the userPoolClient reference with a direct reference to the client ID + const userPoolClientId = process.env.AWS_COGNITO_CLIENT_ID || 'default-client-id'; + // Task Definition const taskDefinition = new ecs.FargateTaskDefinition( this, @@ -59,7 +104,17 @@ export class BackendStack extends cdk.Stack { }, }), environment: { + // Basic environment variables NODE_ENV: props.environment, + PORT: '3000', + + // AWS related + AWS_REGION: this.region, + AWS_COGNITO_USER_POOL_ID: userPoolId, + AWS_COGNITO_CLIENT_ID: userPoolClientId, + DYNAMODB_REPORTS_TABLE: reportsTable.tableName, + + // Perplexity related PERPLEXITY_API_KEY_SECRET_NAME: `medical-reports-explainer/${props.environment}/perplexity-api-key`, PERPLEXITY_MODEL: 'sonar', PERPLEXITY_MAX_TOKENS: '2048', @@ -75,20 +130,6 @@ export class BackendStack extends cdk.Stack { protocol: ecs.Protocol.TCP, }); - // Look up existing Cognito User Pool - const userPool = cognito.UserPool.fromUserPoolId( - this, - `${appName}UserPool`, - 'us-east-1_PszlvSmWc', - ); - - // Create a Cognito domain if it doesn't exist - const userPoolDomain = cognito.UserPoolDomain.fromDomainName( - this, - `${appName}ExistingDomain-${props.environment}`, - 'us-east-1pszlvsmwc', // The domain prefix without the .auth.region.amazoncognito.com part - ); - // 1. Create ALB const alb = new elbv2.ApplicationLoadBalancer(this, `${appName}ALB-${props.environment}`, { vpc, @@ -145,29 +186,6 @@ export class BackendStack extends cdk.Stack { // 7. Now register the service with the target group targetGroup.addTarget(fargateService); - // Create a Cognito User Pool Client for the ALB - const userPoolClient = new cognito.UserPoolClient( - this, - `${appName}UserPoolClient-${props.environment}`, - { - userPool, - generateSecret: true, - authFlows: { - userPassword: true, - userSrp: true, - }, - oAuth: { - flows: { - authorizationCodeGrant: true, - }, - // Update callback URLs to use HTTPS - callbackUrls: props.domainName - ? [`https://${props.domainName}/oauth2/idpresponse`] - : [`https://${alb.loadBalancerDnsName}/oauth2/idpresponse`], - }, - }, - ); - // Add autoscaling for production if (isProd) { const scaling = fargateService.autoScaleTaskCount({ @@ -182,34 +200,6 @@ export class BackendStack extends cdk.Stack { }); } - // Create DynamoDB table for reports - const reportsTable = new Table(this, `${appName}ReportsTable-${props.environment}`, { - tableName: `${appName}ReportsTable${props.environment}`, - partitionKey: { - name: 'userId', - type: AttributeType.STRING, - }, - sortKey: { - name: 'id', - type: AttributeType.STRING, - }, - billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY, - }); - - // Add a GSI for querying by date (most recent first) - reportsTable.addGlobalSecondaryIndex({ - indexName: 'userIdDateIndex', - partitionKey: { - name: 'userId', - type: AttributeType.STRING, - }, - sortKey: { - name: 'date', - type: AttributeType.STRING, - }, - }); - // Add output for the table name new cdk.CfnOutput(this, 'ReportsTableName', { value: reportsTable.tableName, @@ -227,15 +217,5 @@ export class BackendStack extends cdk.Stack { value: alb.loadBalancerDnsName, description: 'Load Balancer DNS Name', }); - - new cdk.CfnOutput(this, 'UserPoolId', { - value: userPool.userPoolId, - description: 'Cognito User Pool ID', - }); - - new cdk.CfnOutput(this, 'UserPoolClientId', { - value: userPoolClient.userPoolClientId, - description: 'Cognito User Pool Client ID', - }); } } From 5352828256f48d485e9786f9c615d7b318395d40 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 17:15:11 -0400 Subject: [PATCH 13/24] Get env variables --- backend/src/iac/backend-stack.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 1ad2ca79..b8e78f63 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -69,11 +69,9 @@ export class BackendStack extends cdk.Stack { }); // Look up existing Cognito User Pool - const userPoolId = process.env.AWS_COGNITO_CLIENT_ID || cognito.UserPool.fromUserPoolId( - this, - `${appName}UserPool`, - 'us-east-1_PszlvSmWc', - ).userPoolId; + const userPoolId = + process.env.AWS_COGNITO_CLIENT_ID || + cognito.UserPool.fromUserPoolId(this, `${appName}UserPool`, 'us-east-1_PszlvSmWc').userPoolId; // Create a Cognito domain if it doesn't exist const userPoolDomain = cognito.UserPoolDomain.fromDomainName( From e8ca2d5b505c4347c61d309d67527ac8979fc444 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 17:39:13 -0400 Subject: [PATCH 14/24] Add healthcheck endpoint --- backend/src/app.module.ts | 4 ++-- backend/src/health/health.controller.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 backend/src/health/health.controller.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 61dfc9e6..2cf9b087 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -8,7 +8,7 @@ import { PerplexityService } from './services/perplexity.service'; import { PerplexityController } from './controllers/perplexity/perplexity.controller'; import { UserController } from './user/user.controller'; import { ReportsModule } from './reports/reports.module'; - +import { HealthController } from './health/health.controller'; @Module({ imports: [ ConfigModule.forRoot({ @@ -17,7 +17,7 @@ import { ReportsModule } from './reports/reports.module'; }), ReportsModule, ], - controllers: [AppController, PerplexityController, UserController], + controllers: [AppController, HealthController, PerplexityController, UserController], providers: [AppService, AwsSecretsService, PerplexityService], }) export class AppModule implements NestModule { diff --git a/backend/src/health/health.controller.ts b/backend/src/health/health.controller.ts new file mode 100644 index 00000000..8d187fdf --- /dev/null +++ b/backend/src/health/health.controller.ts @@ -0,0 +1,9 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('health') +export class HealthController { + @Get() + health() { + return { status: 'ok' }; + } +} From 9010b2eceac20b0ff99e0162e39369faf90cee18 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 17:56:43 -0400 Subject: [PATCH 15/24] Add env variables --- backend/src/iac/backend-stack.ts | 8 ++++---- backend/src/iac/cdk.index.ts | 14 +++++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index b8e78f63..6963f13f 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -12,6 +12,8 @@ interface BackendStackProps extends cdk.StackProps { environment: string; domainName?: string; // Optional domain name for certificate hostedZoneId?: string; // Optional hosted zone ID for domain + cognitoClientId: string; + cognitoUserPoolId: string; } export class BackendStack extends cdk.Stack { @@ -69,9 +71,7 @@ export class BackendStack extends cdk.Stack { }); // Look up existing Cognito User Pool - const userPoolId = - process.env.AWS_COGNITO_CLIENT_ID || - cognito.UserPool.fromUserPoolId(this, `${appName}UserPool`, 'us-east-1_PszlvSmWc').userPoolId; + const userPoolId = props.cognitoUserPoolId || cognito.UserPool.fromUserPoolId(this, `${appName}UserPool`, 'us-east-1_PszlvSmWc').userPoolId; // Create a Cognito domain if it doesn't exist const userPoolDomain = cognito.UserPoolDomain.fromDomainName( @@ -81,7 +81,7 @@ export class BackendStack extends cdk.Stack { ); // Replace the userPoolClient reference with a direct reference to the client ID - const userPoolClientId = process.env.AWS_COGNITO_CLIENT_ID || 'default-client-id'; + const userPoolClientId = props.cognitoClientId; // Task Definition const taskDefinition = new ecs.FargateTaskDefinition( diff --git a/backend/src/iac/cdk.index.ts b/backend/src/iac/cdk.index.ts index ce325479..0011449e 100644 --- a/backend/src/iac/cdk.index.ts +++ b/backend/src/iac/cdk.index.ts @@ -2,19 +2,27 @@ import 'source-map-support/register'; import * as cdk from 'aws-cdk-lib'; import { BackendStack } from './backend-stack'; +import * as dotenv from 'dotenv'; + +// Load environment variables from .env file +dotenv.config(); export * from './backend-stack'; -// This function can be called to create the stack export function main() { const app = new cdk.App(); - new BackendStack(app, 'ai-medical-reports-backend-stack', { - environment: 'development', + console.log('NODE_ENV', process.env.NODE_ENV); + console.log('AWS_COGNITO_CLIENT_ID', process.env.AWS_COGNITO_CLIENT_ID); + + new BackendStack(app, `ai-team-medical-reports-stack-${process.env.NODE_ENV}`, { + environment: process.env.NODE_ENV || 'development', env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION || 'us-east-1', }, + cognitoClientId: process.env.AWS_COGNITO_CLIENT_ID, + cognitoUserPoolId: process.env.AWS_COGNITO_USER_POOL_ID, }); return app; From 740369f9f210bf9504ad1bc575c65697e50f6664 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 17:56:55 -0400 Subject: [PATCH 16/24] Add env variables --- backend/src/iac/backend-stack.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 6963f13f..8c87d174 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -71,7 +71,9 @@ export class BackendStack extends cdk.Stack { }); // Look up existing Cognito User Pool - const userPoolId = props.cognitoUserPoolId || cognito.UserPool.fromUserPoolId(this, `${appName}UserPool`, 'us-east-1_PszlvSmWc').userPoolId; + const userPoolId = + props.cognitoUserPoolId || + cognito.UserPool.fromUserPoolId(this, `${appName}UserPool`, 'us-east-1_PszlvSmWc').userPoolId; // Create a Cognito domain if it doesn't exist const userPoolDomain = cognito.UserPoolDomain.fromDomainName( From 85263b77714a44e249c91a41e90f2176f5cd4047 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 18:01:21 -0400 Subject: [PATCH 17/24] Make sure that cognito env variables are read --- backend/src/iac/backend-stack.ts | 4 ++-- backend/src/iac/cdk.index.ts | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 8c87d174..b50a49be 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -10,10 +10,10 @@ import { RemovalPolicy } from 'aws-cdk-lib'; interface BackendStackProps extends cdk.StackProps { environment: string; - domainName?: string; // Optional domain name for certificate - hostedZoneId?: string; // Optional hosted zone ID for domain cognitoClientId: string; cognitoUserPoolId: string; + domainName?: string; // Optional domain name for certificate + hostedZoneId?: string; // Optional hosted zone ID for domain } export class BackendStack extends cdk.Stack { diff --git a/backend/src/iac/cdk.index.ts b/backend/src/iac/cdk.index.ts index 0011449e..0ed3c2e1 100644 --- a/backend/src/iac/cdk.index.ts +++ b/backend/src/iac/cdk.index.ts @@ -13,7 +13,13 @@ export function main() { const app = new cdk.App(); console.log('NODE_ENV', process.env.NODE_ENV); - console.log('AWS_COGNITO_CLIENT_ID', process.env.AWS_COGNITO_CLIENT_ID); + + const cognitoClientId = process.env.AWS_COGNITO_CLIENT_ID; + const cognitoUserPoolId = process.env.AWS_COGNITO_USER_POOL_ID; + + if (!cognitoClientId || !cognitoUserPoolId) { + throw new Error('AWS_COGNITO_CLIENT_ID and AWS_COGNITO_USER_POOL_ID must be set'); + } new BackendStack(app, `ai-team-medical-reports-stack-${process.env.NODE_ENV}`, { environment: process.env.NODE_ENV || 'development', @@ -21,8 +27,8 @@ export function main() { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION || 'us-east-1', }, - cognitoClientId: process.env.AWS_COGNITO_CLIENT_ID, - cognitoUserPoolId: process.env.AWS_COGNITO_USER_POOL_ID, + cognitoClientId: cognitoClientId, + cognitoUserPoolId: cognitoUserPoolId, }); return app; From eb55e9ca58554e5aa6911d44ec8cc69eccb7311e Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 18:04:14 -0400 Subject: [PATCH 18/24] Fix test file --- backend/src/iac/backend-stack.test.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/src/iac/backend-stack.test.ts b/backend/src/iac/backend-stack.test.ts index 0ee5f6aa..8ec24e98 100644 --- a/backend/src/iac/backend-stack.test.ts +++ b/backend/src/iac/backend-stack.test.ts @@ -10,22 +10,32 @@ describe('BackendStack', () => { let stagingTemplate: Template; let productionTemplate: Template; - beforeEach(() => { + beforeAll(() => { app = new cdk.App(); + + // Common props for all stacks stackProps = { - env: { account: '123456789012', region: 'us-east-1' }, + env: { + account: '123456789012', + region: 'us-east-1', + }, }; - // Create both staging and production stacks for testing + // Create staging stack stagingStack = new BackendStack(app, 'StagingStack', { ...stackProps, environment: 'staging', + cognitoClientId: 'test-client-id', + cognitoUserPoolId: 'test-user-pool-id', }); stagingTemplate = Template.fromStack(stagingStack); + // Create production stack productionStack = new BackendStack(app, 'ProductionStack', { ...stackProps, environment: 'production', + cognitoClientId: 'test-client-id', + cognitoUserPoolId: 'test-user-pool-id', }); productionTemplate = Template.fromStack(productionStack); }); From df51ed52b85b8a969adfb597e2c514d0be19117c Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 18:26:51 -0400 Subject: [PATCH 19/24] Fix complain from SonarQube --- backend/src/iac/cdk.index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/iac/cdk.index.ts b/backend/src/iac/cdk.index.ts index 0ed3c2e1..c04e300b 100644 --- a/backend/src/iac/cdk.index.ts +++ b/backend/src/iac/cdk.index.ts @@ -21,7 +21,7 @@ export function main() { throw new Error('AWS_COGNITO_CLIENT_ID and AWS_COGNITO_USER_POOL_ID must be set'); } - new BackendStack(app, `ai-team-medical-reports-stack-${process.env.NODE_ENV}`, { + void new BackendStack(app, `ai-team-medical-reports-stack-${process.env.NODE_ENV}`, { environment: process.env.NODE_ENV || 'development', env: { account: process.env.CDK_DEFAULT_ACCOUNT, From c184cf120686a36eaf8da45a11df5910855524db Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 20:51:41 -0400 Subject: [PATCH 20/24] Add credentials to DynamoDB --- backend/src/config/configuration.ts | 6 +++- backend/src/reports/reports.service.ts | 38 ++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/backend/src/config/configuration.ts b/backend/src/config/configuration.ts index c6bde2f1..a54b87f0 100644 --- a/backend/src/config/configuration.ts +++ b/backend/src/config/configuration.ts @@ -10,10 +10,14 @@ export default () => ({ secretsManager: { perplexityApiKeySecret: process.env.PERPLEXITY_API_KEY_SECRET_NAME || 'medical-reports-explainer/perplexity-api-key', }, + aws: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + } }, perplexity: { apiBaseUrl: 'https://api.perplexity.ai', model: process.env.PERPLEXITY_MODEL || 'sonar', maxTokens: parseInt(process.env.PERPLEXITY_MAX_TOKENS || '2048', 10), }, -}); \ No newline at end of file +}); diff --git a/backend/src/reports/reports.service.ts b/backend/src/reports/reports.service.ts index 88def05b..5e4c4c0d 100644 --- a/backend/src/reports/reports.service.ts +++ b/backend/src/reports/reports.service.ts @@ -17,10 +17,28 @@ export class ReportsService { private readonly tableName: string; constructor(private configService: ConfigService) { - this.dynamoClient = new DynamoDBClient({ - region: this.configService.get('AWS_REGION', 'us-east-1'), - }); - this.tableName = this.configService.get('DYNAMODB_TABLE_NAME', 'reports'); + const region = this.configService.get('AWS_REGION', 'us-east-1'); + + try { + this.dynamoClient = new DynamoDBClient({ + region: this.configService.get('AWS_REGION', 'us-east-1') + }); + } catch (error: unknown) { + console.error('DynamoDB Client Config:', JSON.stringify(error, null, 2)); + const accessKeyId = this.configService.get('AWS_ACCESS_KEY_ID'); + const secretAccessKey = this.configService.get('AWS_SECRET_ACCESS_KEY'); + + const clientConfig: any = { region }; + + // Only add credentials if both values are present + if (accessKeyId && secretAccessKey) { + clientConfig.credentials = { accessKeyId, secretAccessKey }; + } + + this.dynamoClient = new DynamoDBClient(clientConfig); + } + + this.tableName = this.configService.get('DYNAMODB_REPORTS_TABLE', 'reports'); } async findAll(): Promise { @@ -28,8 +46,16 @@ export class ReportsService { TableName: this.tableName, }); - const response = await this.dynamoClient.send(command); - return (response.Items || []).map(item => unmarshall(item) as Report); + try { + const response = await this.dynamoClient.send(command); + return (response.Items || []).map(item => unmarshall(item) as Report); + } catch (error: unknown) { + console.error('DynamoDB Error Details:', JSON.stringify(error, null, 2)); + if (error instanceof Error && error.name === 'UnrecognizedClientException') { + throw new Error('Invalid AWS credentials. Please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.'); + } + throw error; + } } async findLatest(queryDto: GetReportsQueryDto): Promise { From 7903ad18303126e4195d17fa1ac8aa37b674945d Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 20:51:53 -0400 Subject: [PATCH 21/24] Add credentials to DynamoDB --- backend/src/reports/reports.service.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/backend/src/reports/reports.service.ts b/backend/src/reports/reports.service.ts index 5e4c4c0d..b6bce926 100644 --- a/backend/src/reports/reports.service.ts +++ b/backend/src/reports/reports.service.ts @@ -21,23 +21,23 @@ export class ReportsService { try { this.dynamoClient = new DynamoDBClient({ - region: this.configService.get('AWS_REGION', 'us-east-1') + region: this.configService.get('AWS_REGION', 'us-east-1'), }); } catch (error: unknown) { - console.error('DynamoDB Client Config:', JSON.stringify(error, null, 2)); - const accessKeyId = this.configService.get('AWS_ACCESS_KEY_ID'); - const secretAccessKey = this.configService.get('AWS_SECRET_ACCESS_KEY'); + console.error('DynamoDB Client Config:', JSON.stringify(error, null, 2)); + const accessKeyId = this.configService.get('AWS_ACCESS_KEY_ID'); + const secretAccessKey = this.configService.get('AWS_SECRET_ACCESS_KEY'); - const clientConfig: any = { region }; + const clientConfig: any = { region }; - // Only add credentials if both values are present - if (accessKeyId && secretAccessKey) { - clientConfig.credentials = { accessKeyId, secretAccessKey }; - } - - this.dynamoClient = new DynamoDBClient(clientConfig); + // Only add credentials if both values are present + if (accessKeyId && secretAccessKey) { + clientConfig.credentials = { accessKeyId, secretAccessKey }; } + this.dynamoClient = new DynamoDBClient(clientConfig); + } + this.tableName = this.configService.get('DYNAMODB_REPORTS_TABLE', 'reports'); } @@ -52,7 +52,9 @@ export class ReportsService { } catch (error: unknown) { console.error('DynamoDB Error Details:', JSON.stringify(error, null, 2)); if (error instanceof Error && error.name === 'UnrecognizedClientException') { - throw new Error('Invalid AWS credentials. Please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.'); + throw new Error( + 'Invalid AWS credentials. Please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.', + ); } throw error; } From 95846a89122f4a7cd776fb0921c3e05f68542d12 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 21:14:53 -0400 Subject: [PATCH 22/24] Fix bug in backend reports related to is int check --- backend/src/reports/dto/get-reports.dto.ts | 6 +- backend/src/reports/reports.service.ts | 4 +- frontend/src/common/api/reportService.ts | 71 +++++----------------- 3 files changed, 22 insertions(+), 59 deletions(-) diff --git a/backend/src/reports/dto/get-reports.dto.ts b/backend/src/reports/dto/get-reports.dto.ts index 62e90d4f..6e60e030 100644 --- a/backend/src/reports/dto/get-reports.dto.ts +++ b/backend/src/reports/dto/get-reports.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNumber, IsOptional, Min, Max } from 'class-validator'; +import { IsOptional, IsInt, Min, Max } from 'class-validator'; import { Type } from 'class-transformer'; export class GetReportsQueryDto { @@ -10,8 +10,8 @@ export class GetReportsQueryDto { }) @IsOptional() @Type(() => Number) - @IsNumber() + @IsInt() @Min(1) @Max(100) - limit?: number = 10; + limit?: number; } diff --git a/backend/src/reports/reports.service.ts b/backend/src/reports/reports.service.ts index b6bce926..efa1a01b 100644 --- a/backend/src/reports/reports.service.ts +++ b/backend/src/reports/reports.service.ts @@ -61,7 +61,9 @@ export class ReportsService { } async findLatest(queryDto: GetReportsQueryDto): Promise { - const limit = queryDto.limit || 10; + // Convert limit to a number to avoid serialization errors + const limit = + typeof queryDto.limit === 'string' ? parseInt(queryDto.limit, 10) : queryDto.limit || 10; const command = new ScanCommand({ TableName: this.tableName, diff --git a/frontend/src/common/api/reportService.ts b/frontend/src/common/api/reportService.ts index e5186e0a..b5cec3b8 100644 --- a/frontend/src/common/api/reportService.ts +++ b/frontend/src/common/api/reportService.ts @@ -1,8 +1,8 @@ import axios from 'axios'; -import { MedicalReport, ReportStatus, ReportCategory } from '../models/medicalReport'; +import { MedicalReport, ReportStatus } from '../models/medicalReport'; -// Base API URL - should be configured from environment variables in a real app -// Removed unused API_URL variable +// Get the API URL from environment variables +const API_URL = import.meta.env.VITE_BASE_URL_API || ''; /** * Error thrown when report operations fail. @@ -21,12 +21,10 @@ export class ReportError extends Error { */ export const fetchLatestReports = async (limit = 3): Promise => { try { - // In a real app, this would be an actual API call - // const response = await axios.get(`/api/reports/latest?limit=${limit}`); - // return response.data; - - // For now, return mock data - return mockReports.slice(0, limit); + const response = await axios.get(`${API_URL}/reports/latest?limit=${limit}`); + console.log('response', response.data); + console.log('API_URL', API_URL); + return response.data; } catch (error) { if (axios.isAxiosError(error)) { throw new ReportError(`Failed to fetch latest reports: ${error.message}`); @@ -41,12 +39,8 @@ export const fetchLatestReports = async (limit = 3): Promise => */ export const fetchAllReports = async (): Promise => { try { - // In a real app, this would be an actual API call - // const response = await axios.get(`/api/reports`); - // return response.data; - - // For now, return mock data - return mockReports; + const response = await axios.get(`${API_URL}/reports`); + return response.data; } catch (error) { if (axios.isAxiosError(error)) { throw new ReportError(`Failed to fetch all reports: ${error.message}`); @@ -62,18 +56,16 @@ export const fetchAllReports = async (): Promise => { */ export const markReportAsRead = async (reportId: string): Promise => { try { - // In a real app, this would be an actual API call - // const response = await axios.patch(`/api/reports/${reportId}`, { - // status: ReportStatus.READ - // }); - // return response.data; - - // For now, update mock data - const report = mockReports.find(r => r.id === reportId); + const response = await axios.patch(`${API_URL}/reports/${reportId}`, { + status: ReportStatus.READ + }); + + const report = response.data; + if (!report) { throw new Error(`Report with ID ${reportId} not found`); } - + report.status = ReportStatus.READ; return { ...report }; } catch (error) { @@ -83,34 +75,3 @@ export const markReportAsRead = async (reportId: string): Promise throw new ReportError('Failed to mark report as read'); } }; - -// Mock data for development -const mockReports: MedicalReport[] = [ - { - id: '1', - title: 'Blood Test', - category: ReportCategory.GENERAL, - date: '2025-01-27', - status: ReportStatus.UNREAD, - doctor: 'Dr. Smith', - facility: 'City Hospital' - }, - { - id: '2', - title: 'Cranial Nerve Exam', - category: ReportCategory.NEUROLOGICAL, - date: '2025-01-19', - status: ReportStatus.UNREAD, - doctor: 'Dr. Johnson', - facility: 'Neurology Center' - }, - { - id: '3', - title: 'Stress Test', - category: ReportCategory.HEART, - date: '2024-12-26', - status: ReportStatus.READ, - doctor: 'Dr. Williams', - facility: 'Heart Institute' - } -]; \ No newline at end of file From 59b6f7a48d453406c0093f795aa640d3ac1bf0a7 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 21:17:27 -0400 Subject: [PATCH 23/24] Add /api prefix --- backend/src/iac/backend-stack.ts | 2 +- backend/src/main.ts | 18 ++++++++++++++++++ frontend/src/common/api/reportService.ts | 6 +++--- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index b50a49be..ae522ffe 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -147,7 +147,7 @@ export class BackendStack extends cdk.Stack { protocol: elbv2.ApplicationProtocol.HTTP, targetType: elbv2.TargetType.IP, healthCheck: { - path: '/health', + path: '/api/health', interval: cdk.Duration.seconds(30), timeout: cdk.Duration.seconds(5), }, diff --git a/backend/src/main.ts b/backend/src/main.ts index d7933ba8..d0a6a661 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -2,15 +2,33 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ConfigService } from '@nestjs/config'; import { setupSwagger } from './swagger.config'; +import { ValidationPipe } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); const port = configService.get('port') ?? 3000; + // Add global prefix 'api' to all routes + app.setGlobalPrefix('api'); + // Setup Swagger setupSwagger(app); + // Rest of your bootstrap code... + app.useGlobalPipes(new ValidationPipe()); + + const config = new DocumentBuilder() + .setTitle('Medical Reports API') + .setDescription('API for medical reports application') + .setVersion('1.0') + .addBearerAuth() + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('docs', app, document); + await app.listen(port); console.log(`Application is running on: ${await app.getUrl()}`); } diff --git a/frontend/src/common/api/reportService.ts b/frontend/src/common/api/reportService.ts index b5cec3b8..7cdacb4f 100644 --- a/frontend/src/common/api/reportService.ts +++ b/frontend/src/common/api/reportService.ts @@ -21,7 +21,7 @@ export class ReportError extends Error { */ export const fetchLatestReports = async (limit = 3): Promise => { try { - const response = await axios.get(`${API_URL}/reports/latest?limit=${limit}`); + const response = await axios.get(`${API_URL}/api/reports/latest?limit=${limit}`); console.log('response', response.data); console.log('API_URL', API_URL); return response.data; @@ -39,7 +39,7 @@ export const fetchLatestReports = async (limit = 3): Promise => */ export const fetchAllReports = async (): Promise => { try { - const response = await axios.get(`${API_URL}/reports`); + const response = await axios.get(`${API_URL}/api/reports`); return response.data; } catch (error) { if (axios.isAxiosError(error)) { @@ -56,7 +56,7 @@ export const fetchAllReports = async (): Promise => { */ export const markReportAsRead = async (reportId: string): Promise => { try { - const response = await axios.patch(`${API_URL}/reports/${reportId}`, { + const response = await axios.patch(`${API_URL}/api/reports/${reportId}`, { status: ReportStatus.READ }); From 2fe39a1e6816bf1bd2341266e100f25787f995c9 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 20 Mar 2025 21:20:19 -0400 Subject: [PATCH 24/24] Add configuration for CORS --- backend/src/main.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/src/main.ts b/backend/src/main.ts index d0a6a661..2687ebcb 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -10,6 +10,18 @@ async function bootstrap() { const configService = app.get(ConfigService); const port = configService.get('port') ?? 3000; + // Enable CORS + app.enableCors({ + origin: [ + 'http://localhost:5173', // Vite default dev server + 'http://localhost:3000', + 'http://localhost:4173', // Vite preview + ...(process.env.FRONTEND_URL ? [process.env.FRONTEND_URL] : []), + ], + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', + credentials: true, + }); + // Add global prefix 'api' to all routes app.setGlobalPrefix('api');