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. 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} 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/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..2cf9b087 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,27 +6,23 @@ 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'; - +import { HealthController } from './health/health.controller'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, load: [configuration], }), - AuthModule, - UserModule, ReportsModule, ], - controllers: [AppController, PerplexityController, UserController], + controllers: [AppController, HealthController, 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 180c3e97..00000000 --- a/backend/src/auth/auth.middleware.spec.ts +++ /dev/null @@ -1,116 +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', () => { - jwtService.verify.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/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/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' }; + } +} 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); }); diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index adcd0adf..ae522ffe 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -4,11 +4,16 @@ 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 { 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; + 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 { @@ -19,23 +24,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, }); @@ -46,14 +42,61 @@ export class BackendStack extends cdk.Stack { removalPolicy: isProd ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY, }); - // Task Definition - const taskDefinition = new ecs.FargateTaskDefinition(this, `${appName}TaskDef`, { - memoryLimitMiB: isProd ? 1024 : 512, - cpu: isProd ? 512 : 256, + // 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 = + 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( + 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 = props.cognitoClientId; + + // Task Definition + 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: { @@ -61,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', @@ -77,58 +130,62 @@ export class BackendStack extends cdk.Stack { protocol: ecs.Protocol.TCP, }); - // Look up existing Cognito User Pool - const userPool = cognito.UserPool.fromUserPoolId( - this, - `${appName}UserPool`, - 'ai-cognito-medical-reports-user-pool', - ); - - // Create a Cognito domain if it doesn't exist - const userPoolDomain = new cognito.UserPoolDomain(this, `${appName}UserPoolDomain`, { - userPool, - cognitoDomain: { - domainPrefix: `${appName.toLowerCase()}-auth`, - }, + // 1. Create ALB + const alb = new elbv2.ApplicationLoadBalancer(this, `${appName}ALB-${props.environment}`, { + vpc, + internetFacing: true, + loadBalancerName: `${appName}-${props.environment}`, }); - // 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, + // 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: '/api/health', + interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(5), }, - callbackUrls: [`http://${appName.toLowerCase()}.example.com/oauth2/idpresponse`], // Update with your actual domain }, - }); + ); - // Create ALB - const alb = new elbv2.ApplicationLoadBalancer(this, `${appName}ALB`, { - vpc, - internetFacing: true, - loadBalancerName: `${appName}-${props.environment}`, + // 3. HTTP 80 Listener + const httpListener = alb.addListener(`${appName}HttpListener-${props.environment}`, { + port: 80, + protocol: elbv2.ApplicationProtocol.HTTP, + defaultAction: elbv2.ListenerAction.forward([targetGroup]), }); - // Create Fargate Service - const fargateService = new ecs.FargateService(this, `${appName}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`, { - vpc, - allowAllOutbound: true, - }), - ], + securityGroups: [serviceSecurityGroup], }); + // 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); + // Add autoscaling for production if (isProd) { const scaling = fargateService.autoScaleTaskCount({ @@ -143,31 +200,10 @@ 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), - }, - targets: [fargateService], - }); - - // Create HTTP Listener - alb.addListener(`${appName}HttpListener`, { - port: 80, - protocol: elbv2.ApplicationProtocol.HTTP, - defaultAction: new elbv2_actions.AuthenticateCognitoAction({ - userPool, - userPoolClient, - userPoolDomain: userPoolDomain, - next: elbv2.ListenerAction.forward([targetGroup]), - onUnauthenticatedRequest: elbv2.UnauthenticatedAction.AUTHENTICATE, - }), + // Add output for the table name + new cdk.CfnOutput(this, 'ReportsTableName', { + value: reportsTable.tableName, + description: 'DynamoDB Reports Table Name', }); // Add output for Cognito domain @@ -181,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', - }); } } diff --git a/backend/src/iac/cdk.index.ts b/backend/src/iac/cdk.index.ts new file mode 100644 index 00000000..c04e300b --- /dev/null +++ b/backend/src/iac/cdk.index.ts @@ -0,0 +1,40 @@ +#!/usr/bin/env node +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'; + +export function main() { + const app = new cdk.App(); + + console.log('NODE_ENV', process.env.NODE_ENV); + + 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'); + } + + 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, + region: process.env.CDK_DEFAULT_REGION || 'us-east-1', + }, + cognitoClientId: cognitoClientId, + cognitoUserPoolId: cognitoUserPoolId, + }); + + 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'; diff --git a/backend/src/main.ts b/backend/src/main.ts index d7933ba8..2687ebcb 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -2,15 +2,45 @@ 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; + // 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'); + // 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/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.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/reports/reports.service.ts b/backend/src/reports/reports.service.ts index 88def05b..efa1a01b 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,12 +46,24 @@ 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 { - 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/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: [], }) diff --git a/frontend/src/common/api/reportService.ts b/frontend/src/common/api/reportService.ts index e5186e0a..7cdacb4f 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}/api/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}/api/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}/api/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