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
52 changes: 31 additions & 21 deletions apps/api/src/tasks/tasks.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ export class TasksController {
private readonly attachmentsService: AttachmentsService,
) {}

private async resolveTaskMutationUserId(
authContext: AuthContextType,
organizationId: string,
missingUserMessage: string,
): Promise<string> {
if (authContext.userId) {
return authContext.userId;
}

if (authContext.isApiKey) {
return this.tasksService.getApiKeyActorUserId(organizationId);
}

throw new BadRequestException(missingUserMessage);
}

// ==================== TASKS ====================

@Get()
Expand Down Expand Up @@ -171,13 +187,11 @@ export class TasksController {
}
}

// Get userId from auth context
if (!authContext.userId) {
throw new BadRequestException(
'User ID is required. Bulk operations require authenticated user session.',
);
}
const userId = authContext.userId;
const userId = await this.resolveTaskMutationUserId(
authContext,
organizationId,
'User ID is required. Bulk operations require authenticated user session.',
);

return await this.tasksService.updateTasksStatus(
organizationId,
Expand Down Expand Up @@ -241,13 +255,11 @@ export class TasksController {
throw new BadRequestException('taskIds must be a non-empty array');
}

// Get userId from auth context
if (!authContext.userId) {
throw new BadRequestException(
'User ID is required. Bulk operations require authenticated user session.',
);
}
const userId = authContext.userId;
const userId = await this.resolveTaskMutationUserId(
authContext,
organizationId,
'User ID is required. Bulk operations require authenticated user session.',
);

return await this.tasksService.updateTasksAssignee(
organizationId,
Expand Down Expand Up @@ -517,13 +529,11 @@ export class TasksController {
reviewDate?: string;
},
): Promise<TaskResponseDto> {
// Get userId from auth context
if (!authContext.userId) {
throw new BadRequestException(
'User ID is required. Task updates require authenticated user session.',
);
}
const userId = authContext.userId;
const userId = await this.resolveTaskMutationUserId(
authContext,
organizationId,
'User ID is required. Task updates require authenticated user session.',
);

let parsedReviewDate: Date | null | undefined;
if (body.reviewDate !== undefined) {
Expand Down
31 changes: 31 additions & 0 deletions apps/api/src/tasks/tasks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,37 @@ import { TaskNotifierService } from './task-notifier.service';
export class TasksService {
constructor(private readonly taskNotifierService: TaskNotifierService) {}

/**
* Resolve a user actor for API-key authenticated requests.
* We attribute changes to an active organization owner to preserve audit trail requirements.
*/
async getApiKeyActorUserId(organizationId: string): Promise<string> {
const ownerMember = await db.member.findFirst({
where: {
organizationId,
deactivated: false,
isActive: true,
role: {
contains: 'owner',
},
},
orderBy: {
createdAt: 'asc',
},
select: {
userId: true,
},
});

if (!ownerMember?.userId) {
throw new BadRequestException(
'No active organization owner found. API key task updates require an active owner member.',
);
}

return ownerMember.userId;
}

/**
* Get all tasks for an organization
*/
Expand Down
Loading