issue observers Наблюдатели задачи видят её и комментируют, но не редактируют. - таблица issue_observer, наблюдатели — часть сущности Issue - видимость: reporter, assignee или наблюдатель (задачи, история, SSE) - правка/удаление задачи и вложений — только reporter/assignee (403 наблюдателю) - observers в create/update, DELETE /issues/:id/observers/me, фильтр role - история изменений и уведомления новым наблюдателям Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
46903e9ec5
commit
b1f6b61994
54 files changed
+901
-69
No files matched your search
+1
-1
@@ -36,7 +36,7 @@ export class IssueAttachmentFileOwnerService implements FileOwner, OnModuleInit
|
||||
/**
|
||||
* Файл виден, если видна хоть одна задача, к которой он прикреплён.
|
||||
* Видимость проверяем через IIssueRepository.findById, чтобы правило
|
||||
* (reporter/assignee) жило в одном месте. Обычно файл прикреплён к одной задаче.
|
||||
* (reporter/assignee/наблюдатель) жило в одном месте. Обычно файл прикреплён к одной задаче.
|
||||
*/
|
||||
async isVisibleThrough(fileId: string, userId: string): Promise<boolean> {
|
||||
const issueIds = await this.attachmentRepository.findIssueIdsByFile(fileId);
|
||||
|
||||
+7
-2
@@ -8,6 +8,7 @@ import {
|
||||
type IIssueRepository,
|
||||
} from '../../../../issue/domain/repositories/issue.repository.interface.js';
|
||||
import { IssueNotFoundException } from '../../../../issue/domain/exceptions/issue-not-found.exception.js';
|
||||
import { IssueEditForbiddenException } from '../../../../issue/domain/exceptions/issue-edit-forbidden.exception.js';
|
||||
import { FileAccessService } from '../../../../file/application/services/file-access.service.js';
|
||||
import { IssueAttachment } from '../../../domain/entities/issue-attachment.entity.js';
|
||||
import { AttachFileCommand } from './attach-file.command.js';
|
||||
@@ -15,7 +16,7 @@ import { IssueAttachmentDto } from '../../dto/issue-attachment.dto.js';
|
||||
|
||||
/**
|
||||
* Прикрепляет к задаче файл, заранее загруженный через POST /files.
|
||||
* Прикреплять может любой, кому видна задача (как и править её),
|
||||
* Прикреплять может тот, кто может править задачу (не наблюдатель),
|
||||
* но только собственный файл — см. StoredFile.assertOwnedBy.
|
||||
*/
|
||||
@Injectable()
|
||||
@@ -29,9 +30,13 @@ export class AttachFileUseCase {
|
||||
) {}
|
||||
|
||||
async execute(command: AttachFileCommand): Promise<IssueAttachmentDto> {
|
||||
if (!(await this.issueRepository.findById(command.issueId, command.userId))) {
|
||||
const issue = await this.issueRepository.findById(command.issueId, command.userId);
|
||||
if (!issue) {
|
||||
throw new IssueNotFoundException(command.issueId);
|
||||
}
|
||||
if (!issue.canBeEditedBy(command.userId)) {
|
||||
throw new IssueEditForbiddenException(command.issueId);
|
||||
}
|
||||
|
||||
const file = await this.fileAccess.getAttachableFile(command.fileId, command.userId);
|
||||
const attachment = await this.attachmentRepository.create(
|
||||
|
||||
+7
-1
@@ -8,12 +8,14 @@ import {
|
||||
type IIssueRepository,
|
||||
} from '../../../../issue/domain/repositories/issue.repository.interface.js';
|
||||
import { IssueNotFoundException } from '../../../../issue/domain/exceptions/issue-not-found.exception.js';
|
||||
import { IssueEditForbiddenException } from '../../../../issue/domain/exceptions/issue-edit-forbidden.exception.js';
|
||||
import { IssueAttachmentNotFoundException } from '../../../domain/exceptions/issue-attachment-not-found.exception.js';
|
||||
import { DetachFileCommand } from './detach-file.command.js';
|
||||
|
||||
/**
|
||||
* Открепляет файл от задачи. Сам файл не удаляется: если он больше
|
||||
* ни к чему не прикреплён, его удалит GC file-модуля.
|
||||
* Наблюдателям задачи недоступно — как и её правка.
|
||||
*/
|
||||
@Injectable()
|
||||
export class DetachFileUseCase {
|
||||
@@ -25,9 +27,13 @@ export class DetachFileUseCase {
|
||||
) {}
|
||||
|
||||
async execute(command: DetachFileCommand): Promise<void> {
|
||||
if (!(await this.issueRepository.findById(command.issueId, command.userId))) {
|
||||
const issue = await this.issueRepository.findById(command.issueId, command.userId);
|
||||
if (!issue) {
|
||||
throw new IssueNotFoundException(command.issueId);
|
||||
}
|
||||
if (!issue.canBeEditedBy(command.userId)) {
|
||||
throw new IssueEditForbiddenException(command.issueId);
|
||||
}
|
||||
|
||||
if (!(await this.attachmentRepository.delete(command.issueId, command.fileId))) {
|
||||
throw new IssueAttachmentNotFoundException(command.issueId, command.fileId);
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import { IssueAttachmentNotFoundException } from '../../domain/exceptions/issue-
|
||||
import { IssueNotFoundException } from '../../../issue/domain/exceptions/issue-not-found.exception.js';
|
||||
import { FileNotFoundException } from '../../../file/domain/exceptions/file-not-found.exception.js';
|
||||
import { FileForbiddenException } from '../../../file/domain/exceptions/file-forbidden.exception.js';
|
||||
import { IssueEditForbiddenException } from '../../../issue/domain/exceptions/issue-edit-forbidden.exception.js';
|
||||
|
||||
/**
|
||||
* Вложения задачи. Сам файл загружается отдельно (POST /files),
|
||||
@@ -78,7 +79,7 @@ export class IssueAttachmentController {
|
||||
) {
|
||||
throw new NotFoundException(error.message);
|
||||
}
|
||||
if (error instanceof FileForbiddenException) {
|
||||
if (error instanceof FileForbiddenException || error instanceof IssueEditForbiddenException) {
|
||||
throw new ForbiddenException(error.message);
|
||||
}
|
||||
throw error;
|
||||
|
||||
Reference in new issue
Block a user