fix26: полный прод-фикс auth/jwt, import-export peer и frontend flow
This commit is contained in:
@@ -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<AdminLoginVo> {
|
||||
return request({
|
||||
@@ -16,3 +22,18 @@ export function getAdminInfoApi(): AxiosPromise<AdminInfo> {
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
export function adminChangePasswordApi(data: AdminChangePasswordDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/admin/change-password",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function adminSecurityApi(): AxiosPromise<AdminSecurityVo> {
|
||||
return request({
|
||||
url: "/admin/security",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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. <a href="/#/peers/list?focus=change-pass" style="color: #00BFFF">Click here</a> 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. <a href="/#/admin/change-password" style="color: #00BFFF">Click here</a> to change`,
|
||||
noHttpsTip: `Your website is not using HTTPS, making data transmission insecure, Please enable HTTPS as soon as possible to protect user information. <a href="/#/config/list?focus=huiHttps" style="color: #00BFFF">Click here</a> to enable`,
|
||||
required: "Required",
|
||||
warning: "Warning",
|
||||
|
||||
@@ -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: `Смените пароль по умолчанию как можно скорее. <a href="/#/peers/list?focus=change-pass" style="color: #00BFFF">Перейти к смене</a>`,
|
||||
defaultPassTip: `Смените пароль по умолчанию как можно скорее. <a href="/#/admin/change-password" style="color: #00BFFF">Перейти к смене</a>`,
|
||||
noHttpsTip: `Панель работает без HTTPS. Включите HTTPS для защиты данных. <a href="/#/config/list?focus=huiHttps" style="color: #00BFFF">Открыть настройки</a>`,
|
||||
required: "Обязательное поле",
|
||||
warning: "Внимание",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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<Array<string>>([]);
|
||||
@@ -18,8 +19,9 @@ export const useAdminStore = defineStore("admin", () => {
|
||||
return new Promise<void>((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,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never" class="change-password-card">
|
||||
<template #header>{{ $t("admin.changePasswordTitle") }}</template>
|
||||
|
||||
<el-alert
|
||||
:title="$t('admin.forcePasswordChangeNotice')"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="180px">
|
||||
<el-form-item :label="$t('admin.oldPassword')" prop="oldPassword">
|
||||
<el-input v-model="form.oldPassword" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('admin.newPassword')" prop="newPassword">
|
||||
<el-input v-model="form.newPassword" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ $t("common.confirm") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, FormInstance, FormRules } from "element-plus";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { adminChangePasswordApi } from "@/api/admin";
|
||||
import { useAdminStore } from "@/store/modules/admin";
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const adminStore = useAdminStore();
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const saving = ref(false);
|
||||
const form = reactive({
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
});
|
||||
|
||||
const passwordPattern = /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,64}$/;
|
||||
const rules: FormRules = {
|
||||
oldPassword: [
|
||||
{ required: true, message: t("common.required"), trigger: ["change", "blur"] },
|
||||
{ pattern: passwordPattern, message: t("common.invalid"), trigger: ["change", "blur"] },
|
||||
],
|
||||
newPassword: [
|
||||
{ required: true, message: t("common.required"), trigger: ["change", "blur"] },
|
||||
{ pattern: passwordPattern, message: t("common.invalid"), trigger: ["change", "blur"] },
|
||||
],
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
const valid = await formRef.value.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await adminChangePasswordApi({ oldPassword: form.oldPassword, newPassword: form.newPassword });
|
||||
adminStore.forcePasswordChange = false;
|
||||
ElMessage.success(t("admin.changedSuccess"));
|
||||
const redirect = typeof route.query.redirect === "string" ? route.query.redirect : "/";
|
||||
router.push(redirect);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.change-password-card {
|
||||
max-width: 760px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-actions mb-2">
|
||||
<el-button size="small" @click="loadDashboard">Refresh</el-button>
|
||||
<el-button size="small" @click="loadDashboard">{{ $t("common.refresh") }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
@@ -13,7 +13,7 @@
|
||||
/>
|
||||
<el-alert
|
||||
v-else-if="isStale"
|
||||
title="Dashboard data is stale. Retrying automatically..."
|
||||
:title="$t('dashboard.stale')"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="mb-2"
|
||||
@@ -22,7 +22,7 @@
|
||||
<el-alert
|
||||
v-for="risk in securityRisks"
|
||||
:key="risk.key"
|
||||
:title="risk.key"
|
||||
:title="$t(risk.key)"
|
||||
:type="risk.severity === 'critical' ? 'error' : risk.severity === 'warning' ? 'warning' : 'info'"
|
||||
:closable="risk.dismissible"
|
||||
class="mb-2"
|
||||
@@ -40,7 +40,7 @@
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" class="mt-3">
|
||||
<template #header>Top peers (24h)</template>
|
||||
<template #header>{{ $t("dashboard.topPeers24h") }}</template>
|
||||
<el-table :data="topPeers" size="small">
|
||||
<el-table-column prop="name" label="Peer" />
|
||||
<el-table-column prop="download" label="Download">
|
||||
@@ -59,10 +59,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useIntervalFn } from "@vueuse/core";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { dashboardSecurityApi, dashboardSummaryApi, dashboardTopPeersApi } from "@/api/dashboard";
|
||||
import { DashboardSummaryVo, DashboardTopPeerVo, SecurityRiskVo } from "@/api/dashboard/types";
|
||||
import { formatBytes } from "@/utils/byte";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const summary = ref<DashboardSummaryVo>({
|
||||
collectedAt: 0,
|
||||
system: { cpuPercent: 0, memUsedBytes: 0, memTotalBytes: 0, memPercent: 0, diskUsedBytes: 0, diskTotalBytes: 0, diskPercent: 0 },
|
||||
@@ -108,7 +111,7 @@ const loadDashboard = async () => {
|
||||
lastSuccessAt.value = Date.now();
|
||||
loadError.value = "";
|
||||
} catch (error) {
|
||||
loadError.value = "Failed to refresh dashboard data";
|
||||
loadError.value = t("dashboard.refreshFailed");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -160,6 +160,10 @@ const handleLogin = () => {
|
||||
adminStore
|
||||
.login(params)
|
||||
.then(() => {
|
||||
if (adminStore.forcePasswordChange) {
|
||||
router.push({ path: "/admin/change-password" });
|
||||
return;
|
||||
}
|
||||
const query: LocationQuery = route.query;
|
||||
|
||||
const redirect = (query.redirect as LocationQueryValue) ?? "/";
|
||||
|
||||
Reference in New Issue
Block a user