Skip to content
Draft
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
1 change: 1 addition & 0 deletions api/src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { ExternalOrganizationsController } from "./external-organizations-contro
export { GroupsController } from "./groups-controller"
export { InformationSharingAgreementAccessGrantsController } from "./information-sharing-agreement-access-grants-controller"
export { InformationSharingAgreementArchiveItemsController } from "./information-sharing-agreement-archive-items-controller"
export { InformationSharingAgreementAuditsController } from "./information-sharing-agreement-audits-controller"
export { InformationSharingAgreementsController } from "./information-sharing-agreements-controller"
export { NotificationsController } from "./notifications-controller"
export { RetentionsController } from "./retentions-controller"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import logger from "@/utils/logger"

import { InformationSharingAgreementAudit } from "@/models"
import { InformationSharingAgreementAuditsPolicy } from "@/policies"
import { IndexSerializer } from "@/serializers/information-sharing-agreement-audits"
import BaseController from "@/controllers/base-controller"

export class InformationSharingAgreementAuditsController extends BaseController<InformationSharingAgreementAudit> {
async index() {
try {
const where = this.buildWhere({
informationSharingAgreementId: this.params.informationSharingAgreementId,
})
const scopes = this.buildFilterScopes()
const scopedItems = InformationSharingAgreementAuditsPolicy.applyScope(
scopes,
this.currentUser
)

const totalCount = await scopedItems.count({ where })
const informationSharingAgreementAudits = await scopedItems.findAll({
where,
limit: this.pagination.limit,
offset: this.pagination.offset,
include: ["user"],
order: [["createdAt", "DESC"]],
})

const serializedItems = IndexSerializer.perform(informationSharingAgreementAudits)
return this.response.json({
informationSharingAgreementAudits: serializedItems,
totalCount,
})
} catch (error) {
logger.error("Error fetching information sharing agreement audits" + error)
return this.response.status(400).json({
message: `Error fetching information sharing agreement audits: ${error}`,
})
}
}
}

export default InformationSharingAgreementAuditsController
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Knex } from "knex"

export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable("information_sharing_agreement_audits", function (table) {
table.increments("id").notNullable().primary()
table.integer("user_id").nullable().references("id").inTable("users")
table
.integer("information_sharing_agreement_id")
.notNullable()
.references("id")
.inTable("information_sharing_agreements")
table.string("action", 200).notNullable()
table.string("description", 2000).nullable()

table
.specificType("created_at", "DATETIME2(0)")
.notNullable()
.defaultTo(knex.raw("GETUTCDATE()"))
table
.specificType("updated_at", "DATETIME2(0)")
.notNullable()
.defaultTo(knex.raw("GETUTCDATE()"))
table.specificType("deleted_at", "DATETIME2(0)")
})

await knex.schema.alterTable("information_sharing_agreements", function (table) {
table.boolean("audit_enabled").notNullable().defaultTo(false)
})

await knex("information_sharing_agreements")
.whereNot({ status: "draft" })
.update({ audit_enabled: true })
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable("information_sharing_agreements", function (table) {
table.dropColumn("audit_enabled")
})

