Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
14 changes: 14 additions & 0 deletions api/src/controllers/application.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import { ApiKeyGuard } from '../guards/api-key.guard';
import { PublicAppsViewQueryParams } from '../dtos/applications/public-apps-view-params.dto';
import { PublicAppsViewResponse } from '../dtos/applications/public-apps-view-response.dto';
import { ApplicationBulkUploadService } from '../services/application-bulk-upload.service';
import { ApplicationBulkUrl } from '../dtos/applications/application-bulk-url.dto';
import { ApplicationBulkPresignedUrl } from '../dtos/applications/application-bulk-presigned-url.dto';

@Controller('applications')
@ApiTags('applications')
Expand Down Expand Up @@ -327,6 +329,18 @@ export class ApplicationController {
);
}

@Post('bulk-update/upload-url')
@ApiOperation({
summary: 'Generates an presigned URL link to a private S3 bucket',
operationId: 'uploadBulkUpdate',
})
@ApiOkResponse({ type: ApplicationBulkPresignedUrl })
async uploadBulkUpdate(
@Body() dto: ApplicationBulkUrl,
): Promise<ApplicationBulkPresignedUrl> {
return this.applicationBulkUploadService.uploadUrl(dto);
}

@Delete()
@ApiOperation({ summary: 'Delete application by id', operationId: 'delete' })
@ApiOkResponse({ type: SuccessDTO })
Expand Down
18 changes: 18 additions & 0 deletions api/src/dtos/applications/application-bulk-presigned-url.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { IsDefined, IsString } from 'class-validator';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';

export class ApplicationBulkPresignedUrl {
@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
key: string;

@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
presignedUrl: string;
}
22 changes: 22 additions & 0 deletions api/src/dtos/applications/application-bulk-url.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { IsDefined, MinLength, IsString, IsUUID } from 'class-validator';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';

export class ApplicationBulkUrl {
@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@MinLength(1, { groups: [ValidationsGroupsEnum.default] })
@IsUUID(4, { groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.applicants] })
@ApiProperty()
listingId: string;

@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@MinLength(1, { groups: [ValidationsGroupsEnum.default] })
@IsUUID(4, { groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.applicants] })
@ApiProperty()
userId: string;
}
3 changes: 2 additions & 1 deletion api/src/modules/application-bulk-upload.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { ApplicationBulkUploadService } from '../services/application-bulk-uploa
import { PrismaModule } from './prisma.module';
import { ListingModule } from './listing.module';
import { PermissionModule } from './permission.module';
import { S3Module } from './s3.module';

@Global()
@Module({
imports: [PrismaModule, ListingModule, PermissionModule],
imports: [PrismaModule, ListingModule, PermissionModule, S3Module],
providers: [ApplicationBulkUploadService],
exports: [ApplicationBulkUploadService],
})
Expand Down
72 changes: 72 additions & 0 deletions api/src/services/application-bulk-upload.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ import { ListingService } from './listing.service';
import { PermissionService } from './permission.service';
import { permissionActions } from '../enums/permissions/permission-actions-enum';
import { convertApplicationDeclineReasonToReadable } from '../utilities/application-export-helpers';
import { ApplicationBulkUrl } from '../dtos/applications/application-bulk-url.dto';
import { doJurisdictionHaveFeatureFlagSet } from '../utilities/feature-flag-utilities';
import { FeatureFlagEnum } from '../enums/feature-flags/feature-flags-enum';
import { Jurisdiction } from '../dtos/jurisdictions/jurisdiction.dto';
import { ApplicationBulkPresignedUrl } from '../dtos/applications/application-bulk-presigned-url.dto';
import { S3Service } from './s3.service';

const NUMBER_TO_PAGINATE_BY = 500;

Expand All @@ -30,6 +36,7 @@ export class ApplicationBulkUploadService {
private prisma: PrismaService,
private listingService: ListingService,
private permissionService: PermissionService,
private s3Service: S3Service,
) {}

