From c13841b61a395427d169047e34bd487156393799 Mon Sep 17 00:00:00 2001 From: Crimson Date: Sat, 9 May 2026 01:18:13 +0500 Subject: [PATCH] =?UTF-8?q?fix26:=20=D0=BF=D0=BE=D0=BB=D0=BD=D1=8B=D0=B9?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B4-=D1=84=D0=B8=D0=BA=D1=81=20auth/jwt?= =?UTF-8?q?,=20import-export=20peer=20=D0=B8=20frontend=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cmd/reset.go | 60 ++++++--- apps/controller/hysteria2.go | 4 +- apps/controller/peer.go | 8 +- apps/dao/sqlite.go | 45 ++++--- apps/frontend/src/api/admin/index.ts | 23 +++- apps/frontend/src/api/admin/types.ts | 11 ++ apps/frontend/src/lang/package/en.ts | 15 ++- apps/frontend/src/lang/package/ru.ts | 14 +- apps/frontend/src/permission.ts | 11 +- apps/frontend/src/router/index.ts | 26 +++- apps/frontend/src/store/modules/admin.ts | 7 +- .../src/views/admin/change-password/index.vue | 85 ++++++++++++ apps/frontend/src/views/dashboard/index.vue | 13 +- apps/frontend/src/views/login/index.vue | 4 + apps/middleware/jwt.go | 17 ++- apps/model/bo/account.go | 1 + apps/service/admin_user.go | 5 + apps/service/peer.go | 123 ++++++++++++++---- apps/util/encrypt.go | 8 -- 19 files changed, 390 insertions(+), 90 deletions(-) create mode 100644 apps/frontend/src/views/admin/change-password/index.vue diff --git a/apps/cmd/reset.go b/apps/cmd/reset.go index 6edc00b..bdff36b 100644 --- a/apps/cmd/reset.go +++ b/apps/cmd/reset.go @@ -4,8 +4,10 @@ import ( "fmt" "github.com/spf13/cobra" "hy2xs-admin/dao" + "hy2xs-admin/model/entity" "hy2xs-admin/util" "os" + "time" ) var resetCmd = &cobra.Command{ @@ -30,25 +32,53 @@ func runReset(cmd *cobra.Command, args []string) { fmt.Println(err.Error()) os.Exit(1) } - if err = dao.InitSqliteDB(); err != nil { + if err = dao.InitSql(""); err != nil { fmt.Println(err.Error()) os.Exit(1) } - admin, err := dao.GetAdminUser("id = ?", 1) + admin, err := dao.GetAdminUser("1 = 1") + nowMs := time.Now().UnixMilli() if err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } - if err = dao.UpdateAdminUser([]int64{*admin.Id}, map[string]interface{}{ - "username": username, - "password_hash": func() string { - hash, _ := util.HashPassword(password) - return hash - }(), - "force_password_change": 1, - }); err != nil { - fmt.Println(err.Error()) - os.Exit(1) + tokenVersion := int64(1) + status := int64(1) + forcePasswordChange := int64(1) + passwordChangedAt := nowMs + hash, hashErr := util.HashPassword(password) + if hashErr != nil { + fmt.Println(hashErr.Error()) + os.Exit(1) + } + adminUser := entity.AdminUser{ + Username: &username, + PasswordHash: &hash, + Status: &status, + ForcePasswordChange: &forcePasswordChange, + PasswordChangedAt: &passwordChangedAt, + TokenVersion: &tokenVersion, + } + if _, saveErr := dao.SaveAdminUser(adminUser); saveErr != nil { + fmt.Println(saveErr.Error()) + os.Exit(1) + } + } else { + tokenVersion := int64(1) + if admin.TokenVersion != nil && *admin.TokenVersion > 0 { + tokenVersion = *admin.TokenVersion + 1 + } + if err = dao.UpdateAdminUser([]int64{*admin.Id}, map[string]interface{}{ + "username": username, + "password_hash": func() string { + hash, _ := util.HashPassword(password) + return hash + }(), + "force_password_change": 1, + "password_changed_at": nowMs, + "token_version": tokenVersion, + "status": 1, + }); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } } if err = dao.CloseSqliteDB(); err != nil { fmt.Println(err.Error()) diff --git a/apps/controller/hysteria2.go b/apps/controller/hysteria2.go index 24e791e..6fe2569 100644 --- a/apps/controller/hysteria2.go +++ b/apps/controller/hysteria2.go @@ -2,6 +2,7 @@ package controller import ( "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" "hy2xs-admin/model/dto" "hy2xs-admin/model/vo" "hy2xs-admin/service" @@ -27,8 +28,7 @@ func Hysteria2Auth(c *gin.Context) { // Обновление времени последнего подключения now := time.Now().UnixMilli() if err = service.UpdatePeerLastConnectionAt(id, now); err != nil { - vo.Fail(err.Error(), c) - return + logrus.WithError(err).Warnf("failed to update peer last_connection_at: peer_id=%d", id) } vo.Hysteria2AuthSuccess(username, c) } diff --git a/apps/controller/peer.go b/apps/controller/peer.go index 32dc751..a1f2253 100644 --- a/apps/controller/peer.go +++ b/apps/controller/peer.go @@ -10,9 +10,9 @@ import ( "time" "github.com/gin-gonic/gin" + "hy2xs-admin/model/bo" "hy2xs-admin/model/constant" "hy2xs-admin/model/dto" - "hy2xs-admin/model/entity" "hy2xs-admin/model/vo" "hy2xs-admin/service" "hy2xs-admin/util" @@ -147,12 +147,12 @@ func ImportPeer(c *gin.Context) { vo.Fail("json file read err", c) return } - var accounts []entity.Account - if err = json.Unmarshal(content, &accounts); err != nil { + var peerExports []bo.PeerExport + if err = json.Unmarshal(content, &peerExports); err != nil { vo.Fail("content Unmarshal err", c) return } - if err = service.UpsertPeerLegacy(accounts); err != nil { + if err = service.UpsertPeerExport(peerExports); err != nil { vo.Fail(err.Error(), c) return } diff --git a/apps/dao/sqlite.go b/apps/dao/sqlite.go index a956d47..6e8f1ce 100644 --- a/apps/dao/sqlite.go +++ b/apps/dao/sqlite.go @@ -101,15 +101,19 @@ func ensureSecureBootstrapAdmin() error { if adminUser == "" { adminUser = "hy2xsadmin" } + if _, err := GetAdminUser("username = ?", adminUser); err == nil { + return nil + } + adminPassword := strings.TrimSpace(os.Getenv("HY2XS_ADMIN_INITIAL_PASSWORD")) + passwordGenerated := false if adminPassword == "" { password, pwdErr := util.RandomString(18) if pwdErr != nil { return pwdErr } adminPassword = password - logrus.Warnf("Initial admin username: %s", adminUser) - logrus.Warnf("Initial admin password: %s", adminPassword) + passwordGenerated = true } forcePasswordChange := envBoolAsInt("HY2XS_FORCE_PASSWORD_CHANGE", 1) status := int64(1) @@ -120,25 +124,21 @@ func ensureSecureBootstrapAdmin() error { return hashErr } - admin, err := GetAdminUser("username = ?", adminUser) - if err != nil { - username := adminUser - account := entity.AdminUser{ - Username: &username, - PasswordHash: &hash, - Status: &status, - TokenVersion: &tokenVersion, - PasswordChangedAt: &passwordChangedAt, - ForcePasswordChange: func() *int64 { v := int64(forcePasswordChange); return &v }(), - } - if _, saveErr := SaveAdminUser(account); saveErr != nil { - return saveErr - } - return nil + username := adminUser + account := entity.AdminUser{ + Username: &username, + PasswordHash: &hash, + Status: &status, + TokenVersion: &tokenVersion, + PasswordChangedAt: &passwordChangedAt, + ForcePasswordChange: func() *int64 { v := int64(forcePasswordChange); return &v }(), } - - if admin.PasswordHash == nil { - return nil + if _, saveErr := SaveAdminUser(account); saveErr != nil { + return saveErr + } + if passwordGenerated { + logrus.Warnf("Initial admin username: %s", adminUser) + logrus.Warnf("Initial admin password: %s", adminPassword) } return nil } @@ -581,6 +581,11 @@ func ensureTrafficStatsSecret() error { if existing.Value != nil && strings.TrimSpace(*existing.Value) != "" { return nil } + secret, secErr := util.RandomString(32) + if secErr != nil { + return secErr + } + return UpdateConfig([]string{constant.Hysteria2TrafficStatsSecret}, map[string]interface{}{"value": secret}) } secret, secErr := util.RandomString(32) if secErr != nil { diff --git a/apps/frontend/src/api/admin/index.ts b/apps/frontend/src/api/admin/index.ts index 8318e8a..8716e3b 100644 --- a/apps/frontend/src/api/admin/index.ts +++ b/apps/frontend/src/api/admin/index.ts @@ -1,6 +1,12 @@ import request from "@/utils/request"; import { AxiosPromise } from "axios"; -import { AdminInfo, AdminLoginDto, AdminLoginVo } from "./types"; +import { + AdminChangePasswordDto, + AdminInfo, + AdminLoginDto, + AdminLoginVo, + AdminSecurityVo, +} from "./types"; export function loginApi(data: AdminLoginDto): AxiosPromise { return request({ @@ -16,3 +22,18 @@ export function getAdminInfoApi(): AxiosPromise { method: "get", }); } + +export function adminChangePasswordApi(data: AdminChangePasswordDto): AxiosPromise { + return request({ + url: "/admin/change-password", + method: "post", + data, + }); +} + +export function adminSecurityApi(): AxiosPromise { + return request({ + url: "/admin/security", + method: "get", + }); +} diff --git a/apps/frontend/src/api/admin/types.ts b/apps/frontend/src/api/admin/types.ts index fb71679..5900f4d 100644 --- a/apps/frontend/src/api/admin/types.ts +++ b/apps/frontend/src/api/admin/types.ts @@ -6,11 +6,22 @@ export interface AdminLoginDto { export interface AdminLoginVo { accessToken: string; tokenType: string; + forcePasswordChange: boolean; } export interface AdminInfo { id: number; username: string; roles: string[]; + forcePasswordChange: boolean; +} + +export interface AdminChangePasswordDto { + oldPassword: string; + newPassword: string; +} + +export interface AdminSecurityVo { + forcePasswordChange: boolean; } diff --git a/apps/frontend/src/lang/package/en.ts b/apps/frontend/src/lang/package/en.ts index 2b983bc..5164b76 100644 --- a/apps/frontend/src/lang/package/en.ts +++ b/apps/frontend/src/lang/package/en.ts @@ -21,6 +21,19 @@ export default { password: "Password", login: "Login", }, + dashboard: { + stale: "Dashboard data is stale. Retrying automatically...", + topPeers24h: "Top peers (24h)", + refreshFailed: "Failed to refresh dashboard data", + }, + admin: { + changePasswordTitle: "Change password", + oldPassword: "Old password", + newPassword: "New password", + forcePasswordChangeNotice: + "You must change your password before continuing.", + changedSuccess: "Password updated", + }, // Локализация навигационной панели navbar: { logout: "Logout", @@ -57,7 +70,7 @@ export default { yes: "Yes", no: "No", securityRisk: "Security Risks", - defaultPassTip: `Please change the default login password as soon as possible, It is recommended to set a strong password to protect your account security. Click here to change`, + defaultPassTip: `Please change the default login password as soon as possible, It is recommended to set a strong password to protect your account security. Click here to change`, noHttpsTip: `Your website is not using HTTPS, making data transmission insecure, Please enable HTTPS as soon as possible to protect user information. Click here to enable`, required: "Required", warning: "Warning", diff --git a/apps/frontend/src/lang/package/ru.ts b/apps/frontend/src/lang/package/ru.ts index d277e9b..b1159d8 100644 --- a/apps/frontend/src/lang/package/ru.ts +++ b/apps/frontend/src/lang/package/ru.ts @@ -19,6 +19,18 @@ export default { password: "Пароль", login: "Войти", }, + dashboard: { + stale: "Данные дашборда устарели. Выполняется автоматическая повторная попытка...", + topPeers24h: "Топ пиров (24ч)", + refreshFailed: "Не удалось обновить данные дашборда", + }, + admin: { + changePasswordTitle: "Смена пароля", + oldPassword: "Старый пароль", + newPassword: "Новый пароль", + forcePasswordChangeNotice: "Перед продолжением необходимо сменить пароль.", + changedSuccess: "Пароль обновлён", + }, navbar: { logout: "Выйти", }, @@ -54,7 +66,7 @@ export default { yes: "Да", no: "Нет", securityRisk: "Риски безопасности", - defaultPassTip: `Смените пароль по умолчанию как можно скорее. Перейти к смене`, + defaultPassTip: `Смените пароль по умолчанию как можно скорее. Перейти к смене`, noHttpsTip: `Панель работает без HTTPS. Включите HTTPS для защиты данных. Открыть настройки`, required: "Обязательное поле", warning: "Внимание", diff --git a/apps/frontend/src/permission.ts b/apps/frontend/src/permission.ts index c15d127..146ef81 100644 --- a/apps/frontend/src/permission.ts +++ b/apps/frontend/src/permission.ts @@ -24,6 +24,10 @@ router.beforeEach(async (to, from, next) => { const adminStore = useAdminStoreHook(); const hasRoles = adminStore.roles && adminStore.roles.length > 0; if (hasRoles) { + if (adminStore.forcePasswordChange && to.path !== "/admin/change-password") { + next({ path: "/admin/change-password", query: { redirect: to.fullPath } }); + return; + } // Если маршрут не найден, перейти на 404 if (to.matched.length === 0) { from.name ? next({ name: from.name }) : next("/404"); @@ -32,11 +36,15 @@ router.beforeEach(async (to, from, next) => { } } else { try { - const { roles } = await adminStore.getAdminInfo(); + const { roles, forcePasswordChange } = await adminStore.getAdminInfo(); const accessRoutes = permissionStore.generateRoutes(roles); accessRoutes.forEach((route) => { router.addRoute(route); }); + if (forcePasswordChange && to.path !== "/admin/change-password") { + next({ path: "/admin/change-password", query: { redirect: to.fullPath }, replace: true }); + return; + } next({ ...to, replace: true }); } catch (error) { // Удалить token и перейти на страницу входа @@ -61,4 +69,3 @@ router.afterEach(() => { NProgress.done(); }); - diff --git a/apps/frontend/src/router/index.ts b/apps/frontend/src/router/index.ts index 00120f1..1747d2d 100644 --- a/apps/frontend/src/router/index.ts +++ b/apps/frontend/src/router/index.ts @@ -53,7 +53,7 @@ export const asyncRoutes: any[] = [ name: "Dashboard", meta: { title: "dashboard", - icon: "dashboard", + icon: "report", roles: ["admin"], }, children: [ @@ -63,16 +63,30 @@ export const asyncRoutes: any[] = [ name: "DashboardIndex", meta: { title: "dashboard", - icon: "dashboard", + icon: "report", roles: ["admin"], }, }, ], }, + { + path: "/admin", + component: "Layout", + redirect: "/admin/change-password", + meta: { hidden: true, roles: ["admin"] }, + children: [ + { + path: "change-password", + component: "admin/change-password/index", + name: "AdminChangePassword", + meta: { title: "changePassword", hidden: true, roles: ["admin"] }, + }, + ], + }, { path: "/peers", component: "Layout", - redirect: "/list", + redirect: "/peers/list", name: "Peer", meta: { title: "peer", icon: "users", roles: ["admin"] }, children: [ @@ -94,7 +108,7 @@ export const asyncRoutes: any[] = [ { path: "/hysteria", component: "Layout", - redirect: "/list", + redirect: "/hysteria/list", name: "Hysteria", meta: { title: "hysteria", icon: "hysteria", roles: ["admin"] }, children: [ @@ -113,7 +127,7 @@ export const asyncRoutes: any[] = [ { path: "/config", component: "Layout", - redirect: "/list", + redirect: "/config/list", name: "Config", meta: { title: "config", icon: "setting", roles: ["admin"] }, children: [ @@ -135,7 +149,7 @@ export const asyncRoutes: any[] = [ { path: "/log", component: "Layout", - redirect: "/system", + redirect: "/log/system", name: "Log", meta: { title: "log", icon: "error", roles: ["admin"] }, children: [ diff --git a/apps/frontend/src/store/modules/admin.ts b/apps/frontend/src/store/modules/admin.ts index 18a47a7..3835088 100644 --- a/apps/frontend/src/store/modules/admin.ts +++ b/apps/frontend/src/store/modules/admin.ts @@ -10,6 +10,7 @@ import { useStorage } from "@vueuse/core"; export const useAdminStore = defineStore("admin", () => { const token = useStorage("accessToken", ""); + const forcePasswordChange = useStorage("forcePasswordChange", false); const id = ref(0); const username = ref(""); const roles = ref>([]); @@ -18,8 +19,9 @@ export const useAdminStore = defineStore("admin", () => { return new Promise((resolve, reject) => { loginApi(adminLoginDto) .then((response) => { - const { tokenType, accessToken } = response.data; + const { tokenType, accessToken, forcePasswordChange: mustChange } = response.data; token.value = tokenType + " " + accessToken; + forcePasswordChange.value = !!mustChange; resolve(); }) .catch((error) => { @@ -41,6 +43,7 @@ export const useAdminStore = defineStore("admin", () => { id.value = data.id; username.value = data.username; roles.value = data.roles; + forcePasswordChange.value = !!data.forcePasswordChange; resolve(data); }) .catch((error) => { @@ -59,6 +62,7 @@ export const useAdminStore = defineStore("admin", () => { function resetToken() { token.value = ""; + forcePasswordChange.value = false; id.value = 0; username.value = ""; roles.value = []; @@ -66,6 +70,7 @@ export const useAdminStore = defineStore("admin", () => { return { token, + forcePasswordChange, id, username, roles, diff --git a/apps/frontend/src/views/admin/change-password/index.vue b/apps/frontend/src/views/admin/change-password/index.vue new file mode 100644 index 0000000..9a72bb0 --- /dev/null +++ b/apps/frontend/src/views/admin/change-password/index.vue @@ -0,0 +1,85 @@ + + + + + + diff --git a/apps/frontend/src/views/dashboard/index.vue b/apps/frontend/src/views/dashboard/index.vue index be33a64..ed55cc2 100644 --- a/apps/frontend/src/views/dashboard/index.vue +++ b/apps/frontend/src/views/dashboard/index.vue @@ -1,7 +1,7 @@