Skip to content

Commit

Permalink
Merge branch 'release/0.6.9' into CE-1254-rebranch
Browse files Browse the repository at this point in the history
  • Loading branch information
jon-funk authored Dec 9, 2024
2 parents 31b480e + 174ef7f commit 6e655fd
Show file tree
Hide file tree
Showing 31 changed files with 388 additions and 96 deletions.
10 changes: 9 additions & 1 deletion backend/src/auth/decorators/token.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,13 @@ import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const Token = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
//Extract token from request
const request = ctx.switchToHttp().getRequest();
return request.token;
if (request.token) {
return request.token;
}
// If the token is not directly accessible in the request object, take it from the headers
let token = request.headers.authorization;
if (token && token.indexOf("Bearer ") === 0) {
token = token.substring(7);
}
return token;
});
8 changes: 8 additions & 0 deletions backend/src/auth/decorators/user.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createParamDecorator, ExecutionContext } from "@nestjs/common";

// Returns the user off of the request object.
// Sample usage: foo(@User() user) {...}
export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
});
7 changes: 5 additions & 2 deletions backend/src/auth/jwtrole.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { Role } from "../enum/role.enum";
import { ROLES_KEY } from "./decorators/roles.decorator";
import { IS_PUBLIC_KEY } from "./decorators/public.decorator";

// A list of routes that are exceptions to the READ_ONLY role only being allowed to make get requests
const READ_ONLY_EXCEPTIONS = ["/api/v1/officer/request-coms-access/:officer_guid"];

