Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
50ac20e
adding migrations
piersolh Sep 14, 2025
749cdab
adding back example env
piersolh Sep 14, 2025
b144c4b
Merge pull request #1 from Code-4-Community/HP-adding-migrations
piersolh Sep 14, 2025
f3d586e
adding comments to explain type error
piersolh Sep 17, 2025
9dc4a94
Merge pull request #24 from Code-4-Community/HP-adding-migrations
piersolh Sep 17, 2025
7cbd8a8
put interceptor decorators in auth controller and module
chnnick Sep 24, 2025
e47cf24
added new application entity and new enums
ItsEricSun Sep 30, 2025
b14ae2a
setting up scaffolding for disciplines entity
piersolh Oct 1, 2025
4cef618
adding seeding and creating a disciplines enum
piersolh Oct 1, 2025
683505a
fixing typo
piersolh Oct 1, 2025
d0cc629
add initial aws module
rayyanmridha Oct 1, 2025
d990085
added admin controller, entity, module, and service
ostepan8 Oct 1, 2025
ab3e30c
small changes to ensure functionality
ostepan8 Oct 1, 2025
0d93119
fix s3 403 error
maxn990 Oct 2, 2025
2962fa2
make sure test doesn't run automatically
rayyanmridha Oct 2, 2025
25719d0
Merge pull request #35 from Code-4-Community/create-application-entity
piersolh Oct 5, 2025
a3ad5b4
Merge pull request #36 from Code-4-Community/HP-create-discipline-entity
piersolh Oct 5, 2025
bbdd87e
updated pr for comments
ostepan8 Oct 8, 2025
ce1d5a7
changed the update to only update the email
ostepan8 Oct 9, 2025
3ddea89
make migration dynamic
ostepan8 Oct 9, 2025
16c2a80
Merge pull request #31 from Code-4-Community/nc-19-cognito
piersolh Nov 13, 2025
59127a2
Merge pull request #44 from Code-4-Community/rm-15-awsS3Entity
piersolh Nov 13, 2025
6d58fa1
fix s3 bugs, merge into main
piersolh Dec 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,25 @@ import { TypeOrmModule } from '@nestjs/typeorm';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TaskModule } from './task/task.module';
import { AWSS3Module } from './aws-s3/aws-s3.module';
import AppDataSource from './data-source';
import { AdminsModule } from './users/admins.module';
import { Admin } from './users/admin.entity';
import { ConfigModule } from '@nestjs/config';

@Module({
imports: [TypeOrmModule.forRoot(AppDataSource.options), TaskModule],
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '../../.env',
}),
TypeOrmModule.forRoot({
...AppDataSource.options,
entities: [Admin],
}),
AdminsModule,
AWSS3Module,
],
controllers: [AppController],
providers: [AppService],
})
Expand Down
49 changes: 49 additions & 0 deletions apps/backend/src/applications/application.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Entity, Column } from 'typeorm';

import type { AppStatus, ExperienceType, Interest, School } from './types';

@Entity()
export class Application {
@Column({ primary: true })
appId: number;

@Column()
appStatus: AppStatus;

@Column()
daysAvailable: string;

@Column()
experienceType: ExperienceType;

// Array of S3 file URLs
@Column()
fileUploads: string[];

@Column()
interest: Interest;

@Column()
license: string;

@Column()
isInternational: boolean;

@Column()
isLearner: boolean;

@Column()
phone: string;

@Column()
school: School;

@Column()
referred: boolean;

@Column()
referredEmail: string;

@Column()
weeklyHours: number;
}
27 changes: 27 additions & 0 deletions apps/backend/src/applications/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export enum AppStatus {
APP_SUBMITTED = 'APP_SUBMITTED',
IN_REVIEW = 'IN_REVIEW',
FORMS_SENT = 'FORMS_SENT',
ACCEPTED = 'ACCEPTED',
REJECTED = 'REJECTED',
}

export enum ExperienceType {
// Dummy values, replace with actual experience types
VOLUNTEER = 'VOLUNTEER',
}

export enum Interest {
// Dummy values, replace with actual interests
PHARMACY = 'Pharmacy',
TECHNOLOGY = 'Technology',
FINANCE = 'Finance',
HEALTH = 'Health',
EDUCATION = 'Education',
}

export enum School {
// Dummy values, replace with actual schools
HARVARD_MEDICAL_SCHOOL = 'Harvard Medical School',
JOHNS_HOPKINS = 'Johns Hopkins',
}
3 changes: 3 additions & 0 deletions apps/backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Post,
Request,
UseGuards,
UseInterceptors,
} from '@nestjs/common';

