|
| 1 | +# Страницы приложения |
| 2 | + |
| 3 | +## RBAC |
| 4 | + |
| 5 | +Иерархия: `USER <- MODER <- ADMIN` (старшая наследует права младшей). |
| 6 | +Доступ `USER` означает `USER, MODER, ADMIN`; `MODER` означает `MODER, ADMIN`. |
| 7 | + |
| 8 | +Маршрут — путь к FXML для `Navigator.goTo(...)`. |
| 9 | + |
| 10 | +## Паттерн взаимодействия с API |
| 11 | + |
| 12 | +UI не ходит в HTTP напрямую. Контроллер вызывает сервис («middleware»): |
| 13 | +`Controller → services.getXService() → ApiClient → HTTP + Session (Bearer)`. |
| 14 | +Сервис кладёт пользователя/токены в `Session`, ошибки пробрасывает как |
| 15 | +`ServiceException`, контроллер показывает их через `Alert`. |
| 16 | +Пример: docs/examples/how-to-interact-with-api-from-ui-via-middleware.md. |
| 17 | + |
| 18 | +| № | Название страницы | Маршрут (FXML) | Доступ | Назначение | Ответственный | Статус | |
| 19 | +| --- | -------------------------------------------- | ----------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ----------------------------- | |
| 20 | +| 1 | Вход (email + пароль) | `/fxml/LoginForm.fxml` | гость (публичная) | Аутентификация через `AuthService`, после успеха → №2 | @vladcheck | ✅ готово | |
| 21 | +| 2 | Список ресурсов и сортировка (главная) | `/fxml/ResourceList.fxml` | `USER` | Поиск по `title`, сортировка, пагинация `offset/count` (`PAGE_SIZE=20`) через `ResourceService.search` | @miyagi18 | ⚠️ частично (см. DOD №2) | |
| 22 | +| 3 | Просмотр метаданных ресурса | `/fxml/ResourceDetails.fxml` | `USER` | Только чтение: `ResourceService.getById` → `title, description, createdAt, updatedAt, uuid`; таблица файлов (имя/размер/тип + «Скачать») | @vladcheck | ⚠️ частично (см. DOD №3) | |
| 23 | +| 4 | Форма ресурса (создание/редактирование) | `/fxml/ResourceForm.fxml` | `MODER` | `create/update` через `ResourceService`; удаление — действием из №2/№3 с подтверждением, без отдельной страницы | @miyagi18 | ⚠️ частично (см. DOD №4) | |
| 24 | +| 5 | Список пользователей | `/fxml/UserList.fxml` (нет) | `ADMIN` | Поиск/фильтр через `UsersService.search` + `UserFilter`; удаление действием из списка | @deatheh | ❌ нет (сервис готов, UI нет) | |
| 25 | +| 6 | Форма пользователя (создание/редактирование) | `/fxml/UserForm.fxml` (нет) | `ADMIN` | `create` через `RegisterData`, `update` через `UpdateUserData`, `getById` для предзаполнения | @deatheh | ❌ нет (сервис готов, UI нет) | |
| 26 | + |
| 27 | +## DOD для ⚠️-страниц |
| 28 | + |
| 29 | +### №2 — Список ресурсов |
| 30 | + |
| 31 | +* Есть: `ResourceListController` + `ResourceList.fxml`: поле поиска по `title`, `ComboBox` сортировки (4 опции: `createdAt desc/asc`, `title asc/desc`), таблица (`title, description, createdAt, updatedAt`), пагинация `offset/count` с `PAGE_SIZE=20`, переход дабл-кликом в №3 через `Navigator.goTo(path, c -> setResourceId(uuid))`, кнопка «+ Создать документ» только для `MODER/ADMIN`. |
| 32 | +* Нет: поля фильтра по `description` в UI нет (`ResourceFilter.description` всегда `null`); кнопки удаления в списке нет. |
| 33 | +* Готово когда: заведено поле `description` (или зафиксировано решение «искать только по title») и заведена кнопка удаления с подтверждением (см. DOD №4). |
| 34 | + |
| 35 | +### №3 — Просмотр метаданных ресурса |
| 36 | + |
| 37 | +* Есть: `ResourceDetailsController` (read-only): метаданные + таблица файлов (`name, size, contentType` + колонка «Скачать»), скачивание через `FileService.downloadFile` (`DirectoryChooser`, имя из `Content-Disposition`, fallback `fileUuid`), вход из №2 через `setResourceId(uuid)` + `load()`, кнопка «Редактировать» только для `MODER/ADMIN` (переход в №4). |
| 38 | +* Нет: кнопок загрузки/удаления файлов в UI нет; кнопки экспорта архива файлов нет. |
| 39 | +* Готово когда: заведены upload/delete (`FileService.{uploadFile, deleteFile}` уже есть) и экспорт (`ExportService.exportResourceFiles` уже есть) либо зафиксировано решение «в №3 только скачивание». |
| 40 | + |
| 41 | +### №4 — Форма ресурса |
| 42 | + |
| 43 | +* Есть: `ResourceFormController` + `ResourceForm.fxml`: режимы `create/edit` по наличию `resourceId`, предзаполнение через `getById`, ручная проверка `title` (`обязательно, 3–255 символов`, пустой `description` → `null`), сохранение через `create/update`, отмена → №2. |
| 44 | +* Нет: кнопки удаления в №2/№3/№4 нет, диалога подтверждения нет (`ResourceService.delete` реализован, UI не заведено). |
| 45 | +* Готово когда: удаление вызывается действием из №2/№3 с диалогом подтверждения, без отдельной страницы. |
| 46 | + |
| 47 | +## Интеграции страниц с эндпоинтами (через сервисы) |
| 48 | + |
| 49 | +Базовый URL: `api.baseUrl` (`http://localhost:8080`, см. `src/main/resources/api.properties`). |
| 50 | + |
| 51 | +| № | Действие UI | Сервис (middleware) | ApiClient → HTTP | |
| 52 | +| --- | --------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | |
| 53 | +| 1 | Войти | `AuthService.login(email, password)` | `POST /api/v1/auth/login` (`LoginRequest`), затем `GET /api/v1/user/me`; токены → `Session` | |
| 54 | +| 1 | Выйти | `AuthService.logout()` | `POST /api/v1/auth/logout` (`RefreshTokenRequest`), затем `Session.clear()` | |
| 55 | +| 2 | Поиск/сортировка ресурсов (только UI-факт: поиск по `title`) | `ResourceService.search(filter, offset, count[, sortBy, sortDir])` | `GET /api/v1/resource?title=&description=&offset=&count=&sortBy=&sortDir=` (UI шлёт только `title`; `description` поддерживается сервисом, из UI всегда `null`; сортировка `createdAt/title asc/desc`) | |
| 56 | +| 3 | Открыть метаданные | `ResourceService.getById(id)` | `GET /api/v1/resource/{id}` | |
| 57 | +| 3 | Скачать файл (UI-факт) | `FileService.downloadFile` | `GET /api/v1/resource/{id}/file/{fileId}` (имя из `Content-Disposition`) | |
| 58 | +| 3 | Загрузить/удалить файл (сервис готов, UI не заведено) | `FileService.{uploadFile, deleteFile}` | `POST /api/v1/resource/{id}/file` (multipart), `DELETE /api/v1/resource/{id}/file/{fileId}` | |
| 59 | +| 3 | Экспорт файлов ресурса (сервис готов, UI не заведено) | `ExportService.exportResourceFiles(id, dir)` | `GET /api/v1/admin/resource/{id}/files/export` (бинарный ответ → файл `files-{uuid}.xlsx`) | |
| 60 | +| 4 | Создать ресурс | `ResourceService.create(title, description)` | `POST /api/v1/resource` (`CreateResourceRequest`) | |
| 61 | +| 4 | Обновить ресурс | `ResourceService.update(id, title, description)` | `PUT /api/v1/resource/{id}` (`UpdateResourceRequest`) | |
| 62 | +| 4 | Удалить ресурс из №2/№3 (сервис готов, UI не заведено) | `ResourceService.delete(id)` | `DELETE /api/v1/resource/{id}` | |
| 63 | +| 5 | Поиск/фильтр пользователей (сервис готов, UI не заведено) | `UsersService.search(filter, offset, count)` | `GET /api/v1/admin?email=&firstName=&lastName=&role=&enabled=&offset=&count=&sortBy=createdAt&sortDir=desc` (сортировка захардкожена в `UsersServiceImpl`) | |
| 64 | +| 5 | Удалить пользователя (сервис готов, UI не заведено) | `UsersService.delete(id)` | `DELETE /api/v1/admin/{id}` | |
| 65 | +| 5 | Экспорт пользователей/ресурсов (сервис готов, UI не заведено) | `ExportService.{exportUsers, exportResources}` | `GET /api/v1/admin/users/export` (→ `users.xlsx`), `GET /api/v1/admin/resource/export` (→ `resources.xlsx`) | |
| 66 | +| 6 | Создать пользователя (сервис готов, UI не заведено) | `UsersService.create(RegisterData)` | `POST /api/v1/admin` (`RegisterRequest`: email, password, role, firstName, lastName) | |
| 67 | +| 6 | Открыть пользователя для редактирования (сервис готов, UI не заведено) | `UsersService.getById(id)` | `GET /api/v1/admin/{id}` | |
| 68 | +| 6 | Обновить пользователя (сервис готов, UI не заведено) | `UsersService.update(id, UpdateUserData)` | `PUT /api/v1/admin/{id}` (`UpdateUserRequest`: email, enabled, role, firstName, lastName) | |
| 69 | + |
| 70 | +Примечания: |
| 71 | +* `ApiClient` при 401 (кроме `/api/v1/auth/*`) один раз обновляет токены через |
| 72 | + `POST /api/v1/auth/refresh` и повторяет запрос; если refresh нет/не удался — |
| 73 | + исходный ответ уходит в `ServiceException`. |
| 74 | +* Серверные ошибки (`ApiException` → `ServiceException`) показывает контроллер; |
| 75 | + проверка ввода выполняется до вызова сервиса (см. «Примечания по реализации»). |
| 76 | + |
| 77 | +## Примечания по реализации |
| 78 | + |
| 79 | +* №1: ✅ готово — `LoginFormController` только email + пароль: проверка непустоты + `EmailValidator.validate`, вызов `AuthService.login`, после успеха → №2. |
| 80 | +* №2: переименование `Search.fxml → ResourceList.fxml` готово; список наполнен: `ResourceService.search` + пагинация `PAGE_SIZE=20`, фильтр `ResourceFilter` (факт: из UI заполняется только `title`). |
| 81 | +* №3: частично — `ResourceDetailsController` (read-only): метаданные + таблица файлов |
| 82 | + со скачиванием (`FileService.downloadFile`), вход из №2 — `Navigator.goTo(path, c -> ...)` |
| 83 | + с `setResourceId(uuid)` + `load()`. Upload/delete/экспорт не заведены. |
| 84 | +* №4: частично — `ResourceFormController` в режимах `create/edit` по наличию `resourceId` |
| 85 | + (предзаполнение через `getById`); проверка `title` вручную (`3–255`), не через `validation/*Validator`. |
| 86 | +* №5–№6: ❌ UI нет (`UserList.fxml`/`UserForm.fxml`, контроллеров нет). Сервисный слой готов: |
| 87 | + `UsersServiceImpl` + `ApiClient` (`getUsersByFilters/createUser/getUserById/updateUser/deleteUser`). |
| 88 | +* Роли: `AuthServiceImpl.hasRole` сравнивает по уровню (`USER=0 < MODER=1 < ADMIN=2`), |
| 89 | + поэтому `isModerator()` возвращает true и для `ADMIN` — соответствует RBAC выше. |
| 90 | +* SoC: списки (№2) не содержат логики форм; форма №4 работает в двух режимах `create/edit` по наличию `uuid`. |
| 91 | +* Кнопки: `create` (№2) и `edit` (№3) рендерятся по роли (`isAdmin() || isModerator()` из `Session.getCurrentUser().role`), а не отдельными страницами. Кнопки `delete` не рендерятся нигде. |
| 92 | +* Проверка ввода (факт): №1 — `EmailValidator`; №4 — ручные проверки длины; классы |
| 93 | + `PasswordValidator/FirstNameValidator/LastNameValidator/ThirdNameValidator` на FormsFX существуют как задел под №6, в текущих страницах не используются. |
| 94 | +* Терминология: в доке используется «ресурс» per `GLOSSARY.md` (avoid «документ»). |
| 95 | + Факт расхождения: строки UI/FXML до сих пор говорят «документ» («Новый документ», |
| 96 | + «Редактирование документа», «Архив документов», «Документов пока нет») — правится отдельно, не в этом доке. |
| 97 | + |
| 98 | +## Тесты (факт) |
| 99 | + |
| 100 | +Стек: JUnit5 + AssertJ; TestFX headless настроен в `maven-surefire-plugin` |
| 101 | +(`-Dtestfx.headless=true`, Monocle, `--enable-native-access=ALL-UNNAMED`, |
| 102 | +`-XX:+EnableDynamicAgentLoading`). WireMock в `pom.xml` отсутствует. |
| 103 | + |
| 104 | +* Дефолтный прогон (без бэкенда): unit + UI на in-memory моках — `ServiceRegistry.mock` |
| 105 | + (`MockAuthService/MockUsersService/MockResourceService/MockFileService/MockExportService` + `MockStore`), |
| 106 | + `MOCK_DATA=true` / `INITIAL_PAGE_PATH` в `Program`. Покрыты: `LoginFormController`, |
| 107 | + `ResourceListController`, `ResourceDetailsController`, `ResourceFormController`, мапперы, валидаторы. |
| 108 | +* Live-интеграционные (`@Tag("integration")`, `src/test/.../ui/integration/`): |
| 109 | + исключены из дефолтного билда/CI через `excludedGroups` в surefire; требуют живой |
| 110 | + бэкенд `localhost:8080` и `.env` (`IT_ADMIN_EMAIL`, `IT_ADMIN_PASSWORD`, `dotenv.path`). |
| 111 | + Запуск явно: `mvn test -Dgroups=integration -DexcludedGroups=`. |
| 112 | +* Скачивание/экспорт (факт кода, без UI-кнопок): `ApiClient.downloadFile` сохраняет в |
| 113 | + `destinationDir`, имя из `Content-Disposition` (fallback `fileUuid`); `ExportServiceImpl` |
| 114 | + пишет `users.xlsx` / `resources.xlsx` / `files-{uuid}.xlsx`. |
| 115 | +* Refresh (факт кода): `ApiClient.executeWithRetry` при 401 (кроме `/api/v1/auth/*`) |
| 116 | + один раз делает `POST /api/v1/auth/refresh` и повторяет запрос с новым Bearer без участия UI. |
0 commit comments