Полный продакшен-рефактор fix25: split peer/admin, удаление legacy, шифрование secret, auth_id, новые API/роуты и зачистка subscription
This commit is contained in:
+176
-245
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user