import { SignInDto } from './dtos/sign-in.dto';
Expand All @@ -20,9 +21,11 @@ import { AuthGuard } from '@nestjs/passport';
import { ConfirmPasswordDto } from './dtos/confirm-password.dto';
import { ForgotPasswordDto } from './dtos/forgot-password.dto';
import { ApiTags } from '@nestjs/swagger';
import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor';

@ApiTags('Auth')
@Controller('auth')
@UseInterceptors(CurrentUserInterceptor)
export class AuthController {
constructor(
private authService: AuthService,
Expand Down
5 changes: 3 additions & 2 deletions apps/backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PassportModule } from '@nestjs/passport';

import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UsersService } from '../users/users.service';
import { User } from '../users/user.entity';
import { JwtStrategy } from './jwt.strategy';
import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor';

@Module({
imports: [
TypeOrmModule.forFeature([User]),
PassportModule.register({ defaultStrategy: 'jwt' }),
],
controllers: [AuthController],
providers: [AuthService, UsersService, JwtStrategy],
providers: [AuthService, UsersService, JwtStrategy, CurrentUserInterceptor],
exports: [AuthService, UsersService],
})
export class AuthModule {}
10 changes: 10 additions & 0 deletions apps/backend/src/aws-s3/aws-s3.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Global, Module } from '@nestjs/common';
import { AWSS3Service } from './aws-s3.service';

@Global()
@Module({
imports: [],
providers: [AWSS3Service],
exports: [AWSS3Service],
})
export class AWSS3Module {}
85 changes: 85 additions & 0 deletions apps/backend/src/aws-s3/aws-s3.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as dotenv from 'dotenv';
import path from 'path';

dotenv.config({ path: path.join(__dirname, '../../../.env') });

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { AWSS3Service } from './aws-s3.service';
import { mockClient } from 'aws-sdk-client-mock';
import axios from 'axios';

const s3Mock = mockClient(S3Client);

describe('AWSS3Service', () => {
let service: AWSS3Service;
const bucketName = process.env.AWS_BUCKET_NAME;
const bucketRegion = process.env.AWS_REGION;

beforeEach(() => {
s3Mock.reset();
service = new AWSS3Service();
});

it('should throw error if AWS_BUCKET_NAME is not defined', () => {
delete process.env.AWS_BUCKET_NAME;
expect(() => new AWSS3Service()).toThrow(
'AWS_BUCKET_NAME is not defined in environment variables',
);
process.env.AWS_BUCKET_NAME = bucketName; // restore for other tests
});

it('should create correct S3 link', () => {
const link = service.createLink('JohnDoe', 'Resume');
expect(link).toBe(
`https://${bucketName}.s3.${bucketRegion}.amazonaws.com/JohnDoe-Resume.pdf`,
);
});

it('should upload file and return correct URL', async () => {
s3Mock.on(PutObjectCommand).resolves({});

const buffer = Buffer.from('test');
const fileName = 'file.pdf';
const mimeType = 'application/pdf';

const url = await service.upload(buffer, fileName, mimeType);

expect(s3Mock.calls()).toHaveLength(1);
expect(url).toBe(
`https://${bucketName}.s3.${bucketRegion}.amazonaws.com/${fileName}`,
);
});

it('should throw error on upload failure', async () => {
s3Mock.on(PutObjectCommand).rejects(new Error('fail'));

const buffer = Buffer.from('test');
await expect(
service.upload(buffer, 'file.pdf', 'application/pdf'),
).rejects.toThrow('File upload to AWS failed: Error: fail');
});

// take off ".skip" to run this test but do so sparingly
it.skip('should actually upload a file to S3 (integration)', async () => {
s3Mock.restore();
const fileContent = `integration-test-content-${Date.now()}`;
const buffer = Buffer.from(fileContent);
const fileName = `integration-test-${Date.now()}.txt`;
const mimeType = 'text/plain';
const integrationService = new AWSS3Service();

const url = await integrationService.upload(buffer, fileName, mimeType);
console.log('Uploaded file URL:', url);
expect(url).toContain(fileName);
try {
const response = await axios.get(url);
expect(response.status).toBe(200);
expect(response.data).toBe(fileContent);
} catch (error) {
throw new Error(
`Failed to fetch the uploaded file from S3. Error: ${error.message}.`,
);
}
}, 15000);
// MAKE SURE TO CLEAN UP THE FILES FROM OUR S3 BUCKET AFTER RUNNING THE INTEGRATION TEST
});
61 changes: 61 additions & 0 deletions apps/backend/src/aws-s3/aws-s3.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Injectable } from '@nestjs/common';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

