Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: include files in campaign application #671

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
Param,
Patch,
Post,
Response,
StreamableFile,
UploadedFiles,
UseInterceptors,
} from '@nestjs/common'
Expand Down Expand Up @@ -101,6 +103,33 @@ export class CampaignApplicationController {
return this.campaignApplicationService.deleteFile(id, isAdminFlag, person)
}

@Get('fileById/:id')
async fetchFile(
@Param('id') id: string,
@AuthenticatedUser() user: KeycloakTokenParsed,
@Response({ passthrough: true }) res,
): Promise<StreamableFile> {
const person = await this.personService.findOneByKeycloakId(user.sub)
if (!person) {
Logger.error('No person found in database')
throw new NotFoundException('No person found in database')
}

const isAdminFlag = isAdmin(user)

const file = await this.campaignApplicationService.getFile(id, isAdminFlag, person)

res.set({
'Content-Type': file?.mimetype,
'Content-Disposition': 'attachment; filename="' + file.filename + '"',
'Cache-Control': (file.mimetype ?? '').startsWith('image/')
? 'public, s-maxage=15552000, stale-while-revalidate=15552000, immutable'
: 'no-store',
})

return new StreamableFile(file.stream)
}

@Patch(':id')
async update(
@Param('id') id: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

describe('CampaignApplicationService', () => {
let service: CampaignApplicationService
let configService: ConfigService

Check warning on line 30 in apps/api/src/campaign-application/campaign-application.service.spec.ts

View workflow job for this annotation

GitHub Actions / Run API tests

'configService' is defined but never used

const mockPerson = {
...personMock,
Expand All @@ -46,6 +46,7 @@
const mockS3Service = {
uploadObject: jest.fn(),
deleteObject: jest.fn(),
streamFile: jest.fn().mockResolvedValue(1234),
}

const mockEmailService = {
Expand Down Expand Up @@ -290,6 +291,18 @@

expect(result).toEqual(mockSingleCampaignApplication)
expect(prismaMock.campaignApplication.findUnique).toHaveBeenCalledTimes(1)
expect(prismaMock.campaignApplication.findUnique).toHaveBeenCalledWith({
where: { id: 'id' },
include: {
documents: {
select: {
id: true,
filename: true,
mimetype: true,
},
},
},
})
})

it('should throw a NotFoundException if no campaign-application is found', async () => {
Expand Down Expand Up @@ -422,4 +435,72 @@
})
})
})

describe('getFile', () => {
it('should return a single campaign-application file', async () => {
prismaMock.campaignApplication.findFirst.mockResolvedValue(mockSingleCampaignApplication)
prismaMock.campaignApplicationFile.findFirst.mockResolvedValue({
id: '123',
filename: 'my-file',
} as File)

const result = await service.getFile('id', false, mockPerson)

expect(result).toEqual({
filename: 'my-file',
stream: 1234,
})
expect(prismaMock.campaignApplication.findFirst).toHaveBeenCalledTimes(1)
expect(prismaMock.campaignApplication.findFirst).toHaveBeenCalledWith({
where: {
documents: {
some: {
id: 'id',
},
},
},
})

expect(prismaMock.campaignApplicationFile.findFirst).toHaveBeenNthCalledWith(1, {
where: { id: 'id' },
})
})

it('should throw a NotFoundException if no campaign-application is found', async () => {
prismaMock.campaignApplication.findUnique.mockResolvedValue(null)

await expect(service.getFile('id', false, mockPerson)).rejects.toThrow(
new NotFoundException('File does not exist'),
)
})

it('should handle errors and throw an exception', async () => {
const errorMessage = 'error'
prismaMock.campaignApplication.findFirst.mockRejectedValue(new Error(errorMessage))

await expect(service.getFile('id', false, mockPerson)).rejects.toThrow(errorMessage)
})

it('should not allow non-admin users to see files belonging to other users', async () => {
prismaMock.campaignApplication.findFirst.mockResolvedValue(mockSingleCampaignApplication)
await expect(
service.getFile('id', false, { ...mockPerson, organizer: { id: 'different-id' } }),
).rejects.toThrow(
new ForbiddenException('User is not admin or organizer of the campaignApplication'),
)
})

it('should allow admin users to see files belonging to other users', async () => {
prismaMock.campaignApplication.findFirst.mockResolvedValue(mockSingleCampaignApplication)
prismaMock.campaignApplicationFile.findFirst.mockResolvedValue({
id: '123',
filename: 'my-file',
} as File)
await expect(
service.getFile('id', true, { ...mockPerson, organizer: { id: 'different-id' } }),
).resolves.not.toThrow(
new ForbiddenException('User is not admin or organizer of the campaignApplication'),
)
})
})
})
50 changes: 50 additions & 0 deletions apps/api/src/campaign-application/campaign-application.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@ export class CampaignApplicationService {
try {
const singleCampaignApplication = await this.prisma.campaignApplication.findUnique({
where: { id },
include: {
documents: {
select: {
id: true,
filename: true,
mimetype: true,
},
},
},
})
if (!singleCampaignApplication) {
throw new NotFoundException('Campaign application doesnt exist')
Expand Down Expand Up @@ -338,4 +347,45 @@ export class CampaignApplicationService {
throw error
}
}

async getFile(
id: string,
isAdminFlag: boolean,
person: Prisma.PersonGetPayload<{ include: { organizer: { select: { id: true } } } }>,
) {
try {
const campaignApplication = await this.prisma.campaignApplication.findFirst({
where: {
documents: {
some: {
id: id,
},
},
},
})

if (!campaignApplication) {
throw new NotFoundException('File does not exist')
}

if (isAdminFlag === false && campaignApplication.organizerId !== person.organizer?.id) {
throw new ForbiddenException('User is not admin or organizer of the campaignApplication')
}

const file = await this.prisma.campaignApplicationFile.findFirst({ where: { id: id } })
if (!file) {
Logger.warn('No campaign application file record with ID: ' + id)
throw new NotFoundException('No campaign application file record with ID: ' + id)
}

return {
filename: encodeURIComponent(file.filename),
mimetype: file.mimetype,
stream: await this.s3.streamFile(this.bucketName, id),
}
} catch (error) {
Logger.error('Error in getFile():', error)
throw error
}
}
}
3 changes: 1 addition & 2 deletions apps/api/src/email/template.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ export class CreateCampaignApplicationAdminEmailDto extends EmailTemplate<{
name = TemplateType.createCampaignApplicationAdmin
}


export class CreateCampaignApplicationOrganizerEmailDto extends EmailTemplate<{
campaignApplicationName: string
editLink?: string
Expand All @@ -122,4 +121,4 @@ export class CreateCampaignApplicationOrganizerEmailDto extends EmailTemplate<{
firstName: string
}> {
name = TemplateType.createCampaignApplicationOrganizer
}
}
Loading