Files
HY2XS_flamy/fix24.txt
T

618 lines
18 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Посмотрел архив `HY2XS_flamy-main.zip`. Проблема не косметическая: сейчас в проекте смешаны две разные доменные сущности — администратор панели и peer Hysteria2. Это видно прямо по схеме `account`: в одной таблице лежат `username/pass` для входа в админку, `con_pass` для подключения, `quota/download/upload/device_no`, `role`, `deleted`. Логин панели фильтрует `role = 'admin'`, а Hysteria2 auth ищет только по `con_pass` и не отсекает `role = 'admin'`. Поэтому админ технически остаётся peer’ом.
Ниже план нормального продакшен-исправления без «подмазать CSS и добавить if role != admin».
---
## 1. Развести админов панели и peer’ов Hysteria2
### Текущее состояние
Сейчас:
`account.pass` — пароль входа в панель.
`account.con_pass` — пароль подключения Hysteria2.
`account.role` — попытка различать `admin/user`.
`account.quota/download/upload/device_no/expire_time` — peer-поля.
`login_at` — поле админа.
`con_at` — поле peer’а.
Это плохая модель. Роль не должна превращать одну таблицу в две разные сущности.
### Целевая модель
Сделать минимум две таблицы.
`admin_user`:
```sql
id
username
password_hash
status
force_password_change
last_login_at
password_changed_at
token_version
created_at
updated_at
```
`peer`:
```sql
id
name
remark
auth_id
secret_digest
secret_ciphertext -- если нужно показывать URL/QR после создания
quota_bytes
download_bytes
upload_bytes
expires_at
max_devices
disabled
banned_until
last_connection_at
created_at
updated_at
```
`admin_user` не должен иметь `quota`, `con_pass`, `device_no`, `download`, `upload`.
`peer` не должен иметь пароль входа в панель и `role`.
### Как хранить peer secret
Текущий `con_pass` хранится как plain text. Для продакшена лучше уйти от этого.
Вариант нормальный:
`auth_id` — публичный идентификатор peer’а, например короткий random/base32.
`raw_secret` — генерируется при создании или ротации.
`secret_digest = HMAC-SHA256(raw_secret, HY2XS_PEER_SECRET_KEY)` — используется для auth lookup.
`secret_ciphertext` — опционально, если UI должен уметь повторно показать Node URL/QR. Шифровать ключом из data-dir/env, не хранить просто строкой в SQLite.
Если не хочется вводить шифрование сейчас, допустим компромисс: хранить `secret_plain` временно, но уже в таблице `peer`, не рядом с admin password. Потом отдельной миграцией заменить на digest/encrypted secret.
### Миграция
Сделать версионированные миграции, а не держать огромную строку SQL внутри `dao/sqlite.go`.
Сейчас есть два источника схемы: `apps/docs/sql/h_ui_db.sql` и inline `sqlInitStr` в `apps/dao/sqlite.go`. Они уже расходятся: в `sqlite.go` добавлен `force_password_change`, в SQL-доке его нет; `remark` объявлен как `INTEGER DEFAULT ''`, хотя в Go это `string`. Это надо убрать.
Нужен один механизм:
```sql
schema_migrations(version, applied_at)
```
Миграции:
`001_initial_legacy_snapshot.sql` — текущая схема, только для reference.
`002_admin_peer_split.sql` — создаёт `admin_user`, `peer`.
`003_migrate_legacy_accounts.sql` — переносит данные.
`004_drop_or_archive_legacy_account.sql` — не сразу удалять, а переименовать в `legacy_account_backup`.
Правила переноса:
`role = 'admin'` → `admin_user`. Переносить `username`, `pass`, `force_password_change`, `login_at`. Не переносить `con_pass`.
`role != 'admin'` → `peer`. Переносить `username` как `name`, `remark`, лимиты, трафик, `expire_time`, `kick_util_time`, `con_at`.
После миграции admin credentials больше не могут пройти Hysteria2 auth даже теоретически.
---
## 2. Переписать backend-слой по доменам
### Что заменить
Сейчас всё сидит в `account.go`: DAO, service, controller, DTO, VO. Надо разделить:
```text
dao/admin_user.go
dao/peer.go
service/auth.go
service/admin_user.go
service/peer.go
service/hysteria2_auth.go
controller/auth.go
controller/admin_user.go
controller/peer.go
controller/hysteria2.go
```
### API
Оставить `/hui/auth/login`, но он должен работать только с `admin_user`.
Добавить:
```text
GET /hui/admin/me
POST /hui/admin/change-password
GET /hui/peers
POST /hui/peers
GET /hui/peers/:id
PATCH /hui/peers/:id
DELETE /hui/peers/:id
POST /hui/peers/:id/reset-traffic
POST /hui/peers/:id/kick
POST /hui/peers/:id/release-kick
POST /hui/peers/:id/rotate-secret
GET /hui/peers/:id/client-url
GET /hui/peers/:id/qr
```
Старые `/account/*` можно оставить только как compatibility layer на один релиз, но UI уже должен ходить в `/peers/*`.
### Hysteria2 auth
Сейчас `Hysteria2Auth()` делает:
```go
dao.GetAccount("con_pass = ? and deleted = 0 ...")
```
Нужно заменить на peer-auth:
```go
peer, err := peerRepo.FindBySecretDigest(digest)
```
И проверять только peer-поля:
```text
disabled = false
now < expires_at
quota_bytes < 0 OR quota_bytes > download_bytes + upload_bytes
now > banned_until
online_devices < max_devices
```
Admin-таблица здесь вообще не импортируется.
### Сессии и JWT
JWT сейчас содержит `AccountBo` с `Roles`. Оставить можно, но лучше переименовать в `AdminClaims`.
Добавить `token_version` в `admin_user`. Тогда смена пароля, reset или принудительная инвалидизация токенов делается увеличением `token_version`.
После смены пароля обязательно сбрасывать `force_password_change = 0`. Сейчас флаг возвращается из логина, но нормального dedicated flow для смены admin password не видно.
### Reset command
`apps/cmd/reset.go` сейчас меняет row `id=1` в `account`, печатает ещё и `Connection Password`. После разделения:
```text
hy2xs-admin reset-admin
```
Должен менять только `admin_user`.
Никакого connection password для админа печатать нельзя.
---
## 3. UI: переименовать “Account Manage” в peer management
В интерфейсе сейчас название “Account” вводит в заблуждение. Там смешаны профиль админа и peer’ы. Нужна терминология:
```text
Профиль / Admin Profile
Пиры / Peers
Управление пирами / Peer Management
```
В форме создания peer убрать поле “Пароль входа”. Для peer нужен только generated/rotatable connection secret.
Нормальная форма создания peer:
```text
Комментарий
Имя / label
Квота
Срок действия
Лимит устройств
Статус
[Создать]
```
После создания показать одноразовый блок:
```text
Connection URI
QR
Copy
Сохраните сейчас: после закрытия секрет может быть скрыт
```
Если оставляете encrypted secret, можно показывать URL и позже.
---
## 4. Починить русскую локаль и “плывущие” иконки правильно
### Причина
Это не проблема русского языка как такового. Русские строки длиннее, а sidebar сейчас завязан на дефолтный layout Element Plus плюс ручные margin’ы:
```scss
.svg-icon { margin-right: 16px; }
.hideSidebar .el-sub-menu__title { padding: 0 !important; }
.hideSidebar .svg-icon { margin-left: 20px; }
```
Из-за этого при длинных заголовках, collapse/open state и sub-menu иконки начинают жить отдельно от текста.
### Исправление
В `SidebarItem.vue` обернуть title в отдельный span:
```vue
<span class="menu-title">
{{ translateRouteTitleI18n(...) }}
</span>
```
Для `el-menu-item` и `el-sub-menu__title` задать один стабильный layout:
```scss
.sidebar-container {
.el-menu-item,
.el-sub-menu__title {
display: flex;
align-items: center;
gap: 12px;
height: 48px;
line-height: normal;
padding: 0 16px !important;
}
.svg-icon {
flex: 0 0 18px;
width: 18px;
height: 18px;
margin-right: 0;
}
.menu-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.el-sub-menu__icon-arrow {
margin-left: auto;
}
}
```
Убрать ручные `margin-left: 20px` для collapsed state. Для collapsed меню текст скрывать штатно, а не через плавающие отступы.
Ширину sidebar лучше поднять с `210px` до `240px` или `248px`. Но это вторично. Основной фикс — стабильная flex/grid-разметка.
Для длинных пунктов включить tooltip с полным названием при hover. Не переносить текст на вторую строку внутри sidebar.
---
## 5. Перевести уведомления и ошибки без хардкода
Сейчас часть UI переведена через `$t`, но много строк осталось захардкоженными:
```text
"Required"
"Username format is incorrect"
"Are you sure to reset traffic?"
"Are you sure to delete..."
"Warning"
"A week later"
"file format not supported"
```
Плюс backend возвращает английские строки:
```text
wrong password
system error
permission denied
username already exists
admin cannot be deleted
```
### Frontend
Добавить namespace:
```ts
validation: {
required,
usernameInvalid,
passwordInvalid,
peerSecretInvalid,
integer,
number,
fileFormat,
fileTooLarge,
}
confirm: {
warning,
deletePeer,
resetTraffic,
restartPanel,
}
timeShortcut: {
hourLater,
dayLater,
weekLater,
monthLater,
yearLater,
}
```
Все validation rules сделать через `computed`, чтобы при смене языка сообщения обновлялись:
```ts
const dataFormRules = computed(() => ({
username: [
{ required: true, message: t("validation.required"), trigger: ["blur"] },
],
}))
```
Подключить `ElConfigProvider` в `App.vue` и прокидывать locale Element Plus:
```vue
<el-config-provider :locale="elementLocale">
<router-view />
</el-config-provider>
```
Иначе встроенные компоненты Element Plus, datepicker, pagination и popconfirm будут жить своей локалью.
### Backend
Не возвращать UI-текст как источник истины. Возвращать стабильный error code/message key:
```json
{
"code": 40010,
"type": "no",
"message": "peer.usernameAlreadyExists",
"params": { "username": "testuser" }
}
```
Frontend переводит `message` как i18n key. Если key неизвестен — fallback на `common.systemError`.
Для machine endpoint `/hysteria2/auth` локализация не нужна, там протокольный ответ `{ ok: true/false, id }`.
---
## 6. Сжать таблицу peer’ов до production-вида
Сейчас таблица перегружена. В `account/list/index.vue` одновременно выводятся:
```text
ID
Remark
Username
Role
Quota
Download
Upload
Online status
Online devices
Device limit
Offline remaining time
Expire time
Last login time
Last connection time
Create time
Status
Operate
```
Для peer list это слишком много. На широком экране оно всё равно не будет хорошо читаться.
### Основной список
Оставить в таблице только:
```text
Peer
Status
Traffic
Devices
Expires
Last connection
Actions
```
Где:
`Peer` — имя + remark + ID мелким текстом.
`Status` — enabled/disabled + online/offline.
`Traffic` — progress bar: used / quota, а upload/download спрятать в details.
`Devices` — online / max.
`Expires` — дата + “expired soon/expired” tag.
`Last connection` — дата или `-`.
`Actions` — 1–2 основные кнопки и dropdown.
### Details drawer / overview dialog
По клику “Обзор” открыть drawer:
```text
Peer overview
- ID
- Name
- Remark
- Created at
- Updated at
- Download
- Upload
- Quota
- Expires at
- Last connection
- Kick until
- Device limit
- Current online devices
- Node URL
- QR
```
Это лучше, чем прятать половину в троеточие без структуры.
### Actions
В таблице оставить:
```text
Copy URL
Edit
```
В dropdown:
```text
Show QR
Reset traffic
Kick
Release kick
Rotate secret
Disable / Enable
Delete
```
`Subscribe` сейчас в baseline отключён, значит в UI его лучше не показывать, пока delivery layer вне scope. Иначе оператор видит кнопку, которая всегда ведёт к ошибке.
### Поведение на малых экранах
На desktop можно оставить `el-table`.
На tablet/mobile лучше отдельный card layout, а не пытаться ужать таблицу. Например:
```text
Peer card
name / status
traffic progress
devices
expires
actions
```
---
## 7. Конкретный порядок работ
### Этап A. Быстрый security hotfix
Это временный фикс, не финальная архитектура.
1. В `Hysteria2Auth` добавить фильтр `role = 'user'`.
2. В `PageAccount` по умолчанию показывать только `role = 'user'`.
3. В `SaveAccount` явно ставить `role = 'user'`.
4. Запретить выдачу Node URL/QR для `role = 'admin'`.
5. В `reset.go` убрать вывод connection password.
Это закрывает самый опасный баг до большой миграции.
### Этап B. Нормальная доменная миграция
1. Добавить `schema_migrations`.
2. Создать `admin_user`.
3. Создать `peer`.
4. Перенести legacy data.
5. Переписать DAO/service/controller.
6. Оставить legacy `account` только как backup.
7. Добавить `reset-admin`.
8. Убрать `role` из peer flow.
### Этап C. UI/i18n
1. Переименовать раздел `Account Manage` → `Peer Manage`.
2. Убрать peer login password из формы.
3. Сделать i18n keys для всех validation/confirm/toast строк.
4. Подключить Element Plus locale provider.
5. Убрать backend English strings из UI-отображения.
6. Добавить проверку отсутствующих i18n keys в CI.
### Этап D. Layout/sidebar
1. Переписать sidebar CSS на flex/grid.
2. Убрать ручные margin hacks.
3. Добавить `.menu-title` с ellipsis.
4. Поднять sidebar width до 240248px.
5. Проверить RU/EN в expanded/collapsed состояниях.
### Этап E. Peer table redesign
1. Сделать компактные колонки.
2. Вынести details в drawer.
3. Перенести второстепенные действия в dropdown.
4. Скрыть subscription actions, если delivery layer отключён.
5. Добавить responsive card layout.
---
## 8. Тесты и критерии готовности
Backend tests:
```text
admin_user может войти в панель
admin_user не может пройти Hysteria2 auth
peer не может войти в панель
peer может пройти Hysteria2 auth
expired peer rejected
disabled peer rejected
quota-exceeded peer rejected
device-limit peer rejected
legacy migration переносит admin и peers корректно
```
Frontend checks:
```text
pnpm lint:eslint
pnpm build:prod
vue-tsc --noEmit
```
E2E/screenshot:
```text
RU sidebar expanded
RU sidebar collapsed
EN sidebar expanded
EN sidebar collapsed
Peer table 1366px
Peer table 1920px
Peer card/mobile layout
```
Acceptance criteria:
```text
Админ панели не отображается в списке peer’ов.
У админа нет Node URL, QR и connection password.
Peer не имеет password для входа в UI.
Все toast/confirm/validation сообщения переводятся.
Русская локаль не ломает sidebar.
Peer list не требует горизонтального скролла на 1366px.
Второстепенные peer-поля доступны через “Обзор”.
```
Главная мысль: не чинить это через `role` и CSS-отступы. Правильный продакшен-фикс — разделить admin identity и peer identity на уровне схемы, сервисов и UI. После этого локаль и таблица чинятся уже как нормальная фронтенд-задача, а не как борьба с последствиями смешанной модели.