Полный продакшен-рефактор fix25: split peer/admin, удаление legacy, шифрование secret, auth_id, новые API/роуты и зачистка subscription

This commit is contained in:
2026-05-09 00:27:45 +05:00
parent e60594e09e
commit d73bab99ec
34 changed files with 930 additions and 1673 deletions
+176 -245
View File
@@ -3,7 +3,8 @@ package service
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/skip2/go-qrcode"
"hy2xs-admin/dao"
"hy2xs-admin/model/bo"
"hy2xs-admin/model/constant"
@@ -11,311 +12,241 @@ import (
"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) {
func PagePeer(peerPageDto dto.PeerPageDto) ([]vo.PeerVo, int64, error) {
peers, total, err := dao.PagePeer(peerPageDto)
if err != nil {
return nil, 0, err
}
accounts := make([]entity.Account, 0, len(peers))
onlineUsers, _ := Hysteria2Online()
result := make([]vo.PeerVo, 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,
item := vo.PeerVo{
BaseVo: vo.BaseVo{Id: *p.Id, CreateTime: *p.CreateTime},
Name: strVal(p.Name),
Remark: strVal(p.Remark),
AuthId: strVal(p.AuthId),
QuotaBytes: int64Val(p.QuotaBytes),
DownloadBytes: int64Val(p.DownloadBytes),
UploadBytes: int64Val(p.UploadBytes),
ExpiresAt: int64Val(p.ExpiresAt),
MaxDevices: int64Val(p.MaxDevices),
Disabled: int64Val(p.Disabled),
BannedUntil: int64Val(p.BannedUntil),
LastConnectionAt: int64Val(p.LastConnectionAt),
}
accounts = append(accounts, acc)
if v, ok := onlineUsers[item.AuthId]; ok {
item.Online = true
item.OnlineDevices = v
}
result = append(result, item)
}
return accounts, total, nil
return result, total, nil
}
func SavePeer(account entity.Account) error {
if account.Username == nil || *account.Username == "" {
return errors.New(constant.InvalidError)
func CreatePeer(peerDto dto.PeerSaveDto) (vo.PeerVo, error) {
if peerDto.Name == nil || *peerDto.Name == "" {
return vo.PeerVo{}, errors.New(constant.InvalidError)
}
if ExistPeerName(*peerDto.Name, 0) {
return vo.PeerVo{}, errors.New(fmt.Sprintf("name %s already exists", *peerDto.Name))
}
secret := ""
if account.ConPass != nil && *account.ConPass != "" {
secret = *account.ConPass
if peerDto.Secret != nil && *peerDto.Secret != "" {
secret = *peerDto.Secret
} else {
generated, genErr := util.RandomString(24)
if genErr != nil {
return genErr
generated, err := util.RandomString(24)
if err != nil {
return vo.PeerVo{}, err
}
secret = fmt.Sprintf("%s.%s", *account.Username, generated)
secret = fmt.Sprintf("%s.%s", *peerDto.Name, generated)
}
authId, authErr := util.RandomString(18)
if authErr != nil {
return authErr
authId, err := util.RandomString(18)
if err != nil {
return vo.PeerVo{}, err
}
secretDigest, err := PeerSecretDigest(secret)
if err != nil {
return vo.PeerVo{}, err
}
secretEncrypted, err := EncryptPeerSecret(secret)
if err != nil {
return vo.PeerVo{}, err
}
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,
Name: peerDto.Name,
Remark: peerDto.Remark,
AuthId: &authId,
SecretDigest: &secretDigest,
SecretEncrypted: &secretEncrypted,
QuotaBytes: peerDto.QuotaBytes,
ExpiresAt: peerDto.ExpiresAt,
MaxDevices: peerDto.MaxDevices,
Disabled: peerDto.Disabled,
}
_, err := dao.SavePeer(peer)
return err
id, saveErr := dao.SavePeer(peer)
if saveErr != nil {
return vo.PeerVo{}, saveErr
}
return GetPeerVo(id)
}
func DeletePeer(ids []int64) error {
return dao.DeletePeer(ids)
}
func UpdatePeer(account entity.Account) error {
func UpdatePeer(id int64, peerDto dto.PeerUpdateDto) error {
updates := map[string]interface{}{}
if account.Username != nil && *account.Username != "" {
updates["username"] = *account.Username
if peerDto.Name != nil && *peerDto.Name != "" {
updates["name"] = *peerDto.Name
}
_ = 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
if peerDto.Secret != nil && *peerDto.Secret != "" {
digest, err := PeerSecretDigest(*peerDto.Secret)
if err != nil {
return err
}
enc, err := EncryptPeerSecret(*peerDto.Secret)
if err != nil {
return err
}
updates["secret_digest"] = digest
updates["secret_ciphertext"] = enc
}
return true
if peerDto.QuotaBytes != nil {
updates["quota_bytes"] = *peerDto.QuotaBytes
}
if peerDto.ExpiresAt != nil {
updates["expires_at"] = *peerDto.ExpiresAt
}
if peerDto.MaxDevices != nil {
updates["max_devices"] = *peerDto.MaxDevices
}
if peerDto.Disabled != nil {
updates["disabled"] = *peerDto.Disabled
}
if peerDto.Remark != nil {
updates["remark"] = *peerDto.Remark
}
return dao.UpdatePeer([]int64{id}, updates)
}
func GetPeer(id int64) (entity.Account, error) {
peer, err := dao.GetPeer("id = ?", id)
func DeletePeer(id int64) error { return dao.DeletePeer([]int64{id}) }
func GetPeerVo(id int64) (vo.PeerVo, error) {
p, err := dao.GetPeer("id = ?", id)
if err != nil {
return entity.Account{}, err
return vo.PeerVo{}, 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,
return vo.PeerVo{
BaseVo: vo.BaseVo{Id: *p.Id, CreateTime: *p.CreateTime},
Name: strVal(p.Name),
Remark: strVal(p.Remark),
AuthId: strVal(p.AuthId),
QuotaBytes: int64Val(p.QuotaBytes),
DownloadBytes: int64Val(p.DownloadBytes),
UploadBytes: int64Val(p.UploadBytes),
ExpiresAt: int64Val(p.ExpiresAt),
MaxDevices: int64Val(p.MaxDevices),
Disabled: int64Val(p.Disabled),
BannedUntil: int64Val(p.BannedUntil),
LastConnectionAt: int64Val(p.LastConnectionAt),
}, nil
}
func GetAdminAccount(id int64) (entity.AdminUser, error) {
return dao.GetAdminUser("id = ?", id)
func ResetPeerTraffic(id int64) error { 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 {
if err := dao.UpdatePeer([]int64{id}, map[string]interface{}{"banned_until": bannedUntil}); err != nil {
return err
}
return Hysteria2Kick([]int64{id}, bannedUntil)
}
func ListExportPeer() ([]bo.AccountExport, error) {
func BuildPeerClientConfig(id int64) (vo.PeerClientConfigVo, error) {
url, err := Hysteria2Url(id)
if err != nil {
return vo.PeerClientConfigVo{}, err
}
qrCode, err := qrcode.Encode(url, qrcode.Medium, 300)
if err != nil {
return vo.PeerClientConfigVo{}, err
}
return vo.PeerClientConfigVo{Url: url, QrCode: qrCode}, nil
}
func ListExportPeer(includeSecrets bool) ([]bo.PeerExport, error) {
peers, err := dao.ListPeer("1=1")
if err != nil {
return nil, errors.New(constant.SysError)
}
var accountExports []bo.AccountExport
out := make([]bo.PeerExport, 0, len(peers))
for _, item := range peers {
role := "user"
conPass := ""
if item.SecretCiphertext != nil {
conPass = *item.SecretCiphertext
ex := bo.PeerExport{
Id: int64Val(item.Id),
Name: strVal(item.Name),
Remark: strVal(item.Remark),
QuotaBytes: int64Val(item.QuotaBytes),
DownloadBytes: int64Val(item.DownloadBytes),
UploadBytes: int64Val(item.UploadBytes),
ExpiresAt: int64Val(item.ExpiresAt),
MaxDevices: int64Val(item.MaxDevices),
Disabled: int64Val(item.Disabled),
BannedUntil: int64Val(item.BannedUntil),
LastConnectionAt: int64Val(item.LastConnectionAt),
}
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,
if includeSecrets && item.SecretEncrypted != nil {
if dec, derr := DecryptPeerSecret(*item.SecretEncrypted); derr == nil {
ex.Secret = dec
}
}
accountExports = append(accountExports, accountExport)
out = append(out, ex)
}
return accountExports, nil
return out, nil
}
func ReleaseKickPeer(id int64) error {
return dao.UpdatePeer([]int64{id}, map[string]interface{}{"banned_until": 0})
}
func UpsertPeer(accounts []entity.Account) error {
func UpsertPeerLegacy(accounts []entity.Account) error {
for _, account := range accounts {
if account.Id != nil && *account.Id > 0 {
if err := UpdatePeer(account); err != nil {
upd := dto.PeerUpdateDto{}
upd.Name = account.Username
upd.Remark = account.Remark
upd.QuotaBytes = account.Quota
upd.ExpiresAt = account.ExpireTime
upd.MaxDevices = account.DeviceNo
upd.Disabled = account.Deleted
if err := UpdatePeer(*account.Id, upd); err != nil {
return err
}
continue
}
if err := SavePeer(account); err != nil {
save := dto.PeerSaveDto{
Name: account.Username,
Secret: account.ConPass,
QuotaBytes: account.Quota,
ExpiresAt: account.ExpireTime,
MaxDevices: account.DeviceNo,
Disabled: account.Deleted,
Remark: account.Remark,
}
if _, err := CreatePeer(save); 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
func ExistPeerName(name string, id int64) bool {
var err error
if id != 0 {
_, err = dao.GetPeer("name = ? and id != ?", name, id)
} else {
_, err = dao.GetPeer("name = ?", name)
}
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
return err == 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 strVal(v *string) string { if v == nil { return "" }; return *v }
func int64Val(v *int64) int64 { if v == nil { return 0 }; return *v }
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,
})
}