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

This commit is contained in:
2026-05-08 23:16:25 +05:00
parent de4a65a959
commit 1bca73814e
85 changed files with 4119 additions and 1911 deletions
-181
View File
@@ -1,181 +0,0 @@
package service
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"hy2xs-admin/dao"
"hy2xs-admin/model/bo"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/dto"
"hy2xs-admin/model/entity"
"hy2xs-admin/model/vo"
"hy2xs-admin/util"
)
func Login(username string, plainPassword string) (string, bool, error) {
account, err := dao.GetAccount("username = ? and role = 'admin' and deleted = 0", username)
if err != nil {
return "", false, err
}
verified, legacy := util.VerifyPassword(plainPassword, *account.Pass)
if !verified {
return "", false, errors.New(constant.WrongPassword)
}
if legacy {
hash, hashErr := util.HashPassword(plainPassword)
if hashErr == nil {
_ = dao.UpdateAccount([]int64{*account.Id}, map[string]interface{}{"pass": hash})
}
}
accountBo := bo.AccountBo{
Id: *account.Id,
Username: *account.Username,
Roles: []string{*account.Role},
Deleted: *account.Deleted,
}
token, tokenErr := GenToken(accountBo)
if tokenErr != nil {
return "", false, tokenErr
}
requirePasswordChange := legacy
if account.ForcePasswordChange != nil {
requirePasswordChange = *account.ForcePasswordChange != 0
}
return token, requirePasswordChange, nil
}
func PageAccount(accountPageDto dto.AccountPageDto) ([]entity.Account, int64, error) {
return dao.PageAccount(accountPageDto)
}
func SaveAccount(account entity.Account) error {
_, err := dao.SaveAccount(account)
return err
}
func DeleteAccount(ids []int64) error {
return dao.DeleteAccount(ids)
}
func UpdateAccount(account entity.Account) error {
updates := map[string]interface{}{}
if account.Username != nil && *account.Username != "" {
updates["username"] = *account.Username
}
if account.Pass != nil && *account.Pass != "" {
updates["pass"] = *account.Pass
}
if account.ConPass != nil && *account.ConPass != "" {
updates["con_pass"] = fmt.Sprintf("%s.%s", *account.Username, *account.ConPass)
}
if account.Quota != nil {
updates["quota"] = *account.Quota
}
if account.ExpireTime != nil {
updates["expire_time"] = *account.ExpireTime
}
if account.Download != nil {
updates["download"] = *account.Download
}
if account.Upload != nil {
updates["upload"] = *account.Upload
}
if account.DeviceNo != nil {
updates["device_no"] = *account.DeviceNo
}
if account.Deleted != nil {
updates["deleted"] = *account.Deleted
}
if account.LoginAt != nil && *account.LoginAt > 0 {
updates["login_at"] = *account.LoginAt
}
if account.ConAt != nil && *account.ConAt > 0 {
updates["con_at"] = *account.ConAt
}
if account.Remark != nil {
updates["remark"] = *account.Remark
}
return dao.UpdateAccount([]int64{*account.Id}, updates)
}
func ResetTraffic(id int64) error {
return dao.UpdateAccount([]int64{id}, map[string]interface{}{"download": 0, "upload": 0})
}
func ExistAccountUsername(username string, id int64) bool {
var err error
if id != 0 {
_, err = dao.GetAccount("username = ? and id != ?", username, id)
} else {
_, err = dao.GetAccount("username = ?", username)
}
if err != nil {
if err.Error() == constant.WrongPassword {
return false
}
}
return true
}
func GetAccount(id int64) (entity.Account, error) {
return dao.GetAccount("id = ?", id)
}
func ListExportAccount() ([]bo.AccountExport, error) {
accounts, err := dao.ListAccount(nil, nil)
if err != nil {
return nil, errors.New(constant.SysError)
}
var accountExports []bo.AccountExport
for _, item := range accounts {
accountExport := bo.AccountExport{
Id: *item.Id,
Username: *item.Username,
Pass: *item.Pass,
ConPass: *item.ConPass,
Quota: *item.Quota,
Download: *item.Download,
Upload: *item.Upload,
ExpireTime: *item.ExpireTime,
DeviceNo: *item.DeviceNo,
KickUtilTime: *item.KickUtilTime,
Role: *item.Role,
Deleted: *item.Deleted,
CreateTime: *item.CreateTime,
UpdateTime: *item.UpdateTime,
LoginAt: *item.LoginAt,
ConAt: *item.ConAt,
Remark: *item.Remark,
}
accountExports = append(accountExports, accountExport)
}
return accountExports, nil
}
func ReleaseKickAccount(id int64) error {
return dao.UpdateAccount([]int64{id}, map[string]interface{}{"kick_util_time": 0})
}
func UpsertAccount(accounts []entity.Account) error {
return dao.UpsertAccount(accounts)
}
func GetAccountInfo(c *gin.Context) (vo.AccountInfoVo, error) {
myClaims, err := ParseToken(GetToken(c))
if err != nil {
return vo.AccountInfoVo{}, err
}
if myClaims.AccountBo.Deleted != 0 {
return vo.AccountInfoVo{}, errors.New("this account has been disabled")
}
return vo.AccountInfoVo{
Id: myClaims.AccountBo.Id,
Username: myClaims.AccountBo.Username,
Roles: myClaims.AccountBo.Roles,
}, nil
}
+61 -34
View File
@@ -2,12 +2,12 @@ package service
import (
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"hy2xs-admin/dao"
"hy2xs-admin/model/bo"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/entity"
"hy2xs-admin/proxy"
"hy2xs-admin/util"
"strconv"
"sync"
"time"
)
@@ -40,17 +40,17 @@ func CronHandleAccount() {
}
func CronResetTraffic() {
accounts, err := dao.ListAccount(nil, nil)
peers, err := dao.ListPeer("1=1")
if err != nil {
return
}
var ids []int64
for _, item := range accounts {
for _, item := range peers {
ids = append(ids, *item.Id)
}
idsList := util.SplitArr(ids, 100)
for _, item := range idsList {
if err := dao.UpdateAccount(item, map[string]interface{}{"download": 0, "upload": 0}); err != nil {
if err := dao.UpdatePeer(item, map[string]interface{}{"download_bytes": 0, "upload_bytes": 0}); err != nil {
continue
}
}
@@ -62,38 +62,65 @@ func saveAccountTraffic(apiPort int64, trafficStatsSecret string) {
}
defer trafficMutex.Unlock()
hysteria2TrafficTime, err := dao.GetConfig("key = ?", constant.Hysteria2TrafficTime)
if err != nil {
return
}
hysteria2TrafficTimeFloat, err := strconv.ParseFloat(*hysteria2TrafficTime.Value, 64)
if err != nil {
logrus.Errorf("hysteria2TrafficTime string conv int64 err: %v", err)
return
}
users, err := proxy.NewHysteria2Api(apiPort).ListUsers(true, trafficStatsSecret)
if err != nil {
return
}
if len(users) > 0 {
userLists := util.SplitMap(users, 10)
var wg sync.WaitGroup
for _, userList := range userLists {
wg.Add(1)
go func(userList map[string]bo.Hysteria2UserTraffic) {
defer wg.Done()
for username, traffic := range userList {
if err = dao.UpdateAccountTraffic(username, int64(float64(traffic.Rx)*hysteria2TrafficTimeFloat), int64(float64(traffic.Tx)*hysteria2TrafficTimeFloat)); err != nil {
continue
}
}
}(userList)
if len(users) == 0 {
return
}
nowMs := time.Now().UnixMilli()
hourStart := nowMs - (nowMs % int64(time.Hour/time.Millisecond))
dayStart := nowMs - (nowMs % int64(24*time.Hour/time.Millisecond))
for key, traffic := range users {
rxBytes := traffic.Rx
txBytes := traffic.Tx
if rxBytes == 0 && txBytes == 0 {
continue
}
wg.Wait()
peer, peerErr := dao.GetPeer("auth_id = ?", key)
if peerErr != nil {
peer, peerErr = dao.GetPeer("name = ?", key)
if peerErr != nil {
continue
}
}
if peer.Id == nil {
continue
}
authId := key
if peer.AuthId != nil && *peer.AuthId != "" {
authId = *peer.AuthId
}
sample := entity.TrafficSample{
PeerId: peer.Id,
AuthId: &authId,
RxBytes: &rxBytes,
TxBytes: &txBytes,
SampledAt: &nowMs,
}
if err = dao.SaveTrafficSample(sample); err != nil {
logrus.Errorf("save traffic_sample failed: %v", err)
continue
}
if err = dao.UpdatePeer([]int64{*peer.Id}, map[string]interface{}{
"download_bytes": gorm.Expr("download_bytes + ?", rxBytes),
"upload_bytes": gorm.Expr("upload_bytes + ?", txBytes),
}); err != nil {
logrus.Errorf("update peer traffic failed: %v", err)
continue
}
_ = dao.UpsertTrafficAggregateHourly(*peer.Id, hourStart, rxBytes, txBytes)
_ = dao.UpsertTrafficAggregateDaily(*peer.Id, dayStart, rxBytes, txBytes)
}
}
func kickAccount(apiPort int64, trafficStatsSecret string) {
if !kickMutex.TryLock() {
return
@@ -118,14 +145,14 @@ func kickAccount(apiPort int64, trafficStatsSecret string) {
go func(usernameList []string) {
defer wg.Done()
now := time.Now().UnixMilli()
accounts, err := dao.ListAccount("username in ? and (deleted = 1 or (quota > 0 and quota < download + upload)) or ? > expire_time or ? < kick_util_time", usernameList, now, now)
peers, err := dao.ListPeer("name in ? and (disabled = 1 or (quota_bytes > 0 and quota_bytes < download_bytes + upload_bytes) or ? > expires_at or ? < banned_until)", usernameList, now, now)
if err != nil {
return
}
kickUsernames := make([]string, len(accounts))
kickUsernames := make([]string, len(peers))
j := 0
for _, item := range accounts {
kickUsernames[j] = *item.Username
for _, item := range peers {
kickUsernames[j] = *item.Name
j++
}
if err = proxy.NewHysteria2Api(apiPort).KickUsers(kickUsernames, trafficStatsSecret); err != nil {
+105
View File
@@ -0,0 +1,105 @@
package service
import (
"hy2xs-admin/dao"
"hy2xs-admin/model/vo"
"strings"
"time"
)
func DashboardSummary() (vo.DashboardSummaryVo, error) {
nowMs := time.Now().UnixMilli()
summary := vo.DashboardSummaryVo{CollectedAt: nowMs}
summary.Health.Collector = vo.DataHealthVo{Status: "stale", MessageKey: "dashboard.error.collectorStale"}
summary.Health.Hysteria = vo.DataHealthVo{Status: "ok"}
snapshot := DashboardSnapshot()
if snapshot.CollectedAt > 0 {
summary.CollectedAt = snapshot.CollectedAt
summary.System = snapshot.System
summary.Hysteria = snapshot.Hysteria
summary.Peers.OnlinePeers = snapshot.OnlinePeers
summary.Peers.OnlineDevices = snapshot.OnlineDevices
summary.Health.Collector = snapshot.CollectorState
summary.Health.Hysteria = snapshot.HysteriaState
if nowMs-snapshot.CollectedAt > int64(90*time.Second/time.Millisecond) {
summary.Health.Collector = vo.DataHealthVo{Status: "stale", MessageKey: "dashboard.error.collectorStale", LastSuccessAt: snapshot.CollectedAt}
}
}
if peerSummary, err := dao.DashboardPeerSummary(nowMs); err == nil {
summary.Peers.Total = peerSummary.Total
summary.Peers.Enabled = peerSummary.Enabled
summary.Peers.Disabled = peerSummary.Disabled
summary.Peers.Expired = peerSummary.Expired
}
if trafficSummary, err := dao.DashboardTrafficSummary(); err == nil {
summary.Traffic = trafficSummary
}
summary.SecurityRisks = DashboardSecurityRisks(summary)
return summary, nil
}
func DashboardTimeseries(rangeKey string) (vo.DashboardTimeseriesVo, error) {
nowMs := time.Now().UnixMilli()
fromMs := nowMs - int64(24*time.Hour/time.Millisecond)
r := strings.TrimSpace(rangeKey)
if r == "1h" {
fromMs = nowMs - int64(time.Hour/time.Millisecond)
} else if r == "7d" {
fromMs = nowMs - int64(7*24*time.Hour/time.Millisecond)
} else if r == "30d" {
fromMs = nowMs - int64(30*24*time.Hour/time.Millisecond)
r = "30d"
} else if r == "" {
r = "24h"
}
traffic, err := dao.DashboardTrafficTimeseries(fromMs, nowMs)
if err != nil {
return vo.DashboardTimeseriesVo{}, err
}
systemRows, err := dao.DashboardSystemTimeseries(fromMs, nowMs)
if err != nil {
return vo.DashboardTimeseriesVo{}, err
}
return vo.DashboardTimeseriesVo{
Range: r,
Traffic: traffic,
System: systemRows,
CollectedAt: nowMs,
}, nil
}
func DashboardTopPeers(rangeKey string, limit int) ([]vo.DashboardTopPeerVo, error) {
nowMs := time.Now().UnixMilli()
fromMs := nowMs - int64(24*time.Hour/time.Millisecond)
if strings.TrimSpace(rangeKey) == "7d" {
fromMs = nowMs - int64(7*24*time.Hour/time.Millisecond)
} else if strings.TrimSpace(rangeKey) == "30d" {
fromMs = nowMs - int64(30*24*time.Hour/time.Millisecond)
}
return dao.DashboardTopPeers(fromMs, nowMs, limit)
}
func DashboardSecurity() ([]vo.SecurityRiskVo, error) {
summary, err := DashboardSummary()
if err != nil {
return []vo.SecurityRiskVo{}, err
}
return summary.SecurityRisks, nil
}
func DashboardSecurityRisks(summary vo.DashboardSummaryVo) []vo.SecurityRiskVo {
risks := make([]vo.SecurityRiskVo, 0)
if !summary.Hysteria.Running {
risks = append(risks, vo.SecurityRiskVo{Key: "dashboard.security.hysteriaStopped", Severity: "critical", Dismissible: false})
}
if !summary.Hysteria.ApiReachable {
risks = append(risks, vo.SecurityRiskVo{Key: "dashboard.security.trafficApiUnavailable", Severity: "warning", Dismissible: false})
}
return risks
}
+12 -147
View File
@@ -2,12 +2,10 @@ package service
import (
"errors"
"fmt"
"gopkg.in/yaml.v3"
"hy2xs-admin/dao"
"hy2xs-admin/model/bo"
"hy2xs-admin/model/constant"
"hy2xs-admin/proxy"
"hy2xs-admin/util"
"net"
"net/url"
"os"
@@ -38,7 +36,8 @@ func Hysteria2Auth(conPass string) (int64, string, error) {
}
now := time.Now().UnixMilli()
account, err := dao.GetAccount("con_pass = ? and deleted = 0 and (quota < 0 or quota > download + upload) and ? < expire_time and ? > kick_util_time", conPass, now, now)
secretDigest := util.PeerSecretDigest(conPass)
peer, err := dao.GetPeer("secret_digest = ? and disabled = 0 and (quota_bytes < 0 or quota_bytes > download_bytes + upload_bytes) and ? < expires_at and ? > banned_until", secretDigest, now, now)
if err != nil {
return 0, "", err
}
@@ -48,12 +47,12 @@ func Hysteria2Auth(conPass string) (int64, string, error) {
if err != nil {
return 0, "", err
}
device, exist := onlineUsers[*account.Username]
if exist && *account.DeviceNo <= device {
device, exist := onlineUsers[*peer.Name]
if exist && *peer.MaxDevices <= device {
return 0, "", errors.New("device limited")
}
return *account.Id, *account.Username, nil
return *peer.Id, *peer.Name, nil
}
func Hysteria2Online() (map[string]int64, error) {
@@ -79,17 +78,17 @@ func Hysteria2Kick(ids []int64, kickUtilTime int64) error {
if !Hysteria2IsRunning() {
return errors.New("hysteria2 is not running")
}
if err := dao.UpdateAccount(ids, map[string]interface{}{"kick_util_time": kickUtilTime}); err != nil {
if err := dao.UpdatePeer(ids, map[string]interface{}{"banned_until": kickUtilTime}); err != nil {
return err
}
accounts, err := dao.ListAccount("id in ?", ids)
peers, err := dao.ListPeer("id in ?", ids)
if err != nil {
return err
}
var keys []string
for _, item := range accounts {
keys = append(keys, *item.Username)
for _, item := range peers {
keys = append(keys, *item.Name)
}
apiPort, err := GetHysteria2ApiPort()
if err != nil {
@@ -105,140 +104,6 @@ func Hysteria2Kick(ids []int64, kickUtilTime int64) error {
return nil
}
func Hysteria2SubscribeUrl(accountId int64, protocol string) (string, error) {
account, err := dao.GetAccount("id = ?", accountId)
if err != nil {
return "", err
}
publicHost, publicPort, err := resolvePublicEndpoint()
if err != nil {
return "", err
}
config, err := dao.GetConfig("key = ?", constant.HUIWebContext)
if err != nil {
return "", err
}
webContext := ""
if config.Value != nil && *config.Value != "/" && strings.HasPrefix(*config.Value, "/") {
webContext = *config.Value
}
return fmt.Sprintf("%s//%s:%d%s/hui/%s", protocol, publicHost, publicPort, webContext, url.QueryEscape(*account.ConPass)), nil
}
func Hysteria2Subscribe(conPass string, clientType string) (string, string, error) {
hysteria2Config, err := GetHysteria2Config()
if err != nil {
return "", "", err
}
if hysteria2Config.Listen == nil || *hysteria2Config.Listen == "" {
return "", "", errors.New("hysteria2 config is empty")
}
account, err := dao.GetAccount("con_pass = ?", conPass)
if err != nil {
return "", "", err
}
publicHost, publicPort, err := resolvePublicEndpoint()
if err != nil {
return "", "", err
}
hysteria2Name := "hysteria2"
hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark)
if err != nil {
return "", "", err
}
if *hysteria2ConfigRemark.Value != "" {
hysteria2Name = *hysteria2ConfigRemark.Value
}
userInfo := ""
configStr := ""
if clientType == constant.Shadowrocket || clientType == constant.Clash {
userInfo = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d",
*account.Upload,
*account.Download,
*account.Quota,
*account.ExpireTime/1000)
hysteria2 := bo.Hysteria2{
Name: hysteria2Name,
Type: "hysteria2",
Server: publicHost,
Port: strconv.Itoa(publicPort),
Password: conPass,
}
if hysteria2Config.Bandwidth != nil {
if hysteria2Config.Bandwidth.Up != nil &&
*hysteria2Config.Bandwidth.Up != "" {
hysteria2.Up = *hysteria2Config.Bandwidth.Up
}
if hysteria2Config.Bandwidth.Down != nil &&
*hysteria2Config.Bandwidth.Down != "" {
hysteria2.Down = *hysteria2Config.Bandwidth.Down
}
}
if hysteria2Config.Obfs != nil &&
hysteria2Config.Obfs.Type != nil &&
*hysteria2Config.Obfs.Type == "salamander" &&
hysteria2Config.Obfs.Salamander != nil &&
hysteria2Config.Obfs.Salamander.Password != nil &&
*hysteria2Config.Obfs.Salamander.Password != "" {
if clientType == constant.Shadowrocket {
hysteria2.Obfs = *hysteria2Config.Obfs.Salamander.Password
} else {
hysteria2.Obfs = "salamander"
hysteria2.ObfsPassword = *hysteria2Config.Obfs.Salamander.Password
}
}
if hysteria2Config.ACME != nil &&
hysteria2Config.ACME.Domains != nil &&
len(hysteria2Config.ACME.Domains) > 0 {
hysteria2.Sni = hysteria2Config.ACME.Domains[0]
}
hysteria2.SkipCertVerify = false
proxyGroup := bo.ProxyGroup{
Name: "PROXY",
Type: "select",
Proxies: []string{hysteria2Name},
}
clashConfig := bo.ClashConfig{
ProxyGroups: []bo.ProxyGroup{
proxyGroup,
},
Proxies: []interface{}{hysteria2},
}
clashConfigYaml, err := yaml.Marshal(&clashConfig)
if err != nil {
return "", "", err
}
configStr = string(clashConfigYaml)
if clientType == constant.Clash {
clashExtension, err := GetConfig(constant.ClashExtension)
if err != nil {
return "", "", err
}
if clashExtension.Value != nil && *clashExtension.Value != "" {
configStr = fmt.Sprintf("%s%s", configStr, *clashExtension.Value)
}
}
} else if clientType == constant.V2rayN {
hysteria2Url, err := Hysteria2Url(*account.Id)
if err != nil {
return "", "", err
}
configStr = hysteria2Url
}
return userInfo, configStr, nil
}
func Hysteria2Url(accountId int64) (string, error) {
hysteria2Config, err := GetHysteria2Config()
if err != nil {
@@ -252,7 +117,7 @@ func Hysteria2Url(accountId int64) (string, error) {
return "", err
}
account, err := dao.GetAccount("id = ?", accountId)
peer, err := dao.GetPeer("id = ?", accountId)
if err != nil {
return "", err
}
@@ -281,7 +146,7 @@ func Hysteria2Url(accountId int64) (string, error) {
sni = hysteria2Config.ACME.Domains[0]
}
return buildHysteria2Url(*account.ConPass, hostname, port, obfsType, obfsPassword, sni, remark), nil
return buildHysteria2Url(*peer.SecretCiphertext, hostname, port, obfsType, obfsPassword, sni, remark), nil
}
func buildHysteria2Url(conPass string, hostname string, port int, obfsType string, obfsPassword string, sni string, remark string) string {
+2 -2
View File
@@ -14,13 +14,13 @@ import (
const TokenExpireDuration = time.Hour * 24
type MyClaims struct {
AccountBo bo.AccountBo `json:"account"`
Admin bo.AccountBo `json:"admin"`
jwt.StandardClaims
}
func GenToken(accountBo bo.AccountBo) (string, error) {
c := MyClaims{
AccountBo: accountBo,
Admin: accountBo,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
Issuer: "hy2xs-admin",
+111
View File
@@ -0,0 +1,111 @@
package service
import (
"hy2xs-admin/dao"
"hy2xs-admin/model/entity"
"hy2xs-admin/model/vo"
"hy2xs-admin/util"
"sync"
"time"
)
type metricsSnapshot struct {
CollectedAt int64
System vo.DashboardSystemVo
Hysteria vo.DashboardHysteriaVo
OnlinePeers int64
OnlineDevices int64
CollectorState vo.DataHealthVo
HysteriaState vo.DataHealthVo
}
var metricsStore = struct {
sync.RWMutex
snapshot metricsSnapshot
lastSuccessAt int64
}{}
func CollectMetricsSnapshot() {
nowMs := time.Now().UnixMilli()
s := metricsSnapshot{
CollectedAt: nowMs,
CollectorState: vo.DataHealthVo{Status: "ok", LastSuccessAt: nowMs},
HysteriaState: vo.DataHealthVo{Status: "ok", LastSuccessAt: nowMs},
}
if cpuPercent, err := util.GetCpuPercent(); err == nil {
s.System.CpuPercent = cpuPercent
}
if memPercent, err := util.GetMemPercent(); err == nil {
s.System.MemPercent = memPercent
}
if memInfo, err := util.GetMemInfo(); err == nil {
s.System.MemUsedBytes = memInfo.Used
s.System.MemTotalBytes = memInfo.Total
}
if diskPercent, err := util.GetDiskPercent(); err == nil {
s.System.DiskPercent = diskPercent
}
if diskInfo, err := util.GetDiskInfo(); err == nil {
s.System.DiskUsedBytes = diskInfo.Used
s.System.DiskTotalBytes = diskInfo.Total
}
s.Hysteria.Running = Hysteria2IsRunning()
s.Hysteria.Version = "-"
if content, err := util.Exec(util.GetHysteria2BinPath() + " version"); err == nil {
s.Hysteria.Version = content
}
if onlineMap, err := Hysteria2Online(); err == nil {
s.Hysteria.ApiReachable = true
s.OnlinePeers = int64(len(onlineMap))
for _, c := range onlineMap {
s.OnlineDevices += c
}
} else {
s.Hysteria.ApiReachable = false
s.Hysteria.LastApiError = "dashboard.error.trafficApiUnavailable"
s.HysteriaState.Status = "error"
s.HysteriaState.MessageKey = "dashboard.error.trafficApiUnavailable"
}
metricsStore.Lock()
metricsStore.snapshot = s
metricsStore.lastSuccessAt = nowMs
metricsStore.Unlock()
running := int64(0)
if s.Hysteria.Running {
running = 1
}
diskPath := "/"
metric := entity.MetricSample{
SampledAt: &nowMs,
CpuPercent: &s.System.CpuPercent,
Load1: func() *float64 { v := float64(0); return &v }(),
MemUsedBytes: func() *int64 { v := int64(s.System.MemUsedBytes); return &v }(),
MemTotalBytes: func() *int64 { v := int64(s.System.MemTotalBytes); return &v }(),
MemPercent: &s.System.MemPercent,
DiskPath: &diskPath,
DiskUsedBytes: func() *int64 { v := int64(s.System.DiskUsedBytes); return &v }(),
DiskTotalBytes: func() *int64 { v := int64(s.System.DiskTotalBytes); return &v }(),
DiskPercent: &s.System.DiskPercent,
HysteriaRunning: &running,
OnlinePeers: &s.OnlinePeers,
OnlineDevices: &s.OnlineDevices,
}
_ = dao.SaveMetricSample(metric)
}
func CleanupMetricsRetention() {
cutoff := time.Now().Add(-72 * time.Hour).UnixMilli()
_ = dao.CleanupMetricSample(cutoff)
}
func DashboardSnapshot() metricsSnapshot {
metricsStore.RLock()
defer metricsStore.RUnlock()
return metricsStore.snapshot
}
-64
View File
@@ -1,64 +0,0 @@
package service
import (
"errors"
"fmt"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/vo"
"hy2xs-admin/util"
"regexp"
"strings"
)
func MonitorSystem() (vo.SystemMonitorVo, error) {
cpuPercent, err := util.GetCpuPercent()
if err != nil {
return vo.SystemMonitorVo{}, errors.New("cpu query failed")
}
memPercent, err := util.GetMemPercent()
if err != nil {
return vo.SystemMonitorVo{}, errors.New("mem query failed")
}
diskPercent, err := util.GetDiskPercent()
if err != nil {
return vo.SystemMonitorVo{}, errors.New("disk query failed")
}
return vo.SystemMonitorVo{
HUIVersion: constant.Version,
CpuPercent: cpuPercent,
MemPercent: memPercent,
DiskPercent: diskPercent,
}, nil
}
func MonitorHysteria2() (vo.Hysteria2MonitorVo, error) {
var hysteria2MonitorVo vo.Hysteria2MonitorVo
onlineUsers, err := Hysteria2Online()
if err != nil {
return hysteria2MonitorVo, err
}
if len(onlineUsers) > 0 {
hysteria2MonitorVo.UserTotal = int64(len(onlineUsers))
var deviceTotal int64 = 0
for _, value := range onlineUsers {
deviceTotal += value
}
hysteria2MonitorVo.DeviceTotal = deviceTotal
}
hysteria2MonitorVo.Version = "-"
content, err := util.Exec(fmt.Sprintf("%s version", util.GetHysteria2BinPath()))
if err == nil {
pattern := `v\d+\.\d+\.\d+`
re := regexp.MustCompile(pattern)
matches := re.FindAllString(strings.TrimSpace(content), -1)
if len(matches) > 0 {
hysteria2MonitorVo.Version = matches[0]
}
}
running := Hysteria2IsRunning()
hysteria2MonitorVo.Running = running
return hysteria2MonitorVo, nil
}
+321
View File
@@ -0,0 +1,321 @@
package service
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"hy2xs-admin/dao"
"hy2xs-admin/model/bo"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/dto"
"hy2xs-admin/model/entity"
"hy2xs-admin/model/vo"
"hy2xs-admin/util"
"time"
)
func Login(username string, plainPassword string) (string, bool, error) {
account, err := dao.GetAdminUser("username = ? and status = 1", username)
if err != nil {
return "", false, err
}
verified, legacy := util.VerifyPassword(plainPassword, *account.PasswordHash)
if !verified {
return "", false, errors.New(constant.WrongPassword)
}
if legacy {
hash, hashErr := util.HashPassword(plainPassword)
if hashErr == nil {
_ = dao.UpdateAdminUser([]int64{*account.Id}, map[string]interface{}{"password_hash": hash})
}
}
accountBo := bo.AccountBo{
Id: *account.Id,
Username: *account.Username,
Roles: []string{"admin"},
Deleted: 0,
}
token, tokenErr := GenToken(accountBo)
if tokenErr != nil {
return "", false, tokenErr
}
requirePasswordChange := legacy
if account.ForcePasswordChange != nil {
requirePasswordChange = *account.ForcePasswordChange != 0
}
return token, requirePasswordChange, nil
}
func PagePeer(peerPageDto dto.PeerPageDto) ([]entity.Account, int64, error) {
peers, total, err := dao.PagePeer(peerPageDto)
if err != nil {
return nil, 0, err
}
accounts := make([]entity.Account, 0, len(peers))
for _, p := range peers {
role := "user"
acc := entity.Account{
BaseEntity: p.BaseEntity,
Username: p.Name,
ConPass: p.SecretCiphertext,
Quota: p.QuotaBytes,
Download: p.DownloadBytes,
Upload: p.UploadBytes,
ExpireTime: p.ExpiresAt,
KickUtilTime: p.BannedUntil,
DeviceNo: p.MaxDevices,
Role: &role,
Deleted: p.Disabled,
ConAt: p.LastConnectionAt,
Remark: p.Remark,
}
accounts = append(accounts, acc)
}
return accounts, total, nil
}
func SavePeer(account entity.Account) error {
if account.Username == nil || *account.Username == "" {
return errors.New(constant.InvalidError)
}
secret := ""
if account.ConPass != nil && *account.ConPass != "" {
secret = *account.ConPass
} else {
generated, genErr := util.RandomString(24)
if genErr != nil {
return genErr
}
secret = fmt.Sprintf("%s.%s", *account.Username, generated)
}
authId, authErr := util.RandomString(18)
if authErr != nil {
return authErr
}
secretDigest := util.PeerSecretDigest(secret)
peer := entity.Peer{
Name: account.Username,
Remark: account.Remark,
AuthId: &authId,
SecretDigest: &secretDigest,
SecretCiphertext: &secret,
QuotaBytes: account.Quota,
ExpiresAt: account.ExpireTime,
MaxDevices: account.DeviceNo,
Disabled: account.Deleted,
}
_, err := dao.SavePeer(peer)
return err
}
func DeletePeer(ids []int64) error {
return dao.DeletePeer(ids)
}
func UpdatePeer(account entity.Account) error {
updates := map[string]interface{}{}
if account.Username != nil && *account.Username != "" {
updates["username"] = *account.Username
}
_ = account.Pass
if account.ConPass != nil && *account.ConPass != "" {
updates["secret_ciphertext"] = *account.ConPass
updates["secret_digest"] = util.PeerSecretDigest(*account.ConPass)
}
if account.Quota != nil {
updates["quota_bytes"] = *account.Quota
}
if account.ExpireTime != nil {
updates["expires_at"] = *account.ExpireTime
}
if account.Download != nil {
updates["download_bytes"] = *account.Download
}
if account.Upload != nil {
updates["upload_bytes"] = *account.Upload
}
if account.DeviceNo != nil {
updates["max_devices"] = *account.DeviceNo
}
if account.Deleted != nil {
updates["disabled"] = *account.Deleted
}
if account.LoginAt != nil && *account.LoginAt > 0 {
updates["login_at"] = *account.LoginAt
}
if account.ConAt != nil && *account.ConAt > 0 {
updates["last_connection_at"] = *account.ConAt
}
if account.Remark != nil {
updates["remark"] = *account.Remark
}
return dao.UpdatePeer([]int64{*account.Id}, updates)
}
func ResetTraffic(id int64) error {
return dao.UpdatePeer([]int64{id}, map[string]interface{}{"download_bytes": 0, "upload_bytes": 0})
}
func ExistPeerName(username string, id int64) bool {
var err error
if id != 0 {
_, err = dao.GetPeer("name = ? and id != ?", username, id)
} else {
_, err = dao.GetPeer("name = ?", username)
}
if err != nil {
if err.Error() == constant.WrongPassword {
return false
}
}
return true
}
func GetPeer(id int64) (entity.Account, error) {
peer, err := dao.GetPeer("id = ?", id)
if err != nil {
return entity.Account{}, err
}
role := "user"
return entity.Account{
BaseEntity: peer.BaseEntity,
Username: peer.Name,
ConPass: peer.SecretCiphertext,
Quota: peer.QuotaBytes,
Download: peer.DownloadBytes,
Upload: peer.UploadBytes,
ExpireTime: peer.ExpiresAt,
DeviceNo: peer.MaxDevices,
KickUtilTime: peer.BannedUntil,
ConAt: peer.LastConnectionAt,
Deleted: peer.Disabled,
Remark: peer.Remark,
Role: &role,
}, nil
}
func GetAdminAccount(id int64) (entity.AdminUser, error) {
return dao.GetAdminUser("id = ?", id)
}
func ListExportPeer() ([]bo.AccountExport, error) {
peers, err := dao.ListPeer("1=1")
if err != nil {
return nil, errors.New(constant.SysError)
}
var accountExports []bo.AccountExport
for _, item := range peers {
role := "user"
conPass := ""
if item.SecretCiphertext != nil {
conPass = *item.SecretCiphertext
}
accountExport := bo.AccountExport{
Id: *item.Id,
Username: *item.Name,
Pass: "",
ConPass: conPass,
Quota: *item.QuotaBytes,
Download: *item.DownloadBytes,
Upload: *item.UploadBytes,
ExpireTime: *item.ExpiresAt,
DeviceNo: *item.MaxDevices,
KickUtilTime: *item.BannedUntil,
Role: role,
Deleted: *item.Disabled,
CreateTime: *item.CreateTime,
UpdateTime: *item.UpdateTime,
LoginAt: 0,
ConAt: *item.LastConnectionAt,
Remark: *item.Remark,
}
accountExports = append(accountExports, accountExport)
}
return accountExports, nil
}
func ReleaseKickPeer(id int64) error {
return dao.UpdatePeer([]int64{id}, map[string]interface{}{"banned_until": 0})
}
func UpsertPeer(accounts []entity.Account) error {
for _, account := range accounts {
if account.Id != nil && *account.Id > 0 {
if err := UpdatePeer(account); err != nil {
return err
}
continue
}
if err := SavePeer(account); err != nil {
return err
}
}
return nil
}
func GetAdminInfo(c *gin.Context) (vo.AccountInfoVo, error) {
myClaims, err := ParseToken(GetToken(c))
if err != nil {
return vo.AccountInfoVo{}, err
}
if myClaims.Admin.Deleted != 0 {
return vo.AccountInfoVo{}, errors.New("this account has been disabled")
}
admin, err := dao.GetAdminUser("id = ?", myClaims.Admin.Id)
if err != nil {
return vo.AccountInfoVo{}, err
}
if admin.Status != nil && *admin.Status == 0 {
return vo.AccountInfoVo{}, errors.New("this account has been disabled")
}
return vo.AccountInfoVo{
Id: myClaims.Admin.Id,
Username: myClaims.Admin.Username,
Roles: myClaims.Admin.Roles,
}, nil
}
func UpdatePeerLastConnectionAt(id int64, conAt int64) error {
return dao.UpdatePeer([]int64{id}, map[string]interface{}{"last_connection_at": conAt})
}
func UpdateAdminLastLoginAt(id int64, loginAt int64) error {
return dao.UpdateAdminUser([]int64{id}, map[string]interface{}{"last_login_at": loginAt})
}
func ChangeAdminPassword(c *gin.Context, oldPassword string, newPassword string) error {
info, err := GetAdminInfo(c)
if err != nil {
return err
}
admin, err := dao.GetAdminUser("id = ?", info.Id)
if err != nil {
return err
}
if admin.PasswordHash == nil {
return errors.New(constant.SysError)
}
verified, _ := util.VerifyPassword(oldPassword, *admin.PasswordHash)
if !verified {
return errors.New(constant.WrongPassword)
}
hash, hashErr := util.HashPassword(newPassword)
if hashErr != nil {
return hashErr
}
nowMs := time.Now().UnixMilli()
currentTokenVersion := int64(1)
if admin.TokenVersion != nil && *admin.TokenVersion > 0 {
currentTokenVersion = *admin.TokenVersion
}
return dao.UpdateAdminUser([]int64{info.Id}, map[string]interface{}{
"password_hash": hash,
"force_password_change": 0,
"password_changed_at": nowMs,
"token_version": currentTokenVersion + 1,
})
}