@Injectable()
/**
* An API guard used to authorize controller methods. This guard checks for othe @Roles decorator, and compares it against the role_names of the authenticated user's jwt.
Expand Down Expand Up @@ -57,8 +60,8 @@ export class JwtRoleGuard extends AuthGuard("jwt") implements CanActivate {
// Check if the user has the readonly role
const hasReadOnlyRole = userRoles.includes(Role.READ_ONLY);

// If the user has readonly role, allow only GET requests
if (hasReadOnlyRole) {
// If the user has readonly role, allow only GET requests unless the route is in the list of exceptions
if (hasReadOnlyRole && !READ_ONLY_EXCEPTIONS.includes(request.route.path)) {
if (request.method !== "GET") {
this.logger.debug(`User with readonly role attempted ${request.method} method`);
throw new ForbiddenException("Access denied: Read-only users cannot perform this action");
Expand Down
5 changes: 5 additions & 0 deletions backend/src/helpers/axios-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ export const post = async (apiToken: string, url: string, data: any, headers?: a
const config = generateConfig(apiToken, headers);
return axios.post(url, data, config);
};

export const put = async (apiToken: string, url: string, data: any, headers?: any) => {
const config = generateConfig(apiToken, headers);
return axios.put(url, data, config);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ export interface ComplaintFilterParameters {
complaintMethod?: string;
actionTaken?: string;
outcomeAnimal?: string;
outcomeAnimalStartDate?: Date;
outcomeAnimalEndDate?: Date;
}
22 changes: 17 additions & 5 deletions backend/src/v1/complaint/complaint.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,9 +817,11 @@ export class ComplaintService {
private readonly _getComplaintsByOutcomeAnimal = async (
token: string,
outcomeAnimalCode: string,
startDate: Date | undefined,
endDate: Date | undefined,
): Promise<string[]> => {
const { data, errors } = await get(token, {
query: `{getLeadsByOutcomeAnimal (outcomeAnimalCode: "${outcomeAnimalCode}")}`,
query: `{getLeadsByOutcomeAnimal (outcomeAnimalCode: "${outcomeAnimalCode}", startDate: "${startDate}" , endDate: "${endDate}")}`,
});
if (errors) {
this.logger.error("GraphQL errors:", errors);
Expand Down Expand Up @@ -975,8 +977,13 @@ export class ComplaintService {
}

// -- filter by complaint identifiers returned by case management if outcome animal filter is present
if (agency === "COS" && filters.outcomeAnimal) {
const complaintIdentifiers = await this._getComplaintsByOutcomeAnimal(token, filters.outcomeAnimal);
if (agency === "COS" && (filters.outcomeAnimal || filters.outcomeAnimalStartDate)) {
const complaintIdentifiers = await this._getComplaintsByOutcomeAnimal(
token,
filters.outcomeAnimal,
filters.outcomeAnimalStartDate,
filters.outcomeAnimalEndDate,
);

builder.andWhere("complaint.complaint_identifier IN(:...complaint_identifiers)", {
complaint_identifiers: complaintIdentifiers,
Expand Down Expand Up @@ -1144,8 +1151,13 @@ export class ComplaintService {
}

// -- filter by complaint identifiers returned by case management if outcome animal filter is present
if (agency === "COS" && filters.outcomeAnimal) {
const complaintIdentifiers = await this._getComplaintsByOutcomeAnimal(token, filters.outcomeAnimal);
if (agency === "COS" && (filters.outcomeAnimal || filters.outcomeAnimalStartDate)) {
const complaintIdentifiers = await this._getComplaintsByOutcomeAnimal(
token,
filters.outcomeAnimal,
filters.outcomeAnimalStartDate,
filters.outcomeAnimalEndDate,
);
complaintBuilder.andWhere("complaint.complaint_identifier IN(:...complaint_identifiers)", {
complaint_identifiers: complaintIdentifiers,
});
Expand Down
7 changes: 7 additions & 0 deletions backend/src/v1/officer/entities/officer.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ export class Officer {
@Column()
auth_user_guid: UUID;

@ApiProperty({
example: false,
description: "Indicates whether an officer has been enrolled in COMS",
})
@Column()
coms_enrolled_ind: boolean;

user_roles: string[];
@AfterLoad()
updateUserRoles() {
Expand Down
10 changes: 9 additions & 1 deletion backend/src/v1/officer/officer.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from "@nestjs/common";
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Put } from "@nestjs/common";
import { OfficerService } from "./officer.service";
import { CreateOfficerDto } from "./dto/create-officer.dto";
import { UpdateOfficerDto } from "./dto/update-officer.dto";
Expand All @@ -7,6 +7,8 @@ import { Role } from "../../enum/role.enum";
import { JwtRoleGuard } from "../../auth/jwtrole.guard";
import { ApiTags } from "@nestjs/swagger";
import { UUID } from "crypto";
import { User } from "../../auth/decorators/user.decorator";
import { Token } from "../../auth/decorators/token.decorator";

@ApiTags("officer")
@UseGuards(JwtRoleGuard)
Expand Down Expand Up @@ -65,6 +67,12 @@ export class OfficerController {
return this.officerService.update(id, updateOfficerDto);
}

@Put("/request-coms-access/:officer_guid")
@Roles(Role.CEEB, Role.COS_OFFICER, Role.READ_ONLY)
requestComsAccess(@Token() token, @Param("officer_guid") officer_guid: UUID, @User() user) {
return this.officerService.requestComsAccess(token, officer_guid, user);
}

@Delete(":id")
@Roles(Role.COS_OFFICER)
remove(@Param("id") id: string) {
Expand Down
36 changes: 36 additions & 0 deletions backend/src/v1/officer/officer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { PersonService } from "../person/person.service";
import { OfficeService } from "../office/office.service";
import { UUID } from "crypto";
import { CssService } from "../../external_api/css/css.service";
import { Role } from "../../enum/role.enum";
import { put } from "../../helpers/axios-api";

@Injectable()
export class OfficerService {
Expand Down Expand Up @@ -172,6 +174,40 @@ export class OfficerService {
return this.findOne(officer_guid);
}

/**
* This function requests the appropriate level of access to the storage bucket in COMS.
* If successful, the officer's record in the officer table has its `coms_enrolled_ind` indicator set to true.
* @param requestComsAccessDto An object containing the officer guid
* @returns the updated record of the officer who was granted access to COMS
*/
async requestComsAccess(token: string, officer_guid: UUID, user: any): Promise<Officer> {
try {
const currentRoles = user.client_roles;
const permissions = currentRoles.includes(Role.READ_ONLY) ? ["READ"] : ["READ", "CREATE", "UPDATE", "DELETE"];
const comsPayload = {
accessKeyId: process.env.OBJECTSTORE_ACCESS_KEY,
bucket: process.env.OBJECTSTORE_BUCKET,
bucketName: process.env.OBJECTSTORE_BUCKET_NAME,
key: process.env.OBJECTSTORE_KEY,
endpoint: process.env.OBJECTSTORE_HTTPS_URL,
secretAccessKey: process.env.OBJECTSTORE_SECRET_KEY,
permCodes: permissions,
};
const comsUrl = `${process.env.OBJECTSTORE_API_URL}/bucket`;
await put(token, comsUrl, comsPayload);
const officerRes = await this.officerRepository
.createQueryBuilder("officer")
.update()
.set({ coms_enrolled_ind: true })
.where({ officer_guid: officer_guid })
.returning("*")
.execute();
return officerRes.raw[0];
} catch (error) {
this.logger.error("An error occurred while requesting COMS access.", error);
}
}

