Gitea Actions / Deploy (push) Canceled after 5m13s
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>
89 lines
3.6 KiB
TypeScript
89 lines
3.6 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
ForbiddenException,
|
|
Get,
|
|
HttpCode,
|
|
HttpStatus,
|
|
NotFoundException,
|
|
Param,
|
|
ParseIntPipe,
|
|
Post,
|
|
} from '@nestjs/common';
|
|
import { Session, type UserSession } from '@thallesp/nestjs-better-auth';
|
|
import { ListIssueAttachmentsUseCase } from '../../application/use-cases/list-issue-attachments/list-issue-attachments.use-case.js';
|
|
import { AttachFileUseCase } from '../../application/use-cases/attach-file/attach-file.use-case.js';
|
|
import { DetachFileUseCase } from '../../application/use-cases/detach-file/detach-file.use-case.js';
|
|
import { ListIssueAttachmentsQuery } from '../../application/use-cases/list-issue-attachments/list-issue-attachments.query.js';
|
|
import { AttachFileCommand } from '../../application/use-cases/attach-file/attach-file.command.js';
|
|
import { DetachFileCommand } from '../../application/use-cases/detach-file/detach-file.command.js';
|
|
import { AttachFileRequestDto } from '../dto/attach-file.dto.js';
|
|
import { IssueAttachmentNotFoundException } from '../../domain/exceptions/issue-attachment-not-found.exception.js';
|
|
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),
|
|
* здесь — только привязка к задаче. Скачивание — GET /files/:id/download.
|
|
*/
|
|
@Controller('issues/:issueId/attachments')
|
|
export class IssueAttachmentController {
|
|
constructor(
|
|
private readonly listIssueAttachmentsUseCase: ListIssueAttachmentsUseCase,
|
|
private readonly attachFileUseCase: AttachFileUseCase,
|
|
private readonly detachFileUseCase: DetachFileUseCase,
|
|
) {}
|
|
|
|
@Get()
|
|
list(@Param('issueId', ParseIntPipe) issueId: number, @Session() session: UserSession) {
|
|
return this.handleErrors(() =>
|
|
this.listIssueAttachmentsUseCase.execute(new ListIssueAttachmentsQuery(issueId, session.user.id)),
|
|
);
|
|
}
|
|
|
|
@Post()
|
|
attach(
|
|
@Param('issueId', ParseIntPipe) issueId: number,
|
|
@Body() dto: AttachFileRequestDto,
|
|
@Session() session: UserSession,
|
|
) {
|
|
return this.handleErrors(() =>
|
|
this.attachFileUseCase.execute(new AttachFileCommand(issueId, dto.fileId, session.user.id)),
|
|
);
|
|
}
|
|
|
|
@Delete(':fileId')
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
async detach(
|
|
@Param('issueId', ParseIntPipe) issueId: number,
|
|
@Param('fileId') fileId: string,
|
|
@Session() session: UserSession,
|
|
) {
|
|
await this.handleErrors(() =>
|
|
this.detachFileUseCase.execute(new DetachFileCommand(issueId, fileId, session.user.id)),
|
|
);
|
|
}
|
|
|
|
/** Транслирует доменные исключения в HTTP-ответ — это забота presentation-слоя */
|
|
private async handleErrors<T>(fn: () => Promise<T>): Promise<T> {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
if (
|
|
error instanceof IssueNotFoundException ||
|
|
error instanceof FileNotFoundException ||
|
|
error instanceof IssueAttachmentNotFoundException
|
|
) {
|
|
throw new NotFoundException(error.message);
|
|
}
|
|
if (error instanceof FileForbiddenException || error instanceof IssueEditForbiddenException) {
|
|
throw new ForbiddenException(error.message);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|