Files
ulitka/src/modules/issue-attachment/presentation/controllers/issue-attachment.controller.ts
T
vadimandClaude Opus 5.5 b1f6b61994
Gitea Actions / Deploy (push) Canceled after 5m13s
create issues fix
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>
2026-10-25 22:23:49 +03:00

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;
}
}
}