Полная зачистка legacy + закрытие fix24.1/fix24.2 + обновление логотипа

This commit is contained in:
2026-05-08 23:16:25 +05:00
parent de4a65a959
commit 1bca73814e
85 changed files with 4119 additions and 1911 deletions
+617
View File
@@ -0,0 +1,617 @@
Посмотрел архив `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. После этого локаль и таблица чинятся уже как нормальная фронтенд-задача, а не как борьба с последствиями смешанной модели.