mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-28 17:44:56 +08:00
feat: 升级 libtnb/validator v0.2.1,用上新校验能力
- 切片字段加 unique 防重复配置(域名/监听/IP/白名单/网卡/磁盘设备/ NTP 服务器/应用标识/压缩路径/TLS 版本/索引/定时任务目标) - 网站统计日期范围 End 加 after_or_equal:Start(结束不早于开始) - 新增 tags_test.go:CheckRules 体检关键请求 + 全包 validate tag 编译校验,坏规则名/语法错/未注册自定义规则在测试期即报错 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,7 +43,7 @@ require (
|
||||
github.com/libtnb/sessions v1.4.2
|
||||
github.com/libtnb/sqlite v1.1.2
|
||||
github.com/libtnb/utils v1.2.2
|
||||
github.com/libtnb/validator v0.1.1
|
||||
github.com/libtnb/validator v0.2.1
|
||||
github.com/medama-io/go-useragent v1.2.3
|
||||
github.com/mholt/acmez/v3 v3.1.6
|
||||
github.com/moby/moby/api v1.55.0
|
||||
|
||||
@@ -288,8 +288,8 @@ github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7 h1:ZzONsNhw69uC/wJQ
|
||||
github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7/go.mod h1:HeQeTfKU6tj2Lx1z79UacwYeDioo6M4ZD7BDDI6+rrg=
|
||||
github.com/libtnb/utils v1.2.2 h1:HUqgAIb+TIrZPXRytYB5XqzkEXG2U4a4/ZGpprNtL5A=
|
||||
github.com/libtnb/utils v1.2.2/go.mod h1:KUCx2+Phw3cLXOwuQDy6XwFOliaYuU1rpAuJQuHf+lQ=
|
||||
github.com/libtnb/validator v0.1.1 h1:8D3YkPGR9N2Ildb2PeApvgzCgIfKznEb6cTHWPskmNs=
|
||||
github.com/libtnb/validator v0.1.1/go.mod h1:j08Lydxwy7ModkoKeYIHxvZvAtnzzdE8xi2J+SGVrgc=
|
||||
github.com/libtnb/validator v0.2.1 h1:ZbFx7bEXB9ri0Eb7p81LOSAq0M+VrizDR4aD06TcCvg=
|
||||
github.com/libtnb/validator v0.2.1/go.mod h1:j08Lydxwy7ModkoKeYIHxvZvAtnzzdE8xi2J+SGVrgc=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
|
||||
@@ -19,5 +19,5 @@ type AppUpdateShow struct {
|
||||
}
|
||||
|
||||
type AppUpdateOrder struct {
|
||||
Slugs []string `json:"slugs" form:"slugs" validate:"required"`
|
||||
Slugs []string `json:"slugs" form:"slugs" validate:"required && unique"`
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ type CertUpload struct {
|
||||
|
||||
type CertCreate struct {
|
||||
Type string `form:"type" json:"type" validate:"required && in:P256,P384,2048,3072,4096"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"required && dive && required"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"required && unique && dive && required"`
|
||||
Alias map[string]string `form:"alias" json:"alias"`
|
||||
AutoRenewal bool `form:"auto_renewal" json:"auto_renewal"`
|
||||
AccountID uint `form:"account_id" json:"account_id"`
|
||||
@@ -18,7 +18,7 @@ type CertCreate struct {
|
||||
type CertUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required && exists:certs,id"`
|
||||
Type string `form:"type" json:"type" validate:"required && in:P256,P384,2048,3072,4096,upload"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"required && dive && required"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"required && unique && dive && required"`
|
||||
Alias map[string]string `form:"alias" json:"alias"`
|
||||
Cert string `form:"cert" json:"cert"`
|
||||
Key string `form:"key" json:"key"`
|
||||
|
||||
@@ -8,7 +8,7 @@ type CronCreate struct {
|
||||
SubType string `form:"sub_type" json:"sub_type" validate:"required_if:Type,backup,cutoff"`
|
||||
Flock bool `form:"flock" json:"flock"`
|
||||
Storage uint `form:"storage" json:"storage"`
|
||||
Targets []string `form:"targets" json:"targets" validate:"required_if:Type,backup,cutoff"`
|
||||
Targets []string `form:"targets" json:"targets" validate:"required_if:Type,backup,cutoff && unique"`
|
||||
Keep uint `form:"keep" json:"keep" validate:"required"`
|
||||
URL string `form:"url" json:"url"`
|
||||
Method string `form:"method" json:"method"`
|
||||
|
||||
@@ -66,7 +66,7 @@ type FilePermission struct {
|
||||
|
||||
type FileCompress struct {
|
||||
Dir string `form:"dir" json:"dir" validate:"required && unix_path"`
|
||||
Paths []string `form:"paths" json:"paths" validate:"required && dive && required"`
|
||||
Paths []string `form:"paths" json:"paths" validate:"required && unique && dive && required"`
|
||||
File string `form:"file" json:"file" validate:"required && unix_path"`
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ package request
|
||||
type FirewallScanSetting struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Days uint `json:"days" validate:"min:1 && max:365"`
|
||||
Interfaces []string `json:"interfaces"`
|
||||
Interfaces []string `json:"interfaces" validate:"unique"`
|
||||
AutoBlock bool `json:"auto_block"`
|
||||
BlockThreshold uint `json:"block_threshold" validate:"min:1 && max:100000"`
|
||||
BlockWindow uint `json:"block_window" validate:"min:1 && max:1440"`
|
||||
BlockDuration uint `json:"block_duration" validate:"max:87600"`
|
||||
Whitelist []string `json:"whitelist" validate:"dive && ipcidr"`
|
||||
Whitelist []string `json:"whitelist" validate:"unique && dive && ipcidr"`
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type SettingPanel struct {
|
||||
Lifetime uint `json:"lifetime" validate:"required && min:10 && max:43200"` // 登录超时,单位:分
|
||||
IPHeader string `json:"ip_header"`
|
||||
BindDomain []string `json:"bind_domain"`
|
||||
BindIP []string `json:"bind_ip" validate:"dive && ipcidr"`
|
||||
BindIP []string `json:"bind_ip" validate:"unique && dive && ipcidr"`
|
||||
BindUA []string `json:"bind_ua"`
|
||||
WebsitePath string `json:"website_path" validate:"required"`
|
||||
BackupPath string `json:"backup_path" validate:"required"`
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/libtnb/validator"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/http/rule"
|
||||
)
|
||||
|
||||
// tagValidator mirrors service.NewValidator's rule set. db is nil: CheckRules and
|
||||
// AddRules only compile expressions, they never invoke a rule's Passes or touch
|
||||
// the database.
|
||||
func tagValidator() *validator.Validator {
|
||||
v := validator.NewValidator(validator.WithStrictRequired())
|
||||
rule.RegisterRules(v, nil)
|
||||
return v
|
||||
}
|
||||
|
||||
// TestCheckRulesKeyRequests runs the validator's own tag checker over the
|
||||
// requests with the richest rules (custom rules, dive, cross-field, and the
|
||||
// v0.2 additions unique / after_or_equal), catching bad tags at test time.
|
||||
func TestCheckRulesKeyRequests(t *testing.T) {
|
||||
v := tagValidator()
|
||||
for _, req := range []any{
|
||||
&WebsiteCreate{}, &WebsiteUpdate{}, &WebsiteDefaultConfig{},
|
||||
&CertCreate{}, &CertUpdate{},
|
||||
&FileCompress{}, &FilePermission{},
|
||||
&SettingPanel{}, &UserTokenCreate{}, &FirewallScanSetting{},
|
||||
&WebsiteStatDateRange{}, &BackupCreate{},
|
||||
} {
|
||||
if err := v.CheckRules(req); err != nil {
|
||||
t.Errorf("%T: %v", req, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateTagsCompile is the whole-package backstop: every validate tag in
|
||||
// this package must compile against the real validator, so a typo'd rule name, a
|
||||
// DSL syntax error, or an unregistered custom rule fails the build. This is the
|
||||
// engine-backed successor to the ad-hoc checktag used during the gookit migration
|
||||
// (which, being a plain tag scanner, could not see the Rules() maps that hid the
|
||||
// Domains.* bug, nor tell whether a custom rule was registered).
|
||||
func TestValidateTagsCompile(t *testing.T) {
|
||||
v := tagValidator()
|
||||
fset := token.NewFileSet()
|
||||
files, err := filepath.Glob("*.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seen := 0
|
||||
for _, file := range files {
|
||||
if strings.HasSuffix(file, "_test.go") {
|
||||
continue
|
||||
}
|
||||
f, err := parser.ParseFile(fset, file, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
field, ok := n.(*ast.Field)
|
||||
if !ok || field.Tag == nil {
|
||||
return true
|
||||
}
|
||||
expr := reflect.StructTag(strings.Trim(field.Tag.Value, "`")).Get("validate")
|
||||
if strings.TrimSpace(expr) == "" {
|
||||
return true
|
||||
}
|
||||
name := "field"
|
||||
if len(field.Names) > 0 {
|
||||
name = field.Names[0].Name
|
||||
}
|
||||
seen++
|
||||
if err := v.Map(map[string]any{}, nil).AddRules(name, expr); err != nil {
|
||||
t.Errorf("%s: %s `validate:%q`: %v", file, name, expr, err)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
if seen == 0 {
|
||||
t.Fatal("no validate tags found — the scanner is broken")
|
||||
}
|
||||
t.Logf("compiled %d validate tags", seen)
|
||||
}
|
||||
@@ -27,7 +27,7 @@ type ToolboxDiskFormat struct {
|
||||
// ToolboxDiskVG 卷组请求
|
||||
type ToolboxDiskVG struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Devices []string `form:"devices" json:"devices" validate:"required"`
|
||||
Devices []string `form:"devices" json:"devices" validate:"required && unique"`
|
||||
}
|
||||
|
||||
// ToolboxDiskLV 逻辑卷请求
|
||||
|
||||
@@ -36,5 +36,5 @@ type ToolboxSystemSyncTime struct {
|
||||
}
|
||||
|
||||
type ToolboxSystemNTPServers struct {
|
||||
Servers []string `form:"servers" json:"servers" validate:"required"`
|
||||
Servers []string `form:"servers" json:"servers" validate:"required && unique"`
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ type UserTokenList struct {
|
||||
|
||||
type UserTokenCreate struct {
|
||||
UserID uint `json:"user_id" validate:"required && exists:users,id"`
|
||||
IPs []string `json:"ips" validate:"dive && ipcidr"`
|
||||
IPs []string `json:"ips" validate:"unique && dive && ipcidr"`
|
||||
ExpiredAt int64 `json:"expired_at" validate:"required"`
|
||||
}
|
||||
|
||||
type UserTokenUpdate struct {
|
||||
ID uint `uri:"id"`
|
||||
IPs []string `json:"ips" validate:"dive && ipcidr"`
|
||||
IPs []string `json:"ips" validate:"unique && dive && ipcidr"`
|
||||
ExpiredAt int64 `json:"expired_at" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ type WebsiteDefaultConfig struct {
|
||||
Index string `json:"index" form:"index" validate:"required"`
|
||||
Stop string `json:"stop" form:"stop" validate:"required"`
|
||||
NotFound string `json:"not_found" form:"not_found"`
|
||||
TLSVersions []string `json:"tls_versions" form:"tls_versions" validate:"required && dive && required"`
|
||||
TLSVersions []string `json:"tls_versions" form:"tls_versions" validate:"required && unique && dive && required"`
|
||||
}
|
||||
|
||||
type WebsiteList struct {
|
||||
@@ -19,8 +19,8 @@ type WebsiteList struct {
|
||||
type WebsiteCreate struct {
|
||||
Type string `json:"type" form:"type" validate:"required && in:proxy,static,php"`
|
||||
Name string `form:"name" json:"name" validate:"required && not_exists:websites,name && not_in:phpmyadmin,default && regex:\"^[a-zA-Z0-9_-]+$\""`
|
||||
Listens []string `form:"listens" json:"listens" validate:"required && dive && required"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"required && dive && required"`
|
||||
Listens []string `form:"listens" json:"listens" validate:"required && unique && dive && required"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"required && unique && dive && required"`
|
||||
Path string `form:"path" json:"path"`
|
||||
DB bool `form:"db" json:"db"`
|
||||
DBType string `form:"db_type" json:"db_type" validate:"required_if:DB,true"`
|
||||
@@ -42,10 +42,10 @@ type WebsiteDelete struct {
|
||||
type WebsiteUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required && exists:websites,id"`
|
||||
Listens []types.Listen `form:"listens" json:"listens" validate:"required && dive && required"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"required && dive && required"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"required && unique && dive && required"`
|
||||
Path string `form:"path" json:"path" validate:"required && unix_path"` // 网站目录
|
||||
Root string `form:"root" json:"root" validate:"required && unix_path"` // 运行目录
|
||||
Index []string `form:"index" json:"index" validate:"required && dive && required"`
|
||||
Index []string `form:"index" json:"index" validate:"required && unique && dive && required"`
|
||||
|
||||
// SSL 相关
|
||||
SSL bool `form:"ssl" json:"ssl"`
|
||||
|
||||
@@ -21,7 +21,7 @@ type WebsiteStatSetting struct {
|
||||
// WebsiteStatDateRange 统计日期范围查询参数
|
||||
type WebsiteStatDateRange struct {
|
||||
Start string `json:"start" form:"start" query:"start" validate:"datetime:2006-01-02"`
|
||||
End string `json:"end" form:"end" query:"end" validate:"datetime:2006-01-02"`
|
||||
End string `json:"end" form:"end" query:"end" validate:"datetime:2006-01-02 && after_or_equal:Start"`
|
||||
Sites string `json:"sites" form:"sites" query:"sites"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user