0d84b07f68
CI / docker (push) Successful in 1m58s
Go + Vue 管理台:Agent 隧道、域名反代、IP 安全、访客验证与监控大盘。 Gitea Actions CI 多架构镜像;纯 Go SQLite、无 CGO Docker 构建。 Co-authored-by: Cursor <cursoragent@cursor.com>
205 lines
5.0 KiB
Go
205 lines
5.0 KiB
Go
package store
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type PageParams struct {
|
|
Page int
|
|
PageSize int
|
|
Q string
|
|
}
|
|
|
|
func NormalizePage(p PageParams) (offset, limit int) {
|
|
if p.Page < 1 {
|
|
p.Page = 1
|
|
}
|
|
if p.PageSize < 1 {
|
|
p.PageSize = 20
|
|
}
|
|
if p.PageSize > 200 {
|
|
p.PageSize = 200
|
|
}
|
|
return (p.Page - 1) * p.PageSize, p.PageSize
|
|
}
|
|
|
|
func tunnelOwnerScope(db *gorm.DB, clientID, ownerID uint, isAdmin bool) *gorm.DB {
|
|
q := db.Model(&Tunnel{})
|
|
if clientID > 0 {
|
|
return q.Where("client_id = ?", clientID)
|
|
}
|
|
if !isAdmin {
|
|
var ids []uint
|
|
db.Model(&Client{}).Where("owner_user_id = ?", ownerID).Pluck("id", &ids)
|
|
if len(ids) == 0 {
|
|
return q.Where("1 = 0")
|
|
}
|
|
return q.Where("client_id IN ?", ids)
|
|
}
|
|
return q
|
|
}
|
|
|
|
func hostOwnerScope(db *gorm.DB, clientID, ownerID uint, isAdmin bool) *gorm.DB {
|
|
q := db.Model(&Host{})
|
|
if clientID > 0 {
|
|
return q.Where("client_id = ?", clientID)
|
|
}
|
|
if !isAdmin {
|
|
var ids []uint
|
|
db.Model(&Client{}).Where("owner_user_id = ?", ownerID).Pluck("id", &ids)
|
|
if len(ids) == 0 {
|
|
return q.Where("1 = 0")
|
|
}
|
|
return q.Where("client_id IN ?", ids)
|
|
}
|
|
return q
|
|
}
|
|
|
|
func applyTunnelSearch(q *gorm.DB, keyword string) *gorm.DB {
|
|
if keyword == "" {
|
|
return q
|
|
}
|
|
like := "%" + keyword + "%"
|
|
portLike := "%" + keyword + "%"
|
|
return q.Where(
|
|
"name LIKE ? OR target_addr LIKE ? OR CAST(listen_port AS TEXT) LIKE ?",
|
|
like, like, portLike,
|
|
)
|
|
}
|
|
|
|
func applyHostSearch(q *gorm.DB, keyword string) *gorm.DB {
|
|
if keyword == "" {
|
|
return q
|
|
}
|
|
like := "%" + keyword + "%"
|
|
return q.Where("host LIKE ? OR target_addr LIKE ? OR location LIKE ?", like, like, like)
|
|
}
|
|
|
|
func (s *Store) ListTunnelsPaged(clientID, ownerID uint, isAdmin bool, p PageParams) ([]Tunnel, int64, error) {
|
|
var tunnels []Tunnel
|
|
var total int64
|
|
q := tunnelOwnerScope(s.db, clientID, ownerID, isAdmin)
|
|
q = applyTunnelSearch(q, p.Q)
|
|
if err := q.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
offset, limit := NormalizePage(p)
|
|
err := q.Order("id asc").Offset(offset).Limit(limit).Find(&tunnels).Error
|
|
return tunnels, total, err
|
|
}
|
|
|
|
func (s *Store) HostRouteConflict(host, location string, excludeHostID uint) error {
|
|
if location == "" {
|
|
location = "/"
|
|
}
|
|
inUse, err := s.HostRouteInUse(host, location, excludeHostID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if inUse {
|
|
return fmt.Errorf("域名与路径前缀已被穿透域名占用")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) ListHostsPaged(clientID, ownerID uint, isAdmin bool, p PageParams) ([]Host, int64, error) {
|
|
var hosts []Host
|
|
var total int64
|
|
q := hostOwnerScope(s.db, clientID, ownerID, isAdmin)
|
|
q = applyHostSearch(q, p.Q)
|
|
if err := q.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
offset, limit := NormalizePage(p)
|
|
err := q.Order("id asc").Offset(offset).Limit(limit).Find(&hosts).Error
|
|
return hosts, total, err
|
|
}
|
|
|
|
func (s *Store) TunnelPortInUse(port int, excludeID uint) (bool, error) {
|
|
if port <= 0 {
|
|
return false, fmt.Errorf("invalid port")
|
|
}
|
|
q := s.db.Model(&Tunnel{}).Where("listen_port = ?", port)
|
|
if excludeID > 0 {
|
|
q = q.Where("id <> ?", excludeID)
|
|
}
|
|
var count int64
|
|
if err := q.Count(&count).Error; err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func (s *Store) HostRouteInUse(host, location string, excludeID uint) (bool, error) {
|
|
if host == "" {
|
|
return false, fmt.Errorf("host required")
|
|
}
|
|
if location == "" {
|
|
location = "/"
|
|
}
|
|
q := s.db.Model(&Host{}).Where("host = ? AND location = ?", host, location)
|
|
if excludeID > 0 {
|
|
q = q.Where("id <> ?", excludeID)
|
|
}
|
|
var count int64
|
|
if err := q.Count(&count).Error; err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func (s *Store) ListTunnelsByFlow(limit, offset int) ([]Tunnel, int64, error) {
|
|
var tunnels []Tunnel
|
|
var total int64
|
|
q := s.db.Model(&Tunnel{})
|
|
if err := q.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if limit <= 0 {
|
|
limit = 10
|
|
}
|
|
err := q.Order("(inlet_flow + export_flow) desc").Offset(offset).Limit(limit).Find(&tunnels).Error
|
|
return tunnels, total, err
|
|
}
|
|
|
|
func (s *Store) ListHostsByFlow(limit, offset int) ([]Host, int64, error) {
|
|
var hosts []Host
|
|
var total int64
|
|
q := s.db.Model(&Host{})
|
|
if err := q.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if limit <= 0 {
|
|
limit = 10
|
|
}
|
|
err := q.Order("(inlet_flow + export_flow) desc").Offset(offset).Limit(limit).Find(&hosts).Error
|
|
return hosts, total, err
|
|
}
|
|
|
|
func (s *Store) ListAllRequestIPs(limit, offset int, qstr string) ([]RequestIP, int64, error) {
|
|
var items []RequestIP
|
|
var total int64
|
|
q := s.db.Model(&RequestIP{})
|
|
if qstr != "" {
|
|
like := "%" + qstr + "%"
|
|
q = q.Where("ip LIKE ? OR direct_ip LIKE ?", like, like)
|
|
}
|
|
if err := q.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
err := q.Order("hit_count desc").Offset(offset).Limit(limit).Find(&items).Error
|
|
return items, total, err
|
|
}
|
|
|
|
func ParsePageQuery(pageStr, pageSizeStr, q string) PageParams {
|
|
page, _ := strconv.Atoi(pageStr)
|
|
pageSize, _ := strconv.Atoi(pageSizeStr)
|
|
return PageParams{Page: page, PageSize: pageSize, Q: q}
|
|
}
|