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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "ActivityLogs" ADD COLUMN "userCompanyId" UUID;

-- CreateIndex
CREATE INDEX "IX_ActivityLogs_userId_userCompanyId" ON "ActivityLogs"("userId", "userCompanyId");
25 changes: 14 additions & 11 deletions prisma/schema/activityLog.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,23 @@ enum ActivityType {
}

model ActivityLog {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
taskId String @db.Uuid
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
workspaceId String @db.VarChar(32)
type ActivityType
details Json @db.JsonB
userId String @db.Uuid
userRole AssigneeType
createdAt DateTime @default(now()) @db.Timestamptz()
updatedAt DateTime @updatedAt @ignore @db.Timestamptz()
deletedAt DateTime?
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
taskId String @db.Uuid
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
workspaceId String @db.VarChar(32)
type ActivityType
details Json @db.JsonB
userId String @db.Uuid
userCompanyId String? @db.Uuid
userRole AssigneeType
createdAt DateTime @default(now()) @db.Timestamptz()
updatedAt DateTime @updatedAt @ignore @db.Timestamptz()
deletedAt DateTime?

@@index([createdAt])
@@index([taskId], name: "IX_ActivityLogs_taskId")
// Single + composite index on userId for IU + Client initiated activities
@@index([userId], name: "IX_ActivityLogs_userId")
@@index([userId, userCompanyId], name: "IX_ActivityLogs_userId_userCompanyId")
@@map("ActivityLogs")
}
1 change: 1 addition & 0 deletions src/app/api/activity-logs/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const DBActivityLogSchema = z.object({
details: DBActivityLogDetailsSchema,
taskId: z.string().uuid(),
userId: z.string().uuid(),
userCompanyId: z.string().uuid().nullable(),
userRole: z.nativeEnum(AssigneeType),
workspaceId: z.string(),
createdAt: z.date(),
Expand Down
63 changes: 51 additions & 12 deletions src/app/api/activity-logs/services/activity-log.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MAX_FETCH_ASSIGNEE_COUNT } from '@/constants/users'
import { ClientsResponse, CompaniesResponse, CopilotListArgs, InternalUsers, InternalUsersResponse } from '@/types/common'
import { CopilotListArgs, InternalUsers } from '@/types/common'
import { CopilotAPI } from '@/utils/CopilotAPI'
import { signMediaForComments } from '@/utils/signedUrlReplacer'
import {
Expand All @@ -13,7 +13,7 @@ import { CommentService } from '@api/comment/comment.service'
import APIError from '@api/core/exceptions/api'
import User from '@api/core/models/User.model'
import { BaseService } from '@api/core/services/base.service'
import { ActivityType, AssigneeType, Comment, CommentInitiator } from '@prisma/client'
import { ActivityType, AssigneeType, Comment } from '@prisma/client'
import httpStatus from 'http-status'
import { z } from 'zod'

Expand Down Expand Up @@ -54,20 +54,23 @@ export class ActivityLogService extends BaseService {
END;
`
const parsedActivityLogs = DBActivityLogArraySchema.parse(activityLogs)
const copilotService = new CopilotAPI(this.user.token)
const copilot = new CopilotAPI(this.user.token)

let filteredActivityLogs = parsedActivityLogs

if (this.user.role == AssigneeType.internalUser && this.user.internalUserId) {
const currentInternalUser = await copilotService.getInternalUser(this.user.internalUserId)
const currentInternalUser = await copilot.getInternalUser(this.user.internalUserId)
if (currentInternalUser?.isClientAccessLimited) {
filteredActivityLogs = await this.filterActivityLogsForLimitedAccess(
parsedActivityLogs,
copilotService,
copilot,
currentInternalUser,
)
}
}
if (this.user.clientId) {
filteredActivityLogs = await this.filterActivityLogsForClient(taskId, parsedActivityLogs, copilot)
}

const commentIds = filteredActivityLogs
.filter((activityLog) => activityLog.type === ActivityType.COMMENT_ADDED)
Expand Down Expand Up @@ -127,13 +130,7 @@ export class ActivityLogService extends BaseService {
throw new APIError(httpStatus.NOT_FOUND, `Error while finding comment with id ${payload.id}`)
}

let replies = allReplies.filter((reply) => reply.parentId === comment.id)

replies = replies
.map((comment) => ({
...comment,
}))
.reverse()
const replies = allReplies.filter((reply) => reply.parentId === comment.id).reverse()

return {
...payload,
Expand All @@ -151,6 +148,48 @@ export class ActivityLogService extends BaseService {
}
}

private async filterActivityLogsForClient(taskId: string, parsedActivityLogs: DBActivityLogArray, copilot: CopilotAPI) {
const task = await this.db.task.findFirstOrThrow({
where: { id: taskId, workspaceId: this.user.workspaceId },
})

// If task is a client task, then we only show activity logs authored by
// IUs, or self client
const isIuLog = (log: { userRole: AssigneeType }) => log.userRole === AssigneeType.internalUser

// Check if log is from the current company's client, do not include logs from the same client but different company
const isCurrentCompanysClientLog = (log: { userId: string; userCompanyId?: string | null }) => {
const isCorrectClient = log.userId === this.user.clientId
// The reason we do !log.userCompanyId is because there are a lot of legacy logs that don't have userCompanyId
const isCorrectCompany = !log.userCompanyId || log.userCompanyId === this.user.companyId
return isCorrectClient && isCorrectCompany
}

const isCompanyMemberLog = (
companyClients: { id: string; companyId: string }[],
log: { userId: string; userCompanyId?: string | null },
) => {
const isCorrectClient = companyClients.some((client) => client.id === log.userId)
// Backwards compatibility for legacy logs that don't have userCompanyId
const isCorrectCompany = !log.userCompanyId || log.userCompanyId === this.user.companyId
return isCorrectClient && isCorrectCompany
}

if (task.clientId) {
return parsedActivityLogs.filter((log) => isIuLog(log) || isCurrentCompanysClientLog(log))
}
// If task is a company task, then we only show activity logs authored by IU, or other clients
// within the same company
if (!task.clientId && task.companyId) {
const companyClients = (await copilot.getClients({ companyId: task.companyId }))?.data || []
return parsedActivityLogs.filter((log) => {
return isIuLog(log) || isCurrentCompanysClientLog(log) || isCompanyMemberLog(companyClients, log)
})
}

return parsedActivityLogs
}

private async filterActivityLogsForLimitedAccess(
parsedActivityLogs: DBActivityLogArray,
copilotService: CopilotAPI,
Expand Down
3 changes: 3 additions & 0 deletions src/app/api/activity-logs/services/activity-logger.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ export class ActivityLogger extends BaseService {
async log<ActivityLog extends keyof typeof SchemaByActivityType = keyof typeof SchemaByActivityType>(
activityType: ActivityLog,
payload: NonNullable<z.input<(typeof SchemaByActivityType)[ActivityLog]>>,
// Overrides inferring createdBy data from the user's token payload
createdBy?: {
userId: string
userCompanyId?: string
role: AssigneeType
},
) {
Expand All @@ -27,6 +29,7 @@ export class ActivityLogger extends BaseService {
workspaceId: this.user.workspaceId,
type: activityType,
userId: createdBy?.userId ?? z.string().parse(this.user.internalUserId || this.user.clientId),
userCompanyId: createdBy?.userCompanyId || this.user.companyId,
userRole: createdBy?.role ?? this.user.role,
details: payload,
},
Expand Down
8 changes: 7 additions & 1 deletion src/app/api/tasks/tasks.logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ export class TasksActivityLogger extends BaseService {
this.activityLogger = new ActivityLogger({ taskId: this.task.id, user })
}

async logNewTask(createdBy?: { userId: string; role: AssigneeType }) {
async logNewTask(createdBy?: {
userId: string
// We don't need to pass userCompanyId here because Clients currently cannot create tasks
// Remove this commented code if this feature is implemented in the future
// userCompanyId?: string
role: AssigneeType
}) {
await this.logTaskCreated(createdBy)
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/patch-jsdom-xhr.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ fs.readFile(filePath, 'utf8', (err, data) => {
const newLine = `const syncWorkerFile = "${resolvedPath}";`

if (!oldLine.test(data)) {
console.log('⚠️ Patch not applied: Pattern not found.')
console.info('⚠️ Patch not applied: Pattern not found.')
return
}

const updated = data.replace(oldLine, newLine)

fs.writeFile(filePath, updated, 'utf8', (err) => {
if (err) throw err
console.log('✅ Patched XMLHttpRequest-impl.js with resolved syncWorker path.')
console.info('✅ Patched XMLHttpRequest-impl.js with resolved syncWorker path.')
})
})
Loading