Implement support ticket system with role request and bug report functionalities
CI / Backend (build + test) (push) Successful in 1m18s
CI / Frontend (lint + typecheck + build) (push) Successful in 31s

- Introduced a new support ticket system allowing users to submit bug reports and role requests.
- Implemented endpoints for creating, updating, and managing support tickets, including file attachments.
- Enhanced Telegram bot integration to handle role requests directly within the bot, enabling admins to approve or reject requests without accessing the website.
- Updated database schema to include support ticket entities and their relationships.
- Improved API documentation to reflect new support ticket endpoints and their usage.
- Added necessary localization for support ticket features in both Russian and English.
This commit is contained in:
Leonid Pershin
2026-07-14 06:49:05 +03:00
parent 14b64a3140
commit b5630b2685
98 changed files with 4463 additions and 6 deletions
+77
View File
@@ -22,6 +22,10 @@ VpnConfig ─*─ TrafficSample (история трафика; пишетс
AuditLog (append-only журнал действий; ссылается на ActorId/TargetId)
ClientApp (каталог приложений-клиентов; группируется по OperatingSystem)
NewsPost (лента новостей; публикуется админом, видна всем аутентифицированным пользователям)
AppUser
└─0..*─ SupportTicket (баг-репорт/предложение либо заявка на роль)
└─1───*─ TicketComment (переписка; первое сообщение = описание/обоснование)
└─0..*─ TicketAttachment (изображения, диск-хранилище)
```
## Сущности
@@ -272,6 +276,71 @@ UI **настойчиво напоминает** привязать его (ед
Переходы: `Pending → Approved/Rejected/Expired`; `Approved → Consumed` (после выпуска JWT сайту).
После `Consumed`/`Expired` — не переиспользуется.
### SupportTicket — обращение в поддержку
Два вида: `BugReport` (свободная форма, с вложениями) и `RoleRequest` (запрос существующей роли —
кроме `admin` — либо параметров новой). Текст/обоснование не хранится отдельным полем — это первое
сообщение в переписке (`TicketComment`), созданное вместе с тикетом в одной операции.
| Поле | Тип | Заметки |
| ------------------- | ----------------- | ---------------------------------------------------------------- |
| `Id` | `Guid` | PK |
| `UserId` | `Guid` | FK → AppUser (автор) |
| `Type` | `TicketType` | `BugReport` / `RoleRequest` |
| `Status` | `TicketStatus` | `Open` / `Resolved` / `Closed` |
| `RequestedRoleId` | `Guid?` | Заполнено для `RoleRequest` при выборе существующей роли |
| `ProposedRoleName` | `string?` | Заполнено для `RoleRequest` при запросе новой роли |
| `ProposedMaxConfigs`| `int?` | Параметры новой роли (см. `AppRole.MaxConfigs`) |
| `ProposedMaxIpLimit`| `int?` | Параметры новой роли (см. `AppRole.MaxIpLimit`) |
| `CreatedAt` | `DateTimeOffset` | |
Инварианты и переходы (`backend/src/PnvPanel.Domain/Support/SupportTicket.cs`): `RequestedRoleId`
и `Proposed*` никогда не заполнены одновременно — гарантируется отдельными фабриками
(`CreateRoleRequestForExistingRole`/`CreateRoleRequestForNewRole`), а не runtime-проверкой.
- `Resolve()` — только из `Open`. Для `RoleRequest` одобрение — оркестрация в Application
(`ApproveRoleRequestCommandHandler`): при новой роли сначала `IRoleService.CreateRoleAsync`, затем
в любом случае `ChangeUserRoleAsync` пользователю, и только потом `ticket.Resolve()`.
- `Close()` — из `Open` или `Resolved`, **финал** (обратного пути нет). Для `RoleRequest` — отклонение.
- `Reopen()` — только из `Resolved` (владелец тикета); `Closed` не переоткрывается.
- Одновременно не более одной **открытой** заявки на роль (`Type == RoleRequest && Status == Open`)
на пользователя — проверяется в Application, аналогично `ActivationRequest.AlreadyPending`.
Баг-репорты такого ограничения не имеют.
- Доступ — только активированному пользователю (`IRequiresActivation`, как и у конфигов/новостей);
админские действия (resolve/close/approve/reject) идут по отдельным `/api/admin/support/*` с
ролевой проверкой, без завязки на активацию.
### TicketComment — сообщение в переписке
Плоская сущность (не навигационная коллекция на `SupportTicket` — конвенция проекта, см.
`TrafficSample`), одна на любое сообщение (включая первое, созданное вместе с тикетом).
| Поле | Тип | Заметки |
| ----------- | ---------------- | --------------------------------------------------------- |
| `Id` | `Guid` | PK |
| `TicketId` | `Guid` | FK → SupportTicket |
| `AuthorId` | `Guid` | FK → AppUser (владелец тикета либо админ) |
| `Body` | `string` | |
| `CreatedAt` | `DateTimeOffset` | |
Комментарий запрещён на `Closed`-тикете; на `Open`/`Resolved` — можно (для `Resolved` это не
переоткрывает тикет автоматически, переоткрытие — отдельное явное действие пользователя `Reopen()`).
### TicketAttachment — вложение (изображение)
Хранится на диске контейнера (`IFileStorage`/`DiskFileStorage`, volume `ticket_uploads` в
docker-compose) — первая в проекте функциональность загрузки файлов. Вайтлист
`image/jpeg|png|webp|gif`, до 5 МБ на файл, до 5 файлов на сообщение.
| Поле | Тип | Заметки |
| ---------------- | ---------------- | ------------------------------------------------------------------ |
| `Id` | `Guid` | PK |
| `CommentId` | `Guid` | FK → TicketComment |
| `FileName` | `string` | Оригинальное имя — только для отображения, не участвует в пути на диске |
| `StoredFileName` | `string` | Серверное GUID-имя на диске (не доверяем пользовательскому вводу) |
| `ContentType` | `string` | |
| `SizeBytes` | `long` | |
| `CreatedAt` | `DateTimeOffset` | |
Отдаётся авторизованным эндпоинтом (`GET /api/support/attachments/{id}`, проверка владения тикетом
или роли admin), не статикой — вложения могут быть чувствительными.
## Value Objects
- **NodeCredentials** (`Nodes/NodeCredentials.cs`) — `Username` + `ProtectedPassword` (шифротекст,
@@ -291,6 +360,8 @@ enum TelegramLoginStatus { Pending, Approved, Rejected, Expired, Consumed }
enum ActivationStatus { Pending, Approved, Rejected }
enum AuditSource { Web, Telegram, System }
enum OsPlatform { IOS, Android, Windows, MacOS, Linux }
enum TicketType { BugReport, RoleRequest }
enum TicketStatus { Open, Resolved, Closed }
```
## Уведомления и аудит (без диспетчера доменных событий)
@@ -316,6 +387,12 @@ enum OsPlatform { IOS, Android, Windows, MacOS, Linux }
| `PublishInboundCommandHandler` | `AuditLog` (`InboundPublished`/`InboundUnpublished`) |
| `NodeHealthCheckService` (фон) | Обновляет `NodeStatus`; realtime `nodeStatusChanged` группе `admins` |
| `TrafficSyncService` (фон) | `UpdateTraffic(...)`; realtime `configTrafficUpdated` владельцу |
| `CreateBugReportTicketCommandHandler` | Realtime `ticketCreated` группе `admins`; Telegram админам — превью текста + кнопка-ссылка на сайт |
| `CreateRoleRequestTicketCommandHandler` | Realtime `ticketCreated` группе `admins`; Telegram админам — инлайн-кнопки «Одобрить/Отклонить» |
| `AddTicketCommentCommandHandler` | Realtime `ticketUpdated` владельцу, только если комментирует не он сам |
| `ApproveRoleRequestCommandHandler` | Создаёт роль (если новая) + назначает пользователю; `AuditLog` (`RoleRequestApproved`); Telegram-DM владельцу |
| `RejectRoleRequestCommandHandler` | `AuditLog` (`RoleRequestRejected`); Telegram-DM владельцу |
| `ResolveTicketCommandHandler` / `CloseTicketCommandHandler` | `AuditLog` (`TicketResolved`/`TicketClosed`); realtime `ticketUpdated` владельцу |
SignalR-события и группы — см. [architecture.md](architecture.md#realtime-signalr) и
[api-design.md](api-design.md#signalr--hub-hubspanel).