Полная зачистка legacy + закрытие fix24.1/fix24.2 + обновление логотипа
This commit is contained in:
@@ -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 до 240–248px.
|
||||
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. После этого локаль и таблица чинятся уже как нормальная фронтенд-задача, а не как борьба с последствиями смешанной модели.
|
||||
Reference in New Issue
Block a user