Files
task-stream/libs/api/tracker/application/src/lib/queries/get-board.query-handler.ts
Josep Piguillem 8bf7d47b2e feat: guest role for clients and outsiders
A project member can be a GUEST. In a private project a guest only sees
the tickets they take part in (opened, assigned or followed): every other
ticket answers 404, also in my work, realtime changes, blockers and
linked tickets, and project-wide views (activity, statistics, dashboard,
releases, sprints, epics, trash) answer 403. Guests open tickets without
assigning or planning them, and comment, attach, follow and vote on their
own; they change nothing else. In a public project they see what anyone
does. The web gives them only Tablero, Lista and Miembros and loads none
of the project-wide data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:02:36 +02:00

61 lines
2.0 KiB
TypeScript

import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import {
DEFAULT_PROJECT_SETTINGS,
StatusCategory,
} from '@task-stream/api/tracker/domain';
import {
FindIssuesRepository,
IssueSort,
} from '../ports/find-issues.repository';
import { FindProjectsRepository } from '../ports/find-projects.repository';
import { BoardColumn, GetBoardQuery } from './get-board.query';
import { ReadAccess } from './read-access';
@QueryHandler(GetBoardQuery)
export class GetBoardQueryHandler
implements IQueryHandler<GetBoardQuery, BoardColumn[]>
{
constructor(
private readonly findIssues: FindIssuesRepository,
private readonly findProjects: FindProjectsRepository,
private readonly access: ReadAccess,
) {}
/**
* Columns follow the project's statuses in order. Cancelled ones stay off the
* board; archived ones only show while they still hold tickets.
*/
async execute(query: GetBoardQuery): Promise<BoardColumn[]> {
await this.access.assertCanView(query.filter.projectId, query.viewerId);
const participantId =
(await this.access.participantScope(
query.filter.projectId,
query.viewerId,
)) ?? undefined;
const project = await this.findProjects.findById(query.filter.projectId);
const statuses = (
project?.settings ?? DEFAULT_PROJECT_SETTINGS
).statuses.filter((status) => status.category !== StatusCategory.CANCELLED);
const columns = await Promise.all(
statuses.map(async (status) => {
const page = await this.findIssues.search({
...query.filter,
participantId,
statuses: [status.id],
// Each column follows the backlog order.
sort: IssueSort.RANK,
limit: Math.min(query.perColumn, 200),
offset: 0,
});
return {
column: { status: status.id, issues: page.items, total: page.total },
archived: status.archived,
};
}),
);
return columns
.filter(({ column, archived }) => !archived || column.total > 0)
.map(({ column }) => column);
}
}