private formatApplicationStatus(statusEnum: ApplicationStatusEnum): string {
Expand Down Expand Up @@ -295,6 +302,71 @@ export class ApplicationBulkUploadService {
return stringData;
}

// TODO Yazeed - Integrate the uploadUrl method with S3 presigned URL generation logic
async uploadUrl(
dto: ApplicationBulkUrl,
): Promise<ApplicationBulkPresignedUrl> {
Comment thread
matzduniuk marked this conversation as resolved.
const { userId, listingId } = dto;

const listingData = await this.prisma.listings.findUnique({
select: {
// id: true,
jurisdictionId: true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should specify the listing id is responded here so we aren't returning the full listing object

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are already limiting the listing response object to only have the jurisdictionId and linked jurisdiction feature flags array

jurisdictions: {
select: {
featureFlags: true,
},
},
},
where: {
id: listingId,
},
});

if (!listingData) {
throw new NotFoundException(
`Listing with id: ${listingId} can not be found`,
);
}

const requestingUser = await this.prisma.userAccounts.findFirst({
where: {
id: userId,
},
});

await this.permissionService.canOrThrow(
mapTo(User, requestingUser),
'listing',
permissionActions.update,
{
id: listingId,
jurisdictionId: listingData.jurisdictionId,
},
);

if (
!doJurisdictionHaveFeatureFlagSet(
mapTo(Jurisdiction, listingData.jurisdictions),
FeatureFlagEnum.enableApplicationStatus,
)
) {
throw new ForbiddenException(
`Jurisdiction with id: ${listingData.jurisdictionId} does not have the enableApplicationStatus flag enabled`,
);
}

const s3KeyTemplate = `bulk-application-updates-${listingId}-${userId}-${new Date().toISOString()}`;
const presignedUrl = await this.s3Service.uploadURLForPrivate(
s3KeyTemplate,
);

return {
key: s3KeyTemplate,
presignedUrl: presignedUrl,
};
}

async authorizeExport(user, listingId): Promise<void> {
/**
* Checking authorization for each application is very expensive.
Expand Down
24 changes: 24 additions & 0 deletions api/src/services/s3.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,30 @@ export class S3Service {
return getSignedUrl(this.s3Client, command);
}

async uploadURLForPrivate(
key: string,
metadata?: CreateS3UploadMetadata,
expiresIn: number = 60 * 5,
): Promise<string> {
const input: PutObjectCommandInput = {
Bucket: this.privateBucket,
Key: key,
};

if (metadata?.contentDisposition) {
input.ContentDisposition = metadata.contentDisposition;
}
if (metadata?.contentType) {
input.ContentType = metadata.contentType;
}

const command = new PutObjectCommand(input);

return getSignedUrl(this.s3Client, command, {
expiresIn,
});
}

urlForPublic(key: string): string {
return `https://${this.publicBucket}.s3.${this.region}.amazonaws.com/${key}`;
}
Expand Down
11 changes: 11 additions & 0 deletions api/test/unit/services/application-bulk-upload.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ApplicationBulkUploadService } from '../../../src/services/application-
import { ListingService } from '../../../src/services/listing.service';
import { PermissionService } from '../../../src/services/permission.service';
import { PrismaService } from '../../../src/services/prisma.service';
import { S3Service } from '../../../src/services/s3.service';

const mockApplication = ({
markedAsDuplicate = false,
Expand Down Expand Up @@ -91,6 +92,16 @@ describe('Testing application bulk upload services', () => {
provide: PermissionService,
useValue: { canOrThrow: canOrThrowMock },
},
{
provide: S3Service,
useValue: {
uploadToPrivate: jest.fn(),
urlForPrivate: jest.fn(),
uploadURLForPublic: jest.fn(),
uploadURLForPrivate: jest.fn(),
urlForPublic: jest.fn(),
},
},
],
}).compile();

Expand Down
Loading