Полный продакшен-рефактор fix25: split peer/admin, удаление legacy, шифрование secret, auth_id, новые API/роуты и зачистка subscription
This commit is contained in:
@@ -28,3 +28,12 @@ func AdminChangePassword(c *gin.Context) {
|
|||||||
vo.Success(nil, c)
|
vo.Success(nil, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AdminSecurity(c *gin.Context) {
|
||||||
|
info, err := service.GetAdminInfo(c)
|
||||||
|
if err != nil {
|
||||||
|
vo.Fail(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vo.Success(gin.H{"forcePasswordChange": info.ForcePasswordChange}, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +2,12 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/skip2/go-qrcode"
|
|
||||||
"hy2xs-admin/model/dto"
|
"hy2xs-admin/model/dto"
|
||||||
"hy2xs-admin/model/vo"
|
"hy2xs-admin/model/vo"
|
||||||
"hy2xs-admin/service"
|
"hy2xs-admin/service"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resolvePeerID(c *gin.Context) (*int64, bool) {
|
|
||||||
raw := strings.TrimSpace(c.Param("id"))
|
|
||||||
if raw == "" {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
parsed, err := strconv.ParseInt(raw, 10, 64)
|
|
||||||
if err != nil || parsed <= 0 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return &parsed, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hysteria2Auth(c *gin.Context) {
|
func Hysteria2Auth(c *gin.Context) {
|
||||||
var req dto.Hysteria2AuthDto
|
var req dto.Hysteria2AuthDto
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -48,19 +33,6 @@ func Hysteria2Auth(c *gin.Context) {
|
|||||||
vo.Hysteria2AuthSuccess(username, c)
|
vo.Hysteria2AuthSuccess(username, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Hysteria2Kick(c *gin.Context) {
|
|
||||||
hysteria2KickDto, err := validateField(c, dto.Hysteria2KickDto{})
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = service.Hysteria2Kick(hysteria2KickDto.Ids, *hysteria2KickDto.KickUtilTime)
|
|
||||||
if err != nil {
|
|
||||||
vo.Fail(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vo.Success(nil, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hysteria2ChangeVersion(c *gin.Context) {
|
func Hysteria2ChangeVersion(c *gin.Context) {
|
||||||
vo.Fail("Смена версии Hysteria2 отключена: runtime управляется install-оркестратором HY2XS", c)
|
vo.Fail("Смена версии Hysteria2 отключена: runtime управляется install-оркестратором HY2XS", c)
|
||||||
}
|
}
|
||||||
@@ -68,33 +40,3 @@ func Hysteria2ChangeVersion(c *gin.Context) {
|
|||||||
func ListRelease(c *gin.Context) {
|
func ListRelease(c *gin.Context) {
|
||||||
vo.Success([]string{}, c)
|
vo.Success([]string{}, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Hysteria2Url(c *gin.Context) {
|
|
||||||
hysteria2UrlDto := dto.Hysteria2UrlDto{}
|
|
||||||
if id, ok := resolvePeerID(c); ok {
|
|
||||||
hysteria2UrlDto.AccountId = id
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
hysteria2UrlDto, err = validateField(c, dto.Hysteria2UrlDto{})
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
url, err := service.Hysteria2Url(*hysteria2UrlDto.AccountId)
|
|
||||||
if err != nil {
|
|
||||||
vo.Fail(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Генерация QR-кода
|
|
||||||
qrCode, err := qrcode.Encode(url, qrcode.Medium, 300)
|
|
||||||
if err != nil {
|
|
||||||
vo.Fail(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hysteria2UrlVo := vo.Hysteria2UrlVo{
|
|
||||||
Url: url,
|
|
||||||
QrCode: qrCode,
|
|
||||||
}
|
|
||||||
vo.Success(hysteria2UrlVo, c)
|
|
||||||
}
|
|
||||||
|
|||||||
+50
-232
@@ -3,19 +3,19 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"hy2xs-admin/model/constant"
|
"hy2xs-admin/model/constant"
|
||||||
"hy2xs-admin/model/dto"
|
"hy2xs-admin/model/dto"
|
||||||
"hy2xs-admin/model/entity"
|
"hy2xs-admin/model/entity"
|
||||||
"hy2xs-admin/model/vo"
|
"hy2xs-admin/model/vo"
|
||||||
"hy2xs-admin/service"
|
"hy2xs-admin/service"
|
||||||
"hy2xs-admin/util"
|
"hy2xs-admin/util"
|
||||||
"io"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func resolveID(c *gin.Context) (int64, error) {
|
func resolveID(c *gin.Context) (int64, error) {
|
||||||
@@ -37,18 +37,12 @@ func Login(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, forcePasswordChange, err := service.Login(*loginDto.Username, *loginDto.Pass)
|
token, forcePasswordChange, err := service.Login(*loginDto.Username, *loginDto.Pass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jwtVo := vo.JwtVo{
|
vo.Success(vo.JwtVo{TokenType: constant.TokenType, AccessToken: token, ForcePasswordChange: forcePasswordChange}, c)
|
||||||
TokenType: constant.TokenType,
|
|
||||||
AccessToken: token,
|
|
||||||
ForcePasswordChange: forcePasswordChange,
|
|
||||||
}
|
|
||||||
vo.Success(jwtVo, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PagePeer(c *gin.Context) {
|
func PagePeer(c *gin.Context) {
|
||||||
@@ -56,85 +50,25 @@ func PagePeer(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
accounts, total, err := service.PagePeer(peerPageDto)
|
records, total, err := service.PagePeer(peerPageDto)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
vo.Success(vo.PeerPageVo{Records: records, Total: total}, c)
|
||||||
onlineUsers, err := service.Hysteria2Online()
|
|
||||||
if err != nil {
|
|
||||||
vo.Fail(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var accountVos []vo.AccountVo
|
|
||||||
for _, item := range accounts {
|
|
||||||
accountVo := vo.AccountVo{
|
|
||||||
Username: *item.Username,
|
|
||||||
Quota: *item.Quota,
|
|
||||||
Download: *item.Download,
|
|
||||||
Upload: *item.Upload,
|
|
||||||
ExpireTime: *item.ExpireTime,
|
|
||||||
KickUtilTime: *item.KickUtilTime,
|
|
||||||
DeviceNo: *item.DeviceNo,
|
|
||||||
Role: *item.Role,
|
|
||||||
Deleted: *item.Deleted,
|
|
||||||
BaseVo: vo.BaseVo{
|
|
||||||
Id: *item.Id,
|
|
||||||
CreateTime: *item.CreateTime,
|
|
||||||
},
|
|
||||||
LoginAt: *item.LoginAt,
|
|
||||||
ConAt: *item.ConAt,
|
|
||||||
Remark: *item.Remark,
|
|
||||||
}
|
|
||||||
if value, exists := onlineUsers[*item.Username]; exists {
|
|
||||||
accountVo.Online = true
|
|
||||||
accountVo.Device = value
|
|
||||||
delete(onlineUsers, *item.Username)
|
|
||||||
}
|
|
||||||
accountVos = append(accountVos, accountVo)
|
|
||||||
}
|
|
||||||
accountPageVo := vo.AccountPageVo{
|
|
||||||
AccountVos: accountVos,
|
|
||||||
Total: total,
|
|
||||||
}
|
|
||||||
vo.Success(accountPageVo, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SavePeer(c *gin.Context) {
|
func SavePeer(c *gin.Context) {
|
||||||
accountSaveDto, err := validateField(c, dto.AccountSaveDto{})
|
peerSaveDto, err := validateField(c, dto.PeerSaveDto{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
peerVo, err := service.CreatePeer(peerSaveDto)
|
||||||
if service.ExistPeerName(*accountSaveDto.Username, 0) {
|
|
||||||
vo.Fail(fmt.Sprintf("username %s already exists", *accountSaveDto.Username), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
passEncrypt, err := util.HashPassword(*accountSaveDto.Pass)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
conPass := fmt.Sprintf("%s.%s", *accountSaveDto.Username, *accountSaveDto.ConPass)
|
vo.Success(peerVo, c)
|
||||||
account := entity.Account{
|
|
||||||
Username: accountSaveDto.Username,
|
|
||||||
Pass: &passEncrypt,
|
|
||||||
ConPass: &conPass,
|
|
||||||
Quota: accountSaveDto.Quota,
|
|
||||||
ExpireTime: accountSaveDto.ExpireTime,
|
|
||||||
DeviceNo: accountSaveDto.DeviceNo,
|
|
||||||
Deleted: accountSaveDto.Deleted,
|
|
||||||
Remark: accountSaveDto.Remark,
|
|
||||||
}
|
|
||||||
err = service.SavePeer(account)
|
|
||||||
if err != nil {
|
|
||||||
vo.Fail(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vo.Success(nil, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeletePeer(c *gin.Context) {
|
func DeletePeer(c *gin.Context) {
|
||||||
@@ -142,17 +76,7 @@ func DeletePeer(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
account, err := service.GetPeer(id)
|
if err = service.DeletePeer(id); err != nil {
|
||||||
if err != nil {
|
|
||||||
vo.Fail(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if *account.Role == "admin" {
|
|
||||||
vo.Fail("admin cannot be deleted", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = service.DeletePeer([]int64{id})
|
|
||||||
if err != nil {
|
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -160,105 +84,19 @@ func DeletePeer(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdatePeer(c *gin.Context) {
|
func UpdatePeer(c *gin.Context) {
|
||||||
accountUpdateDto, err := validateField(c, dto.AccountUpdateDto{})
|
id, err := resolveID(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// PATCH /peers/:id compatibility flow: id in route param, payload without id
|
|
||||||
if c.Request.Method == "PATCH" && strings.TrimSpace(c.Param("id")) != "" {
|
|
||||||
id, parseErr := strconv.ParseInt(strings.TrimSpace(c.Param("id")), 10, 64)
|
|
||||||
if parseErr != nil || id <= 0 {
|
|
||||||
vo.Fail(constant.InvalidError, c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body map[string]interface{}
|
peerUpdateDto, err := validateField(c, dto.PeerUpdateDto{})
|
||||||
if bindErr := c.ShouldBindJSON(&body); bindErr != nil {
|
|
||||||
vo.Fail(constant.InvalidError, c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
legacy := entity.Account{BaseEntity: entity.BaseEntity{Id: &id}}
|
|
||||||
if v, ok := body["username"].(string); ok {
|
|
||||||
legacy.Username = &v
|
|
||||||
}
|
|
||||||
if v, ok := body["pass"].(string); ok && strings.TrimSpace(v) != "" {
|
|
||||||
hash, hashErr := util.HashPassword(v)
|
|
||||||
if hashErr != nil {
|
|
||||||
vo.Fail(hashErr.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
legacy.Pass = &hash
|
|
||||||
}
|
|
||||||
if v, ok := body["conPass"].(string); ok {
|
|
||||||
legacy.ConPass = &v
|
|
||||||
}
|
|
||||||
if v, ok := body["quota"].(float64); ok {
|
|
||||||
t := int64(v)
|
|
||||||
legacy.Quota = &t
|
|
||||||
}
|
|
||||||
if v, ok := body["expireTime"].(float64); ok {
|
|
||||||
t := int64(v)
|
|
||||||
legacy.ExpireTime = &t
|
|
||||||
}
|
|
||||||
if v, ok := body["deviceNo"].(float64); ok {
|
|
||||||
t := int64(v)
|
|
||||||
legacy.DeviceNo = &t
|
|
||||||
}
|
|
||||||
if v, ok := body["deleted"].(float64); ok {
|
|
||||||
t := int64(v)
|
|
||||||
legacy.Deleted = &t
|
|
||||||
}
|
|
||||||
if v, ok := body["remark"].(string); ok {
|
|
||||||
legacy.Remark = &v
|
|
||||||
}
|
|
||||||
if uErr := service.UpdatePeer(legacy); uErr != nil {
|
|
||||||
vo.Fail(uErr.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vo.Success(nil, c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if accountUpdateDto.Username != nil && *accountUpdateDto.Username != "" && service.ExistPeerName(*accountUpdateDto.Username, *accountUpdateDto.Id) {
|
|
||||||
vo.Fail(fmt.Sprintf("username %s already exists", *accountUpdateDto.Username), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if accountUpdateDto.Deleted != nil && *accountUpdateDto.Deleted == 1 {
|
|
||||||
account, err := service.GetPeer(*accountUpdateDto.Id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if *account.Role == "admin" {
|
if peerUpdateDto.Name != nil && *peerUpdateDto.Name != "" && service.ExistPeerName(*peerUpdateDto.Name, id) {
|
||||||
vo.Fail("the admin account cannot be deleted", c)
|
vo.Fail(fmt.Sprintf("name %s already exists", *peerUpdateDto.Name), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
if err = service.UpdatePeer(id, peerUpdateDto); err != nil {
|
||||||
|
|
||||||
var passEncrypt *string
|
|
||||||
if accountUpdateDto.Pass != nil && *accountUpdateDto.Pass != "" {
|
|
||||||
passEncryptHash, hashErr := util.HashPassword(*accountUpdateDto.Pass)
|
|
||||||
if hashErr != nil {
|
|
||||||
vo.Fail(hashErr.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
passEncrypt = &passEncryptHash
|
|
||||||
}
|
|
||||||
|
|
||||||
account := entity.Account{
|
|
||||||
Username: accountUpdateDto.Username,
|
|
||||||
Pass: passEncrypt,
|
|
||||||
ConPass: accountUpdateDto.ConPass,
|
|
||||||
Quota: accountUpdateDto.Quota,
|
|
||||||
ExpireTime: accountUpdateDto.ExpireTime,
|
|
||||||
DeviceNo: accountUpdateDto.DeviceNo,
|
|
||||||
Deleted: accountUpdateDto.Deleted,
|
|
||||||
Remark: accountUpdateDto.Remark,
|
|
||||||
BaseEntity: entity.BaseEntity{
|
|
||||||
Id: accountUpdateDto.Id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err = service.UpdatePeer(account); err != nil {
|
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -270,54 +108,24 @@ func ResetTraffic(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = service.ResetTraffic(id); err != nil {
|
if err = service.ResetPeerTraffic(id); err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
vo.Success(nil, c)
|
vo.Success(nil, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAdminInfo(c *gin.Context) {
|
|
||||||
accountInfoVo, err := service.GetAdminInfo(c)
|
|
||||||
if err != nil {
|
|
||||||
vo.Fail(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Обновление времени последнего входа
|
|
||||||
now := time.Now().UnixMilli()
|
|
||||||
if err = service.UpdateAdminLastLoginAt(accountInfoVo.Id, now); err != nil {
|
|
||||||
vo.Fail(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vo.Success(accountInfoVo, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetPeer(c *gin.Context) {
|
func GetPeer(c *gin.Context) {
|
||||||
id, err := resolveID(c)
|
id, err := resolveID(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
account, err := service.GetPeer(id)
|
peer, err := service.GetPeerVo(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
accountVo := vo.AccountVo{
|
vo.Success(peer, c)
|
||||||
BaseVo: vo.BaseVo{
|
|
||||||
Id: *account.Id,
|
|
||||||
CreateTime: *account.CreateTime,
|
|
||||||
},
|
|
||||||
Username: *account.Username,
|
|
||||||
Quota: *account.Quota,
|
|
||||||
Download: *account.Download,
|
|
||||||
Upload: *account.Upload,
|
|
||||||
ExpireTime: *account.ExpireTime,
|
|
||||||
DeviceNo: *account.DeviceNo,
|
|
||||||
Role: *account.Role,
|
|
||||||
Deleted: *account.Deleted,
|
|
||||||
Remark: *account.Remark,
|
|
||||||
}
|
|
||||||
vo.Success(accountVo, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ImportPeer(c *gin.Context) {
|
func ImportPeer(c *gin.Context) {
|
||||||
@@ -326,12 +134,10 @@ func ImportPeer(c *gin.Context) {
|
|||||||
vo.Fail(constant.SysError, c)
|
vo.Fail(constant.SysError, c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Размер файла 2 MB
|
|
||||||
if header.Size > 1024*1024*2 {
|
if header.Size > 1024*1024*2 {
|
||||||
vo.Fail("the file is too big", c)
|
vo.Fail("the file is too big", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Расширение файла .json
|
|
||||||
if !strings.HasSuffix(header.Filename, ".json") {
|
if !strings.HasSuffix(header.Filename, ".json") {
|
||||||
vo.Fail(constant.InvalidError, c)
|
vo.Fail(constant.InvalidError, c)
|
||||||
return
|
return
|
||||||
@@ -346,7 +152,7 @@ func ImportPeer(c *gin.Context) {
|
|||||||
vo.Fail("content Unmarshal err", c)
|
vo.Fail("content Unmarshal err", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = service.UpsertPeer(accounts); err != nil {
|
if err = service.UpsertPeerLegacy(accounts); err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -354,21 +160,18 @@ func ImportPeer(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ExportPeer(c *gin.Context) {
|
func ExportPeer(c *gin.Context) {
|
||||||
accountExports, err := service.ListExportPeer()
|
includeSecrets := strings.EqualFold(strings.TrimSpace(c.Query("includeSecrets")), "true")
|
||||||
|
peerExports, err := service.ListExportPeer(includeSecrets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fileName := fmt.Sprintf("PeerExport-%s.json", time.Now().Format("20060102150405"))
|
||||||
fileName := fmt.Sprintf("AccountExport-%s.json", time.Now().Format("20060102150405"))
|
|
||||||
filePath := filepath.Join(constant.ExportPathDir, fileName)
|
filePath := filepath.Join(constant.ExportPathDir, fileName)
|
||||||
|
if err = util.ExportFile(filePath, peerExports, 0); err != nil {
|
||||||
if err = util.ExportFile(filePath, accountExports, 0); err != nil {
|
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Скачивание
|
|
||||||
if !util.Exists(filePath) {
|
if !util.Exists(filePath) {
|
||||||
vo.Fail("file not exist", c)
|
vo.Fail("file not exist", c)
|
||||||
return
|
return
|
||||||
@@ -385,23 +188,38 @@ func ReleaseKickPeer(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = service.ReleaseKickPeer(id); err != nil {
|
if err = service.ReleaseKickPeer(id); err != nil {
|
||||||
logrus.Debugf("release kick err: %v", err)
|
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
vo.Success(nil, c)
|
vo.Success(nil, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func VerifyDefaultPass(c *gin.Context) {
|
func KickPeer(c *gin.Context) {
|
||||||
info, err := service.GetAdminInfo(c)
|
id, err := resolveID(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
kickDto, err := validateField(c, dto.PeerKickDto{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = service.KickPeer(id, *kickDto.BannedUntil); err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
admin, err := service.GetAdminAccount(info.Id)
|
vo.Success(nil, c)
|
||||||
if err != nil {
|
|
||||||
vo.Fail(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vo.Success(admin.PasswordHash != nil && !util.IsBcryptHash(*admin.PasswordHash), c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PeerClientConfig(c *gin.Context) {
|
||||||
|
id, err := resolveID(c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := service.BuildPeerClientConfig(id)
|
||||||
|
if err != nil {
|
||||||
|
vo.Fail(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vo.Success(data, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -64,11 +64,11 @@ func PagePeer(peerPageDto dto.PeerPageDto) ([]entity.Peer, int64, error) {
|
|||||||
var peers []entity.Peer
|
var peers []entity.Peer
|
||||||
var total int64
|
var total int64
|
||||||
tx := sqliteDB.Model(&entity.Peer{})
|
tx := sqliteDB.Model(&entity.Peer{})
|
||||||
if peerPageDto.Username != nil && *peerPageDto.Username != "" {
|
if peerPageDto.Name != nil && *peerPageDto.Name != "" {
|
||||||
tx.Where("name like ?", fmt.Sprintf("%%%s%%", *peerPageDto.Username))
|
tx.Where("name like ?", fmt.Sprintf("%%%s%%", *peerPageDto.Name))
|
||||||
}
|
}
|
||||||
if peerPageDto.Deleted != nil {
|
if peerPageDto.Disabled != nil {
|
||||||
tx.Where("disabled = ?", *peerPageDto.Deleted)
|
tx.Where("disabled = ?", *peerPageDto.Disabled)
|
||||||
}
|
}
|
||||||
if peerPageDto.Remark != nil && *peerPageDto.Remark != "" {
|
if peerPageDto.Remark != nil && *peerPageDto.Remark != "" {
|
||||||
tx.Where("remark like ?", fmt.Sprintf("%%%s%%", *peerPageDto.Remark))
|
tx.Where("remark like ?", fmt.Sprintf("%%%s%%", *peerPageDto.Remark))
|
||||||
|
|||||||
+51
-2
@@ -105,6 +105,8 @@ func ensureSecureBootstrapAdmin() error {
|
|||||||
return pwdErr
|
return pwdErr
|
||||||
}
|
}
|
||||||
adminPassword = password
|
adminPassword = password
|
||||||
|
logrus.Warnf("Initial admin username: %s", adminUser)
|
||||||
|
logrus.Warnf("Initial admin password: %s", adminPassword)
|
||||||
}
|
}
|
||||||
forcePasswordChange := envBoolAsInt("HY2XS_FORCE_PASSWORD_CHANGE", 1)
|
forcePasswordChange := envBoolAsInt("HY2XS_FORCE_PASSWORD_CHANGE", 1)
|
||||||
status := int64(1)
|
status := int64(1)
|
||||||
@@ -153,6 +155,7 @@ func runMigrations() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
migrations := []migration{
|
migrations := []migration{
|
||||||
|
{version: "000_base_config", apply: migrateBaseConfig},
|
||||||
{version: "001_admin_peer_split", apply: migrateAdminPeerSplit},
|
{version: "001_admin_peer_split", apply: migrateAdminPeerSplit},
|
||||||
{version: "002_migrate_legacy_accounts", apply: migrateLegacyAccounts},
|
{version: "002_migrate_legacy_accounts", apply: migrateLegacyAccounts},
|
||||||
{version: "003_archive_legacy_account", apply: archiveLegacyAccount},
|
{version: "003_archive_legacy_account", apply: archiveLegacyAccount},
|
||||||
@@ -178,6 +181,52 @@ func runMigrations() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func seedBaseConfig() error {
|
||||||
|
defaults := map[string]string{
|
||||||
|
constant.HUIWebPort: "8080",
|
||||||
|
constant.HUIWebContext: "/",
|
||||||
|
constant.HUICrtPath: "",
|
||||||
|
constant.HUIKeyPath: "",
|
||||||
|
constant.JwtSecret: "",
|
||||||
|
constant.Hysteria2Enable: "0",
|
||||||
|
constant.Hysteria2Config: "",
|
||||||
|
constant.Hysteria2TrafficTime: "10",
|
||||||
|
constant.Hysteria2ConfigRemark: "",
|
||||||
|
constant.ResetTrafficCron: "0 0 * * *",
|
||||||
|
constant.Hysteria2TrafficStatsSecret: "",
|
||||||
|
constant.PeerSecretKey: "",
|
||||||
|
constant.PeerSecretEncryptionKey: "",
|
||||||
|
}
|
||||||
|
for k, v := range defaults {
|
||||||
|
if tx := sqliteDB.Exec("INSERT OR IGNORE INTO config(key, value, remark) VALUES(?, ?, ?)", k, v, k); tx.Error != nil {
|
||||||
|
logrus.Errorf("sqlite seed config err: %v", tx.Error)
|
||||||
|
return errors.New("sqlite seed config err")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateBaseConfig() error {
|
||||||
|
stmts := []string{
|
||||||
|
`CREATE TABLE IF NOT EXISTS config (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
key TEXT NOT NULL UNIQUE DEFAULT '',
|
||||||
|
value TEXT NOT NULL DEFAULT '',
|
||||||
|
remark TEXT NOT NULL DEFAULT '',
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS config_key_index ON config (key)`,
|
||||||
|
}
|
||||||
|
for _, stmt := range stmts {
|
||||||
|
if tx := sqliteDB.Exec(stmt); tx.Error != nil {
|
||||||
|
logrus.Errorf("sqlite migration exec err: %v", tx.Error)
|
||||||
|
return errors.New("sqlite migration exec err")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seedBaseConfig()
|
||||||
|
}
|
||||||
|
|
||||||
func migrationApplied(version string) (bool, error) {
|
func migrationApplied(version string) (bool, error) {
|
||||||
var count int64
|
var count int64
|
||||||
if tx := sqliteDB.Raw("SELECT COUNT(1) FROM schema_migrations WHERE version = ?", version).Scan(&count); tx.Error != nil {
|
if tx := sqliteDB.Raw("SELECT COUNT(1) FROM schema_migrations WHERE version = ?", version).Scan(&count); tx.Error != nil {
|
||||||
@@ -289,7 +338,7 @@ func migrateLegacyAccounts() error {
|
|||||||
return authErr
|
return authErr
|
||||||
}
|
}
|
||||||
secretDigest := util.PeerSecretDigest(*acc.ConPass)
|
secretDigest := util.PeerSecretDigest(*acc.ConPass)
|
||||||
secretCiphertext := *acc.ConPass
|
secretEncrypted := *acc.ConPass
|
||||||
quota := int64(0)
|
quota := int64(0)
|
||||||
if acc.Quota != nil {
|
if acc.Quota != nil {
|
||||||
quota = *acc.Quota
|
quota = *acc.Quota
|
||||||
@@ -331,7 +380,7 @@ func migrateLegacyAccounts() error {
|
|||||||
Remark: &remark,
|
Remark: &remark,
|
||||||
AuthId: &authId,
|
AuthId: &authId,
|
||||||
SecretDigest: &secretDigest,
|
SecretDigest: &secretDigest,
|
||||||
SecretCiphertext: &secretCiphertext,
|
SecretEncrypted: &secretEncrypted,
|
||||||
QuotaBytes: "a,
|
QuotaBytes: "a,
|
||||||
DownloadBytes: &download,
|
DownloadBytes: &download,
|
||||||
UploadBytes: &upload,
|
UploadBytes: &upload,
|
||||||
|
|||||||
+100
-62
@@ -1,32 +1,11 @@
|
|||||||
CREATE TABLE IF NOT EXISTS account
|
-- Reference schema only. Runtime schema is managed by dao/sqlite.go migrations.
|
||||||
(
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
CREATE TABLE schema_migrations (
|
||||||
username TEXT NOT NULL UNIQUE DEFAULT '',
|
version TEXT PRIMARY KEY,
|
||||||
pass TEXT NOT NULL DEFAULT '',
|
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
con_pass TEXT NOT NULL DEFAULT '',
|
|
||||||
quota INTEGER NOT NULL DEFAULT 0,
|
|
||||||
download INTEGER NOT NULL DEFAULT 0,
|
|
||||||
upload INTEGER NOT NULL DEFAULT 0,
|
|
||||||
expire_time INTEGER NOT NULL DEFAULT 0,
|
|
||||||
kick_util_time INTEGER NOT NULL DEFAULT 0,
|
|
||||||
device_no INTEGER NOT NULL DEFAULT 3,
|
|
||||||
role TEXT NOT NULL DEFAULT 'user',
|
|
||||||
deleted INTEGER NOT NULL DEFAULT 0,
|
|
||||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
);
|
||||||
ALTER TABLE account
|
|
||||||
ADD COLUMN login_at INTEGER NOT NULL DEFAULT 0;
|
CREATE TABLE config (
|
||||||
ALTER TABLE account
|
|
||||||
ADD COLUMN con_at INTEGER NOT NULL DEFAULT 0;
|
|
||||||
ALTER TABLE account
|
|
||||||
ADD COLUMN remark INTEGER NOT NULL DEFAULT '';
|
|
||||||
CREATE INDEX IF NOT EXISTS account_deleted_index ON account (deleted);
|
|
||||||
CREATE INDEX IF NOT EXISTS account_username_index ON account (username);
|
|
||||||
CREATE INDEX IF NOT EXISTS account_con_pass_index ON account (con_pass);
|
|
||||||
CREATE INDEX IF NOT EXISTS account_pass_index ON account (pass);
|
|
||||||
CREATE TABLE IF NOT EXISTS config
|
|
||||||
(
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
key TEXT NOT NULL UNIQUE DEFAULT '',
|
key TEXT NOT NULL UNIQUE DEFAULT '',
|
||||||
value TEXT NOT NULL DEFAULT '',
|
value TEXT NOT NULL DEFAULT '',
|
||||||
@@ -34,37 +13,96 @@ CREATE TABLE IF NOT EXISTS config
|
|||||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS config_key_index ON config (key);
|
CREATE INDEX config_key_index ON config (key);
|
||||||
INSERT INTO config (key, value, remark)
|
|
||||||
SELECT 'H_UI_WEB_PORT', '8081', 'HY2XS admin Web Port'
|
CREATE TABLE admin_user (
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_WEB_PORT');
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
INSERT INTO config (key, value, remark)
|
username TEXT NOT NULL UNIQUE DEFAULT '',
|
||||||
SELECT 'H_UI_WEB_CONTEXT', '/', 'HY2XS admin Web Context'
|
password_hash TEXT NOT NULL DEFAULT '',
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_WEB_CONTEXT');
|
status INTEGER NOT NULL DEFAULT 1,
|
||||||
INSERT INTO config (key, value, remark)
|
force_password_change INTEGER NOT NULL DEFAULT 0,
|
||||||
SELECT 'H_UI_CRT_PATH', '', 'HY2XS admin CRT File Path'
|
last_login_at INTEGER NOT NULL DEFAULT 0,
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_CRT_PATH');
|
password_changed_at INTEGER NOT NULL DEFAULT 0,
|
||||||
INSERT INTO config (key, value, remark)
|
token_version INTEGER NOT NULL DEFAULT 1,
|
||||||
SELECT 'H_UI_KEY_PATH', '', 'HY2XS admin KEY File Path'
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_KEY_PATH');
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
INSERT INTO config (key, value, remark)
|
);
|
||||||
SELECT 'JWT_SECRET', hex(randomblob(10)), 'JWT Secret'
|
CREATE INDEX admin_user_username_index ON admin_user (username);
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'JWT_SECRET');
|
|
||||||
INSERT INTO config (key, value, remark)
|
CREATE TABLE peer (
|
||||||
SELECT 'HYSTERIA2_ENABLE', '0', 'Hysteria2 Switch'
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_ENABLE');
|
name TEXT NOT NULL UNIQUE DEFAULT '',
|
||||||
INSERT INTO config (key, value, remark)
|
remark TEXT NOT NULL DEFAULT '',
|
||||||
SELECT 'HYSTERIA2_CONFIG', '', 'Hysteria2 Config'
|
auth_id TEXT NOT NULL UNIQUE DEFAULT '',
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_CONFIG');
|
secret_digest TEXT NOT NULL UNIQUE DEFAULT '',
|
||||||
INSERT INTO config (key, value, remark)
|
secret_ciphertext TEXT NOT NULL DEFAULT '',
|
||||||
SELECT 'HYSTERIA2_TRAFFIC_TIME', '1', 'Hysteria2 Traffic Time'
|
quota_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_TRAFFIC_TIME');
|
download_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
INSERT INTO config (key, value, remark)
|
upload_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
SELECT 'HYSTERIA2_CONFIG_REMARK', '', 'Hysteria2 Config Remark'
|
expires_at INTEGER NOT NULL DEFAULT 0,
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_CONFIG_REMARK');
|
max_devices INTEGER NOT NULL DEFAULT 3,
|
||||||
INSERT INTO config (key, value, remark)
|
disabled INTEGER NOT NULL DEFAULT 0,
|
||||||
SELECT 'RESET_TRAFFIC_CRON', '', 'Reset Traffic Cron'
|
banned_until INTEGER NOT NULL DEFAULT 0,
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'RESET_TRAFFIC_CRON');
|
last_connection_at INTEGER NOT NULL DEFAULT 0,
|
||||||
INSERT INTO config (key, value, remark)
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
SELECT 'CLASH_EXTENSION', '', 'Clash Subscription Extension'
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'CLASH_EXTENSION');
|
);
|
||||||
|
CREATE INDEX peer_name_index ON peer (name);
|
||||||
|
CREATE INDEX peer_auth_id_index ON peer (auth_id);
|
||||||
|
CREATE INDEX peer_secret_digest_index ON peer (secret_digest);
|
||||||
|
|
||||||
|
CREATE TABLE traffic_sample (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
peer_id INTEGER NOT NULL,
|
||||||
|
auth_id TEXT NOT NULL,
|
||||||
|
rx_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
tx_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
sampled_at INTEGER NOT NULL,
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_traffic_sample_time ON traffic_sample(sampled_at);
|
||||||
|
CREATE INDEX idx_traffic_sample_peer_time ON traffic_sample(peer_id, sampled_at);
|
||||||
|
|
||||||
|
CREATE TABLE traffic_aggregate_hourly (
|
||||||
|
peer_id INTEGER NOT NULL,
|
||||||
|
hour_start INTEGER NOT NULL,
|
||||||
|
rx_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
tx_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY(peer_id, hour_start)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_traffic_hourly_hour_start ON traffic_aggregate_hourly(hour_start);
|
||||||
|
|
||||||
|
CREATE TABLE traffic_aggregate_daily (
|
||||||
|
peer_id INTEGER NOT NULL,
|
||||||
|
day_start INTEGER NOT NULL,
|
||||||
|
rx_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
tx_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY(peer_id, day_start)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_traffic_daily_day_start ON traffic_aggregate_daily(day_start);
|
||||||
|
|
||||||
|
CREATE TABLE metric_sample (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
sampled_at INTEGER NOT NULL,
|
||||||
|
cpu_percent REAL NOT NULL DEFAULT 0,
|
||||||
|
load1 REAL NOT NULL DEFAULT 0,
|
||||||
|
mem_used_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
mem_total_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
mem_percent REAL NOT NULL DEFAULT 0,
|
||||||
|
disk_path TEXT NOT NULL DEFAULT '/',
|
||||||
|
disk_used_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
disk_total_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
disk_percent REAL NOT NULL DEFAULT 0,
|
||||||
|
hysteria_running INTEGER NOT NULL DEFAULT 0,
|
||||||
|
online_peers INTEGER NOT NULL DEFAULT 0,
|
||||||
|
online_devices INTEGER NOT NULL DEFAULT 0,
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_metric_sample_time ON metric_sample(sampled_at);
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,3 @@ export function getAdminInfoApi(): AxiosPromise<AdminInfo> {
|
|||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function verifyDefaultPassApi(): AxiosPromise<boolean> {
|
|
||||||
return request({
|
|
||||||
url: "/admin/verify-default-pass",
|
|
||||||
method: "get",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { AxiosPromise } from "axios";
|
|
||||||
import { Hysteria2ServerConfig } from "@/api/config/types";
|
|
||||||
import request from "@/utils/request";
|
|
||||||
import {
|
|
||||||
Hysteria2KickDto,
|
|
||||||
Hysteria2UrlDto,
|
|
||||||
Hysteria2UrlVo,
|
|
||||||
} from "@/api/hysteria2/types";
|
|
||||||
|
|
||||||
export function hysteria2KickApi(
|
|
||||||
data: Hysteria2KickDto
|
|
||||||
): AxiosPromise<Hysteria2ServerConfig> {
|
|
||||||
return request({
|
|
||||||
url: "/hysteria2/hysteria2Kick",
|
|
||||||
method: "post",
|
|
||||||
data: data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hysteria2UrlApi(
|
|
||||||
dto: Hysteria2UrlDto
|
|
||||||
): AxiosPromise<Hysteria2UrlVo> {
|
|
||||||
return request({
|
|
||||||
url: "/hysteria2/hysteria2Url",
|
|
||||||
method: "get",
|
|
||||||
params: dto,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
export interface Hysteria2KickDto {
|
|
||||||
ids: number[];
|
|
||||||
kickUtilTime: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Hysteria2UrlDto {
|
|
||||||
accountId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Hysteria2UrlVo {
|
|
||||||
url: string;
|
|
||||||
qrCode: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
import { AxiosPromise } from "axios";
|
import { AxiosPromise } from "axios";
|
||||||
import {
|
import {
|
||||||
|
KickPeerDto,
|
||||||
|
PeerClientConfigVo,
|
||||||
PeerPageDto,
|
PeerPageDto,
|
||||||
PeerSaveDto,
|
PeerSaveDto,
|
||||||
PeerUpdateDto,
|
PeerUpdateDto,
|
||||||
@@ -59,6 +61,23 @@ export function releaseKickPeerApi(data: IdDto): AxiosPromise {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function kickPeerApi(id: number, data: KickPeerDto): AxiosPromise {
|
||||||
|
return request({
|
||||||
|
url: `/peers/${id}/kick`,
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPeerClientConfigApi(
|
||||||
|
id: number
|
||||||
|
): AxiosPromise<PeerClientConfigVo> {
|
||||||
|
return request({
|
||||||
|
url: `/peers/${id}/client-config`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function importPeerApi(data: FormData): AxiosPromise {
|
export function importPeerApi(data: FormData): AxiosPromise {
|
||||||
return request({
|
return request({
|
||||||
url: "/peers/import",
|
url: "/peers/import",
|
||||||
|
|||||||
@@ -1,62 +1,51 @@
|
|||||||
export interface PeerPageDto extends BaseDto {
|
export interface PeerPageDto extends BaseDto {
|
||||||
username?: string;
|
name?: string;
|
||||||
deleted?: number;
|
disabled?: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PeerUpdateDto extends IdDto {
|
export interface PeerUpdateDto extends IdDto {
|
||||||
username: string;
|
name?: string;
|
||||||
pass: string;
|
secret?: string;
|
||||||
conPass: string;
|
quotaBytes?: number;
|
||||||
quota: number;
|
expiresAt?: number;
|
||||||
expireTime: number;
|
maxDevices?: number;
|
||||||
deviceNo: number;
|
disabled?: number;
|
||||||
deleted: number;
|
remark?: string;
|
||||||
remark: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PeerSaveDto {
|
export interface PeerSaveDto {
|
||||||
username: string;
|
name: string;
|
||||||
pass: string;
|
secret?: string;
|
||||||
conPass: string;
|
quotaBytes: number;
|
||||||
quota: number;
|
expiresAt: number;
|
||||||
expireTime: number;
|
maxDevices: number;
|
||||||
deviceNo: number;
|
disabled: number;
|
||||||
deleted: number;
|
|
||||||
remark: string;
|
remark: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PeerVo extends IdDto {
|
export interface PeerVo extends IdDto {
|
||||||
username: string;
|
name: string;
|
||||||
quota: number;
|
remark: string;
|
||||||
download: number;
|
authId: string;
|
||||||
upload: number;
|
quotaBytes: number;
|
||||||
expireTime: number;
|
downloadBytes: number;
|
||||||
kickUtilTime: number;
|
uploadBytes: number;
|
||||||
deviceNo: number;
|
expiresAt: number;
|
||||||
role: string;
|
maxDevices: number;
|
||||||
deleted: number;
|
disabled: number;
|
||||||
|
bannedUntil: number;
|
||||||
|
lastConnectionAt: number;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
online: boolean;
|
online: boolean;
|
||||||
device: number;
|
onlineDevices: number;
|
||||||
loginAt: number;
|
|
||||||
conAt: number;
|
|
||||||
remark: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PeerForm extends IdDto {
|
export interface PeerClientConfigVo {
|
||||||
username: string;
|
url: string;
|
||||||
pass: string;
|
qrCode: string | Uint8Array;
|
||||||
conPass: string;
|
|
||||||
quota: number;
|
|
||||||
expireTime: number;
|
|
||||||
deviceNo: number;
|
|
||||||
deleted: number;
|
|
||||||
remark: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KickPeerForm {
|
export interface KickPeerDto {
|
||||||
ids: number[];
|
bannedUntil: number;
|
||||||
kickUtilTime: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-3
@@ -26,9 +26,6 @@ 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']
|
||||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
|
||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
|
||||||
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']
|
||||||
|
|||||||
Vendored
+1
-2
@@ -8,8 +8,7 @@ declare module "*.vue" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TypeScript-подсказки для переменных окружения
|
// TypeScript-подсказки для переменных окружения
|
||||||
interface ImportMetaEnv {
|
type ImportMetaEnv = Record<string, string | boolean | undefined>;
|
||||||
}
|
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv;
|
readonly env: ImportMetaEnv;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ type AccountBo struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Roles []string `json:"roles"`
|
Roles []string `json:"roles"`
|
||||||
Deleted int64 `json:"deleted"`
|
Deleted int64 `json:"deleted"`
|
||||||
|
TokenVersion int64 `json:"tokenVersion"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountExport struct {
|
type AccountExport struct {
|
||||||
@@ -28,3 +29,18 @@ type AccountExport struct {
|
|||||||
ConAt int64 `json:"conAt"`
|
ConAt int64 `json:"conAt"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PeerExport struct {
|
||||||
|
Id int64 `json:"id,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
Secret string `json:"secret,omitempty"`
|
||||||
|
QuotaBytes int64 `json:"quotaBytes"`
|
||||||
|
DownloadBytes int64 `json:"downloadBytes,omitempty"`
|
||||||
|
UploadBytes int64 `json:"uploadBytes,omitempty"`
|
||||||
|
ExpiresAt int64 `json:"expiresAt"`
|
||||||
|
MaxDevices int64 `json:"maxDevices"`
|
||||||
|
Disabled int64 `json:"disabled"`
|
||||||
|
BannedUntil int64 `json:"bannedUntil,omitempty"`
|
||||||
|
LastConnectionAt int64 `json:"lastConnectionAt,omitempty"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package bo
|
|
||||||
|
|
||||||
type Hysteria2 struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Server string `yaml:"server"`
|
|
||||||
Port string `yaml:"port"`
|
|
||||||
Ports string `yaml:"ports,omitempty"`
|
|
||||||
Password string `yaml:"password"`
|
|
||||||
Up string `yaml:"up,omitempty"`
|
|
||||||
Down string `yaml:"down,omitempty"`
|
|
||||||
Obfs string `yaml:"obfs,omitempty"`
|
|
||||||
ObfsPassword string `yaml:"obfs-password,omitempty"`
|
|
||||||
Sni string `yaml:"sni,omitempty"`
|
|
||||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProxyGroup struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Proxies []string `yaml:"proxies"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClashConfig struct {
|
|
||||||
Proxies []interface{} `yaml:"proxies"`
|
|
||||||
ProxyGroups []ProxyGroup `yaml:"proxy-groups"`
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package constant
|
|
||||||
|
|
||||||
const (
|
|
||||||
Shadowrocket = "shadowrocket"
|
|
||||||
Clash = "clash"
|
|
||||||
V2rayN = "v2rayn"
|
|
||||||
NekoBox = "nekobox"
|
|
||||||
)
|
|
||||||
@@ -6,6 +6,8 @@ const (
|
|||||||
HUICrtPath = "H_UI_CRT_PATH"
|
HUICrtPath = "H_UI_CRT_PATH"
|
||||||
HUIKeyPath = "H_UI_KEY_PATH"
|
HUIKeyPath = "H_UI_KEY_PATH"
|
||||||
JwtSecret = "JWT_SECRET"
|
JwtSecret = "JWT_SECRET"
|
||||||
|
PeerSecretKey = "PEER_SECRET_KEY"
|
||||||
|
PeerSecretEncryptionKey = "PEER_SECRET_ENCRYPTION_KEY"
|
||||||
Hysteria2TrafficStatsSecret = "HYSTERIA2_TRAFFIC_STATS_SECRET"
|
Hysteria2TrafficStatsSecret = "HYSTERIA2_TRAFFIC_STATS_SECRET"
|
||||||
Hysteria2Enable = "HYSTERIA2_ENABLE"
|
Hysteria2Enable = "HYSTERIA2_ENABLE"
|
||||||
Hysteria2Config = "HYSTERIA2_CONFIG"
|
Hysteria2Config = "HYSTERIA2_CONFIG"
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
type PeerPageDto struct {
|
|
||||||
BaseDto
|
|
||||||
Username *string `json:"username" form:"username" validate:"omitempty,min=1,max=32"`
|
|
||||||
Deleted *int64 `json:"deleted" form:"deleted" validate:"omitempty,oneof=0 1"`
|
|
||||||
Remark *string `json:"remark" form:"remark" validate:"omitempty,min=0,max=32"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoginDto struct {
|
|
||||||
Username *string `json:"username" form:"username" validate:"required,min=6,max=32,validateStr"`
|
|
||||||
Pass *string `json:"pass" form:"pass" validate:"required,min=6,max=32,validateStr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccountSaveDto struct {
|
|
||||||
Username *string `json:"username" form:"username" validate:"required,min=6,max=32,validateStr"`
|
|
||||||
Pass *string `json:"pass" form:"pass" validate:"required,min=6,max=32,validateStr"`
|
|
||||||
ConPass *string `json:"conPass" form:"conPass" validate:"required,min=6,max=32,validateStr"`
|
|
||||||
Quota *int64 `json:"quota" form:"quota" validate:"required,min=-1"`
|
|
||||||
ExpireTime *int64 `json:"expireTime" form:"expireTime" validate:"required,min=0"`
|
|
||||||
DeviceNo *int64 `json:"deviceNo" form:"deviceNo" validate:"required,min=1"`
|
|
||||||
Deleted *int64 `json:"deleted" form:"deleted" validate:"required,oneof=0 1"`
|
|
||||||
Remark *string `json:"remark" form:"remark" validate:"omitempty,min=0,max=32"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccountUpdateDto struct {
|
|
||||||
IdDto
|
|
||||||
Username *string `json:"username" form:"username" validate:"omitempty,min=6,max=32,validateStr"`
|
|
||||||
Pass *string `json:"pass" form:"pass" validate:"omitempty,min=6,max=32,validateStr"`
|
|
||||||
ConPass *string `json:"conPass" form:"conPass" validate:"omitempty,min=6,max=32,validateStr"`
|
|
||||||
Quota *int64 `json:"quota" form:"quota" validate:"omitempty,min=-1"`
|
|
||||||
ExpireTime *int64 `json:"expireTime" form:"expireTime" validate:"omitempty,min=0"`
|
|
||||||
DeviceNo *int64 `json:"deviceNo" form:"deviceNo" validate:"omitempty,min=1"`
|
|
||||||
Deleted *int64 `json:"deleted" form:"deleted" validate:"omitempty,oneof=0 1"`
|
|
||||||
Remark *string `json:"remark" form:"remark" validate:"omitempty,min=0,max=32"`
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type LoginDto struct {
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,15 +6,6 @@ type Hysteria2AuthDto struct {
|
|||||||
Tx *int64 `json:"tx" form:"tx" validate:"required"`
|
Tx *int64 `json:"tx" form:"tx" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Hysteria2KickDto struct {
|
|
||||||
Ids []int64 `json:"ids" form:"ids" validate:"required"`
|
|
||||||
KickUtilTime *int64 `json:"kickUtilTime" form:"kickUtilTime" validate:"required"` // Время снятия блокировки
|
|
||||||
}
|
|
||||||
|
|
||||||
type Hysteria2VersionDto struct {
|
type Hysteria2VersionDto struct {
|
||||||
Version *string `json:"version" form:"version" validate:"required,min=1,max=10"`
|
Version *string `json:"version" form:"version" validate:"required,min=1,max=10"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Hysteria2UrlDto struct {
|
|
||||||
AccountId *int64 `json:"accountId" form:"accountId" validate:"required,gt=0"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type PeerPageDto struct {
|
||||||
|
BaseDto
|
||||||
|
Name *string `json:"name" form:"name" validate:"omitempty,min=1,max=32"`
|
||||||
|
Disabled *int64 `json:"disabled" form:"disabled" validate:"omitempty,oneof=0 1"`
|
||||||
|
Remark *string `json:"remark" form:"remark" validate:"omitempty,min=0,max=64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerSaveDto struct {
|
||||||
|
Name *string `json:"name" form:"name" validate:"required,min=1,max=32,validateStr"`
|
||||||
|
Secret *string `json:"secret" form:"secret" validate:"omitempty,min=6,max=128"`
|
||||||
|
QuotaBytes *int64 `json:"quotaBytes" form:"quotaBytes" validate:"required,min=-1"`
|
||||||
|
ExpiresAt *int64 `json:"expiresAt" form:"expiresAt" validate:"required,min=0"`
|
||||||
|
MaxDevices *int64 `json:"maxDevices" form:"maxDevices" validate:"required,min=1"`
|
||||||
|
Disabled *int64 `json:"disabled" form:"disabled" validate:"required,oneof=0 1"`
|
||||||
|
Remark *string `json:"remark" form:"remark" validate:"omitempty,min=0,max=64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerUpdateDto struct {
|
||||||
|
IdDto
|
||||||
|
Name *string `json:"name" form:"name" validate:"omitempty,min=1,max=32,validateStr"`
|
||||||
|
Secret *string `json:"secret" form:"secret" validate:"omitempty,min=6,max=128"`
|
||||||
|
QuotaBytes *int64 `json:"quotaBytes" form:"quotaBytes" validate:"omitempty,min=-1"`
|
||||||
|
ExpiresAt *int64 `json:"expiresAt" form:"expiresAt" validate:"omitempty,min=0"`
|
||||||
|
MaxDevices *int64 `json:"maxDevices" form:"maxDevices" validate:"omitempty,min=1"`
|
||||||
|
Disabled *int64 `json:"disabled" form:"disabled" validate:"omitempty,oneof=0 1"`
|
||||||
|
Remark *string `json:"remark" form:"remark" validate:"omitempty,min=0,max=64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerKickDto struct {
|
||||||
|
BannedUntil *int64 `json:"bannedUntil" form:"bannedUntil" validate:"required,min=0"`
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ type Peer struct {
|
|||||||
Remark *string `gorm:"column:remark;default:''" json:"remark"`
|
Remark *string `gorm:"column:remark;default:''" json:"remark"`
|
||||||
AuthId *string `gorm:"column:auth_id;default:''" json:"authId"`
|
AuthId *string `gorm:"column:auth_id;default:''" json:"authId"`
|
||||||
SecretDigest *string `gorm:"column:secret_digest;default:''" json:"secretDigest"`
|
SecretDigest *string `gorm:"column:secret_digest;default:''" json:"secretDigest"`
|
||||||
SecretCiphertext *string `gorm:"column:secret_ciphertext;default:''" json:"secretCiphertext"`
|
SecretEncrypted *string `gorm:"column:secret_ciphertext;default:''" json:"-"`
|
||||||
QuotaBytes *int64 `gorm:"column:quota_bytes;default:0" json:"quotaBytes"`
|
QuotaBytes *int64 `gorm:"column:quota_bytes;default:0" json:"quotaBytes"`
|
||||||
DownloadBytes *int64 `gorm:"column:download_bytes;default:0" json:"downloadBytes"`
|
DownloadBytes *int64 `gorm:"column:download_bytes;default:0" json:"downloadBytes"`
|
||||||
UploadBytes *int64 `gorm:"column:upload_bytes;default:0" json:"uploadBytes"`
|
UploadBytes *int64 `gorm:"column:upload_bytes;default:0" json:"uploadBytes"`
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package vo
|
|
||||||
|
|
||||||
type AccountVo struct {
|
|
||||||
BaseVo
|
|
||||||
Username string `json:"username"`
|
|
||||||
Quota int64 `json:"quota"`
|
|
||||||
Download int64 `json:"download"`
|
|
||||||
Upload int64 `json:"upload"`
|
|
||||||
ExpireTime int64 `json:"expireTime"`
|
|
||||||
KickUtilTime int64 `json:"kickUtilTime"` // Offline remaining time
|
|
||||||
DeviceNo int64 `json:"deviceNo"` // Limit the number of devices
|
|
||||||
Role string `json:"role"`
|
|
||||||
Deleted int64 `json:"deleted"`
|
|
||||||
|
|
||||||
Online bool `json:"online"` // online status
|
|
||||||
Device int64 `json:"device"` // Number of online devices
|
|
||||||
|
|
||||||
LoginAt int64 `json:"loginAt"`
|
|
||||||
ConAt int64 `json:"conAt"`
|
|
||||||
Remark string `json:"remark"`
|
|
||||||
}
|
|
||||||
type AccountPageVo struct {
|
|
||||||
AccountVos []AccountVo `json:"records"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccountInfoVo struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Roles []string `json:"roles"`
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
type AdminInfoVo struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Roles []string `json:"roles"`
|
||||||
|
ForcePasswordChange bool `json:"forcePasswordChange"`
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
type PeerVo struct {
|
||||||
|
BaseVo
|
||||||
|
Name string `json:"name"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
AuthId string `json:"authId"`
|
||||||
|
QuotaBytes int64 `json:"quotaBytes"`
|
||||||
|
DownloadBytes int64 `json:"downloadBytes"`
|
||||||
|
UploadBytes int64 `json:"uploadBytes"`
|
||||||
|
ExpiresAt int64 `json:"expiresAt"`
|
||||||
|
MaxDevices int64 `json:"maxDevices"`
|
||||||
|
Disabled int64 `json:"disabled"`
|
||||||
|
BannedUntil int64 `json:"bannedUntil"`
|
||||||
|
LastConnectionAt int64 `json:"lastConnectionAt"`
|
||||||
|
|
||||||
|
Online bool `json:"online"`
|
||||||
|
OnlineDevices int64 `json:"onlineDevices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerPageVo struct {
|
||||||
|
Records []PeerVo `json:"records"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerClientConfigVo struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
QrCode []byte `json:"qrCode"`
|
||||||
|
}
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ func initAdminRouter(adminApi *gin.RouterGroup) {
|
|||||||
{
|
{
|
||||||
admin.GET("/me", controller.AdminMe)
|
admin.GET("/me", controller.AdminMe)
|
||||||
admin.POST("/change-password", controller.AdminChangePassword)
|
admin.POST("/change-password", controller.AdminChangePassword)
|
||||||
admin.GET("/verify-default-pass", controller.VerifyDefaultPass)
|
admin.GET("/security", controller.AdminSecurity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ func initHysteria2MachineAuthRouter(hysteria2Api *gin.RouterGroup) {
|
|||||||
func initHysteria2Router(hysteria2Api *gin.RouterGroup) {
|
func initHysteria2Router(hysteria2Api *gin.RouterGroup) {
|
||||||
hysteria2 := hysteria2Api.Group("/hysteria2")
|
hysteria2 := hysteria2Api.Group("/hysteria2")
|
||||||
{
|
{
|
||||||
hysteria2.POST("/hysteria2Kick", controller.Hysteria2Kick)
|
|
||||||
hysteria2.POST("/hysteria2ChangeVersion", controller.Hysteria2ChangeVersion)
|
hysteria2.POST("/hysteria2ChangeVersion", controller.Hysteria2ChangeVersion)
|
||||||
hysteria2.GET("/listRelease", controller.ListRelease)
|
hysteria2.GET("/listRelease", controller.ListRelease)
|
||||||
hysteria2.GET("/hysteria2Url", controller.Hysteria2Url)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -16,12 +16,13 @@ func initPeerRouter(peerApi *gin.RouterGroup) {
|
|||||||
|
|
||||||
peers.POST("/:id/reset-traffic", controller.ResetTraffic)
|
peers.POST("/:id/reset-traffic", controller.ResetTraffic)
|
||||||
peers.POST("/:id/release-kick", controller.ReleaseKickPeer)
|
peers.POST("/:id/release-kick", controller.ReleaseKickPeer)
|
||||||
peers.POST("/:id/kick", controller.Hysteria2Kick)
|
peers.POST("/:id/kick", controller.KickPeer)
|
||||||
|
peers.GET("/:id/client-config", controller.PeerClientConfig)
|
||||||
peers.POST("/import", controller.ImportPeer)
|
peers.POST("/import", controller.ImportPeer)
|
||||||
peers.POST("/export", controller.ExportPeer)
|
peers.POST("/export", controller.ExportPeer)
|
||||||
|
|
||||||
// совместимость с текущим transport-форматом (query accountId)
|
// aliases на один миграционный релиз
|
||||||
peers.GET("/:id/client-url", controller.Hysteria2Url)
|
peers.GET("/:id/client-url", controller.PeerClientConfig)
|
||||||
peers.GET("/:id/qr", controller.Hysteria2Url)
|
peers.GET("/:id/qr", controller.PeerClientConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"hy2xs-admin/dao"
|
||||||
|
"hy2xs-admin/model/bo"
|
||||||
|
"hy2xs-admin/model/constant"
|
||||||
|
"hy2xs-admin/model/vo"
|
||||||
|
"hy2xs-admin/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Login(username string, plainPassword string) (string, bool, error) {
|
||||||
|
admin, err := dao.GetAdminUser("username = ? and status = 1", username)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
verified, legacy := util.VerifyPassword(plainPassword, *admin.PasswordHash)
|
||||||
|
if !verified {
|
||||||
|
return "", false, errors.New(constant.WrongPassword)
|
||||||
|
}
|
||||||
|
if legacy {
|
||||||
|
hash, hashErr := util.HashPassword(plainPassword)
|
||||||
|
if hashErr == nil {
|
||||||
|
_ = dao.UpdateAdminUser([]int64{*admin.Id}, map[string]interface{}{"password_hash": hash})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokenVersion := int64(1)
|
||||||
|
if admin.TokenVersion != nil && *admin.TokenVersion > 0 {
|
||||||
|
tokenVersion = *admin.TokenVersion
|
||||||
|
}
|
||||||
|
accountBo := bo.AccountBo{Id: *admin.Id, Username: *admin.Username, Roles: []string{"admin"}, Deleted: 0, TokenVersion: tokenVersion}
|
||||||
|
token, tokenErr := GenToken(accountBo)
|
||||||
|
if tokenErr != nil {
|
||||||
|
return "", false, tokenErr
|
||||||
|
}
|
||||||
|
requirePasswordChange := legacy
|
||||||
|
if admin.ForcePasswordChange != nil {
|
||||||
|
requirePasswordChange = *admin.ForcePasswordChange != 0
|
||||||
|
}
|
||||||
|
return token, requirePasswordChange, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAdminInfo(c *gin.Context) (vo.AdminInfoVo, error) {
|
||||||
|
myClaims, err := ParseToken(GetToken(c))
|
||||||
|
if err != nil {
|
||||||
|
return vo.AdminInfoVo{}, err
|
||||||
|
}
|
||||||
|
admin, err := dao.GetAdminUser("id = ?", myClaims.Admin.Id)
|
||||||
|
if err != nil {
|
||||||
|
return vo.AdminInfoVo{}, err
|
||||||
|
}
|
||||||
|
if admin.Status != nil && *admin.Status == 0 {
|
||||||
|
return vo.AdminInfoVo{}, errors.New("this account has been disabled")
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@@ -146,7 +146,15 @@ func Hysteria2Url(accountId int64) (string, error) {
|
|||||||
sni = hysteria2Config.ACME.Domains[0]
|
sni = hysteria2Config.ACME.Domains[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildHysteria2Url(*peer.SecretCiphertext, hostname, port, obfsType, obfsPassword, sni, remark), nil
|
secret := ""
|
||||||
|
if peer.SecretEncrypted != nil {
|
||||||
|
decrypted, decErr := DecryptPeerSecret(*peer.SecretEncrypted)
|
||||||
|
if decErr != nil {
|
||||||
|
return "", decErr
|
||||||
|
}
|
||||||
|
secret = decrypted
|
||||||
|
}
|
||||||
|
return buildHysteria2Url(secret, hostname, port, obfsType, obfsPassword, sni, remark), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildHysteria2Url(conPass string, hostname string, port int, obfsType string, obfsPassword string, sni string, remark string) string {
|
func buildHysteria2Url(conPass string, hostname string, port int, obfsType string, obfsPassword string, sni string, remark string) string {
|
||||||
|
|||||||
+173
-242
@@ -3,7 +3,8 @@ package service
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
"hy2xs-admin/dao"
|
"hy2xs-admin/dao"
|
||||||
"hy2xs-admin/model/bo"
|
"hy2xs-admin/model/bo"
|
||||||
"hy2xs-admin/model/constant"
|
"hy2xs-admin/model/constant"
|
||||||
@@ -11,311 +12,241 @@ import (
|
|||||||
"hy2xs-admin/model/entity"
|
"hy2xs-admin/model/entity"
|
||||||
"hy2xs-admin/model/vo"
|
"hy2xs-admin/model/vo"
|
||||||
"hy2xs-admin/util"
|
"hy2xs-admin/util"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Login(username string, plainPassword string) (string, bool, error) {
|
func PagePeer(peerPageDto dto.PeerPageDto) ([]vo.PeerVo, int64, 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)
|
peers, total, err := dao.PagePeer(peerPageDto)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
accounts := make([]entity.Account, 0, len(peers))
|
onlineUsers, _ := Hysteria2Online()
|
||||||
|
result := make([]vo.PeerVo, 0, len(peers))
|
||||||
for _, p := range peers {
|
for _, p := range peers {
|
||||||
role := "user"
|
item := vo.PeerVo{
|
||||||
acc := entity.Account{
|
BaseVo: vo.BaseVo{Id: *p.Id, CreateTime: *p.CreateTime},
|
||||||
BaseEntity: p.BaseEntity,
|
Name: strVal(p.Name),
|
||||||
Username: p.Name,
|
Remark: strVal(p.Remark),
|
||||||
ConPass: p.SecretCiphertext,
|
AuthId: strVal(p.AuthId),
|
||||||
Quota: p.QuotaBytes,
|
QuotaBytes: int64Val(p.QuotaBytes),
|
||||||
Download: p.DownloadBytes,
|
DownloadBytes: int64Val(p.DownloadBytes),
|
||||||
Upload: p.UploadBytes,
|
UploadBytes: int64Val(p.UploadBytes),
|
||||||
ExpireTime: p.ExpiresAt,
|
ExpiresAt: int64Val(p.ExpiresAt),
|
||||||
KickUtilTime: p.BannedUntil,
|
MaxDevices: int64Val(p.MaxDevices),
|
||||||
DeviceNo: p.MaxDevices,
|
Disabled: int64Val(p.Disabled),
|
||||||
Role: &role,
|
BannedUntil: int64Val(p.BannedUntil),
|
||||||
Deleted: p.Disabled,
|
LastConnectionAt: int64Val(p.LastConnectionAt),
|
||||||
ConAt: p.LastConnectionAt,
|
|
||||||
Remark: p.Remark,
|
|
||||||
}
|
}
|
||||||
accounts = append(accounts, acc)
|
if v, ok := onlineUsers[item.AuthId]; ok {
|
||||||
|
item.Online = true
|
||||||
|
item.OnlineDevices = v
|
||||||
}
|
}
|
||||||
return accounts, total, nil
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
return result, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SavePeer(account entity.Account) error {
|
func CreatePeer(peerDto dto.PeerSaveDto) (vo.PeerVo, error) {
|
||||||
if account.Username == nil || *account.Username == "" {
|
if peerDto.Name == nil || *peerDto.Name == "" {
|
||||||
return errors.New(constant.InvalidError)
|
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 := ""
|
secret := ""
|
||||||
if account.ConPass != nil && *account.ConPass != "" {
|
if peerDto.Secret != nil && *peerDto.Secret != "" {
|
||||||
secret = *account.ConPass
|
secret = *peerDto.Secret
|
||||||
} else {
|
} else {
|
||||||
generated, genErr := util.RandomString(24)
|
generated, err := util.RandomString(24)
|
||||||
if genErr != nil {
|
if err != nil {
|
||||||
return genErr
|
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)
|
authId, err := util.RandomString(18)
|
||||||
if authErr != nil {
|
if err != nil {
|
||||||
return authErr
|
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{
|
peer := entity.Peer{
|
||||||
Name: account.Username,
|
Name: peerDto.Name,
|
||||||
Remark: account.Remark,
|
Remark: peerDto.Remark,
|
||||||
AuthId: &authId,
|
AuthId: &authId,
|
||||||
SecretDigest: &secretDigest,
|
SecretDigest: &secretDigest,
|
||||||
SecretCiphertext: &secret,
|
SecretEncrypted: &secretEncrypted,
|
||||||
QuotaBytes: account.Quota,
|
QuotaBytes: peerDto.QuotaBytes,
|
||||||
ExpiresAt: account.ExpireTime,
|
ExpiresAt: peerDto.ExpiresAt,
|
||||||
MaxDevices: account.DeviceNo,
|
MaxDevices: peerDto.MaxDevices,
|
||||||
Disabled: account.Deleted,
|
Disabled: peerDto.Disabled,
|
||||||
}
|
}
|
||||||
_, err := dao.SavePeer(peer)
|
id, saveErr := dao.SavePeer(peer)
|
||||||
return err
|
if saveErr != nil {
|
||||||
|
return vo.PeerVo{}, saveErr
|
||||||
|
}
|
||||||
|
return GetPeerVo(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeletePeer(ids []int64) error {
|
func UpdatePeer(id int64, peerDto dto.PeerUpdateDto) error {
|
||||||
return dao.DeletePeer(ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdatePeer(account entity.Account) error {
|
|
||||||
updates := map[string]interface{}{}
|
updates := map[string]interface{}{}
|
||||||
if account.Username != nil && *account.Username != "" {
|
if peerDto.Name != nil && *peerDto.Name != "" {
|
||||||
updates["username"] = *account.Username
|
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 peerDto.Secret != nil && *peerDto.Secret != "" {
|
||||||
|
digest, err := PeerSecretDigest(*peerDto.Secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == constant.WrongPassword {
|
return err
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
enc, err := EncryptPeerSecret(*peerDto.Secret)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return true
|
updates["secret_digest"] = digest
|
||||||
|
updates["secret_ciphertext"] = enc
|
||||||
|
}
|
||||||
|
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) {
|
func DeletePeer(id int64) error { return dao.DeletePeer([]int64{id}) }
|
||||||
peer, err := dao.GetPeer("id = ?", id)
|
|
||||||
|
func GetPeerVo(id int64) (vo.PeerVo, error) {
|
||||||
|
p, err := dao.GetPeer("id = ?", id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entity.Account{}, err
|
return vo.PeerVo{}, err
|
||||||
}
|
}
|
||||||
role := "user"
|
return vo.PeerVo{
|
||||||
return entity.Account{
|
BaseVo: vo.BaseVo{Id: *p.Id, CreateTime: *p.CreateTime},
|
||||||
BaseEntity: peer.BaseEntity,
|
Name: strVal(p.Name),
|
||||||
Username: peer.Name,
|
Remark: strVal(p.Remark),
|
||||||
ConPass: peer.SecretCiphertext,
|
AuthId: strVal(p.AuthId),
|
||||||
Quota: peer.QuotaBytes,
|
QuotaBytes: int64Val(p.QuotaBytes),
|
||||||
Download: peer.DownloadBytes,
|
DownloadBytes: int64Val(p.DownloadBytes),
|
||||||
Upload: peer.UploadBytes,
|
UploadBytes: int64Val(p.UploadBytes),
|
||||||
ExpireTime: peer.ExpiresAt,
|
ExpiresAt: int64Val(p.ExpiresAt),
|
||||||
DeviceNo: peer.MaxDevices,
|
MaxDevices: int64Val(p.MaxDevices),
|
||||||
KickUtilTime: peer.BannedUntil,
|
Disabled: int64Val(p.Disabled),
|
||||||
ConAt: peer.LastConnectionAt,
|
BannedUntil: int64Val(p.BannedUntil),
|
||||||
Deleted: peer.Disabled,
|
LastConnectionAt: int64Val(p.LastConnectionAt),
|
||||||
Remark: peer.Remark,
|
|
||||||
Role: &role,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAdminAccount(id int64) (entity.AdminUser, error) {
|
func ResetPeerTraffic(id int64) error { return dao.UpdatePeer([]int64{id}, map[string]interface{}{"download_bytes": 0, "upload_bytes": 0}) }
|
||||||
return dao.GetAdminUser("id = ?", id)
|
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")
|
peers, err := dao.ListPeer("1=1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New(constant.SysError)
|
return nil, errors.New(constant.SysError)
|
||||||
}
|
}
|
||||||
var accountExports []bo.AccountExport
|
out := make([]bo.PeerExport, 0, len(peers))
|
||||||
for _, item := range peers {
|
for _, item := range peers {
|
||||||
role := "user"
|
ex := bo.PeerExport{
|
||||||
conPass := ""
|
Id: int64Val(item.Id),
|
||||||
if item.SecretCiphertext != nil {
|
Name: strVal(item.Name),
|
||||||
conPass = *item.SecretCiphertext
|
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{
|
if includeSecrets && item.SecretEncrypted != nil {
|
||||||
Id: *item.Id,
|
if dec, derr := DecryptPeerSecret(*item.SecretEncrypted); derr == nil {
|
||||||
Username: *item.Name,
|
ex.Secret = dec
|
||||||
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
|
out = append(out, ex)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReleaseKickPeer(id int64) error {
|
func UpsertPeerLegacy(accounts []entity.Account) error {
|
||||||
return dao.UpdatePeer([]int64{id}, map[string]interface{}{"banned_until": 0})
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpsertPeer(accounts []entity.Account) error {
|
|
||||||
for _, account := range accounts {
|
for _, account := range accounts {
|
||||||
if account.Id != nil && *account.Id > 0 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
continue
|
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 err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAdminInfo(c *gin.Context) (vo.AccountInfoVo, error) {
|
func ExistPeerName(name string, id int64) bool {
|
||||||
myClaims, err := ParseToken(GetToken(c))
|
var err error
|
||||||
if err != nil {
|
if id != 0 {
|
||||||
return vo.AccountInfoVo{}, err
|
_, err = dao.GetPeer("name = ? and id != ?", name, id)
|
||||||
|
} else {
|
||||||
|
_, err = dao.GetPeer("name = ?", name)
|
||||||
}
|
}
|
||||||
if myClaims.Admin.Deleted != 0 {
|
return err == nil
|
||||||
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 {
|
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 UpdateAdminLastLoginAt(id int64, loginAt int64) error {
|
func strVal(v *string) string { if v == nil { return "" }; return *v }
|
||||||
return dao.UpdateAdminUser([]int64{id}, map[string]interface{}{"last_login_at": loginAt})
|
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hy2xs-admin/dao"
|
||||||
|
"hy2xs-admin/model/constant"
|
||||||
|
"hy2xs-admin/model/entity"
|
||||||
|
"hy2xs-admin/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getOrCreateConfigKey(key string, size int) (string, error) {
|
||||||
|
cfg, err := dao.GetConfig("key = ?", key)
|
||||||
|
if err == nil && cfg.Value != nil && strings.TrimSpace(*cfg.Value) != "" {
|
||||||
|
return strings.TrimSpace(*cfg.Value), nil
|
||||||
|
}
|
||||||
|
raw, genErr := util.RandomString(size)
|
||||||
|
if genErr != nil {
|
||||||
|
return "", genErr
|
||||||
|
}
|
||||||
|
value := raw
|
||||||
|
remark := key
|
||||||
|
if _, saveErr := dao.SaveConfig(entity.Config{Key: &key, Value: &value, Remark: &remark}); saveErr != nil {
|
||||||
|
if updErr := dao.UpdateConfig([]string{key}, map[string]interface{}{"value": value}); updErr != nil {
|
||||||
|
return "", updErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPeerSecretKey() (string, error) {
|
||||||
|
return getOrCreateConfigKey(constant.PeerSecretKey, 48)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPeerSecretEncryptionKey() ([]byte, error) {
|
||||||
|
raw, err := getOrCreateConfigKey(constant.PeerSecretEncryptionKey, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
decoded, decErr := util.DecodeBase64Key(raw, 32)
|
||||||
|
if decErr == nil {
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||
|
// legacy/plain bootstrap path: convert to stable base64 once
|
||||||
|
plain := []byte(strings.TrimSpace(raw))
|
||||||
|
if len(plain) < 32 {
|
||||||
|
return nil, errors.New("invalid peer secret encryption key")
|
||||||
|
}
|
||||||
|
plain = plain[:32]
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(plain)
|
||||||
|
if updErr := dao.UpdateConfig([]string{constant.PeerSecretEncryptionKey}, map[string]interface{}{"value": encoded}); updErr != nil {
|
||||||
|
return nil, updErr
|
||||||
|
}
|
||||||
|
return plain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func PeerSecretDigest(rawSecret string) (string, error) {
|
||||||
|
secretKey, err := GetPeerSecretKey()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return util.HmacSHA256Hex(rawSecret, secretKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncryptPeerSecret(rawSecret string) (string, error) {
|
||||||
|
key, err := getPeerSecretEncryptionKey()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return util.EncryptAESGCM(rawSecret, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecryptPeerSecret(stored string) (string, error) {
|
||||||
|
if !strings.HasPrefix(stored, "v1:") {
|
||||||
|
return stored, nil
|
||||||
|
}
|
||||||
|
key, err := getPeerSecretEncryptionKey()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return util.DecryptAESGCM(stored, key)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@@ -56,6 +61,69 @@ func HmacSHA256Hex(payload string, secret string) string {
|
|||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func DecodeBase64Key(raw string, expectedLen int) ([]byte, error) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return nil, errors.New("empty key")
|
||||||
|
}
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(decoded) != expectedLen {
|
||||||
|
return nil, fmt.Errorf("invalid key length: expected %d, got %d", expectedLen, len(decoded))
|
||||||
|
}
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncryptAESGCM(plainText string, key []byte) (string, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
cipherText := gcm.Seal(nil, nonce, []byte(plainText), nil)
|
||||||
|
payload := append(nonce, cipherText...)
|
||||||
|
return "v1:" + base64.StdEncoding.EncodeToString(payload), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecryptAESGCM(cipherText string, key []byte) (string, error) {
|
||||||
|
if !strings.HasPrefix(cipherText, "v1:") {
|
||||||
|
return "", errors.New("unsupported ciphertext version")
|
||||||
|
}
|
||||||
|
encoded := strings.TrimPrefix(cipherText, "v1:")
|
||||||
|
payload, err := base64.StdEncoding.DecodeString(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
nonceSize := gcm.NonceSize()
|
||||||
|
if len(payload) < nonceSize {
|
||||||
|
return "", errors.New("invalid ciphertext")
|
||||||
|
}
|
||||||
|
nonce, enc := payload[:nonceSize], payload[nonceSize:]
|
||||||
|
plain, err := gcm.Open(nil, nonce, enc, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(plain), nil
|
||||||
|
}
|
||||||
|
|
||||||
func PeerSecretDigest(rawSecret string) string {
|
func PeerSecretDigest(rawSecret string) string {
|
||||||
secretKey := strings.TrimSpace(os.Getenv("HY2XS_PEER_SECRET_KEY"))
|
secretKey := strings.TrimSpace(os.Getenv("HY2XS_PEER_SECRET_KEY"))
|
||||||
if secretKey == "" {
|
if secretKey == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user