@Injectable()
export class AWSS3Service {
private client: S3Client;
private readonly bucketName = process.env.AWS_BUCKET_NAME;
private readonly region: string;

constructor() {
this.region = process.env.AWS_REGION || 'us-east-2';
this.bucketName = process.env.AWS_BUCKET_NAME;

if (!this.bucketName) {
throw new Error(
'AWS_BUCKET_NAME is not defined in environment variables',
);
}

if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
throw new Error(
'AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY is not defined in environment variables',
);
}

this.client = new S3Client({
region: this.region,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
}

// In the form of "JohnDoe-Resume.pdf"
createLink(person: string, type: string): string {
const fileName = `${person}-${type}.pdf`;
return `https://${this.bucketName}.s3.us-east-2.amazonaws.com/${fileName}`;
}

async upload(
fileBuffer: Buffer,
fileName: string,
mimeType: string,
): Promise<string> {
try {
const command = new PutObjectCommand({
Bucket: this.bucketName,
Key: fileName,
Body: fileBuffer,
ContentType: mimeType,
});

await this.client.send(command);

return `https://${this.bucketName}.s3.${this.region}.amazonaws.com/${fileName}`;
} catch (error) {
throw new Error('File upload to AWS failed: ' + error);
}
}
}
7 changes: 5 additions & 2 deletions apps/backend/src/data-source.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { DataSource } from 'typeorm';
import { Admin } from './users/admin.entity';
import { PluralNamingStrategy } from './strategies/plural-naming.strategy';
import { Task } from './task/types/task.entity';
import * as dotenv from 'dotenv';
import { Discipline } from './disciplines/disciplines.entity';

dotenv.config();

Expand All @@ -12,8 +13,10 @@ const AppDataSource = new DataSource({
username: process.env.NX_DB_USERNAME,
password: process.env.NX_DB_PASSWORD,
database: process.env.NX_DB_DATABASE,
entities: [Task],
entities: [Admin, Discipline],
migrations: ['apps/backend/src/migrations/*.js'],
// migrations: ['apps/backend/src/migrations/*.ts'], // use this line instead of the above when running migrations locally,
// then switch back to the above before pushing to github so that it works on the deployment server
// Setting synchronize: true shouldn't be used in production - otherwise you can lose production data
synchronize: false,
namingStrategy: new PluralNamingStrategy(),
Expand Down
12 changes: 12 additions & 0 deletions apps/backend/src/disciplines/disciplines.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Controller, UseGuards, UseInterceptors } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor';
import { DisciplinesService } from './disciplines.service';

@Controller('disciplines')
@UseInterceptors(CurrentUserInterceptor)
@UseGuards(AuthGuard('jwt'))
export class DisciplinesController {
constructor(private disciplinesService: DisciplinesService) {}
// TODO: fill out with actual API endpoints
}
22 changes: 22 additions & 0 deletions apps/backend/src/disciplines/disciplines.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { DISCIPLINE_VALUES } from '../disciplines/discplines.constants';

// describes a BHCHP medical discipline
// Current list of disciplines: Volunteers, Nursing, Public Health, MD, PA, NP,
// Research, Social work, Psychiatry, Pharmacy, IT
@Entity('discipline')
export class Discipline {
@PrimaryGeneratedColumn()
id: number;

// enforce discipline names to be one of the predefined values
@Column({
type: 'enum',
enum: DISCIPLINE_VALUES,
nullable: false,
})
name: DISCIPLINE_VALUES;

@Column({ type: 'int', array: true, default: () => "'{}'" })
admin_ids: number[];
}
14 changes: 14 additions & 0 deletions apps/backend/src/disciplines/disciplines.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DisciplinesController } from './disciplines.controller';
import { DisciplinesService } from './disciplines.service';
import { Discipline } from './disciplines.entity';
import { JwtStrategy } from '../auth/jwt.strategy';
import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor';

@Module({
imports: [TypeOrmModule.forFeature([Discipline])],
controllers: [DisciplinesController],
providers: [DisciplinesService, JwtStrategy, CurrentUserInterceptor],
})
export class DiscplinesModule {}
14 changes: 14 additions & 0 deletions apps/backend/src/disciplines/disciplines.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Discipline } from './disciplines.entity';

@Injectable()
export class DisciplinesService {
constructor(
@InjectRepository(Discipline)
private disciplinesRepository: Repository<Discipline>,
) {}

// TODO: fill out with actual methods
}
Loading
Loading