remove(id: number) {
return `This action removes a #${id} officer`;
}
Expand Down
8 changes: 7 additions & 1 deletion charts/app/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@
{{- $objectstoreUrl := (get $secretData "objectstoreUrl" | b64dec | default "") }}
{{- $objectstoreHttpsUrl := (get $secretData "objectstoreHttpsUrl" | b64dec | default "") }}
{{- $objectstoreBackupDirectory := (get $secretData "objectstoreBackupDirectory" | b64dec | default "") }}
{{- $objectstoreKey := (get $secretData "objectstoreKey" | b64dec | default "") }}
{{- $objectstoreBucket := (get $secretData "objectstoreBucket" | b64dec | default "") }}
{{- $objectstoreBucketName := (get $secretData "objectstoreBucketName" | b64dec | default "") }}
{{- $objectstoreSecretKey := (get $secretData "objectstoreSecretKey" | b64dec | default "") }}
{{- $objectstoreApiUrl := (get $secretData "objectstoreApiUrl" | b64dec | default "") }}
{{- $jwksUri := (get $secretData "jwksUri" | b64dec | default "") }}
{{- $jwtIssuer := (get $secretData "jwtIssuer" | b64dec | default "") }}
{{- $keycloakClientId := (get $secretData "keycloakClientId" | b64dec | default "") }}
Expand Down Expand Up @@ -110,8 +113,11 @@ data:
OBJECTSTORE_URL: {{ $objectstoreUrl | b64enc | quote }}
OBJECTSTORE_HTTPS_URL: {{ $objectstoreHttpsUrl | b64enc | quote }}
OBJECTSTORE_BACKUP_DIRECTORY: {{ $objectstoreBackupDirectory | b64enc | quote }}
OBJECTSTORE_KEY: {{ $objectstoreKey | b64enc | quote }}
OBJECTSTORE_BUCKET: {{ $objectstoreBucket | b64enc | quote }}
OBJECTSTORE_BUCKET_NAME: {{ $objectstoreBucketName | b64enc | quote }}
OBJECTSTORE_SECRET_KEY: {{ $objectstoreSecretKey | b64enc | quote }}
OBJECTSTORE_API_URL: {{ $objectstoreApiUrl | b64enc | quote }}
{{- end }}
{{- if not (lookup "v1" "Secret" .Release.Namespace (printf "%s-webeoc" .Release.Name)) }}
---
Expand Down Expand Up @@ -187,4 +193,4 @@ data:

{{- end }}

{{- end }}
{{- end }}
3 changes: 3 additions & 0 deletions charts/app/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ global:
objectstoreAccessKey: ~
objectstoreUrl: ~
objectstoreBackupDirectory: ~
objectstoreKey: ~
objectstoreBucket: ~
objectstoreBucketName: ~
objectstoreSecretKey: ~
objectstoreApiUrl: ~
jwksUri: ~
jwtIssuer: ~
keycloakClientId: ~
Expand Down
97 changes: 97 additions & 0 deletions frontend/src/app/components/common/filter-date.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { FC } from "react";
import DatePicker from "react-datepicker";

interface Props {
id: string;
label: string;
startDate: Date | undefined;
endDate: Date | undefined;
handleDateChange: (dates: [Date, Date]) => void;
}

export const FilterDate: FC<Props> = ({ id, label, startDate, endDate, handleDateChange }) => {
// manual entry of date change listener. Looks for a date range format of {yyyy-mm-dd} - {yyyy-mm-dd}
const handleManualDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e?.target?.value?.includes(" - ")) {
const [startDateStr, endDateStr] = e.target.value.split(" - ");
const startDate = new Date(startDateStr);
const endDate = new Date(endDateStr);

if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
// Invalid date format
return [null, null];
} else {
// add 1 to date because days start at 0
startDate.setDate(startDate.getDate() + 1);
endDate.setDate(endDate.getDate() + 1);

handleDateChange([startDate, endDate]);
}
}
return [null, null];
};

return (
<div id={id}>
<label htmlFor="date-range-picker-id">{label}</label>
<div className="filter-select-padding">
<DatePicker
id={`date-range-picker-${id}`}
showIcon={true}
renderCustomHeader={({ monthDate, customHeaderCount, decreaseMonth, increaseMonth }) => (
<div>
<button
aria-label="Previous Month"
className={`react-datepicker__navigation react-datepicker__navigation--previous ${
customHeaderCount === 1 ? "datepicker-nav-hidden" : "datepicker-nav-visible"
}`}
onClick={decreaseMonth}
>
<span
className={
"react-datepicker__navigation-icon react-datepicker__navigation-icon--previous datepicker-nav-icon"
}
>
{"<"}
</span>
</button>
<span className="react-datepicker__current-month">
{monthDate.toLocaleString("en-US", {
month: "long",
year: "numeric",
})}
</span>
<button
aria-label="Next Month"
className={`react-datepicker__navigation react-datepicker__navigation--next ${
customHeaderCount === 1 ? "datepicker-nav-hidden" : "datepicker-nav-visible"
}`}
onClick={increaseMonth}
>
<span
className={
"react-datepicker__navigation-icon react-datepicker__navigation-icon--next datepicker-nav-icon"
}
>
{">"}
</span>
</button>
</div>
)}
selected={startDate}
onChange={handleDateChange}
onChangeRaw={handleManualDateChange}
startDate={startDate}
endDate={endDate}
dateFormat="yyyy-MM-dd"
monthsShown={2}
selectsRange={true}
isClearable={true}
wrapperClassName="comp-filter-calendar-input"
showPreviousMonths
maxDate={new Date()}
/>
</div>
</div>
);
};
Loading

0 comments on commit 6e655fd

Please sign in to comment.