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>
This commit is contained in:
2026-10-15 18:02:36 +02:00
parent f60d4c8346
commit 8bf7d47b2e
66 changed files with 607 additions and 130 deletions

View File

@@ -200,6 +200,15 @@ export class Issue extends VersionedAggregateRoot implements IssueProps {
this.apply(new IssueUpdatedEvent(actorId, this.id, this.projectId, patch));
}
/** Opened it, has it, or follows it: what a guest may see. */
involves(userId: string): boolean {
return (
this.reporterId === userId ||
this.assigneeId === userId ||
this.watchers.includes(userId)
);
}
/** Moves the ticket along its project's workflow. */
changeStatus(
actorId: string,

View File

@@ -4,4 +4,9 @@ export enum ProjectRole {
MEMBER = 'MEMBER',
/** Read-only stakeholder (PM, client...). */
VIEWER = 'VIEWER',
/**
* Someone from outside (a client): in a private project, sees only the
* tickets they take part in, opens tickets and comments on theirs.
*/
GUEST = 'GUEST',
}

View File

@@ -102,9 +102,31 @@ export class Project extends VersionedAggregateRoot implements ProjectProps {
if (this.roleOf(userId) === ProjectRole.VIEWER) {
throw new ForbiddenError(`Viewers cannot modify project ${this.key}`);
}
if (this.roleOf(userId) === ProjectRole.GUEST) {
throw new ForbiddenError(
`Los invitados solo pueden abrir tickets en ${this.key} y comentar en los suyos`,
);
}
this.assertNotArchived();
}
/** Can open tickets and comment: members and guests (not viewers). */
assertCanReport(userId: string): void {
const role = this.roleOf(userId);
if (!role) {
throw new ForbiddenError(`You are not a member of project ${this.key}`);
}
if (role === ProjectRole.VIEWER) {
throw new ForbiddenError(`Viewers cannot modify project ${this.key}`);
}
this.assertNotArchived();
}
/** A guest of a private project: they only see the tickets they take part in. */
seesOnlyOwnTickets(userId: string): boolean {
return !this.isPublic && this.roleOf(userId) === ProjectRole.GUEST;
}
/** Can change members, releases, settings and rules (owners and admins). */
assertCanManage(userId: string): void {
this.assertManages(userId);