await knex.schema.dropTable("information_sharing_agreement_audits")
}
4 changes: 4 additions & 0 deletions api/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import InformationSharingAgreement from "@/models/information-sharing-agreement"
import InformationSharingAgreementAccessGrant from "@/models/information-sharing-agreement-access-grant"
import InformationSharingAgreementAccessGrantSibling from "@/models/information-sharing-agreement-access-grant-sibling"
import InformationSharingAgreementArchiveItem from "@/models/information-sharing-agreement-archive-item"
import InformationSharingAgreementAudit from "@/models/information-sharing-agreement-audit"
import Notification from "@/models/notification"
import Retention from "@/models/retention"
import User from "@/models/user"
Expand All @@ -33,6 +34,7 @@ db.addModels([
InformationSharingAgreementAccessGrant,
InformationSharingAgreementAccessGrantSibling,
InformationSharingAgreementArchiveItem,
InformationSharingAgreementAudit,
Notification,
Retention,
User,
Expand All @@ -51,6 +53,7 @@ Group.establishScopes()
InformationSharingAgreement.establishScopes()
InformationSharingAgreementAccessGrant.establishScopes()
InformationSharingAgreementArchiveItem.establishScopes()
InformationSharingAgreementAudit.establishScopes()
Notification.establishScopes()
Retention.establishScopes()
User.establishScopes()
Expand All @@ -70,6 +73,7 @@ export {
InformationSharingAgreementAccessGrant,
InformationSharingAgreementAccessGrantSibling,
InformationSharingAgreementArchiveItem,
InformationSharingAgreementAudit,
Notification,
Retention,
User,
Expand Down
83 changes: 83 additions & 0 deletions api/src/models/information-sharing-agreement-audit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
type CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
type NonAttribute,
sql,
} from "@sequelize/core"
import {
Attribute,
AutoIncrement,
BelongsTo,
Default,
NotNull,
PrimaryKey,
} from "@sequelize/core/decorators-legacy"

import BaseModel from "@/models/base-model"
import InformationSharingAgreement from "@/models/information-sharing-agreement"
import User from "@/models/user"

export class InformationSharingAgreementAudit extends BaseModel<
InferAttributes<InformationSharingAgreementAudit>,
InferCreationAttributes<InformationSharingAgreementAudit>
> {
@Attribute(DataTypes.INTEGER)
@PrimaryKey
@AutoIncrement
declare id: CreationOptional<number>

@Attribute(DataTypes.INTEGER)
declare userId?: number

@Attribute(DataTypes.INTEGER)
@NotNull
declare informationSharingAgreementId: number

@Attribute(DataTypes.STRING(200))
@NotNull
declare action: string

@Attribute(DataTypes.STRING(2000))
declare description?: string

@Attribute(DataTypes.DATE(0))
@NotNull
@Default(sql.fn("getutcdate"))
declare createdAt: CreationOptional<Date>

@Attribute(DataTypes.DATE(0))
@NotNull
@Default(sql.fn("getutcdate"))
declare updatedAt: CreationOptional<Date>

@Attribute(DataTypes.DATE(0))
declare deletedAt: Date | null

// Associations
@BelongsTo(() => InformationSharingAgreement, {
foreignKey: "informationSharingAgreementId",
inverse: {
as: "informationSharingAgreementAudits",
type: "hasMany",
},
})
declare informationSharingAgreement?: NonAttribute<InformationSharingAgreement>

@BelongsTo(() => User, {
foreignKey: "userId",
inverse: {
as: "informationSharingAgreementAudits",
type: "hasMany",
},
})
declare user?: NonAttribute<User>

// Scopes
static establishScopes(): void {
this.addSearchScope(["action"])
}
}

export default InformationSharingAgreementAudit
5 changes: 5 additions & 0 deletions api/src/models/information-sharing-agreement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ export class InformationSharingAgreement extends BaseModel<
@Default("draft")
declare status: CreationOptional<InformationSharingAgreementStatuses>

@Attribute(DataTypes.BOOLEAN)
@NotNull
@Default(false)
declare auditEnabled: CreationOptional<boolean>

@Attribute(DataTypes.STRING(100))
declare identifier: string | null

Expand Down
1 change: 1 addition & 0 deletions api/src/policies/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { ExternalOrganizationPolicy } from "./external-organization-policy"
export { GroupPolicy } from "./group-policy"
export { InformationSharingAgreementAccessGrantPolicy } from "./information-sharing-agreement-access-grant-policy"
export { InformationSharingAgreementArchiveItemPolicy } from "./information-sharing-agreement-archive-item-policy"
export { InformationSharingAgreementAuditsPolicy } from "./information-sharing-agreement-audits-policy"
export { InformationSharingAgreementPolicy } from "./information-sharing-agreement-policy"
export { NotificationsPolicy } from "./notifications-policy"
export { RetentionPolicy } from "./retention-policy"
Expand Down
53 changes: 53 additions & 0 deletions api/src/policies/information-sharing-agreement-audits-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Attributes, FindOptions } from "@sequelize/core"

import { Path } from "@/utils/deep-pick"
import { InformationSharingAgreementAudit, User } from "@/models"
import { ALL_RECORDS_SCOPE, PolicyFactory } from "@/policies/base-policy"

export class InformationSharingAgreementAuditsPolicy extends PolicyFactory(
InformationSharingAgreementAudit
) {
show(): boolean {
if (this.user.isSystemAdmin) return true
if (this.user.id === this.record.userId) return true

return false
}

create(): boolean {
return false
}

update(): boolean {
return false
}

destroy(): boolean {
return false
}

permittedAttributes(): Path[] {
return []
}

permittedAttributesForCreate(): Path[] {
return []
}

static policyScope(user: User): FindOptions<Attributes<InformationSharingAgreementAudit>> {
if (user.isSystemAdmin) return ALL_RECORDS_SCOPE

return {
include: [
{
association: "informationSharingAgreement",
where: {
creatorId: user.id,
},
},
],
}
}
}

export default InformationSharingAgreementAuditsPolicy
4 changes: 4 additions & 0 deletions api/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
GroupsController,
InformationSharingAgreementAccessGrantsController,
InformationSharingAgreementArchiveItemsController,
InformationSharingAgreementAuditsController,
InformationSharingAgreements,
InformationSharingAgreementsController,
Notifications,
Expand Down Expand Up @@ -189,6 +190,9 @@ router
router
.route("/api/information-sharing-agreements/:informationSharingAgreementId/archive-items")
.post(InformationSharingAgreements.ArchiveItemsController.create)
router
.route("/api/information-sharing-agreements/:informationSharingAgreementId/audits")
.get(InformationSharingAgreementAuditsController.index)

router
.route("/api/information-sharing-agreement-access-grants")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { pick } from "lodash"

import { InformationSharingAgreementAudit } from "@/models"
import BaseSerializer from "@/serializers/base-serializer"
import ReferenceSerializer, { UserAsReference } from "@/serializers/users/reference-serializer"

export type InformationSharingAgreementAuditIndexView = Pick<
InformationSharingAgreementAudit,
"id" | "informationSharingAgreementId" | "action" | "description" | "createdAt"
> & { user: UserAsReference | null }

export class IndexSerializer extends BaseSerializer<InformationSharingAgreementAudit> {
perform(): InformationSharingAgreementAuditIndexView {
return {
...pick(this.record, [
"id",
"informationSharingAgreementId",
"action",
"description",
"createdAt",
]),
user: this.record.user ? ReferenceSerializer.perform(this.record.user) : null,
}
}
}

export default IndexSerializer
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { IndexSerializer } from "./index-serializer"
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type InformationSharingAgreementAsShow = Pick<
| "internalGroupContactId"
| "internalGroupSecondaryContactId"
| "status"
| "auditEnabled"
| "identifier"
| "externalGroupInfo"
| "internalGroupInfo"
Expand Down Expand Up @@ -91,6 +92,7 @@ export class ShowSerializer extends BaseSerializer<InformationSharingAgreement>
"internalGroupContactId",
"internalGroupSecondaryContactId",
"status",
"auditEnabled",
"identifier",
"externalGroupInfo",
"internalGroupInfo",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import db, { InformationSharingAgreement, User } from "@/models"
import db, {
InformationSharingAgreement,
InformationSharingAgreementAudit,
User,
} from "@/models"
import BaseService from "@/services/base-service"
import { InformationSharingAgreements } from "@/services"

Expand All @@ -25,6 +29,16 @@ export class RevertToDraftService extends BaseService {
signedById: null,
signedAt: null,
})

if (this.informationSharingAgreement.auditEnabled) {
await InformationSharingAgreementAudit.create({
informationSharingAgreementId: this.informationSharingAgreement.id,
userId: this.currentUser.id,
action: "Reverted to draft",
description: `${this.currentUser.displayName} reverted the agreement to draft`,
})
}

return this.informationSharingAgreement.reload({
include: [
"accessGrants",
Expand Down
Loading