fix26.2: полная UI-полировка peer/dashboard и завершение i18n
This commit is contained in:
+2
-2
@@ -66,8 +66,8 @@ func runReset(cmd *cobra.Command, args []string) {
|
|||||||
tokenVersion = *admin.TokenVersion + 1
|
tokenVersion = *admin.TokenVersion + 1
|
||||||
}
|
}
|
||||||
if err = dao.UpdateAdminUser([]int64{*admin.Id}, map[string]interface{}{
|
if err = dao.UpdateAdminUser([]int64{*admin.Id}, map[string]interface{}{
|
||||||
"username": username,
|
"username": username,
|
||||||
"password_hash": func() string {
|
"password_hash": func() string {
|
||||||
hash, _ := util.HashPassword(password)
|
hash, _ := util.HashPassword(password)
|
||||||
return hash
|
return hash
|
||||||
}(),
|
}(),
|
||||||
|
|||||||
@@ -36,4 +36,3 @@ func AdminSecurity(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
vo.Success(gin.H{"forcePasswordChange": info.ForcePasswordChange}, c)
|
vo.Success(gin.H{"forcePasswordChange": info.ForcePasswordChange}, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,4 +50,3 @@ func DashboardSecurity(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
vo.Success(data, c)
|
vo.Success(data, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -222,4 +222,3 @@ func PeerClientConfig(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
vo.Success(data, c)
|
vo.Success(data, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,4 +40,3 @@ func UpdateAdminUser(ids []int64, updates map[string]interface{}) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,4 +152,3 @@ func DashboardSystemTimeseries(fromMs int64, toMs int64) ([]vo.DashboardSeriesPo
|
|||||||
}
|
}
|
||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,23 +80,3 @@ func PagePeer(peerPageDto dto.PeerPageDto) ([]entity.Peer, int64, error) {
|
|||||||
}
|
}
|
||||||
return peers, total, nil
|
return peers, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdatePeerTraffic(name string, download int64, upload int64) error {
|
|
||||||
if upload == 0 && download == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
updates := map[string]interface{}{}
|
|
||||||
if download != 0 {
|
|
||||||
updates["download_bytes"] = gorm.Expr("download_bytes + ?", download)
|
|
||||||
}
|
|
||||||
if upload != 0 {
|
|
||||||
updates["upload_bytes"] = gorm.Expr("upload_bytes + ?", upload)
|
|
||||||
}
|
|
||||||
updates["update_time"] = time.Now().Format("2006-01-02 15:04:05")
|
|
||||||
if tx := sqliteDB.Model(&entity.Peer{}).Where("name = ?", name).Updates(updates); tx.Error != nil {
|
|
||||||
logrus.Errorf("%v", tx.Error)
|
|
||||||
return errors.New(constant.SysError)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
+13
-14
@@ -289,8 +289,8 @@ func migrateLegacyAccounts() error {
|
|||||||
if !tableExists("account") {
|
if !tableExists("account") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var accounts []entity.Account
|
var accounts []entity.LegacyAccount
|
||||||
if tx := sqliteDB.Model(&entity.Account{}).Order("id asc").Find(&accounts); tx.Error != nil {
|
if tx := sqliteDB.Model(&entity.LegacyAccount{}).Order("id asc").Find(&accounts); tx.Error != nil {
|
||||||
logrus.Errorf("sqlite legacy account query err: %v", tx.Error)
|
logrus.Errorf("sqlite legacy account query err: %v", tx.Error)
|
||||||
return errors.New("sqlite legacy account query err")
|
return errors.New("sqlite legacy account query err")
|
||||||
}
|
}
|
||||||
@@ -615,17 +615,17 @@ func CloseSqliteDB() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func IsSqliteReady() bool {
|
func IsSqliteReady() bool {
|
||||||
if sqliteDB == nil {
|
if sqliteDB == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
db, err := sqliteDB.DB()
|
db, err := sqliteDB.DB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if err := db.Ping(); err != nil {
|
if err := db.Ping(); err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func Paginate(pageNum *int64, pageSize *int64) func(db *gorm.DB) *gorm.DB {
|
func Paginate(pageNum *int64, pageSize *int64) func(db *gorm.DB) *gorm.DB {
|
||||||
@@ -641,4 +641,3 @@ func Paginate(pageNum *int64, pageSize *int64) func(db *gorm.DB) *gorm.DB {
|
|||||||
return db.Offset(int((num - 1) * size)).Limit(int(size))
|
return db.Offset(int((num - 1) * size)).Limit(int(size))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,4 +72,3 @@ func UpsertTrafficAggregateDaily(peerId int64, dayStart int64, rxBytes int64, tx
|
|||||||
func gormExprAdd(column string, delta int64) interface{} {
|
func gormExprAdd(column string, delta int64) interface{} {
|
||||||
return gorm.Expr(fmt.Sprintf("%s + ?", column), delta)
|
return gorm.Expr(fmt.Sprintf("%s + ?", column), delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ export default {
|
|||||||
stale: "Dashboard data is stale. Retrying automatically...",
|
stale: "Dashboard data is stale. Retrying automatically...",
|
||||||
topPeers24h: "Top peers (24h)",
|
topPeers24h: "Top peers (24h)",
|
||||||
refreshFailed: "Failed to refresh dashboard data",
|
refreshFailed: "Failed to refresh dashboard data",
|
||||||
|
cpu: "CPU",
|
||||||
|
ram: "RAM",
|
||||||
|
disk: "Disk",
|
||||||
|
peers: "Peers",
|
||||||
|
onlinePeers: "Online peers",
|
||||||
|
onlineDevices: "Online devices",
|
||||||
|
todayDownload: "Today download",
|
||||||
|
todayUpload: "Today upload",
|
||||||
|
trafficChart: "Traffic & System Timeseries",
|
||||||
|
download: "Download",
|
||||||
|
upload: "Upload",
|
||||||
|
total: "Total",
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
changePasswordTitle: "Change password",
|
changePasswordTitle: "Change password",
|
||||||
@@ -102,8 +114,22 @@ export default {
|
|||||||
"I want to be a shooting star, cutting through the darkness, just to illuminate your dreams, good night🌛!",
|
"I want to be a shooting star, cutting through the darkness, just to illuminate your dreams, good night🌛!",
|
||||||
},
|
},
|
||||||
peer: {
|
peer: {
|
||||||
|
name: "Peer",
|
||||||
remark: "Remark",
|
remark: "Remark",
|
||||||
username: "Username",
|
username: "Username",
|
||||||
|
secret: "Secret",
|
||||||
|
maxDevices: "Max devices",
|
||||||
|
disabled: "Disabled",
|
||||||
|
status: "Status",
|
||||||
|
traffic: "Traffic",
|
||||||
|
devices: "Devices",
|
||||||
|
lastConnectionAt: "Last connection",
|
||||||
|
overview: "Overview",
|
||||||
|
authId: "Auth ID",
|
||||||
|
clientUri: "Client URI",
|
||||||
|
clientQr: "Client QR",
|
||||||
|
unlimited: "Unlimited",
|
||||||
|
secretRotateConfirm: "Rotate peer secret? Existing client configurations will stop working until updated.",
|
||||||
pass: "Pass",
|
pass: "Pass",
|
||||||
conPass: "ConPass",
|
conPass: "ConPass",
|
||||||
quota: "Quota",
|
quota: "Quota",
|
||||||
|
|||||||
@@ -23,6 +23,18 @@ export default {
|
|||||||
stale: "Данные дашборда устарели. Выполняется автоматическая повторная попытка...",
|
stale: "Данные дашборда устарели. Выполняется автоматическая повторная попытка...",
|
||||||
topPeers24h: "Топ пиров (24ч)",
|
topPeers24h: "Топ пиров (24ч)",
|
||||||
refreshFailed: "Не удалось обновить данные дашборда",
|
refreshFailed: "Не удалось обновить данные дашборда",
|
||||||
|
cpu: "CPU",
|
||||||
|
ram: "RAM",
|
||||||
|
disk: "Диск",
|
||||||
|
peers: "Пиры",
|
||||||
|
onlinePeers: "Пиры онлайн",
|
||||||
|
onlineDevices: "Устройства онлайн",
|
||||||
|
todayDownload: "Скачано за сегодня",
|
||||||
|
todayUpload: "Отдано за сегодня",
|
||||||
|
trafficChart: "Трафик и системные метрики",
|
||||||
|
download: "Скачано",
|
||||||
|
upload: "Отдано",
|
||||||
|
total: "Всего",
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
changePasswordTitle: "Смена пароля",
|
changePasswordTitle: "Смена пароля",
|
||||||
@@ -97,8 +109,22 @@ export default {
|
|||||||
greeting5: "Доброй ночи,",
|
greeting5: "Доброй ночи,",
|
||||||
},
|
},
|
||||||
peer: {
|
peer: {
|
||||||
|
name: "Пир",
|
||||||
remark: "Комментарий",
|
remark: "Комментарий",
|
||||||
username: "Логин",
|
username: "Логин",
|
||||||
|
secret: "Секрет",
|
||||||
|
maxDevices: "Лимит устройств",
|
||||||
|
disabled: "Отключён",
|
||||||
|
status: "Статус",
|
||||||
|
traffic: "Трафик",
|
||||||
|
devices: "Устройства",
|
||||||
|
lastConnectionAt: "Последнее подключение",
|
||||||
|
overview: "Обзор",
|
||||||
|
authId: "Auth ID",
|
||||||
|
clientUri: "URI клиента",
|
||||||
|
clientQr: "QR клиента",
|
||||||
|
unlimited: "Безлимит",
|
||||||
|
secretRotateConfirm: "Сменить секрет пира? Текущие клиентские конфиги перестанут работать до обновления.",
|
||||||
pass: "Пароль входа",
|
pass: "Пароль входа",
|
||||||
conPass: "Пароль подключения",
|
conPass: "Пароль подключения",
|
||||||
quota: "Квота",
|
quota: "Квота",
|
||||||
|
|||||||
+4
@@ -14,6 +14,7 @@ declare module '@vue/runtime-core' {
|
|||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
|
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
@@ -26,6 +27,9 @@ declare module '@vue/runtime-core' {
|
|||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
|
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||||
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
|||||||
@@ -29,27 +29,48 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<el-row :gutter="10" class="mt-2">
|
<el-row :gutter="10" class="mt-2">
|
||||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">CPU: {{ summary.system.cpuPercent }}%</el-card></el-col>
|
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.cpu") }}: {{ summary.system.cpuPercent }}%</el-card></el-col>
|
||||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">RAM: {{ summary.system.memPercent }}%</el-card></el-col>
|
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.ram") }}: {{ summary.system.memPercent }}%</el-card></el-col>
|
||||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Disk: {{ summary.system.diskPercent }}%</el-card></el-col>
|
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.disk") }}: {{ summary.system.diskPercent }}%</el-card></el-col>
|
||||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Peers: {{ summary.peers.total }}</el-card></el-col>
|
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.peers") }}: {{ summary.peers.total }}</el-card></el-col>
|
||||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Online peers: {{ summary.peers.onlinePeers }}</el-card></el-col>
|
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.onlinePeers") }}: {{ summary.peers.onlinePeers }}</el-card></el-col>
|
||||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Online devices: {{ summary.peers.onlineDevices }}</el-card></el-col>
|
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.onlineDevices") }}: {{ summary.peers.onlineDevices }}</el-card></el-col>
|
||||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Today download: {{ formatBytes(summary.traffic.todayDownloadBytes || 0) }}</el-card></el-col>
|
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.todayDownload") }}: {{ formatBytes(summary.traffic.todayDownloadBytes || 0) }}</el-card></el-col>
|
||||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Today upload: {{ formatBytes(summary.traffic.todayUploadBytes || 0) }}</el-card></el-col>
|
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.todayUpload") }}: {{ formatBytes(summary.traffic.todayUploadBytes || 0) }}</el-card></el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
<el-card shadow="never" class="mt-3">
|
||||||
|
<template #header>
|
||||||
|
<div class="chart-header">
|
||||||
|
<span>{{ $t("dashboard.trafficChart") }}</span>
|
||||||
|
<el-radio-group v-model="range" size="small" @change="loadDashboard">
|
||||||
|
<el-radio-button label="1h">1h</el-radio-button>
|
||||||
|
<el-radio-button label="24h">24h</el-radio-button>
|
||||||
|
<el-radio-button label="7d">7d</el-radio-button>
|
||||||
|
<el-radio-button label="30d">30d</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-table :data="timeseriesRows" size="small" v-loading="timeseriesLoading">
|
||||||
|
<el-table-column prop="ts" :label="$t('common.createTime')" width="180" />
|
||||||
|
<el-table-column prop="download" :label="$t('dashboard.download')" />
|
||||||
|
<el-table-column prop="upload" :label="$t('dashboard.upload')" />
|
||||||
|
<el-table-column prop="cpu" :label="$t('dashboard.cpu')" width="120" />
|
||||||
|
<el-table-column prop="mem" :label="$t('dashboard.ram')" width="120" />
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<el-card shadow="never" class="mt-3">
|
<el-card shadow="never" class="mt-3">
|
||||||
<template #header>{{ $t("dashboard.topPeers24h") }}</template>
|
<template #header>{{ $t("dashboard.topPeers24h") }}</template>
|
||||||
<el-table :data="topPeers" size="small">
|
<el-table :data="topPeers" size="small">
|
||||||
<el-table-column prop="name" label="Peer" />
|
<el-table-column prop="name" :label="$t('peer.name')" />
|
||||||
<el-table-column prop="download" label="Download">
|
<el-table-column prop="download" :label="$t('dashboard.download')">
|
||||||
<template #default="scope">{{ formatBytes(scope.row.download || 0) }}</template>
|
<template #default="scope">{{ formatBytes(scope.row.download || 0) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="upload" label="Upload">
|
<el-table-column prop="upload" :label="$t('dashboard.upload')">
|
||||||
<template #default="scope">{{ formatBytes(scope.row.upload || 0) }}</template>
|
<template #default="scope">{{ formatBytes(scope.row.upload || 0) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="total" label="Total">
|
<el-table-column prop="total" :label="$t('dashboard.total')">
|
||||||
<template #default="scope">{{ formatBytes(scope.row.total || 0) }}</template>
|
<template #default="scope">{{ formatBytes(scope.row.total || 0) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -60,9 +81,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useIntervalFn } from "@vueuse/core";
|
import { useIntervalFn } from "@vueuse/core";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { dashboardSecurityApi, dashboardSummaryApi, dashboardTopPeersApi } from "@/api/dashboard";
|
import { dashboardSecurityApi, dashboardSummaryApi, dashboardTimeseriesApi, dashboardTopPeersApi } from "@/api/dashboard";
|
||||||
import { DashboardSummaryVo, DashboardTopPeerVo, SecurityRiskVo } from "@/api/dashboard/types";
|
import { DashboardSummaryVo, DashboardTimeseriesVo, DashboardTopPeerVo, SecurityRiskVo } from "@/api/dashboard/types";
|
||||||
import { formatBytes } from "@/utils/byte";
|
import { formatBytes } from "@/utils/byte";
|
||||||
|
import { timestampToDateTime } from "@/utils/time";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -80,6 +102,9 @@ const summary = ref<DashboardSummaryVo>({
|
|||||||
});
|
});
|
||||||
const topPeers = ref<DashboardTopPeerVo[]>([]);
|
const topPeers = ref<DashboardTopPeerVo[]>([]);
|
||||||
const securityRisks = ref<SecurityRiskVo[]>([]);
|
const securityRisks = ref<SecurityRiskVo[]>([]);
|
||||||
|
const timeseries = ref<DashboardTimeseriesVo>({ range: "24h", traffic: [], system: [], collectedAt: 0 });
|
||||||
|
const timeseriesLoading = ref(false);
|
||||||
|
const range = ref("24h");
|
||||||
const loadError = ref("");
|
const loadError = ref("");
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const lastSuccessAt = ref(0);
|
const lastSuccessAt = ref(0);
|
||||||
@@ -102,7 +127,7 @@ const loadDashboard = async () => {
|
|||||||
try {
|
try {
|
||||||
const [summaryRes, topRes, secRes] = await Promise.all([
|
const [summaryRes, topRes, secRes] = await Promise.all([
|
||||||
dashboardSummaryApi(),
|
dashboardSummaryApi(),
|
||||||
dashboardTopPeersApi("24h", 10),
|
dashboardTopPeersApi(range.value, 10),
|
||||||
dashboardSecurityApi(),
|
dashboardSecurityApi(),
|
||||||
]);
|
]);
|
||||||
summary.value = summaryRes.data;
|
summary.value = summaryRes.data;
|
||||||
@@ -115,8 +140,33 @@ const loadDashboard = async () => {
|
|||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timeseriesLoading.value = true;
|
||||||
|
try {
|
||||||
|
const tsRes = await dashboardTimeseriesApi(range.value);
|
||||||
|
timeseries.value = tsRes.data;
|
||||||
|
} finally {
|
||||||
|
timeseriesLoading.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const timeseriesRows = computed(() => {
|
||||||
|
const trafficMap: Record<number, { download: number; upload: number }> = {};
|
||||||
|
for (const t of timeseries.value.traffic || []) {
|
||||||
|
trafficMap[t.ts] = { download: t.download || 0, upload: t.upload || 0 };
|
||||||
|
}
|
||||||
|
return (timeseries.value.system || []).map((s) => {
|
||||||
|
const tm = trafficMap[s.ts] || { download: 0, upload: 0 };
|
||||||
|
return {
|
||||||
|
ts: timestampToDateTime(s.ts),
|
||||||
|
download: formatBytes(tm.download),
|
||||||
|
upload: formatBytes(tm.upload),
|
||||||
|
cpu: `${Math.round((s.cpu || 0) * 10) / 10}%`,
|
||||||
|
mem: `${Math.round((s.mem || 0) * 10) / 10}%`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const { pause: stopPolling, resume: startPolling } = useIntervalFn(
|
const { pause: stopPolling, resume: startPolling } = useIntervalFn(
|
||||||
() => {
|
() => {
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
@@ -155,5 +205,11 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -17,27 +17,50 @@
|
|||||||
<el-button type="primary" @click="handleAdd">{{ $t("common.add") }}</el-button>
|
<el-button type="primary" @click="handleAdd">{{ $t("common.add") }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-loading="loading" :data="records">
|
<el-table v-loading="loading" :data="records">
|
||||||
<el-table-column prop="id" :label="$t('common.id')" width="80" />
|
<el-table-column :label="$t('peer.name')" min-width="220">
|
||||||
<el-table-column prop="name" :label="$t('peer.username')" min-width="180" />
|
<template #default="scope">
|
||||||
<el-table-column prop="remark" :label="$t('peer.remark')" min-width="200" />
|
<div class="peer-title">{{ scope.row.name }}</div>
|
||||||
<el-table-column :label="$t('peer.quota')" min-width="140">
|
<div class="peer-sub">{{ scope.row.remark || "-" }}</div>
|
||||||
<template #default="scope">{{ formatBytes(scope.row.quotaBytes) }}</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :label="$t('peer.download')" min-width="140">
|
<el-table-column :label="$t('peer.status')" min-width="180">
|
||||||
<template #default="scope">{{ formatBytes(scope.row.downloadBytes) }}</template>
|
<template #default="scope">
|
||||||
|
<el-tag size="small" :type="scope.row.disabled === 1 ? 'danger' : 'success'">{{ scope.row.disabled === 1 ? $t('common.disable') : $t('common.enable') }}</el-tag>
|
||||||
|
<el-tag size="small" class="ml-1" :type="scope.row.online ? 'success' : 'info'">{{ scope.row.online ? $t('peer.online') : $t('peer.offline') }}</el-tag>
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :label="$t('peer.upload')" min-width="140">
|
<el-table-column :label="$t('peer.traffic')" min-width="260">
|
||||||
<template #default="scope">{{ formatBytes(scope.row.uploadBytes) }}</template>
|
<template #default="scope">
|
||||||
|
<div>{{ formatBytes(scope.row.downloadBytes + scope.row.uploadBytes) }} / {{ quotaText(scope.row.quotaBytes) }}</div>
|
||||||
|
<el-progress :percentage="trafficPercent(scope.row)" :status="scope.row.quotaBytes < 0 ? undefined : (trafficPercent(scope.row) >= 90 ? 'exception' : undefined)" :show-text="false" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="$t('peer.devices')" min-width="120">
|
||||||
|
<template #default="scope">{{ scope.row.onlineDevices }} / {{ scope.row.maxDevices }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :label="$t('peer.expireTime')" min-width="170">
|
<el-table-column :label="$t('peer.expireTime')" min-width="170">
|
||||||
<template #default="scope">{{ timestampToDateTime(scope.row.expiresAt) }}</template>
|
<template #default="scope">{{ scope.row.expiresAt === 0 ? $t('peer.unlimited') : timestampToDateTime(scope.row.expiresAt) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :label="$t('common.operate')" width="320">
|
<el-table-column :label="$t('peer.lastConnectionAt')" min-width="170">
|
||||||
|
<template #default="scope">{{ scope.row.lastConnectionAt ? timestampToDateTime(scope.row.lastConnectionAt) : '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="$t('common.operate')" width="340" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button link type="primary" @click="copyUri(scope.row)">URI</el-button>
|
<el-button link type="primary" @click="openOverview(scope.row)">{{ $t("peer.overview") }}</el-button>
|
||||||
<el-button link type="primary" @click="showQr(scope.row)">QR</el-button>
|
<el-button link type="primary" @click="copyUri(scope.row)">{{ $t("peer.clientUri") }}</el-button>
|
||||||
<el-button link type="primary" @click="handleUpdate(scope.row)">{{ $t("common.edit") }}</el-button>
|
<el-button link type="primary" @click="showQr(scope.row)">{{ $t("peer.clientQr") }}</el-button>
|
||||||
<el-button link type="danger" @click="handleDelete(scope.row)">{{ $t("common.delete") }}</el-button>
|
<el-dropdown>
|
||||||
|
<span class="el-dropdown-link">{{ $t("common.operate") }}</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item @click="handleUpdate(scope.row)">{{ $t("common.edit") }}</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="handleResetTraffic(scope.row)">{{ $t("common.resetTraffic") }}</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="handleKick(scope.row)">{{ $t("peer.kick") }}</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="handleReleaseKick(scope.row)">{{ $t("peer.releaseKick") }}</el-dropdown-item>
|
||||||
|
<el-dropdown-item divided @click="handleDelete(scope.row)">{{ $t("common.delete") }}</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -51,14 +74,14 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="620px">
|
<el-dialog v-model="dialog.visible" :title="dialog.title" width="620px">
|
||||||
<el-form :model="dataForm" label-width="140px">
|
<el-form ref="formRef" :model="dataForm" :rules="rules" label-width="140px">
|
||||||
<el-form-item label="Name"><el-input v-model="dataForm.name" /></el-form-item>
|
<el-form-item :label="$t('peer.name')" prop="name"><el-input v-model="dataForm.name" /></el-form-item>
|
||||||
<el-form-item label="Remark"><el-input v-model="dataForm.remark" /></el-form-item>
|
<el-form-item :label="$t('peer.remark')"><el-input v-model="dataForm.remark" /></el-form-item>
|
||||||
<el-form-item label="Secret"><el-input v-model="dataForm.secret" show-password /></el-form-item>
|
<el-form-item :label="$t('peer.secret')" prop="secret"><el-input v-model="dataForm.secret" show-password /></el-form-item>
|
||||||
<el-form-item label="Quota"><el-input-number v-model="dataForm.quotaBytes" :min="-1" /></el-form-item>
|
<el-form-item :label="$t('peer.quota')"><el-input-number v-model="dataForm.quotaBytes" :min="-1" /></el-form-item>
|
||||||
<el-form-item label="Expires"><el-date-picker v-model="dataForm.expiresAt" type="datetime" value-format="x" /></el-form-item>
|
<el-form-item :label="$t('peer.expireTime')"><el-date-picker v-model="dataForm.expiresAt" type="datetime" value-format="x" /></el-form-item>
|
||||||
<el-form-item label="Max devices"><el-input-number v-model="dataForm.maxDevices" :min="1" /></el-form-item>
|
<el-form-item :label="$t('peer.maxDevices')"><el-input-number v-model="dataForm.maxDevices" :min="1" /></el-form-item>
|
||||||
<el-form-item label="Disabled"><el-switch v-model="disabledBool" /></el-form-item>
|
<el-form-item :label="$t('peer.disabled')"><el-switch v-model="disabledBool" /></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button type="primary" @click="submitForm">{{ $t("common.confirm") }}</el-button>
|
<el-button type="primary" @click="submitForm">{{ $t("common.confirm") }}</el-button>
|
||||||
@@ -68,6 +91,24 @@
|
|||||||
<el-dialog v-model="qrDialog" title="QR" width="420px">
|
<el-dialog v-model="qrDialog" title="QR" width="420px">
|
||||||
<el-image style="width:300px;height:300px" :src="qrSrc" />
|
<el-image style="width:300px;height:300px" :src="qrSrc" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-drawer v-model="overview.visible" :title="$t('peer.overview')" size="45%">
|
||||||
|
<div v-if="overview.data">
|
||||||
|
<p><b>{{ $t('peer.authId') }}:</b> {{ overview.data.authId }}</p>
|
||||||
|
<p><b>{{ $t('peer.name') }}:</b> {{ overview.data.name }}</p>
|
||||||
|
<p><b>{{ $t('peer.remark') }}:</b> {{ overview.data.remark || '-' }}</p>
|
||||||
|
<p><b>{{ $t('peer.quota') }}:</b> {{ quotaText(overview.data.quotaBytes) }}</p>
|
||||||
|
<p><b>{{ $t('peer.download') }}:</b> {{ formatBytes(overview.data.downloadBytes) }}</p>
|
||||||
|
<p><b>{{ $t('peer.upload') }}:</b> {{ formatBytes(overview.data.uploadBytes) }}</p>
|
||||||
|
<p><b>{{ $t('peer.devices') }}:</b> {{ overview.data.onlineDevices }} / {{ overview.data.maxDevices }}</p>
|
||||||
|
<p><b>{{ $t('peer.expireTime') }}:</b> {{ overview.data.expiresAt === 0 ? $t('peer.unlimited') : timestampToDateTime(overview.data.expiresAt) }}</p>
|
||||||
|
<p><b>{{ $t('peer.lastConnectionAt') }}:</b> {{ overview.data.lastConnectionAt ? timestampToDateTime(overview.data.lastConnectionAt) : '-' }}</p>
|
||||||
|
<div class="mt-2">
|
||||||
|
<el-button type="primary" @click="copyUri(overview.data)">{{ $t("peer.clientUri") }}</el-button>
|
||||||
|
<el-button @click="showQr(overview.data)">{{ $t("peer.clientQr") }}</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -75,13 +116,17 @@
|
|||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
|
import { ElMessageBox } from "element-plus";
|
||||||
import { formatBytes } from "@/utils/byte";
|
import { formatBytes } from "@/utils/byte";
|
||||||
import { getMonthLater, timestampToDateTime } from "@/utils/time";
|
import { getMonthLater, timestampToDateTime } from "@/utils/time";
|
||||||
import {
|
import {
|
||||||
deletePeerApi,
|
deletePeerApi,
|
||||||
getPeerApi,
|
getPeerApi,
|
||||||
getPeerClientConfigApi,
|
getPeerClientConfigApi,
|
||||||
|
kickPeerApi,
|
||||||
pagePeerApi,
|
pagePeerApi,
|
||||||
|
releaseKickPeerApi,
|
||||||
|
resetPeerTrafficApi,
|
||||||
savePeerApi,
|
savePeerApi,
|
||||||
updatePeerApi,
|
updatePeerApi,
|
||||||
} from "@/api/peer";
|
} from "@/api/peer";
|
||||||
@@ -93,6 +138,8 @@ const total = ref(0);
|
|||||||
const records = ref<PeerVo[]>([]);
|
const records = ref<PeerVo[]>([]);
|
||||||
const qrDialog = ref(false);
|
const qrDialog = ref(false);
|
||||||
const qrSrc = ref("");
|
const qrSrc = ref("");
|
||||||
|
const formRef = ref();
|
||||||
|
const overview = reactive<{ visible: boolean; data: PeerVo | null }>({ visible: false, data: null });
|
||||||
|
|
||||||
const queryParams = reactive<PeerPageDto>({ pageNum: 1, pageSize: 10, name: undefined, remark: undefined, disabled: undefined });
|
const queryParams = reactive<PeerPageDto>({ pageNum: 1, pageSize: 10, name: undefined, remark: undefined, disabled: undefined });
|
||||||
const dialog = reactive({ visible: false, title: "", editId: 0 });
|
const dialog = reactive({ visible: false, title: "", editId: 0 });
|
||||||
@@ -111,6 +158,25 @@ const disabledBool = computed({
|
|||||||
set: (v: boolean) => (dataForm.disabled = v ? 1 : 0),
|
set: (v: boolean) => (dataForm.disabled = v ? 1 : 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: t("common.required"), trigger: ["change", "blur"] }],
|
||||||
|
};
|
||||||
|
|
||||||
|
function quotaText(v: number) {
|
||||||
|
return v < 0 ? t("peer.unlimited") : formatBytes(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trafficPercent(row: PeerVo) {
|
||||||
|
if (row.quotaBytes < 0 || row.quotaBytes === 0) return 0;
|
||||||
|
const used = row.downloadBytes + row.uploadBytes;
|
||||||
|
return Math.max(0, Math.min(100, Math.round((used / row.quotaBytes) * 100)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openOverview(row: PeerVo) {
|
||||||
|
overview.data = row;
|
||||||
|
overview.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleQuery() {
|
async function handleQuery() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -138,6 +204,13 @@ async function handleUpdate(row: PeerVo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submitForm() {
|
async function submitForm() {
|
||||||
|
if (formRef.value) {
|
||||||
|
const ok = await formRef.value.validate().catch(() => false);
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
|
if (dialog.editId > 0 && dataForm.secret) {
|
||||||
|
await ElMessageBox.confirm(t("peer.secretRotateConfirm"), t("common.warning"), { type: "warning" });
|
||||||
|
}
|
||||||
if (dialog.editId > 0) {
|
if (dialog.editId > 0) {
|
||||||
const payload: PeerUpdateDto = { id: dialog.editId, name: dataForm.name, secret: dataForm.secret || undefined, quotaBytes: dataForm.quotaBytes, expiresAt: dataForm.expiresAt, maxDevices: dataForm.maxDevices, disabled: dataForm.disabled, remark: dataForm.remark };
|
const payload: PeerUpdateDto = { id: dialog.editId, name: dataForm.name, secret: dataForm.secret || undefined, quotaBytes: dataForm.quotaBytes, expiresAt: dataForm.expiresAt, maxDevices: dataForm.maxDevices, disabled: dataForm.disabled, remark: dataForm.remark };
|
||||||
await updatePeerApi(payload);
|
await updatePeerApi(payload);
|
||||||
@@ -149,10 +222,27 @@ async function submitForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete(row: PeerVo) {
|
async function handleDelete(row: PeerVo) {
|
||||||
|
await ElMessageBox.confirm(t("common.deleteConfirm", { username: row.name }), t("common.warning"), { type: "warning" });
|
||||||
await deletePeerApi({ id: row.id });
|
await deletePeerApi({ id: row.id });
|
||||||
await handleQuery();
|
await handleQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleResetTraffic(row: PeerVo) {
|
||||||
|
await ElMessageBox.confirm(t("common.resetTrafficConfirm"), t("common.warning"), { type: "warning" });
|
||||||
|
await resetPeerTrafficApi({ id: row.id });
|
||||||
|
await handleQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleKick(row: PeerVo) {
|
||||||
|
await kickPeerApi(row.id, { bannedUntil: Date.now() + 60 * 60 * 1000 });
|
||||||
|
await handleQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReleaseKick(row: PeerVo) {
|
||||||
|
await releaseKickPeerApi({ id: row.id });
|
||||||
|
await handleQuery();
|
||||||
|
}
|
||||||
|
|
||||||
async function copyUri(row: PeerVo) {
|
async function copyUri(row: PeerVo) {
|
||||||
const { data } = await getPeerClientConfigApi(row.id);
|
const { data } = await getPeerClientConfigApi(row.id);
|
||||||
copy(data.url);
|
copy(data.url);
|
||||||
@@ -167,3 +257,8 @@ async function showQr(row: PeerVo) {
|
|||||||
onMounted(handleQuery);
|
onMounted(handleQuery);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.peer-title { font-weight: 600; }
|
||||||
|
.peer-sub { color: #909399; font-size: 12px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,27 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"hy2xs-admin/model/bo"
|
||||||
"hy2xs-admin/model/constant"
|
"hy2xs-admin/model/constant"
|
||||||
"hy2xs-admin/model/vo"
|
"hy2xs-admin/model/vo"
|
||||||
"hy2xs-admin/service"
|
|
||||||
"hy2xs-admin/util"
|
"hy2xs-admin/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AdminHandler() gin.HandlerFunc {
|
func AdminHandler() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
myClaims, err := service.ParseToken(service.GetToken(c))
|
claimsRaw, ok := c.Get("adminClaims")
|
||||||
if err != nil {
|
if !ok {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(constant.UnauthorizedError, c)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !util.ArrContain(myClaims.Admin.Roles, "admin") {
|
claims, castOK := claimsRaw.(bo.AccountBo)
|
||||||
|
if !castOK {
|
||||||
|
vo.Fail(constant.IllegalTokenError, c)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !util.ArrContain(claims.Roles, "admin") {
|
||||||
vo.Fail(constant.ForbiddenError, c)
|
vo.Fail(constant.ForbiddenError, c)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ func JWTHandler() gin.HandlerFunc {
|
|||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.Set("adminClaims", myClaims.Admin)
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package bo
|
package bo
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type AccountBo struct {
|
type AccountBo struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
@@ -10,26 +8,6 @@ type AccountBo struct {
|
|||||||
TokenVersion int64 `json:"tokenVersion"`
|
TokenVersion int64 `json:"tokenVersion"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountExport struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Pass string `json:"pass"`
|
|
||||||
ConPass string `json:"conPass"`
|
|
||||||
Quota int64 `json:"quota"`
|
|
||||||
Download int64 `json:"download"`
|
|
||||||
Upload int64 `json:"upload"`
|
|
||||||
ExpireTime int64 `json:"expireTime"`
|
|
||||||
DeviceNo int64 `json:"deviceNo"`
|
|
||||||
KickUtilTime int64 `json:"kickUtilTime"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
Deleted int64 `json:"deleted"`
|
|
||||||
CreateTime time.Time `json:"createTime"`
|
|
||||||
UpdateTime time.Time `json:"updateTime"`
|
|
||||||
LoginAt int64 `json:"loginAt"`
|
|
||||||
ConAt int64 `json:"conAt"`
|
|
||||||
Remark string `json:"remark"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PeerExport struct {
|
type PeerExport struct {
|
||||||
Id int64 `json:"id,omitempty"`
|
Id int64 `json:"id,omitempty"`
|
||||||
AuthId string `json:"authId,omitempty"`
|
AuthId string `json:"authId,omitempty"`
|
||||||
|
|||||||
@@ -4,4 +4,3 @@ type AdminChangePasswordDto struct {
|
|||||||
OldPassword *string `json:"oldPassword" form:"oldPassword" validate:"required,min=6,max=64"`
|
OldPassword *string `json:"oldPassword" form:"oldPassword" validate:"required,min=6,max=64"`
|
||||||
NewPassword *string `json:"newPassword" form:"newPassword" validate:"required,min=6,max=64"`
|
NewPassword *string `json:"newPassword" form:"newPassword" validate:"required,min=6,max=64"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,3 @@ type LoginDto struct {
|
|||||||
Username *string `json:"username" form:"username" validate:"required,min=6,max=32,validateStr"`
|
Username *string `json:"username" form:"username" validate:"required,min=6,max=32,validateStr"`
|
||||||
Pass *string `json:"pass" form:"pass" validate:"required,min=6,max=64"`
|
Pass *string `json:"pass" form:"pass" validate:"required,min=6,max=64"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,4 +31,3 @@ type PeerUpdateDto struct {
|
|||||||
type PeerKickDto struct {
|
type PeerKickDto struct {
|
||||||
BannedUntil *int64 `json:"bannedUntil" form:"bannedUntil" validate:"required,min=0"`
|
BannedUntil *int64 `json:"bannedUntil" form:"bannedUntil" validate:"required,min=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
type Account struct {
|
type LegacyAccount struct {
|
||||||
Username *string `gorm:"column:username;default:''" json:"username"`
|
Username *string `gorm:"column:username;default:''" json:"username"`
|
||||||
Pass *string `gorm:"column:pass;default:''" json:"pass"`
|
Pass *string `gorm:"column:pass;default:''" json:"pass"`
|
||||||
ConPass *string `gorm:"column:con_pass;default:''" json:"conPass"`
|
ConPass *string `gorm:"column:con_pass;default:''" json:"conPass"`
|
||||||
@@ -19,3 +19,7 @@ type Account struct {
|
|||||||
Remark *string `gorm:"column:remark;default:''" json:"remark"`
|
Remark *string `gorm:"column:remark;default:''" json:"remark"`
|
||||||
ForcePasswordChange *int64 `gorm:"column:force_password_change;default:0" json:"forcePasswordChange"`
|
ForcePasswordChange *int64 `gorm:"column:force_password_change;default:0" json:"forcePasswordChange"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (LegacyAccount) TableName() string {
|
||||||
|
return "account"
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,4 +14,3 @@ type AdminUser struct {
|
|||||||
func (AdminUser) TableName() string {
|
func (AdminUser) TableName() string {
|
||||||
return "admin_user"
|
return "admin_user"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,22 +2,21 @@ package entity
|
|||||||
|
|
||||||
type MetricSample struct {
|
type MetricSample struct {
|
||||||
BaseEntity
|
BaseEntity
|
||||||
SampledAt *int64 `gorm:"column:sampled_at"`
|
SampledAt *int64 `gorm:"column:sampled_at"`
|
||||||
CpuPercent *float64 `gorm:"column:cpu_percent"`
|
CpuPercent *float64 `gorm:"column:cpu_percent"`
|
||||||
Load1 *float64 `gorm:"column:load1"`
|
Load1 *float64 `gorm:"column:load1"`
|
||||||
MemUsedBytes *int64 `gorm:"column:mem_used_bytes"`
|
MemUsedBytes *int64 `gorm:"column:mem_used_bytes"`
|
||||||
MemTotalBytes *int64 `gorm:"column:mem_total_bytes"`
|
MemTotalBytes *int64 `gorm:"column:mem_total_bytes"`
|
||||||
MemPercent *float64 `gorm:"column:mem_percent"`
|
MemPercent *float64 `gorm:"column:mem_percent"`
|
||||||
DiskPath *string `gorm:"column:disk_path"`
|
DiskPath *string `gorm:"column:disk_path"`
|
||||||
DiskUsedBytes *int64 `gorm:"column:disk_used_bytes"`
|
DiskUsedBytes *int64 `gorm:"column:disk_used_bytes"`
|
||||||
DiskTotalBytes *int64 `gorm:"column:disk_total_bytes"`
|
DiskTotalBytes *int64 `gorm:"column:disk_total_bytes"`
|
||||||
DiskPercent *float64 `gorm:"column:disk_percent"`
|
DiskPercent *float64 `gorm:"column:disk_percent"`
|
||||||
HysteriaRunning *int64 `gorm:"column:hysteria_running"`
|
HysteriaRunning *int64 `gorm:"column:hysteria_running"`
|
||||||
OnlinePeers *int64 `gorm:"column:online_peers"`
|
OnlinePeers *int64 `gorm:"column:online_peers"`
|
||||||
OnlineDevices *int64 `gorm:"column:online_devices"`
|
OnlineDevices *int64 `gorm:"column:online_devices"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (MetricSample) TableName() string {
|
func (MetricSample) TableName() string {
|
||||||
return "metric_sample"
|
return "metric_sample"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,4 +20,3 @@ type Peer struct {
|
|||||||
func (Peer) TableName() string {
|
func (Peer) TableName() string {
|
||||||
return "peer"
|
return "peer"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,3 @@ type TrafficAggregateDaily struct {
|
|||||||
func (TrafficAggregateDaily) TableName() string {
|
func (TrafficAggregateDaily) TableName() string {
|
||||||
return "traffic_aggregate_daily"
|
return "traffic_aggregate_daily"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,3 @@ type TrafficAggregateHourly struct {
|
|||||||
func (TrafficAggregateHourly) TableName() string {
|
func (TrafficAggregateHourly) TableName() string {
|
||||||
return "traffic_aggregate_hourly"
|
return "traffic_aggregate_hourly"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,4 +12,3 @@ type TrafficSample struct {
|
|||||||
func (TrafficSample) TableName() string {
|
func (TrafficSample) TableName() string {
|
||||||
return "traffic_sample"
|
return "traffic_sample"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,3 @@ type AdminInfoVo struct {
|
|||||||
Roles []string `json:"roles"`
|
Roles []string `json:"roles"`
|
||||||
ForcePasswordChange bool `json:"forcePasswordChange"`
|
ForcePasswordChange bool `json:"forcePasswordChange"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+28
-29
@@ -1,13 +1,13 @@
|
|||||||
package vo
|
package vo
|
||||||
|
|
||||||
type DashboardSummaryVo struct {
|
type DashboardSummaryVo struct {
|
||||||
CollectedAt int64 `json:"collectedAt"`
|
CollectedAt int64 `json:"collectedAt"`
|
||||||
System DashboardSystemVo `json:"system"`
|
System DashboardSystemVo `json:"system"`
|
||||||
Hysteria DashboardHysteriaVo `json:"hysteria"`
|
Hysteria DashboardHysteriaVo `json:"hysteria"`
|
||||||
Peers DashboardPeerVo `json:"peers"`
|
Peers DashboardPeerVo `json:"peers"`
|
||||||
Traffic DashboardTrafficVo `json:"traffic"`
|
Traffic DashboardTrafficVo `json:"traffic"`
|
||||||
Health DashboardHealthVo `json:"health"`
|
Health DashboardHealthVo `json:"health"`
|
||||||
SecurityRisks []SecurityRiskVo `json:"securityRisks"`
|
SecurityRisks []SecurityRiskVo `json:"securityRisks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardHealthVo struct {
|
type DashboardHealthVo struct {
|
||||||
@@ -22,13 +22,13 @@ type DataHealthVo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DashboardSystemVo struct {
|
type DashboardSystemVo struct {
|
||||||
CpuPercent float64 `json:"cpuPercent"`
|
CpuPercent float64 `json:"cpuPercent"`
|
||||||
MemUsedBytes uint64 `json:"memUsedBytes"`
|
MemUsedBytes uint64 `json:"memUsedBytes"`
|
||||||
MemTotalBytes uint64 `json:"memTotalBytes"`
|
MemTotalBytes uint64 `json:"memTotalBytes"`
|
||||||
MemPercent float64 `json:"memPercent"`
|
MemPercent float64 `json:"memPercent"`
|
||||||
DiskUsedBytes uint64 `json:"diskUsedBytes"`
|
DiskUsedBytes uint64 `json:"diskUsedBytes"`
|
||||||
DiskTotalBytes uint64 `json:"diskTotalBytes"`
|
DiskTotalBytes uint64 `json:"diskTotalBytes"`
|
||||||
DiskPercent float64 `json:"diskPercent"`
|
DiskPercent float64 `json:"diskPercent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardHysteriaVo struct {
|
type DashboardHysteriaVo struct {
|
||||||
@@ -48,11 +48,11 @@ type DashboardPeerVo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DashboardTrafficVo struct {
|
type DashboardTrafficVo struct {
|
||||||
DownloadBytes int64 `json:"downloadBytes"`
|
DownloadBytes int64 `json:"downloadBytes"`
|
||||||
UploadBytes int64 `json:"uploadBytes"`
|
UploadBytes int64 `json:"uploadBytes"`
|
||||||
TotalBytes int64 `json:"totalBytes"`
|
TotalBytes int64 `json:"totalBytes"`
|
||||||
TodayDownloadBytes int64 `json:"todayDownloadBytes"`
|
TodayDownloadBytes int64 `json:"todayDownloadBytes"`
|
||||||
TodayUploadBytes int64 `json:"todayUploadBytes"`
|
TodayUploadBytes int64 `json:"todayUploadBytes"`
|
||||||
SinceResetDownloadBytes int64 `json:"sinceResetDownloadBytes"`
|
SinceResetDownloadBytes int64 `json:"sinceResetDownloadBytes"`
|
||||||
SinceResetUploadBytes int64 `json:"sinceResetUploadBytes"`
|
SinceResetUploadBytes int64 `json:"sinceResetUploadBytes"`
|
||||||
}
|
}
|
||||||
@@ -65,12 +65,12 @@ type SecurityRiskVo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DashboardTopPeerVo struct {
|
type DashboardTopPeerVo struct {
|
||||||
PeerId int64 `json:"peerId"`
|
PeerId int64 `json:"peerId"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
Download int64 `json:"download"`
|
Download int64 `json:"download"`
|
||||||
Upload int64 `json:"upload"`
|
Upload int64 `json:"upload"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardSeriesPointVo struct {
|
type DashboardSeriesPointVo struct {
|
||||||
@@ -82,9 +82,8 @@ type DashboardSeriesPointVo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DashboardTimeseriesVo struct {
|
type DashboardTimeseriesVo struct {
|
||||||
Range string `json:"range"`
|
Range string `json:"range"`
|
||||||
Traffic []DashboardSeriesPointVo `json:"traffic"`
|
Traffic []DashboardSeriesPointVo `json:"traffic"`
|
||||||
System []DashboardSeriesPointVo `json:"system"`
|
System []DashboardSeriesPointVo `json:"system"`
|
||||||
CollectedAt int64 `json:"collectedAt"`
|
CollectedAt int64 `json:"collectedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,4 +27,3 @@ type PeerClientConfigVo struct {
|
|||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
QrCode []byte `json:"qrCode"`
|
QrCode []byte `json:"qrCode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,4 +13,3 @@ func initAdminRouter(adminApi *gin.RouterGroup) {
|
|||||||
admin.GET("/security", controller.AdminSecurity)
|
admin.GET("/security", controller.AdminSecurity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,4 +14,3 @@ func initDashboardRouter(api *gin.RouterGroup) {
|
|||||||
dashboard.GET("/security", controller.DashboardSecurity)
|
dashboard.GET("/security", controller.DashboardSecurity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ func Router(router *gin.Engine, huiWebContext *string) {
|
|||||||
|
|
||||||
if !sqliteReady || !configReadable {
|
if !sqliteReady || !configReadable {
|
||||||
c.JSON(503, gin.H{
|
c.JSON(503, gin.H{
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"service": "hy2xs-admin",
|
"service": "hy2xs-admin",
|
||||||
"sqlite_ready": sqliteReady,
|
"sqlite_ready": sqliteReady,
|
||||||
"config_readable": configReadable,
|
"config_readable": configReadable,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ import (
|
|||||||
"hy2xs-admin/util"
|
"hy2xs-admin/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func adminClaimsFromContext(c *gin.Context) (bo.AccountBo, bool) {
|
||||||
|
if c == nil {
|
||||||
|
return bo.AccountBo{}, false
|
||||||
|
}
|
||||||
|
v, ok := c.Get("adminClaims")
|
||||||
|
if !ok {
|
||||||
|
return bo.AccountBo{}, false
|
||||||
|
}
|
||||||
|
claims, castOK := v.(bo.AccountBo)
|
||||||
|
return claims, castOK
|
||||||
|
}
|
||||||
|
|
||||||
func Login(username string, plainPassword string) (string, bool, error) {
|
func Login(username string, plainPassword string) (string, bool, error) {
|
||||||
admin, err := dao.GetAdminUser("username = ? and status = 1", username)
|
admin, err := dao.GetAdminUser("username = ? and status = 1", username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -45,11 +57,15 @@ func Login(username string, plainPassword string) (string, bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetAdminInfo(c *gin.Context) (vo.AdminInfoVo, error) {
|
func GetAdminInfo(c *gin.Context) (vo.AdminInfoVo, error) {
|
||||||
myClaims, err := ParseToken(GetToken(c))
|
claims, ok := adminClaimsFromContext(c)
|
||||||
if err != nil {
|
if !ok {
|
||||||
return vo.AdminInfoVo{}, err
|
myClaims, err := ParseToken(GetToken(c))
|
||||||
|
if err != nil {
|
||||||
|
return vo.AdminInfoVo{}, err
|
||||||
|
}
|
||||||
|
claims = myClaims.Admin
|
||||||
}
|
}
|
||||||
admin, err := dao.GetAdminUser("id = ?", myClaims.Admin.Id)
|
admin, err := dao.GetAdminUser("id = ?", claims.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vo.AdminInfoVo{}, err
|
return vo.AdminInfoVo{}, err
|
||||||
}
|
}
|
||||||
@@ -57,7 +73,7 @@ func GetAdminInfo(c *gin.Context) (vo.AdminInfoVo, error) {
|
|||||||
return vo.AdminInfoVo{}, errors.New("this account has been disabled")
|
return vo.AdminInfoVo{}, errors.New("this account has been disabled")
|
||||||
}
|
}
|
||||||
force := admin.ForcePasswordChange != nil && *admin.ForcePasswordChange != 0
|
force := admin.ForcePasswordChange != nil && *admin.ForcePasswordChange != 0
|
||||||
return vo.AdminInfoVo{Id: myClaims.Admin.Id, Username: myClaims.Admin.Username, Roles: myClaims.Admin.Roles, ForcePasswordChange: force}, nil
|
return vo.AdminInfoVo{Id: claims.Id, Username: claims.Username, Roles: claims.Roles, ForcePasswordChange: force}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateAdminLastLoginAt(id int64, loginAt int64) error {
|
func UpdateAdminLastLoginAt(id int64, loginAt int64) error {
|
||||||
@@ -100,4 +116,3 @@ func ChangeAdminPassword(c *gin.Context, oldPassword string, newPassword string)
|
|||||||
func GetAdminForTokenValidation(id int64) (entity.AdminUser, error) {
|
func GetAdminForTokenValidation(id int64) (entity.AdminUser, error) {
|
||||||
return dao.GetAdminUser("id = ?", id)
|
return dao.GetAdminUser("id = ?", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,4 +102,3 @@ func DashboardSecurityRisks(summary vo.DashboardSummaryVo) []vo.SecurityRiskVo {
|
|||||||
}
|
}
|
||||||
return risks
|
return risks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"hy2xs-admin/dao"
|
"hy2xs-admin/dao"
|
||||||
"hy2xs-admin/model/constant"
|
"hy2xs-admin/model/constant"
|
||||||
"hy2xs-admin/proxy"
|
"hy2xs-admin/proxy"
|
||||||
@@ -51,7 +52,8 @@ func Hysteria2Auth(conPass string) (int64, string, error) {
|
|||||||
// Ограничение количества устройств
|
// Ограничение количества устройств
|
||||||
onlineUsers, err := Hysteria2Online()
|
onlineUsers, err := Hysteria2Online()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", err
|
logrus.WithError(err).Warn("hysteria2 online users unavailable; skip device-limit check")
|
||||||
|
return *peer.Id, *peer.AuthId, nil
|
||||||
}
|
}
|
||||||
device, exist := onlineUsers[*peer.AuthId]
|
device, exist := onlineUsers[*peer.AuthId]
|
||||||
if exist && *peer.MaxDevices <= device {
|
if exist && *peer.MaxDevices <= device {
|
||||||
|
|||||||
@@ -71,4 +71,3 @@ func TestBuildHysteria2Url_MinimalConfig(t *testing.T) {
|
|||||||
t.Fatalf("unexpected optional query params in minimal config: %s", parsed.RawQuery)
|
t.Fatalf("unexpected optional query params in minimal config: %s", parsed.RawQuery)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const hysteriaVersionCacheTTL = 15 * time.Minute
|
||||||
|
|
||||||
type metricsSnapshot struct {
|
type metricsSnapshot struct {
|
||||||
CollectedAt int64
|
CollectedAt int64
|
||||||
System vo.DashboardSystemVo
|
System vo.DashboardSystemVo
|
||||||
@@ -23,12 +25,41 @@ var metricsStore = struct {
|
|||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
snapshot metricsSnapshot
|
snapshot metricsSnapshot
|
||||||
lastSuccessAt int64
|
lastSuccessAt int64
|
||||||
|
version string
|
||||||
|
versionAt time.Time
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
|
func getCachedHysteriaVersion(now time.Time) string {
|
||||||
|
metricsStore.RLock()
|
||||||
|
if metricsStore.version != "" && now.Sub(metricsStore.versionAt) < hysteriaVersionCacheTTL {
|
||||||
|
v := metricsStore.version
|
||||||
|
metricsStore.RUnlock()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
metricsStore.RUnlock()
|
||||||
|
|
||||||
|
content, err := util.Exec(util.GetHysteria2BinPath() + " version")
|
||||||
|
if err != nil {
|
||||||
|
metricsStore.RLock()
|
||||||
|
v := metricsStore.version
|
||||||
|
metricsStore.RUnlock()
|
||||||
|
if v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
metricsStore.Lock()
|
||||||
|
metricsStore.version = content
|
||||||
|
metricsStore.versionAt = now
|
||||||
|
metricsStore.Unlock()
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
func CollectMetricsSnapshot() {
|
func CollectMetricsSnapshot() {
|
||||||
nowMs := time.Now().UnixMilli()
|
nowMs := time.Now().UnixMilli()
|
||||||
s := metricsSnapshot{
|
s := metricsSnapshot{
|
||||||
CollectedAt: nowMs,
|
CollectedAt: nowMs,
|
||||||
CollectorState: vo.DataHealthVo{Status: "ok", LastSuccessAt: nowMs},
|
CollectorState: vo.DataHealthVo{Status: "ok", LastSuccessAt: nowMs},
|
||||||
HysteriaState: vo.DataHealthVo{Status: "ok", LastSuccessAt: nowMs},
|
HysteriaState: vo.DataHealthVo{Status: "ok", LastSuccessAt: nowMs},
|
||||||
}
|
}
|
||||||
@@ -52,10 +83,7 @@ func CollectMetricsSnapshot() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.Hysteria.Running = Hysteria2IsRunning()
|
s.Hysteria.Running = Hysteria2IsRunning()
|
||||||
s.Hysteria.Version = "-"
|
s.Hysteria.Version = getCachedHysteriaVersion(time.Now())
|
||||||
if content, err := util.Exec(util.GetHysteria2BinPath() + " version"); err == nil {
|
|
||||||
s.Hysteria.Version = content
|
|
||||||
}
|
|
||||||
|
|
||||||
if onlineMap, err := Hysteria2Online(); err == nil {
|
if onlineMap, err := Hysteria2Online(); err == nil {
|
||||||
s.Hysteria.ApiReachable = true
|
s.Hysteria.ApiReachable = true
|
||||||
@@ -80,6 +108,9 @@ func CollectMetricsSnapshot() {
|
|||||||
running = 1
|
running = 1
|
||||||
}
|
}
|
||||||
diskPath := "/"
|
diskPath := "/"
|
||||||
|
if configured := util.GetEnvDiskPath(); configured != "" {
|
||||||
|
diskPath = configured
|
||||||
|
}
|
||||||
metric := entity.MetricSample{
|
metric := entity.MetricSample{
|
||||||
SampledAt: &nowMs,
|
SampledAt: &nowMs,
|
||||||
CpuPercent: &s.System.CpuPercent,
|
CpuPercent: &s.System.CpuPercent,
|
||||||
@@ -108,4 +139,3 @@ func DashboardSnapshot() metricsSnapshot {
|
|||||||
defer metricsStore.RUnlock()
|
defer metricsStore.RUnlock()
|
||||||
return metricsStore.snapshot
|
return metricsStore.snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-7
@@ -24,7 +24,7 @@ func PagePeer(peerPageDto dto.PeerPageDto) ([]vo.PeerVo, int64, error) {
|
|||||||
result := make([]vo.PeerVo, 0, len(peers))
|
result := make([]vo.PeerVo, 0, len(peers))
|
||||||
for _, p := range peers {
|
for _, p := range peers {
|
||||||
item := vo.PeerVo{
|
item := vo.PeerVo{
|
||||||
BaseVo: vo.BaseVo{Id: *p.Id, CreateTime: *p.CreateTime},
|
BaseVo: vo.BaseVo{Id: *p.Id, CreateTime: *p.CreateTime},
|
||||||
Name: strVal(p.Name),
|
Name: strVal(p.Name),
|
||||||
Remark: strVal(p.Remark),
|
Remark: strVal(p.Remark),
|
||||||
AuthId: strVal(p.AuthId),
|
AuthId: strVal(p.AuthId),
|
||||||
@@ -137,7 +137,7 @@ func GetPeerVo(id int64) (vo.PeerVo, error) {
|
|||||||
return vo.PeerVo{}, err
|
return vo.PeerVo{}, err
|
||||||
}
|
}
|
||||||
return vo.PeerVo{
|
return vo.PeerVo{
|
||||||
BaseVo: vo.BaseVo{Id: *p.Id, CreateTime: *p.CreateTime},
|
BaseVo: vo.BaseVo{Id: *p.Id, CreateTime: *p.CreateTime},
|
||||||
Name: strVal(p.Name),
|
Name: strVal(p.Name),
|
||||||
Remark: strVal(p.Remark),
|
Remark: strVal(p.Remark),
|
||||||
AuthId: strVal(p.AuthId),
|
AuthId: strVal(p.AuthId),
|
||||||
@@ -152,8 +152,12 @@ func GetPeerVo(id int64) (vo.PeerVo, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResetPeerTraffic(id int64) error { return dao.UpdatePeer([]int64{id}, map[string]interface{}{"download_bytes": 0, "upload_bytes": 0}) }
|
func ResetPeerTraffic(id int64) error {
|
||||||
func ReleaseKickPeer(id int64) error { return dao.UpdatePeer([]int64{id}, map[string]interface{}{"banned_until": 0}) }
|
return dao.UpdatePeer([]int64{id}, map[string]interface{}{"download_bytes": 0, "upload_bytes": 0})
|
||||||
|
}
|
||||||
|
func ReleaseKickPeer(id int64) error {
|
||||||
|
return dao.UpdatePeer([]int64{id}, map[string]interface{}{"banned_until": 0})
|
||||||
|
}
|
||||||
|
|
||||||
func KickPeer(id int64, bannedUntil int64) error {
|
func KickPeer(id int64, bannedUntil int64) error {
|
||||||
if err := dao.UpdatePeer([]int64{id}, map[string]interface{}{"banned_until": bannedUntil}); err != nil {
|
if err := dao.UpdatePeer([]int64{id}, map[string]interface{}{"banned_until": bannedUntil}); err != nil {
|
||||||
@@ -324,6 +328,15 @@ func UpdatePeerLastConnectionAt(id int64, conAt int64) error {
|
|||||||
return dao.UpdatePeer([]int64{id}, map[string]interface{}{"last_connection_at": conAt})
|
return dao.UpdatePeer([]int64{id}, map[string]interface{}{"last_connection_at": conAt})
|
||||||
}
|
}
|
||||||
|
|
||||||
func strVal(v *string) string { if v == nil { return "" }; return *v }
|
func strVal(v *string) string {
|
||||||
func int64Val(v *int64) int64 { if v == nil { return 0 }; return *v }
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *v
|
||||||
|
}
|
||||||
|
func int64Val(v *int64) int64 {
|
||||||
|
if v == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return *v
|
||||||
|
}
|
||||||
|
|||||||
@@ -82,4 +82,3 @@ func DecryptPeerSecret(stored string) (string, error) {
|
|||||||
}
|
}
|
||||||
return util.DecryptAESGCM(stored, key)
|
return util.DecryptAESGCM(stored, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ func HmacSHA256Hex(payload string, secret string) string {
|
|||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func DecodeBase64Key(raw string, expectedLen int) ([]byte, error) {
|
func DecodeBase64Key(raw string, expectedLen int) ([]byte, error) {
|
||||||
raw = strings.TrimSpace(raw)
|
raw = strings.TrimSpace(raw)
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
@@ -122,4 +121,3 @@ func DecryptAESGCM(cipherText string, key []byte) (string, error) {
|
|||||||
}
|
}
|
||||||
return string(plain), nil
|
return string(plain), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-7
@@ -11,9 +11,21 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func resolveDiskPath() string {
|
||||||
|
if configured := strings.TrimSpace(os.Getenv("HY2XS_DISK_PATH")); configured != "" {
|
||||||
|
return configured
|
||||||
|
}
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEnvDiskPath() string {
|
||||||
|
return strings.TrimSpace(os.Getenv("HY2XS_DISK_PATH"))
|
||||||
|
}
|
||||||
|
|
||||||
func Exec(cmd string) (string, error) {
|
func Exec(cmd string) (string, error) {
|
||||||
command := exec.Command("bash", "-c", cmd)
|
command := exec.Command("bash", "-c", cmd)
|
||||||
command.Env = os.Environ()
|
command.Env = os.Environ()
|
||||||
@@ -83,22 +95,23 @@ func GetMemInfo() (*mem.VirtualMemoryStat, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetDiskPercent() (float64, error) {
|
func GetDiskPercent() (float64, error) {
|
||||||
var err error
|
diskInfo, err := disk.Usage(resolveDiskPath())
|
||||||
parts, err := disk.Partitions(true)
|
if err != nil {
|
||||||
diskInfo, err := disk.Usage(parts[0].Mountpoint)
|
return 0, err
|
||||||
|
}
|
||||||
value, err := strconv.ParseFloat(fmt.Sprintf("%.1f", diskInfo.UsedPercent), 64)
|
value, err := strconv.ParseFloat(fmt.Sprintf("%.1f", diskInfo.UsedPercent), 64)
|
||||||
return value, err
|
return value, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDiskInfo() (*disk.UsageStat, error) {
|
func GetDiskInfo() (*disk.UsageStat, error) {
|
||||||
parts, err := disk.Partitions(true)
|
usage, err := disk.Usage(resolveDiskPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(parts) == 0 {
|
if usage == nil {
|
||||||
return nil, errors.New("disk partition not found")
|
return nil, errors.New("disk usage not found")
|
||||||
}
|
}
|
||||||
return disk.Usage(parts[0].Mountpoint)
|
return usage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func VerifyPort(port string) error {
|
func VerifyPort(port string) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user