Skip to content
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
33 changes: 27 additions & 6 deletions api/src/controllers/property.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import {
ValidationPipe,
Delete,
UseGuards,
Request,
} from '@nestjs/common';
import {
ApiExtraModels,
ApiOkResponse,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { Request as ExpressRequest } from 'express';
import { PermissionTypeDecorator } from '../decorators/permission-type.decorator';
import { PaginatedPropertyDto } from '../dtos/properties/paginated-property.dto';
import { PropertyQueryParams } from '../dtos/properties/property-query-params.dto';
Expand All @@ -30,11 +32,13 @@ import { SuccessDTO } from '../dtos/shared/success.dto';
import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options';
import { PaginationMeta } from '../dtos/shared/pagination.dto';
import { JwtAuthGuard } from '../guards/jwt.guard';
import { PermissionGuard } from '../guards/permission.guard';
import { PermissionAction } from '../decorators/permission-action.decorator';
import { permissionActions } from '../enums/permissions/permission-actions-enum';
import { ApiKeyGuard } from '../guards/api-key.guard';
import { PropertyFilterParams } from '../dtos/properties/property-filter-params.dto';
import { mapTo } from '../utilities/mapTo';
import { User } from '../../src/dtos/users/user.dto';
import { PermissionGuard } from '../../src/guards/permission.guard';

@Controller('properties')
@ApiTags('properties')
Expand All @@ -48,7 +52,7 @@ import { PropertyFilterParams } from '../dtos/properties/property-filter-params.
IdDTO,
)
@PermissionTypeDecorator('properties')
@UseGuards(ApiKeyGuard, JwtAuthGuard, PermissionGuard)
@UseGuards(ApiKeyGuard, JwtAuthGuard)
export class PropertyController {
constructor(private readonly propertyService: PropertyService) {}

Expand All @@ -59,6 +63,7 @@ export class PropertyController {
})
@UsePipes(new ValidationPipe(defaultValidationPipeOptions))
@ApiOkResponse({ type: PaginatedPropertyDto })
@UseGuards(PermissionGuard)
public async getPaginatedSet(
@Query() queryParams: PropertyQueryParams,
): Promise<PaginatedPropertyDto> {
Expand All @@ -71,6 +76,7 @@ export class PropertyController {
operationId: 'getById',
})
@ApiOkResponse({ type: Property })
@UseGuards(PermissionGuard)
public async getPropertyById(
@Param('id', new ParseUUIDPipe({ version: '4' })) propertyId: string,
): Promise<Property> {
Expand All @@ -85,6 +91,7 @@ export class PropertyController {
@PermissionAction(permissionActions.read)
@UsePipes(new ValidationPipe(defaultValidationPipeOptions))
@ApiOkResponse({ type: PaginatedPropertyDto })
@UseGuards(PermissionGuard)
public async getFiltrablePaginatedSet(
@Body() queryParams: PropertyQueryParams,
): Promise<PaginatedPropertyDto> {
Expand All @@ -100,8 +107,12 @@ export class PropertyController {
@ApiOkResponse({ type: Property })
public async addProperty(
@Body() propertyDto: PropertyCreate,
@Request() req: ExpressRequest,
): Promise<Property> {
return await this.propertyService.create(propertyDto);
return await this.propertyService.create(
propertyDto,
mapTo(User, req['user']),
);
}

@Put()
Expand All @@ -113,8 +124,12 @@ export class PropertyController {
@ApiOkResponse({ type: Property })
public async updateProperty(
@Body() propertyDto: PropertyUpdate,
@Request() req: ExpressRequest,
): Promise<Property> {
return await this.propertyService.update(propertyDto);
return await this.propertyService.update(
propertyDto,
mapTo(User, req['user']),
);
}

@Delete()
Expand All @@ -124,7 +139,13 @@ export class PropertyController {
})
@UsePipes(new ValidationPipe(defaultValidationPipeOptions))
@ApiOkResponse({ type: SuccessDTO })
public async deleteById(@Body() idDto: IdDTO): Promise<SuccessDTO> {
return await this.propertyService.deleteOne(idDto.id);
public async deleteById(
@Body() idDto: IdDTO,
@Request() req: ExpressRequest,
): Promise<SuccessDTO> {
return await this.propertyService.deleteOne(
idDto.id,
mapTo(User, req['user']),
);
}
}
13 changes: 0 additions & 13 deletions api/src/permission-configs/permission_policy.csv
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ p, supportAdmin, properties, true, read
p, jurisdictionAdmin, properties, true, read
p, limitedJurisdictionAdmin, properties, true, read
p, partner, properties, true, read
p, anonymous, properties, true, read

p, admin, agency, true, .*
p, supportAdmin, agency, true, read
Expand All @@ -54,18 +53,6 @@ p, jurisdictionAdmin, listingEvent, true, .*
p, limitedJurisdictionAdmin, listingEvent, true, .*
p, partner, listingEvent, true, read

p, admin, property, true, .*
p, supportAdmin, property, true, .*
p, jurisdictionAdmin, property, true, .*
p, limitedJurisdictionAdmin, property, true, .*
p, partner, property, true, read

p, admin, propertyGroup, true, .*
p, supportAdmin, propertyGroup, true, .*
p, jurisdictionAdmin, propertyGroup, true, .*
p, limitedJurisdictionAdmin, propertyGroup, true, .*
p, partner, propertyGroup, true, read

p, admin, amiChart, true, .*
p, supportAdmin, amiChart, true, .*
p, jurisdictionAdmin, amiChart, true, .*
Expand Down
47 changes: 41 additions & 6 deletions api/src/services/property.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ import { Property } from '../dtos/properties/property.dto';
import { PaginatedPropertyDto } from '../dtos/properties/paginated-property.dto';
import PropertyCreate from '../dtos/properties/property-create.dto';
import { PropertyUpdate } from '../dtos/properties/property-update.dto';
import { User } from '../dtos/users/user.dto';
import { permissionActions } from '../enums/permissions/permission-actions-enum';
import { SuccessDTO } from '../dtos/shared/success.dto';
import { Prisma } from '@prisma/client';
import { buildFilter } from '../utilities/build-filter';
import { PermissionService } from './permission.service';

@Injectable()
export class PropertyService {
constructor(private prisma: PrismaService) {}
constructor(
private prisma: PrismaService,
private permissionService: PermissionService,
) {}

/**
* Returns a paginated list of properties matching the provided query parameters.
Expand Down Expand Up @@ -99,7 +105,7 @@ export class PropertyService {
* @throws {BadRequestException} If a jurisdiction is not provided.
* @throws {NotFoundException} If the linked jurisdiction cannot be found.
*/
async create(propertyDto: PropertyCreate) {
async create(propertyDto: PropertyCreate, requestingUser: User) {
if (!propertyDto.jurisdictions) {
throw new BadRequestException('A jurisdiction must be provided');
}
Expand All @@ -120,6 +126,15 @@ export class PropertyService {
);
}

await this.permissionService.canOrThrow(
requestingUser,
'properties',
permissionActions.create,
{
jurisdictionId: rawJurisdiction.id,
},
);

const rawProperty = await this.prisma.properties.create({
data: {
...propertyDto,
Expand Down Expand Up @@ -147,7 +162,7 @@ export class PropertyService {
* @throws {BadRequestException} If a jurisdiction is not provided.
* @throws {NotFoundException} If the linked jurisdiction cannot be found.
*/
async update(propertyDto: PropertyUpdate) {
async update(propertyDto: PropertyUpdate, requestingUser: User) {
if (!propertyDto.jurisdictions) {
throw new BadRequestException('A jurisdiction must be provided');
}
Expand All @@ -169,6 +184,17 @@ export class PropertyService {

await this.findOrThrow(propertyDto.id);

await this.permissionService.canOrThrow(
requestingUser,
'properties',
permissionActions.update,
{
jurisdictionId: rawJurisdiction.id,
},
);

await this.findOrThrow(propertyDto.id);

const rawProperty = await this.prisma.properties.update({
data: {
...propertyDto,
Expand Down Expand Up @@ -199,7 +225,7 @@ export class PropertyService {
* @throws {BadRequestException} If no property ID is provided.
* @throws {NotFoundException} If the property or its linked jurisdiction is not found.
*/
async deleteOne(propertyId: string) {
async deleteOne(propertyId: string, requestingUser: User) {
if (!propertyId) {
throw new BadRequestException('a property ID must be provided');
}
Expand Down Expand Up @@ -227,6 +253,15 @@ export class PropertyService {
);
}

await this.permissionService.canOrThrow(
requestingUser,
'properties',
permissionActions.delete,
{
jurisdictionId: rawJurisdiction.id,
},
);

await this.prisma.properties.delete({
where: {
id: propertyId,
Expand All @@ -243,7 +278,7 @@ export class PropertyService {
*
* @param propertyId - The ID of the property to look up.
* @returns The raw property entity including its jurisdictions.
* @throws {BadRequestException} If no property is found for the given ID.
* @throws {NotFoundException} If no property is found for the given ID.
*/
async findOrThrow(propertyId: string): Promise<Property> {
const property = await this.prisma.properties.findFirst({
Expand All @@ -256,7 +291,7 @@ export class PropertyService {
});

if (!property) {
throw new BadRequestException(
throw new NotFoundException(
`Property with id ${propertyId} was not found`,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1479,7 +1479,7 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr
.expect(200);
});

it('should error as forbidden for create endpoint', async () => {
it('should succeed for create endpoint', async () => {
const propertyData = {
name: 'New Test Property',
jurisdictions: {
Expand All @@ -1492,7 +1492,7 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr
.send(propertyData)
.set({ passkey: process.env.API_PASS_KEY || '' })
.set('Cookie', cookies)
.expect(403);
.expect(201);
});

it('should succeed for filterable list endpoint', async () => {
Expand All @@ -1504,7 +1504,7 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr
.expect(201);
});

it('should error as forbidden for update endpoint', async () => {
it('should succeed for update endpoint', async () => {
if (!propertyId) {
throw new Error('Property ID not set up for test');
}
Expand All @@ -1522,10 +1522,10 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr
.send(propertyUpdateData)
.set({ passkey: process.env.API_PASS_KEY || '' })
.set('Cookie', cookies)
.expect(403);
.expect(200);
});

it('should error as forbidden for delete endpoint', async () => {
it('should succeed for delete endpoint', async () => {
const propertyData = {
name: 'Property to Delete',
jurisdictions: {
Expand Down Expand Up @@ -1553,7 +1553,7 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr
} as IdDTO)
.set({ passkey: process.env.API_PASS_KEY || '' })
.set('Cookie', cookies)
.expect(403);
.expect(200);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1380,15 +1380,15 @@ describe('Testing Permissioning of endpoints as public user', () => {
}
});

it('should succeed for list endpoint', async () => {
it('should error as forbidden for list endpoint', async () => {
await request(app.getHttpServer())
.get(`/properties?`)
.set({ passkey: process.env.API_PASS_KEY || '' })
.set('Cookie', cookies)
.expect(200);
.expect(403);
});

it('should succeed for retrieve endpoint', async () => {
it('should error as forbidden for retrieve endpoint', async () => {
if (!propertyId) {
throw new Error('Property ID not set up for test');
}
Expand All @@ -1397,7 +1397,7 @@ describe('Testing Permissioning of endpoints as public user', () => {
.get(`/properties/${propertyId}`)
.set({ passkey: process.env.API_PASS_KEY || '' })
.set('Cookie', cookies)
.expect(200);
.expect(403);
});

it('should error as forbidden for create endpoint', async () => {
Expand All @@ -1416,13 +1416,13 @@ describe('Testing Permissioning of endpoints as public user', () => {
.expect(403);
});

it('should succeed for filterable list endpoint', async () => {
it('should error as forbidden for filterable list endpoint', async () => {
await request(app.getHttpServer())
.post(`/properties/list`)
.send({})
.set({ passkey: process.env.API_PASS_KEY || '' })
.set('Cookie', cookies)
.expect(201);
.expect(403);
});

it('should error as forbidden for update endpoint', async () => {
Expand Down
8 changes: 4 additions & 4 deletions api/test/integration/property.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ describe('Properties Controller Tests', () => {
expect(res.body.message[0]).toEqual('id should not be null or undefined');
});

it('should throw error when an given ID does not exist', async () => {
it('should throw error when a given ID does not exist', async () => {
const randId = randomUUID();
const res = await request(app.getHttpServer())
.put('/properties')
Expand All @@ -396,7 +396,7 @@ describe('Properties Controller Tests', () => {
})
.set({ passkey: process.env.API_PASS_KEY || '' })
.set('Cookie', cookies)
.expect(400);
.expect(404);

expect(res.body.message).toEqual(
`Property with id ${randId} was not found`,
Expand Down Expand Up @@ -453,7 +453,7 @@ describe('Properties Controller Tests', () => {
expect(res.body.message[0]).toBe('id should not be null or undefined');
});

it('should throw error when an given ID does not exist', async () => {
it('should throw error when a given ID does not exist', async () => {
const randId = randomUUID();
const res = await request(app.getHttpServer())
.delete('/properties')
Expand All @@ -462,7 +462,7 @@ describe('Properties Controller Tests', () => {
})
.set({ passkey: process.env.API_PASS_KEY || '' })
.set('Cookie', cookies)
.expect(400);
.expect(404);

expect(res.body.message).toBe(`Property with id ${randId} was not found`);
});
Expand Down
2 changes: 0 additions & 2 deletions api/test/unit/services/geocoding.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ describe('GeocodingService', () => {
const date = new Date();
const address: Address = {
id: 'id',
createdAt: date,
updatedAt: date,
city: 'Washington',
county: null,
state: 'DC',
Expand Down
Loading
Loading