package controller import ( "encoding/json" "fmt" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "hy2xs-admin/model/constant" "hy2xs-admin/model/dto" "hy2xs-admin/model/entity" "hy2xs-admin/model/vo" "hy2xs-admin/service" "hy2xs-admin/util" "io" "path/filepath" "strconv" "strings" "time" ) func resolveID(c *gin.Context) (int64, error) { if raw := strings.TrimSpace(c.Param("id")); raw != "" { parsed, err := strconv.ParseInt(raw, 10, 64) if err == nil && parsed > 0 { return parsed, nil } } idDto, err := validateField(c, dto.IdDto{}) if err != nil { return 0, err } return *idDto.Id, nil } func Login(c *gin.Context) { loginDto, err := validateField(c, dto.LoginDto{}) if err != nil { return } token, forcePasswordChange, err := service.Login(*loginDto.Username, *loginDto.Pass) if err != nil { vo.Fail(err.Error(), c) return } jwtVo := vo.JwtVo{ TokenType: constant.TokenType, AccessToken: token, ForcePasswordChange: forcePasswordChange, } vo.Success(jwtVo, c) } func PagePeer(c *gin.Context) { peerPageDto, err := validateField(c, dto.PeerPageDto{}) if err != nil { return } accounts, total, err := service.PagePeer(peerPageDto) if err != nil { vo.Fail(err.Error(), c) return } 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) { accountSaveDto, err := validateField(c, dto.AccountSaveDto{}) if err != nil { return } 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 { vo.Fail(err.Error(), c) return } conPass := fmt.Sprintf("%s.%s", *accountSaveDto.Username, *accountSaveDto.ConPass) 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) { id, err := resolveID(c) if err != nil { return } account, err := service.GetPeer(id) 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) return } vo.Success(nil, c) } func UpdatePeer(c *gin.Context) { accountUpdateDto, err := validateField(c, dto.AccountUpdateDto{}) 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 } var body map[string]interface{} 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 { vo.Fail(err.Error(), c) return } if *account.Role == "admin" { vo.Fail("the admin account cannot be deleted", c) return } } 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) return } vo.Success(nil, c) } func ResetTraffic(c *gin.Context) { id, err := resolveID(c) if err != nil { return } if err = service.ResetTraffic(id); err != nil { vo.Fail(err.Error(), c) return } 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) { id, err := resolveID(c) if err != nil { return } account, err := service.GetPeer(id) if err != nil { vo.Fail(err.Error(), c) return } accountVo := vo.AccountVo{ 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) { file, header, err := c.Request.FormFile("file") if err != nil { vo.Fail(constant.SysError, c) return } // Размер файла 2 MB if header.Size > 1024*1024*2 { vo.Fail("the file is too big", c) return } // Расширение файла .json if !strings.HasSuffix(header.Filename, ".json") { vo.Fail(constant.InvalidError, c) return } content, err := io.ReadAll(file) if err != nil { vo.Fail("json file read err", c) return } var accounts []entity.Account if err = json.Unmarshal(content, &accounts); err != nil { vo.Fail("content Unmarshal err", c) return } if err = service.UpsertPeer(accounts); err != nil { vo.Fail(err.Error(), c) return } vo.Success(nil, c) } func ExportPeer(c *gin.Context) { accountExports, err := service.ListExportPeer() if err != nil { vo.Fail(err.Error(), c) return } fileName := fmt.Sprintf("AccountExport-%s.json", time.Now().Format("20060102150405")) filePath := filepath.Join(constant.ExportPathDir, fileName) if err = util.ExportFile(filePath, accountExports, 0); err != nil { vo.Fail(err.Error(), c) return } // Скачивание if !util.Exists(filePath) { vo.Fail("file not exist", c) return } c.Header("Content-Type", "application/octet-stream") c.Header("Content-Transfer-Encoding", "binary") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName)) c.File(filePath) } func ReleaseKickPeer(c *gin.Context) { id, err := resolveID(c) if err != nil { return } if err = service.ReleaseKickPeer(id); err != nil { logrus.Debugf("release kick err: %v", err) vo.Fail(err.Error(), c) return } vo.Success(nil, c) } func VerifyDefaultPass(c *gin.Context) { info, err := service.GetAdminInfo(c) if err != nil { vo.Fail(err.Error(), c) return } admin, err := service.GetAdminAccount(info.Id) if err != nil { vo.Fail(err.Error(), c) return } vo.Success(admin.PasswordHash != nil && !util.IsBcryptHash(*admin.PasswordHash), c) }