mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-03 15:06:36 +08:00
refactor(backend): rename OpenFlare directory to lowercase openflare
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package analytics defines ClickHouse analytics domain models and query DTOs
|
||||
// (pure data, no IO).
|
||||
package analytics
|
||||
|
||||
import "time"
|
||||
|
||||
// AccessLogFilter scopes user access log queries.
|
||||
// 单一权威字段集(CH 原字段,Task 1 迁入):禁止追加仅某实现使用的字段(避免双字段集分叉)。
|
||||
type AccessLogFilter struct {
|
||||
// UserIDs filters by user IDs. nil means no user filter; an empty slice means no matches.
|
||||
UserIDs []uint64
|
||||
Path string
|
||||
// StartTime filters created_at >= StartTime when non-nil.
|
||||
StartTime *time.Time
|
||||
// EndTime filters created_at <= EndTime when non-nil(闭区间,与 CH/GORM 实现一致)。
|
||||
EndTime *time.Time
|
||||
}
|
||||
|
||||
// NodeAccessLogFilter scopes ClickHouse node access log queries.
|
||||
type NodeAccessLogFilter struct {
|
||||
NodeID string
|
||||
RemoteAddr string
|
||||
Host string
|
||||
// Hosts exact-matches any host (case-insensitive). Prefer over Host for multi-domain scopes.
|
||||
Hosts []string
|
||||
Path string
|
||||
// StatusCode filters by exact HTTP status code when > 0.
|
||||
StatusCode int
|
||||
Since time.Time
|
||||
Until time.Time
|
||||
Page int
|
||||
PageSize int
|
||||
SortBy string
|
||||
SortOrder string
|
||||
}
|
||||
|
||||
// NodeObservabilityFilter scopes ClickHouse node observability queries.
|
||||
type NodeObservabilityFilter struct {
|
||||
NodeID string
|
||||
Since time.Time
|
||||
Limit int
|
||||
}
|
||||
|
||||
// DailyTrend is a single day's access count.
|
||||
type DailyTrend struct {
|
||||
Date string
|
||||
Count uint64
|
||||
}
|
||||
|
||||
// BrowserShare is a browser group's share of access logs.
|
||||
type BrowserShare struct {
|
||||
Browser string
|
||||
Count uint64
|
||||
}
|
||||
|
||||
// TopUser is an active user ranked by access count.
|
||||
type TopUser struct {
|
||||
UserID uint64
|
||||
Count uint64
|
||||
}
|
||||
|
||||
// NodeAccessLogRegionCount aggregates access log regions.
|
||||
type NodeAccessLogRegionCount struct {
|
||||
Region string
|
||||
Count int64
|
||||
}
|
||||
|
||||
// NodeAccessLogTrafficSummary is a window-level access log traffic summary.
|
||||
type NodeAccessLogTrafficSummary struct {
|
||||
RequestCount int64
|
||||
ErrorCount int64
|
||||
UniqueIPCount int64
|
||||
BytesSent int64
|
||||
RequestLength int64
|
||||
NodeCount int64
|
||||
}
|
||||
|
||||
// NodeAccessLogValueCount is a grouped value count (status_code, host, ...).
|
||||
type NodeAccessLogValueCount struct {
|
||||
Value string
|
||||
Count int64
|
||||
}
|
||||
|
||||
// NodeAccessLogNodeAggregate is per-node traffic over a window.
|
||||
type NodeAccessLogNodeAggregate struct {
|
||||
NodeID string
|
||||
RequestCount int64
|
||||
ErrorCount int64
|
||||
UniqueIPCount int64
|
||||
}
|
||||
|
||||
// BatchWriterStats is a point-in-time snapshot of a batch writer queue and failure counters.
|
||||
type BatchWriterStats struct {
|
||||
Name string `json:"name"`
|
||||
Depth int `json:"depth"`
|
||||
Cap int `json:"cap"`
|
||||
Drops int64 `json:"drops"`
|
||||
FlushErrors int64 `json:"flush_errors"`
|
||||
Running bool `json:"running"`
|
||||
}
|
||||
|
||||
// ClickHouseOperationalStats summarizes ClickHouse merge/mutation pressure
|
||||
// and in-process batch writer queue health.
|
||||
type ClickHouseOperationalStats struct {
|
||||
Database string `json:"database"`
|
||||
ActiveParts int64 `json:"active_parts"`
|
||||
TotalRows int64 `json:"total_rows"`
|
||||
PendingMutations int64 `json:"pending_mutations"`
|
||||
AsyncInsertQueue int64 `json:"async_insert_queue"`
|
||||
AsyncInsertBytes int64 `json:"async_insert_bytes"`
|
||||
// BatchWriters reports in-process queue depth/drops/flush errors for CH writers.
|
||||
BatchWriters []BatchWriterStats `json:"batch_writers,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package analytics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
nodeAccessLogTableName = "of_node_access_logs"
|
||||
nodeAccessLogInsertColumns = "id, node_id, logged_at, remote_addr, region, host, path, user_agent, cache_status, status_code, bytes_sent, request_length, request_time_ms, created_at"
|
||||
)
|
||||
|
||||
// NodeAccessLog stores OpenFlare edge node access records in ClickHouse.
|
||||
type NodeAccessLog struct {
|
||||
ID uint64 `gorm:"column:id"`
|
||||
NodeID string `gorm:"column:node_id"`
|
||||
LoggedAt time.Time `gorm:"column:logged_at"`
|
||||
RemoteAddr string `gorm:"column:remote_addr"`
|
||||
Region string `gorm:"column:region"`
|
||||
Host string `gorm:"column:host"`
|
||||
Path string `gorm:"column:path"`
|
||||
UserAgent string `gorm:"column:user_agent"`
|
||||
CacheStatus string `gorm:"column:cache_status"`
|
||||
StatusCode int32 `gorm:"column:status_code"`
|
||||
BytesSent uint64 `gorm:"column:bytes_sent"`
|
||||
RequestLength uint64 `gorm:"column:request_length"`
|
||||
RequestTimeMs uint32 `gorm:"column:request_time_ms"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
// TableName returns the ClickHouse table name.
|
||||
func (NodeAccessLog) TableName() string {
|
||||
return nodeAccessLogTableName
|
||||
}
|
||||
|
||||
// InsertColumns returns comma-separated column names for batch insert.
|
||||
func (NodeAccessLog) InsertColumns() string {
|
||||
return nodeAccessLogInsertColumns
|
||||
}
|
||||
|
||||
// BatchInsertSQL returns the INSERT prefix used by native batch writers.
|
||||
func (NodeAccessLog) BatchInsertSQL() string {
|
||||
return fmt.Sprintf("INSERT INTO %s (%s)", nodeAccessLogTableName, nodeAccessLogInsertColumns)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package analytics
|
||||
|
||||
// NodeAccessLogBucketAggregate is a folded bucket aggregate row.
|
||||
type NodeAccessLogBucketAggregate struct {
|
||||
BucketEpoch int64 `gorm:"column:bucket_epoch"`
|
||||
RequestCount int64 `gorm:"column:request_count"`
|
||||
SuccessCount int64 `gorm:"column:success_count"`
|
||||
ClientErrorCount int64 `gorm:"column:client_error_count"`
|
||||
ServerErrorCount int64 `gorm:"column:server_error_count"`
|
||||
Status2xxCount int64 `gorm:"column:status_2xx_count"`
|
||||
Status4xxCount int64 `gorm:"column:status_4xx_count"`
|
||||
Status5xxCount int64 `gorm:"column:status_5xx_count"`
|
||||
UniqueIPCount int64 `gorm:"column:unique_ip_count"`
|
||||
UniqueHostCount int64 `gorm:"column:unique_host_count"`
|
||||
BytesSent int64 `gorm:"column:bytes_sent"`
|
||||
RequestLength int64 `gorm:"column:request_length"`
|
||||
}
|
||||
|
||||
// NodeAccessLogWAFIPAggregate is a per-IP aggregate row for WAF automatic rules.
|
||||
type NodeAccessLogWAFIPAggregate struct {
|
||||
RemoteAddr string
|
||||
RequestCount int64
|
||||
Status404Count int64
|
||||
ClientErrorCount int64
|
||||
ServerErrorCount int64
|
||||
IPHostCount int64
|
||||
LastSeenEpoch int64
|
||||
StatusCounts map[int]int64
|
||||
}
|
||||
|
||||
// NodeAccessLogBucketDimension is a bucket dimension value.
|
||||
type NodeAccessLogBucketDimension struct {
|
||||
BucketEpoch int64 `gorm:"column:bucket_epoch"`
|
||||
Value string `gorm:"column:value"`
|
||||
}
|
||||
|
||||
// NodeAccessLogIPAggregate is an IP aggregate row.
|
||||
type NodeAccessLogIPAggregate struct {
|
||||
RemoteAddr string `gorm:"column:remote_addr"`
|
||||
RequestCount int64 `gorm:"column:request_count"`
|
||||
SuccessCount int64 `gorm:"column:success_count"`
|
||||
ClientErrorCount int64 `gorm:"column:client_error_count"`
|
||||
ServerErrorCount int64 `gorm:"column:server_error_count"`
|
||||
LastSeenEpoch int64 `gorm:"column:last_seen_epoch"`
|
||||
}
|
||||
|
||||
// NodeAccessLogIPSummary is an IP summary row.
|
||||
type NodeAccessLogIPSummary struct {
|
||||
RemoteAddr string `gorm:"column:remote_addr"`
|
||||
Region string `gorm:"column:region"`
|
||||
TotalRequests int64 `gorm:"column:total_requests"`
|
||||
Success2xxCount int64 `gorm:"column:success_2xx_count"`
|
||||
SuccessRatio float64 `gorm:"column:success_ratio"`
|
||||
BytesReceived int64 `gorm:"column:request_length"`
|
||||
BytesSent int64 `gorm:"column:bytes_sent"`
|
||||
// RecentRequests is deprecated (always 0); kept for wire compatibility.
|
||||
RecentRequests int64 `gorm:"column:recent_requests"`
|
||||
LastSeenEpoch int64 `gorm:"column:last_seen_epoch"`
|
||||
}
|
||||
|
||||
// NodeAccessLogIPTrend is an IP trend bucket row.
|
||||
type NodeAccessLogIPTrend struct {
|
||||
BucketEpoch int64 `gorm:"column:bucket_epoch"`
|
||||
RequestCount int64 `gorm:"column:request_count"`
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package analytics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
nodeMetricSnapshotTableName = "of_node_metric_snapshots"
|
||||
nodeMetricSnapshotInsertColumns = "id, node_id, captured_at, cpu_usage_percent, memory_used_bytes, memory_total_bytes, storage_used_bytes, storage_total_bytes, disk_read_bytes, disk_write_bytes, network_rx_bytes, network_tx_bytes, created_at"
|
||||
|
||||
nodeEdgeHealthTableName = "of_node_edge_health"
|
||||
nodeEdgeHealthInsertColumns = "id, node_id, captured_at, status, connections, created_at"
|
||||
|
||||
nodeObsFrpsTableName = "of_node_obs_frps"
|
||||
nodeObsFrpsInsertColumns = "id, node_id, captured_at, frps_connections, frps_proxy_count, frps_client_count, frps_proxies, created_at"
|
||||
|
||||
nodeObsFrpcTableName = "of_node_obs_frpc"
|
||||
nodeObsFrpcInsertColumns = "id, node_id, captured_at, tunnel_status, connected_relays_count, created_at"
|
||||
)
|
||||
|
||||
// NodeMetricSnapshot stores periodic node resource utilization metrics in ClickHouse.
|
||||
type NodeMetricSnapshot struct {
|
||||
ID uint64 `gorm:"column:id"`
|
||||
NodeID string `gorm:"column:node_id"`
|
||||
CapturedAt time.Time `gorm:"column:captured_at"`
|
||||
CPUUsagePercent float64 `gorm:"column:cpu_usage_percent"`
|
||||
MemoryUsedBytes int64 `gorm:"column:memory_used_bytes"`
|
||||
MemoryTotalBytes int64 `gorm:"column:memory_total_bytes"`
|
||||
StorageUsedBytes int64 `gorm:"column:storage_used_bytes"`
|
||||
StorageTotalBytes int64 `gorm:"column:storage_total_bytes"`
|
||||
DiskReadBytes int64 `gorm:"column:disk_read_bytes"`
|
||||
DiskWriteBytes int64 `gorm:"column:disk_write_bytes"`
|
||||
NetworkRxBytes int64 `gorm:"column:network_rx_bytes"`
|
||||
NetworkTxBytes int64 `gorm:"column:network_tx_bytes"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
// TableName returns the ClickHouse table name.
|
||||
func (NodeMetricSnapshot) TableName() string {
|
||||
return nodeMetricSnapshotTableName
|
||||
}
|
||||
|
||||
// InsertColumns returns comma-separated column names for batch insert.
|
||||
func (NodeMetricSnapshot) InsertColumns() string {
|
||||
return nodeMetricSnapshotInsertColumns
|
||||
}
|
||||
|
||||
// BatchInsertSQL returns the INSERT prefix used by native batch writers.
|
||||
func (NodeMetricSnapshot) BatchInsertSQL() string {
|
||||
return fmt.Sprintf("INSERT INTO %s (%s)", nodeMetricSnapshotTableName, nodeMetricSnapshotInsertColumns)
|
||||
}
|
||||
|
||||
// NodeEdgeHealth stores L2 OpenResty health snapshots (connections + status).
|
||||
type NodeEdgeHealth struct {
|
||||
ID uint64 `gorm:"column:id"`
|
||||
NodeID string `gorm:"column:node_id"`
|
||||
CapturedAt time.Time `gorm:"column:captured_at"`
|
||||
Status string `gorm:"column:status"`
|
||||
Connections int64 `gorm:"column:connections"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
// TableName returns the ClickHouse table name.
|
||||
func (NodeEdgeHealth) TableName() string {
|
||||
return nodeEdgeHealthTableName
|
||||
}
|
||||
|
||||
// InsertColumns returns comma-separated column names for batch insert.
|
||||
func (NodeEdgeHealth) InsertColumns() string {
|
||||
return nodeEdgeHealthInsertColumns
|
||||
}
|
||||
|
||||
// BatchInsertSQL returns the INSERT prefix used by native batch writers.
|
||||
func (NodeEdgeHealth) BatchInsertSQL() string {
|
||||
return fmt.Sprintf("INSERT INTO %s (%s)", nodeEdgeHealthTableName, nodeEdgeHealthInsertColumns)
|
||||
}
|
||||
|
||||
// AccessLogHourly is a Server-side hourly rollup of access logs.
|
||||
type AccessLogHourly struct {
|
||||
NodeID string `gorm:"column:node_id"`
|
||||
Hour time.Time `gorm:"column:hour"`
|
||||
Host string `gorm:"column:host"`
|
||||
RequestCount int64 `gorm:"column:request_count"`
|
||||
ErrorCount int64 `gorm:"column:error_count"`
|
||||
BytesSent int64 `gorm:"column:bytes_sent"`
|
||||
RequestLength int64 `gorm:"column:request_length"`
|
||||
}
|
||||
|
||||
// NodeTrafficHourly is an hourly traffic rollup row.
|
||||
//
|
||||
// UniqueVisitorCount is always 0 when sourced from of_access_log_hourly
|
||||
// (true UV requires raw uniqExact on access logs).
|
||||
type NodeTrafficHourly struct {
|
||||
NodeID string
|
||||
Hour time.Time
|
||||
RequestCount int64
|
||||
ErrorCount int64
|
||||
UniqueVisitorCount int64
|
||||
}
|
||||
|
||||
// NodeMetricHourly is an hourly metric snapshot aggregation row.
|
||||
//
|
||||
// Disk and host network counters are cumulative. Prefer pre-aggregated min/max
|
||||
// deltas from of_node_metric_capacity_hourly; raw fallback uses consecutive
|
||||
// lagInFrame samples per node (negative deltas after counter reset are dropped).
|
||||
type NodeMetricHourly struct {
|
||||
Hour time.Time
|
||||
AverageCPUUsagePercent float64
|
||||
AverageMemoryUsagePercent float64
|
||||
NetworkRxBytes int64
|
||||
NetworkTxBytes int64
|
||||
DiskReadBytes int64
|
||||
DiskWriteBytes int64
|
||||
ReportedNodes int
|
||||
}
|
||||
|
||||
// NodeObsFrps stores FRPS observability snapshots in ClickHouse.
|
||||
type NodeObsFrps struct {
|
||||
ID uint64 `gorm:"column:id"`
|
||||
NodeID string `gorm:"column:node_id"`
|
||||
CapturedAt time.Time `gorm:"column:captured_at"`
|
||||
FrpsConnections int32 `gorm:"column:frps_connections"`
|
||||
FrpsProxyCount int32 `gorm:"column:frps_proxy_count"`
|
||||
FrpsClientCount int32 `gorm:"column:frps_client_count"`
|
||||
FrpsProxies string `gorm:"column:frps_proxies"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
// TableName returns the ClickHouse table name.
|
||||
func (NodeObsFrps) TableName() string {
|
||||
return nodeObsFrpsTableName
|
||||
}
|
||||
|
||||
// InsertColumns returns comma-separated column names for batch insert.
|
||||
func (NodeObsFrps) InsertColumns() string {
|
||||
return nodeObsFrpsInsertColumns
|
||||
}
|
||||
|
||||
// BatchInsertSQL returns the INSERT prefix used by native batch writers.
|
||||
func (NodeObsFrps) BatchInsertSQL() string {
|
||||
return fmt.Sprintf("INSERT INTO %s (%s)", nodeObsFrpsTableName, nodeObsFrpsInsertColumns)
|
||||
}
|
||||
|
||||
// NodeObsFrpc stores FRPC observability snapshots in ClickHouse.
|
||||
type NodeObsFrpc struct {
|
||||
ID uint64 `gorm:"column:id"`
|
||||
NodeID string `gorm:"column:node_id"`
|
||||
CapturedAt time.Time `gorm:"column:captured_at"`
|
||||
TunnelStatus string `gorm:"column:tunnel_status"`
|
||||
ConnectedRelaysCount int32 `gorm:"column:connected_relays_count"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
// TableName returns the ClickHouse table name.
|
||||
func (NodeObsFrpc) TableName() string {
|
||||
return nodeObsFrpcTableName
|
||||
}
|
||||
|
||||
// InsertColumns returns comma-separated column names for batch insert.
|
||||
func (NodeObsFrpc) InsertColumns() string {
|
||||
return nodeObsFrpcInsertColumns
|
||||
}
|
||||
|
||||
// BatchInsertSQL returns the INSERT prefix used by native batch writers.
|
||||
func (NodeObsFrpc) BatchInsertSQL() string {
|
||||
return fmt.Sprintf("INSERT INTO %s (%s)", nodeObsFrpcTableName, nodeObsFrpcInsertColumns)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package analytics
|
||||
|
||||
import (
|
||||
risklogstore "Wavelet/plugins/domain/risk_control/logstore"
|
||||
)
|
||||
|
||||
// UserAccessLog is Wavelet risk_control's w_user_access_logs entity.
|
||||
type UserAccessLog = risklogstore.UserAccessLog
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package analytics
|
||||
|
||||
import "strings"
|
||||
|
||||
// User-Agent 浏览器/OS/设备分类(纯函数,无 IO)。
|
||||
// 与 internal/repository/analytics/browser.go 的判定逻辑保持一致(Task 4 复制,
|
||||
// 因为 model 不得 import analyticsrepo);后续若移除旧 CH 实现,可让 analyticsrepo 改以别名复用本包。
|
||||
|
||||
const (
|
||||
uaLabelUnknown = "Unknown"
|
||||
uaLabelBot = "Bot"
|
||||
uaLabelOther = "Other"
|
||||
uaTokenBot = "bot"
|
||||
uaTokenAndroid = "android"
|
||||
uaTokenSpider = "spider"
|
||||
uaTokenCrawler = "crawler"
|
||||
)
|
||||
|
||||
type uaMatchRule struct {
|
||||
label string
|
||||
contains []string
|
||||
allOf []string
|
||||
noneOf []string
|
||||
}
|
||||
|
||||
func matchUARules(uaLower string, rules []uaMatchRule, fallback string) string {
|
||||
if uaLower == "" {
|
||||
return uaLabelUnknown
|
||||
}
|
||||
for _, rule := range rules {
|
||||
matched := false
|
||||
for _, token := range rule.contains {
|
||||
if strings.Contains(uaLower, token) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched && len(rule.allOf) > 0 {
|
||||
matched = true
|
||||
for _, token := range rule.allOf {
|
||||
if !strings.Contains(uaLower, token) {
|
||||
matched = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
excluded := false
|
||||
for _, token := range rule.noneOf {
|
||||
if strings.Contains(uaLower, token) {
|
||||
excluded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if excluded {
|
||||
continue
|
||||
}
|
||||
return rule.label
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
var browserRules = []uaMatchRule{
|
||||
{label: "WeChat", contains: []string{"micromessenger"}},
|
||||
{label: "Postman", contains: []string{"postman"}},
|
||||
{label: "CLI", contains: []string{"curl/", "wget/"}},
|
||||
{label: "Edge", contains: []string{"edg/", "edgios/", "edga/"}},
|
||||
{label: "Opera", contains: []string{"opr/", "opera"}},
|
||||
{label: "Firefox", contains: []string{"firefox", "fxios"}},
|
||||
{label: "Chrome", contains: []string{"crios", "chrome"}, noneOf: []string{"chromium"}},
|
||||
{label: "Chromium", contains: []string{"chromium"}},
|
||||
{label: "Safari", contains: []string{"safari"}},
|
||||
{label: uaLabelBot, contains: []string{uaTokenBot, uaTokenSpider, uaTokenCrawler, "slurp"}},
|
||||
}
|
||||
|
||||
var osRules = []uaMatchRule{
|
||||
{label: "Android", contains: []string{uaTokenAndroid}},
|
||||
{label: "iOS", contains: []string{"iphone", "ipad", "ipod", "ios"}},
|
||||
{label: "Windows", contains: []string{"windows"}},
|
||||
{label: "macOS", contains: []string{"mac os x", "macintosh", "macos"}},
|
||||
{label: "Chrome OS", contains: []string{"cros"}},
|
||||
{label: "Linux", contains: []string{"linux"}},
|
||||
{label: uaLabelBot, contains: []string{uaTokenBot, uaTokenSpider, uaTokenCrawler}},
|
||||
}
|
||||
|
||||
var deviceRules = []uaMatchRule{
|
||||
{
|
||||
label: uaLabelBot,
|
||||
contains: []string{uaTokenBot, uaTokenSpider, uaTokenCrawler, "slurp", "curl/", "wget/", "python-requests", "go-http-client", "postman"},
|
||||
},
|
||||
{
|
||||
label: "Tablet",
|
||||
contains: []string{"ipad", "tablet"},
|
||||
},
|
||||
{
|
||||
label: "Tablet",
|
||||
allOf: []string{uaTokenAndroid},
|
||||
noneOf: []string{"mobile"},
|
||||
},
|
||||
{
|
||||
label: "Mobile",
|
||||
contains: []string{"mobi", "iphone", "ipod", uaTokenAndroid},
|
||||
},
|
||||
}
|
||||
|
||||
// ParseBrowserName performs lightweight User-Agent browser identification.
|
||||
func ParseBrowserName(ua string) string {
|
||||
return matchUARules(strings.ToLower(ua), browserRules, uaLabelOther)
|
||||
}
|
||||
|
||||
// ParseOSName performs lightweight User-Agent OS identification.
|
||||
func ParseOSName(ua string) string {
|
||||
return matchUARules(strings.ToLower(ua), osRules, uaLabelOther)
|
||||
}
|
||||
|
||||
// ParseDeviceType performs lightweight User-Agent device type identification.
|
||||
func ParseDeviceType(ua string) string {
|
||||
return matchUARules(strings.ToLower(ua), deviceRules, "Desktop")
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
// Domain validation messages used by model.Validate and other no-IO rules.
|
||||
// Persistence / data-access messages belong in internal/repository (do not import repository).
|
||||
const (
|
||||
errTemplateKeyRequired = "模板标识符不能为空"
|
||||
errTemplateNameRequired = "模板名称不能为空"
|
||||
errTemplateContentRequired = "模板内容不能为空"
|
||||
errAuthSourceNameRequired = "认证源名称不能为空"
|
||||
errAuthSourceNameInvalid = "认证源名称只能包含字母、数字、短横线或下划线,且必须以字母或数字开头"
|
||||
errAuthSourceTypeUnsupported = "认证源类型仅支持 oidc"
|
||||
errAuthSourceDiscoveryURLRequired = "OIDC 认证源必须配置 Discovery URL"
|
||||
errAuthSourceClientCredentialsRequired = "启用认证源前必须配置 Client ID 和 Client Secret" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
// OpenFlareAccessLogTrafficSummary is a window-level traffic summary from access logs.
|
||||
type OpenFlareAccessLogTrafficSummary struct {
|
||||
RequestCount int64
|
||||
ErrorCount int64
|
||||
UniqueIPCount int64
|
||||
BytesSent int64
|
||||
RequestLength int64
|
||||
NodeCount int64
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogValueCount is a dimension value count.
|
||||
type OpenFlareAccessLogValueCount struct {
|
||||
Value string
|
||||
Count int64
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogNodeAggregate is per-node traffic over a window.
|
||||
type OpenFlareAccessLogNodeAggregate struct {
|
||||
NodeID string
|
||||
RequestCount int64
|
||||
ErrorCount int64
|
||||
UniqueIPCount int64
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AcmeAccount OpenFlare ACME 账号实体。
|
||||
type AcmeAccount struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Email string `json:"email" gorm:"size:255"`
|
||||
URL string `json:"url" gorm:"size:255"`
|
||||
PrivateKey string `json:"-" gorm:"type:text;not null"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名。
|
||||
func (AcmeAccount) TableName() string {
|
||||
return "of_acme_accounts"
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OpenFlareApplyLogQuery filters apply logs for list queries.
|
||||
type OpenFlareApplyLogQuery struct {
|
||||
NodeID string
|
||||
PageNo int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// OpenFlareApplyLog stores node configuration apply results.
|
||||
type OpenFlareApplyLog struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
NodeID string `json:"node_id" gorm:"index;size:64;not null"`
|
||||
Version string `json:"version" gorm:"size:32;not null"`
|
||||
Result string `json:"result" gorm:"size:32;not null"`
|
||||
Message string `json:"message" gorm:"type:text"`
|
||||
Checksum string `json:"checksum" gorm:"size:64;not null;default:''"`
|
||||
MainConfigChecksum string `json:"main_config_checksum" gorm:"size:64;not null;default:''"`
|
||||
RouteConfigChecksum string `json:"route_config_checksum" gorm:"size:64;not null;default:''"`
|
||||
SupportFileCount int `json:"support_file_count" gorm:"not null;default:0"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
|
||||
}
|
||||
|
||||
// TableName returns the GORM table name.
|
||||
func (OpenFlareApplyLog) TableName() string {
|
||||
return "of_apply_logs"
|
||||
}
|
||||
|
||||
// IsRepeatSuccessApplyLog reports whether the payload repeats an already-recorded success entry.
|
||||
func IsRepeatSuccessApplyLog(latest *OpenFlareApplyLog, version, checksum, result string) bool {
|
||||
if latest == nil || result != "success" {
|
||||
return false
|
||||
}
|
||||
return latest.Result == "success" &&
|
||||
strings.TrimSpace(latest.Version) == strings.TrimSpace(version) &&
|
||||
strings.TrimSpace(latest.Checksum) == strings.TrimSpace(checksum)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
// CFConnectionSourceDNSAccount imports credentials from an existing DNS account.
|
||||
CFConnectionSourceDNSAccount = "dns_account"
|
||||
// CFConnectionSourceStandalone stores an independent API token.
|
||||
CFConnectionSourceStandalone = "standalone"
|
||||
|
||||
// CFConnectionStatusReady indicates the credential passed verification.
|
||||
CFConnectionStatusReady = "ready"
|
||||
// CFConnectionStatusError indicates the latest verification failed.
|
||||
CFConnectionStatusError = "error"
|
||||
|
||||
// CFMemberSyncPending indicates synchronization is queued or required.
|
||||
CFMemberSyncPending = "pending"
|
||||
// CFMemberSyncing indicates a worker is reconciling the record.
|
||||
CFMemberSyncing = "syncing"
|
||||
// CFMemberSyncOK indicates the remote record matches the desired state.
|
||||
CFMemberSyncOK = "ok"
|
||||
// CFMemberSyncError indicates the latest reconciliation failed.
|
||||
CFMemberSyncError = "error"
|
||||
)
|
||||
|
||||
// CFConnection stores the single Cloudflare API credential source.
|
||||
type CFConnection struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Source string `json:"source" gorm:"size:32;not null;default:''"`
|
||||
DNSAccountID *uint `json:"dns_account_id" gorm:"index:idx_of_cf_connections_dns_account_id"`
|
||||
Authorization string `json:"-" gorm:"type:text;not null;default:''"`
|
||||
Status string `json:"status" gorm:"size:16;not null;default:''"`
|
||||
VerifiedAt *time.Time `json:"verified_at"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName returns the Cloudflare connection table name.
|
||||
func (CFConnection) TableName() string { return "of_cf_connections" }
|
||||
|
||||
// CFPointingGroup stores a reusable node target for DNS records.
|
||||
type CFPointingGroup struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"size:128;not null"`
|
||||
PrimaryNodeID uint `json:"primary_node_id" gorm:"not null;index:idx_of_cf_pointing_groups_primary_node_id"`
|
||||
BackupNodeID *uint `json:"backup_node_id" gorm:"index:idx_of_cf_pointing_groups_backup_node_id"`
|
||||
ActiveNodeID uint `json:"active_node_id" gorm:"not null;index:idx_of_cf_pointing_groups_active_node_id"`
|
||||
DefaultProxied bool `json:"default_proxied" gorm:"not null;default:false"`
|
||||
Enabled bool `json:"enabled" gorm:"not null;default:false"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName returns the Cloudflare pointing group table name.
|
||||
func (CFPointingGroup) TableName() string { return "of_cf_pointing_groups" }
|
||||
|
||||
// CFPointingMember stores one managed ZoneDomain A record.
|
||||
type CFPointingMember struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
GroupID uint `json:"group_id" gorm:"not null;index:idx_of_cf_pointing_members_group_id"`
|
||||
ZoneDomainID uint `json:"zone_domain_id" gorm:"not null;uniqueIndex:idx_of_cf_pointing_members_zone_domain_id"`
|
||||
Proxied bool `json:"proxied" gorm:"not null;default:false"`
|
||||
CFZoneID string `json:"cf_zone_id" gorm:"size:64;not null;default:''"`
|
||||
CFRecordID string `json:"cf_record_id" gorm:"size:64;not null;default:''"`
|
||||
DesiredIP string `json:"desired_ip" gorm:"size:64;not null;default:''"`
|
||||
SyncStatus string `json:"sync_status" gorm:"size:16;not null;default:'pending'"`
|
||||
LastError string `json:"last_error" gorm:"type:text;not null;default:''"`
|
||||
SyncedAt *time.Time `json:"synced_at"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName returns the Cloudflare pointing member table name.
|
||||
func (CFPointingMember) TableName() string { return "of_cf_pointing_members" }
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ConfigVersionSummary is the list view for config versions.
|
||||
type ConfigVersionSummary struct {
|
||||
ID string `json:"id" gorm:"-"`
|
||||
Version string `json:"version" gorm:"primaryKey;column:version"`
|
||||
Checksum string `json:"checksum"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedBy string `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// AfterFind hook for ConfigVersionSummary.
|
||||
func (cvs *ConfigVersionSummary) AfterFind(_ *gorm.DB) (err error) {
|
||||
cvs.ID = cvs.Version
|
||||
return
|
||||
}
|
||||
|
||||
// ConfigVersion stores a published OpenResty configuration snapshot.
|
||||
type ConfigVersion struct {
|
||||
ID string `json:"id" gorm:"-"`
|
||||
Version string `json:"version" gorm:"primaryKey;size:32;not null"`
|
||||
SnapshotJSON string `json:"snapshot_json" gorm:"type:text;not null"`
|
||||
MainConfig string `json:"main_config" gorm:"type:text;not null;default:''"`
|
||||
RenderedConfig string `json:"rendered_config" gorm:"type:text;not null"`
|
||||
SupportFilesJSON string `json:"support_files_json" gorm:"type:text;not null;default:'[]'"`
|
||||
Checksum string `json:"checksum" gorm:"size:64;not null"`
|
||||
IsActive bool `json:"is_active" gorm:"not null;default:false;index"`
|
||||
CreatedBy string `json:"created_by" gorm:"size:64;not null"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// AfterFind hook for ConfigVersion.
|
||||
func (cv *ConfigVersion) AfterFind(_ *gorm.DB) (err error) {
|
||||
cv.ID = cv.Version
|
||||
return
|
||||
}
|
||||
|
||||
// AfterCreate hook for ConfigVersion.
|
||||
func (cv *ConfigVersion) AfterCreate(_ *gorm.DB) (err error) {
|
||||
cv.ID = cv.Version
|
||||
return
|
||||
}
|
||||
|
||||
// TableName returns the GORM table name.
|
||||
func (*ConfigVersion) TableName() string {
|
||||
return "of_config_versions"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DNSAccount OpenFlare DNS 账号实体。
|
||||
type DNSAccount struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"size:255;not null"`
|
||||
Type string `json:"type" gorm:"size:64;not null"`
|
||||
Authorization string `json:"-" gorm:"type:text;not null"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名。
|
||||
func (DNSAccount) TableName() string {
|
||||
return "of_dns_accounts"
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// OpenFlareNode stores an edge, relay, or tunnel client node.
|
||||
type OpenFlareNode struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
NodeID string `json:"node_id" gorm:"uniqueIndex;size:64;not null"`
|
||||
Name string `json:"name" gorm:"size:128;not null"`
|
||||
IP string `json:"ip" gorm:"size:64;not null;default:''"`
|
||||
IPManualOverride bool `json:"ip_manual_override" gorm:"not null;default:false"`
|
||||
GeoName string `json:"geo_name" gorm:"size:128;not null;default:''"`
|
||||
GeoLatitude *float64 `json:"geo_latitude"`
|
||||
GeoLongitude *float64 `json:"geo_longitude"`
|
||||
GeoManualOverride bool `json:"geo_manual_override" gorm:"not null;default:false"`
|
||||
AccessToken string `json:"-" gorm:"column:access_token;size:128;index"`
|
||||
AutoUpdateEnabled bool `json:"auto_update_enabled" gorm:"not null;default:false"`
|
||||
UpdateRequested bool `json:"update_requested" gorm:"not null;default:false"`
|
||||
UpdateChannel string `json:"update_channel" gorm:"size:16;not null;default:'stable'"`
|
||||
UpdateTag string `json:"update_tag" gorm:"size:64;not null;default:''"`
|
||||
RestartOpenrestyRequested bool `json:"restart_openresty_requested" gorm:"not null;default:false"`
|
||||
Version string `json:"version" gorm:"size:64;not null;default:''"`
|
||||
ExtVersion string `json:"ext_version" gorm:"size:64;not null;default:''"`
|
||||
OpenrestyStatus string `json:"openresty_status" gorm:"size:16;not null;default:'unknown'"`
|
||||
OpenrestyMessage string `json:"openresty_message" gorm:"type:text"`
|
||||
Status string `json:"status" gorm:"size:16;not null;default:'offline'"`
|
||||
CurrentVersion string `json:"current_version" gorm:"size:32;not null;default:''"`
|
||||
LastSeenAt *time.Time `json:"last_seen_at"`
|
||||
LastError string `json:"last_error" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
NodeType string `json:"node_type" gorm:"size:32;not null;default:'edge_node'"`
|
||||
RelayBindPort int `json:"relay_bind_port" gorm:"not null;default:0"`
|
||||
RelayVhostHTTPPort int `json:"relay_vhost_http_port" gorm:"not null;default:0"`
|
||||
RelayAuthToken string `json:"-" gorm:"size:128;not null;default:''"`
|
||||
RelayAgentAccessAddr string `json:"relay_agent_access_addr" gorm:"size:255;not null;default:''"`
|
||||
RelayClientAccessAddr string `json:"relay_client_access_addr" gorm:"size:255;not null;default:''"`
|
||||
RelayClientProxyURL string `json:"relay_client_proxy_url" gorm:"size:512;not null;default:''"`
|
||||
CapabilitiesJSON string `json:"capabilities_json" gorm:"type:text;not null;default:'[]'"`
|
||||
RelayStatus string `json:"relay_status" gorm:"size:16;not null;default:'unknown'"`
|
||||
RelayWebServerEnabled bool `json:"relay_web_server_enabled" gorm:"not null;default:false"`
|
||||
}
|
||||
|
||||
// TableName returns the GORM table name.
|
||||
func (OpenFlareNode) TableName() string {
|
||||
return "of_nodes"
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// OpenFlareMetricSnapshot stores a node capacity snapshot in ClickHouse (database: openflare, table: of_node_metric_snapshots).
|
||||
// ClickHouse DDL is managed by goose; reads/writes go through internal/repository/analytics.
|
||||
type OpenFlareMetricSnapshot struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
NodeID string `json:"node_id" gorm:"index;size:64;not null"`
|
||||
CapturedAt time.Time `json:"captured_at" gorm:"index"`
|
||||
CPUUsagePercent float64 `json:"cpu_usage_percent"`
|
||||
MemoryUsedBytes int64 `json:"memory_used_bytes"`
|
||||
MemoryTotalBytes int64 `json:"memory_total_bytes"`
|
||||
StorageUsedBytes int64 `json:"storage_used_bytes"`
|
||||
StorageTotalBytes int64 `json:"storage_total_bytes"`
|
||||
DiskReadBytes int64 `json:"disk_read_bytes"`
|
||||
DiskWriteBytes int64 `json:"disk_write_bytes"`
|
||||
NetworkRxBytes int64 `json:"network_rx_bytes"`
|
||||
NetworkTxBytes int64 `json:"network_tx_bytes"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// TableName returns the GORM table name.
|
||||
func (OpenFlareMetricSnapshot) TableName() string {
|
||||
return "of_node_metric_snapshots"
|
||||
}
|
||||
|
||||
// OpenFlareAccessLog stores a single access log row in ClickHouse (database: openflare, table: of_node_access_logs).
|
||||
// ClickHouse DDL is managed by goose; reads/writes go through internal/repository/analytics.
|
||||
type OpenFlareAccessLog struct {
|
||||
ID uint64 `json:"id,string" gorm:"column:id"`
|
||||
NodeID string `json:"node_id" gorm:"index;size:64;not null"`
|
||||
LoggedAt time.Time `json:"logged_at" gorm:"index"`
|
||||
RemoteAddr string `json:"remote_addr" gorm:"index;size:128"`
|
||||
Region string `json:"region" gorm:"size:128"`
|
||||
Host string `json:"host" gorm:"index;size:255"`
|
||||
Path string `json:"path" gorm:"size:2048"`
|
||||
UserAgent string `json:"user_agent" gorm:"column:user_agent;size:512"`
|
||||
CacheStatus string `json:"cache_status" gorm:"column:cache_status;size:32"`
|
||||
StatusCode int `json:"status_code" gorm:"index"`
|
||||
BytesSent int64 `json:"bytes_sent" gorm:"column:bytes_sent;not null;default:0"`
|
||||
RequestLength int64 `json:"request_length" gorm:"column:request_length;not null;default:0"`
|
||||
RequestTimeMs int64 `json:"request_time_ms" gorm:"column:request_time_ms;not null;default:0"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// TableName returns the GORM table name.
|
||||
func (OpenFlareAccessLog) TableName() string {
|
||||
return "of_node_access_logs"
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogRegionCount aggregates access log regions.
|
||||
type OpenFlareAccessLogRegionCount struct {
|
||||
Region string `json:"region"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// OpenFlareHealthEvent stores node health alert events.
|
||||
type OpenFlareHealthEvent struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
NodeID string `json:"node_id" gorm:"index;size:64;not null"`
|
||||
EventType string `json:"event_type" gorm:"index;size:64;not null"`
|
||||
Severity string `json:"severity" gorm:"size:16;not null"`
|
||||
Status string `json:"status" gorm:"index;size:16;not null"`
|
||||
Message string `json:"message" gorm:"type:text"`
|
||||
FirstTriggeredAt time.Time `json:"first_triggered_at" gorm:"index"`
|
||||
LastTriggeredAt time.Time `json:"last_triggered_at" gorm:"index"`
|
||||
ReportedAt time.Time `json:"reported_at" gorm:"index"`
|
||||
ResolvedAt *time.Time `json:"resolved_at" gorm:"index"`
|
||||
MetadataJSON string `json:"metadata_json" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName returns the GORM table name.
|
||||
func (OpenFlareHealthEvent) TableName() string {
|
||||
return "of_node_health_events"
|
||||
}
|
||||
|
||||
// OpenFlareNodeSystemProfile stores the latest node system profile.
|
||||
type OpenFlareNodeSystemProfile struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
NodeID string `json:"node_id" gorm:"uniqueIndex;size:64;not null"`
|
||||
Hostname string `json:"hostname" gorm:"size:255"`
|
||||
OSName string `json:"os_name" gorm:"size:128"`
|
||||
OSVersion string `json:"os_version" gorm:"size:128"`
|
||||
KernelVersion string `json:"kernel_version" gorm:"size:128"`
|
||||
Architecture string `json:"architecture" gorm:"size:64"`
|
||||
CPUModel string `json:"cpu_model" gorm:"size:255"`
|
||||
CPUCores int `json:"cpu_cores"`
|
||||
TotalMemoryBytes int64 `json:"total_memory_bytes"`
|
||||
TotalDiskBytes int64 `json:"total_disk_bytes"`
|
||||
UptimeSeconds int64 `json:"uptime_seconds"`
|
||||
ReportedAt time.Time `json:"reported_at" gorm:"index"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName returns the GORM table name.
|
||||
func (OpenFlareNodeSystemProfile) TableName() string {
|
||||
return "of_node_system_profiles"
|
||||
}
|
||||
|
||||
// OpenFlareEdgeHealth is L2 OpenResty health (of_node_edge_health).
|
||||
type OpenFlareEdgeHealth struct {
|
||||
ID uint `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
CapturedAt time.Time `json:"captured_at"`
|
||||
Status string `json:"status"`
|
||||
Connections int64 `json:"connections"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName returns the ClickHouse table name.
|
||||
func (OpenFlareEdgeHealth) TableName() string {
|
||||
return "of_node_edge_health"
|
||||
}
|
||||
|
||||
// OpenFlareNodeObservationFrpc stores tunnel client frpc observations in ClickHouse (database: openflare, table: of_node_obs_frpc).
|
||||
// ClickHouse DDL is managed by goose; reads/writes go through internal/repository/analytics.
|
||||
type OpenFlareNodeObservationFrpc struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
NodeID string `json:"node_id" gorm:"index;size:64;not null"`
|
||||
CapturedAt time.Time `json:"captured_at" gorm:"index"`
|
||||
TunnelStatus string `json:"tunnel_status" gorm:"size:16"`
|
||||
ConnectedRelaysCount int `json:"connected_relays_count"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// TableName returns the GORM table name.
|
||||
func (OpenFlareNodeObservationFrpc) TableName() string {
|
||||
return "of_node_obs_frpc"
|
||||
}
|
||||
|
||||
// OpenFlareNodeObservationFrps stores tunnel relay frps observations in ClickHouse (database: openflare, table: of_node_obs_frps).
|
||||
// ClickHouse DDL is managed by goose; reads/writes go through internal/repository/analytics.
|
||||
type OpenFlareNodeObservationFrps struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
NodeID string `json:"node_id" gorm:"index;size:64;not null"`
|
||||
CapturedAt time.Time `json:"captured_at" gorm:"index"`
|
||||
FrpsConnections int `json:"frps_connections"`
|
||||
FrpsProxyCount int `json:"frps_proxy_count"`
|
||||
FrpsClientCount int `json:"frps_client_count"`
|
||||
FrpsProxies string `json:"frps_proxies" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// TableName returns the GORM table name.
|
||||
func (OpenFlareNodeObservationFrps) TableName() string {
|
||||
return "of_node_obs_frps"
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogQuery filters access log list queries.
|
||||
type OpenFlareAccessLogQuery struct {
|
||||
NodeID string
|
||||
RemoteAddr string
|
||||
Host string
|
||||
// Hosts exact-matches any host (case-insensitive). Prefer over Host for multi-domain scopes.
|
||||
Hosts []string
|
||||
Path string
|
||||
// StatusCode filters by exact HTTP status code when > 0.
|
||||
StatusCode int
|
||||
Since time.Time
|
||||
Until time.Time
|
||||
Page int
|
||||
PageSize int
|
||||
SortBy string
|
||||
SortOrder string
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogBucketQuery filters folded access log queries (v1 stub).
|
||||
type OpenFlareAccessLogBucketQuery struct {
|
||||
NodeID string
|
||||
RemoteAddr string
|
||||
Host string
|
||||
Hosts []string
|
||||
Path string
|
||||
Since time.Time
|
||||
Until time.Time
|
||||
Page int
|
||||
PageSize int
|
||||
SortBy string
|
||||
SortOrder string
|
||||
FoldMinutes int
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogBucketRow is a folded access log bucket row (v1 stub).
|
||||
type OpenFlareAccessLogBucketRow struct {
|
||||
BucketEpoch int64 `json:"bucket_epoch"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
UniqueIPCount int64 `json:"unique_ip_count"`
|
||||
UniqueHostCount int64 `json:"unique_host_count"`
|
||||
SuccessCount int64 `json:"success_count"`
|
||||
ClientErrorCount int64 `json:"client_error_count"`
|
||||
ServerErrorCount int64 `json:"server_error_count"`
|
||||
Status2xxCount int64 `json:"status_2xx_count"`
|
||||
Status4xxCount int64 `json:"status_4xx_count"`
|
||||
Status5xxCount int64 `json:"status_5xx_count"`
|
||||
BytesSent int64 `json:"bytes_sent"`
|
||||
RequestLength int64 `json:"request_length"`
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogBucketIPQuery filters folded IP summary queries (v1 stub).
|
||||
type OpenFlareAccessLogBucketIPQuery struct {
|
||||
NodeID string
|
||||
RemoteAddr string
|
||||
Host string
|
||||
Path string
|
||||
BucketStartedAt time.Time
|
||||
FoldMinutes int
|
||||
Page int
|
||||
PageSize int
|
||||
SortBy string
|
||||
SortOrder string
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogBucketIPRow is a folded IP row (v1 stub).
|
||||
type OpenFlareAccessLogBucketIPRow struct {
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
SuccessCount int64 `json:"success_count"`
|
||||
ClientErrorCount int64 `json:"client_error_count"`
|
||||
ServerErrorCount int64 `json:"server_error_count"`
|
||||
LastSeenEpoch int64 `json:"last_seen_epoch"`
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogIPSummaryQuery filters IP summary list queries (v1 stub).
|
||||
type OpenFlareAccessLogIPSummaryQuery struct {
|
||||
NodeID string
|
||||
RemoteAddr string
|
||||
Host string
|
||||
Since time.Time
|
||||
Until time.Time
|
||||
Page int
|
||||
PageSize int
|
||||
SortBy string
|
||||
SortOrder string
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogIPSummaryRow is an IP summary row (v1 stub).
|
||||
type OpenFlareAccessLogIPSummaryRow struct {
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Region string `json:"region"`
|
||||
TotalRequests int64 `json:"total_requests"`
|
||||
Success2xxCount int64 `json:"success_2xx_count"`
|
||||
SuccessRatio float64 `json:"success_ratio"`
|
||||
BytesReceived int64 `json:"bytes_received"`
|
||||
BytesSent int64 `json:"bytes_sent"`
|
||||
// RecentRequests is deprecated and always 0.
|
||||
RecentRequests int64 `json:"recent_requests"`
|
||||
LastSeenEpoch int64 `json:"last_seen_epoch"`
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogIPTrendQuery filters IP trend queries (v1 stub).
|
||||
type OpenFlareAccessLogIPTrendQuery struct {
|
||||
NodeID string
|
||||
RemoteAddr string
|
||||
Host string
|
||||
Since time.Time
|
||||
BucketMinutes int
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogIPTrendRow is an IP trend bucket row (v1 stub).
|
||||
type OpenFlareAccessLogIPTrendRow struct {
|
||||
BucketEpoch int64 `json:"bucket_epoch"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogWAFIPAggregate is a per-IP aggregate row for WAF automatic rules.
|
||||
type OpenFlareAccessLogWAFIPAggregate struct {
|
||||
RemoteAddr string
|
||||
RequestCount int
|
||||
Status404Count int
|
||||
ClientErrorCount int
|
||||
ServerErrorCount int
|
||||
IPHostCount int
|
||||
LastSeenEpoch int64
|
||||
StatusCounts map[int]int
|
||||
}
|
||||
|
||||
// OpenFlareTrafficHourly is an hourly traffic rollup row.
|
||||
type OpenFlareTrafficHourly struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Hour time.Time `json:"hour"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
ErrorCount int64 `json:"error_count"`
|
||||
UniqueVisitorCount int64 `json:"unique_visitor_count"`
|
||||
}
|
||||
|
||||
// OpenFlareAccessLogHourly is a per-node/host hourly access log rollup.
|
||||
type OpenFlareAccessLogHourly struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Hour time.Time `json:"hour"`
|
||||
Host string `json:"host"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
ErrorCount int64 `json:"error_count"`
|
||||
BytesSent int64 `json:"bytes_sent"`
|
||||
RequestLength int64 `json:"request_length"`
|
||||
}
|
||||
|
||||
// OpenFlareMetricHourly is an hourly metric snapshot aggregation row.
|
||||
type OpenFlareMetricHourly struct {
|
||||
Hour time.Time `json:"hour"`
|
||||
AverageCPUUsagePercent float64 `json:"average_cpu_usage_percent"`
|
||||
AverageMemoryUsagePercent float64 `json:"average_memory_usage_percent"`
|
||||
NetworkRxBytes int64 `json:"network_rx_bytes"`
|
||||
NetworkTxBytes int64 `json:"network_tx_bytes"`
|
||||
DiskReadBytes int64 `json:"disk_read_bytes"`
|
||||
DiskWriteBytes int64 `json:"disk_write_bytes"`
|
||||
ReportedNodes int `json:"reported_nodes"`
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// OpenFlareOption is the key/value DTO used by the OpenFlare option API surface.
|
||||
// Key 与 w_system_configs.key 一致(snake_case)。此类型仅作为
|
||||
// /api/v1/d/option 接口的请求/响应载体,不再映射到独立的 of_options 表。
|
||||
type OpenFlareOption struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// StartTime records process start time (seconds) for the public /status endpoint.
|
||||
var StartTime = time.Now().Unix()
|
||||
|
||||
// DefaultOpenRestyMainConfigTemplate 是 OpenResty 主配置模板的内置默认值。
|
||||
// 当 SystemConfig 中缺失 openresty_main_config_template 时作为兜底使用。
|
||||
const DefaultOpenRestyMainConfigTemplate = `# This file is generated by OpenFlare. Do not edit manually.
|
||||
user openflare;
|
||||
worker_processes {{OpenRestyWorkerProcesses}};
|
||||
worker_rlimit_nofile {{OpenRestyWorkerRlimitNofile}};
|
||||
pid __OPENFLARE_PID_PATH__;
|
||||
error_log {{OpenRestyErrorLogPath}} warn;
|
||||
|
||||
events {
|
||||
worker_connections {{OpenRestyWorkerConnections}};
|
||||
{{OpenRestyEventsUseDirective}}{{OpenRestyEventsMultiAcceptDirective}}}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
server_tokens off;
|
||||
client_body_temp_path __OPENFLARE_NGINX_CACHE_DIR__/client_temp;
|
||||
proxy_temp_path __OPENFLARE_NGINX_CACHE_DIR__/proxy_temp;
|
||||
fastcgi_temp_path __OPENFLARE_NGINX_CACHE_DIR__/fastcgi_temp;
|
||||
uwsgi_temp_path __OPENFLARE_NGINX_CACHE_DIR__/uwsgi_temp;
|
||||
scgi_temp_path __OPENFLARE_NGINX_CACHE_DIR__/scgi_temp;
|
||||
{{OpenRestyConnectionUpgradeMap}}{{OpenRestyDefaultServerBlock}} log_format openflare_json escape=json '{"ts":"$time_iso8601","host":"$host","path":"$request_uri","remote_addr":"$remote_addr","status":$status,"request_time":$request_time,"bytes_sent":$body_bytes_sent,"request_length":$request_length,"user_agent":"$http_user_agent","cache_status":"$upstream_cache_status"}';
|
||||
access_log {{OpenRestyAccessLogPath}} openflare_json;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout {{OpenRestyKeepaliveTimeout}};
|
||||
keepalive_requests {{OpenRestyKeepaliveRequests}};
|
||||
client_header_timeout {{OpenRestyClientHeaderTimeout}};
|
||||
client_body_timeout {{OpenRestyClientBodyTimeout}};
|
||||
client_max_body_size {{OpenRestyClientMaxBodySize}};
|
||||
large_client_header_buffers {{OpenRestyLargeClientHeaderBuffers}};
|
||||
send_timeout {{OpenRestySendTimeout}};
|
||||
proxy_connect_timeout {{OpenRestyProxyConnectTimeout}};
|
||||
proxy_send_timeout {{OpenRestyProxySendTimeout}};
|
||||
proxy_read_timeout {{OpenRestyProxyReadTimeout}};
|
||||
proxy_request_buffering {{OpenRestyProxyRequestBuffering}};
|
||||
proxy_buffering {{OpenRestyProxyBuffering}};
|
||||
proxy_buffers {{OpenRestyProxyBuffers}};
|
||||
proxy_buffer_size {{OpenRestyProxyBufferSize}};
|
||||
proxy_busy_buffers_size {{OpenRestyProxyBusyBuffersSize}};
|
||||
gzip {{OpenRestyGzip}};
|
||||
gzip_min_length {{OpenRestyGzipMinLength}};
|
||||
gzip_comp_level {{OpenRestyGzipCompLevel}};
|
||||
{{OpenRestyResolverDirective}}{{OpenRestyCacheBlock}} include {{OpenRestyRouteConfigInclude}};
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Origin OpenFlare 源站实体。
|
||||
type Origin struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"size:255;not null"`
|
||||
Address string `json:"address" gorm:"uniqueIndex;size:255;not null"`
|
||||
Remark string `json:"remark" gorm:"size:255"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名。
|
||||
func (Origin) TableName() string {
|
||||
return "of_origins"
|
||||
}
|
||||
|
||||
// OriginRouteCount 源站关联的代理规则数量。
|
||||
type OriginRouteCount struct {
|
||||
OriginID uint `json:"origin_id"`
|
||||
RouteCount int64 `json:"route_count"`
|
||||
}
|
||||
|
||||
// OriginProxyRoute 源站模块查询代理规则时使用的最小字段集。
|
||||
type OriginProxyRoute struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
OriginID *uint `gorm:"column:origin_id"`
|
||||
Domain string `gorm:"column:domain"`
|
||||
OriginURL string `gorm:"column:origin_url"`
|
||||
Upstreams string `gorm:"column:upstreams"`
|
||||
Enabled bool `gorm:"column:enabled"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
}
|
||||
|
||||
// TableName 表名。
|
||||
func (OriginProxyRoute) TableName() string {
|
||||
return tableOfProxyRoutes
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Pages deployment status constants.
|
||||
const (
|
||||
PagesDeploymentStatusUploaded = "uploaded"
|
||||
PagesDeploymentStatusActive = "active"
|
||||
)
|
||||
|
||||
// PagesProject OpenFlare Pages 静态托管项目。
|
||||
type PagesProject struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"size:255;not null"`
|
||||
Slug string `json:"slug" gorm:"uniqueIndex;size:128;not null"`
|
||||
Description string `json:"description" gorm:"type:text;not null;default:''"`
|
||||
Enabled bool `json:"enabled" gorm:"not null;default:true"`
|
||||
SPAFallbackEnabled bool `json:"spa_fallback_enabled" gorm:"not null;default:false"`
|
||||
SPAFallbackPath string `json:"spa_fallback_path" gorm:"size:512;not null;default:'/index.html'"`
|
||||
APIProxyEnabled bool `json:"api_proxy_enabled" gorm:"not null;default:false"`
|
||||
APIProxyPath string `json:"api_proxy_path" gorm:"size:255;not null;default:''"`
|
||||
APIProxyPass string `json:"api_proxy_pass" gorm:"size:2048;not null;default:''"`
|
||||
APIProxyRewrite string `json:"api_proxy_rewrite" gorm:"size:255;not null;default:''"`
|
||||
ActiveDeploymentID *uint `json:"active_deployment_id" gorm:"index"`
|
||||
RootDir string `json:"root_dir" gorm:"size:512;not null;default:''"`
|
||||
EntryFile string `json:"entry_file" gorm:"size:512;not null;default:'index.html'"`
|
||||
ContentConfigVersion int `json:"-" gorm:"not null;default:0"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名。
|
||||
func (PagesProject) TableName() string {
|
||||
return "of_pages_projects"
|
||||
}
|
||||
|
||||
// PagesDeployment OpenFlare Pages 不可变部署记录。
|
||||
type PagesDeployment struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
ProjectID uint `json:"project_id" gorm:"not null;index;uniqueIndex:idx_of_pages_deployments_project_number,priority:1;uniqueIndex:idx_of_pages_deployments_source_revision,priority:1,where:source_identity IS NOT NULL AND source_revision IS NOT NULL"`
|
||||
DeploymentNumber int `json:"deployment_number" gorm:"not null;uniqueIndex:idx_of_pages_deployments_project_number,priority:2"`
|
||||
Checksum string `json:"checksum" gorm:"size:64;not null;index"`
|
||||
Status string `json:"status" gorm:"size:32;not null;default:'uploaded';index"`
|
||||
UploadID uint64 `json:"upload_id,string" gorm:"not null;default:0;index"`
|
||||
ArtifactPath string `json:"artifact_path,omitempty" gorm:"size:2048;not null;default:''"` // legacy only
|
||||
FileCount int `json:"file_count" gorm:"not null;default:0"`
|
||||
TotalSize int64 `json:"total_size" gorm:"not null;default:0"`
|
||||
CreatedBy string `json:"created_by" gorm:"size:64;not null;default:''"`
|
||||
SourceType string `json:"source_type" gorm:"size:32;not null;default:''"`
|
||||
SourceIdentity *string `json:"-" gorm:"type:char(64);uniqueIndex:idx_of_pages_deployments_source_revision,priority:2,where:source_identity IS NOT NULL AND source_revision IS NOT NULL"`
|
||||
SourceRevision *string `json:"-" gorm:"type:char(64);uniqueIndex:idx_of_pages_deployments_source_revision,priority:3,where:source_identity IS NOT NULL AND source_revision IS NOT NULL"`
|
||||
SourceLabel string `json:"source_label" gorm:"size:255;not null;default:''"`
|
||||
SourceMeta string `json:"-" gorm:"type:text;not null;default:''"`
|
||||
TriggerType string `json:"trigger_type" gorm:"size:32;not null;default:''"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
ActivatedAt *time.Time `json:"activated_at"`
|
||||
}
|
||||
|
||||
// TableName 表名。
|
||||
func (PagesDeployment) TableName() string {
|
||||
return "of_pages_deployments"
|
||||
}
|
||||
|
||||
// PagesDeploymentFile OpenFlare Pages 部署文件清单。
|
||||
type PagesDeploymentFile struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
DeploymentID uint `json:"deployment_id" gorm:"not null;index"`
|
||||
Path string `json:"path" gorm:"size:2048;not null"`
|
||||
Size int64 `json:"size" gorm:"not null;default:0"`
|
||||
Checksum string `json:"checksum" gorm:"size:64;not null"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名。
|
||||
func (PagesDeploymentFile) TableName() string {
|
||||
return "of_pages_deployment_files"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// PagesOrphanUploadCandidateLimit bounds one delayed Pages upload cleanup pass.
|
||||
PagesOrphanUploadCandidateLimit = 100
|
||||
// PagesOrphanMarkerPredicatePostgres is the Postgres JSON marker match SQL fragment.
|
||||
PagesOrphanMarkerPredicatePostgres = "w_uploads.metadata #>> '{extra,pages_ingest_marker}' = ?"
|
||||
// PagesOrphanMarkerPredicateSQLite is the SQLite JSON marker match SQL fragment.
|
||||
PagesOrphanMarkerPredicateSQLite = "CASE WHEN json_valid(w_uploads.metadata) THEN json_extract(w_uploads.metadata, '$.extra.pages_ingest_marker') ELSE NULL END = ?"
|
||||
)
|
||||
|
||||
// PagesOrphanUploadCandidateQuery describes the fail-closed SQL candidate set
|
||||
// for delayed Pages upload compensation.
|
||||
type PagesOrphanUploadCandidateQuery struct {
|
||||
SystemUserID uint64
|
||||
UploadType string
|
||||
Marker string
|
||||
CreatedBefore time.Time
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// PagesProjectSource 保存 Pages 项目的持久部署源配置。
|
||||
//
|
||||
// 对外接口必须映射到 pages 包内的 source view,避免直接序列化 model。
|
||||
type PagesProjectSource struct {
|
||||
ID uint `json:"-" gorm:"primaryKey;autoIncrement"`
|
||||
ProjectID uint `json:"-" gorm:"not null;uniqueIndex:idx_of_pages_project_sources_project_id"`
|
||||
SourceType string `json:"-" gorm:"size:32;not null;default:''"`
|
||||
RemoteURL string `json:"-" gorm:"type:text;not null;default:''"`
|
||||
AllowInsecure bool `json:"-" gorm:"not null;default:false"`
|
||||
GitHubRepository string `json:"-" gorm:"column:github_repository;size:255;not null;default:''"`
|
||||
ReleaseSelector string `json:"-" gorm:"size:16;not null;default:''"`
|
||||
ReleaseTag string `json:"-" gorm:"size:255;not null;default:''"`
|
||||
AssetName string `json:"-" gorm:"size:255;not null;default:''"`
|
||||
AutoUpdateEnabled bool `json:"-" gorm:"not null;default:false"`
|
||||
CheckIntervalMinutes int `json:"-" gorm:"not null;default:0"`
|
||||
ConfigVersion int `json:"-" gorm:"not null;default:0"`
|
||||
SourceIdentity string `json:"-" gorm:"type:char(64);not null;default:''"`
|
||||
CreatedAt time.Time `json:"-" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"-" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName 返回 Pages 项目部署源配置表名。
|
||||
func (PagesProjectSource) TableName() string {
|
||||
return "of_pages_project_sources"
|
||||
}
|
||||
|
||||
// PagesProjectSourceRuntime 保存 Pages 项目部署源的可变运行态。
|
||||
//
|
||||
// Runtime 不冗余 project_id;调用方通过 SourceID 关联配置,并在最终提交时
|
||||
// 同时校验 source config version 与 project content config version。
|
||||
type PagesProjectSourceRuntime struct {
|
||||
SourceID uint `json:"-" gorm:"primaryKey;autoIncrement:false"`
|
||||
ETag string `json:"-" gorm:"column:etag;size:512;not null;default:''"`
|
||||
LastSeenRevision string `json:"-" gorm:"type:char(64);not null;default:''"`
|
||||
LastSeenDetail string `json:"-" gorm:"type:text;not null;default:''"`
|
||||
LastAppliedRevision string `json:"-" gorm:"type:char(64);not null;default:''"`
|
||||
LastAppliedDetail string `json:"-" gorm:"type:text;not null;default:''"`
|
||||
SyncStatus string `json:"-" gorm:"size:32;not null;default:''"`
|
||||
LastError string `json:"-" gorm:"type:text;not null;default:''"`
|
||||
LastCheckedAt *time.Time `json:"-"`
|
||||
LastSyncedAt *time.Time `json:"-"`
|
||||
NextCheckAt *time.Time `json:"-" gorm:"index:idx_of_pages_project_source_runtime_next_check_at"`
|
||||
LeaseExpiresAt *time.Time `json:"-"`
|
||||
LeaseToken string `json:"-" gorm:"size:64;not null;default:''"`
|
||||
UpdatedAt time.Time `json:"-" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName 返回 Pages 项目部署源运行态表名。
|
||||
func (PagesProjectSourceRuntime) TableName() string {
|
||||
return "of_pages_project_source_runtime"
|
||||
}
|
||||
|
||||
// PagesExpiredSourceLeaseCandidate is a scanner query DTO for expired runtime leases.
|
||||
type PagesExpiredSourceLeaseCandidate struct {
|
||||
SourceID uint
|
||||
LeaseToken string
|
||||
LeaseExpiresAt time.Time
|
||||
SyncStatus string
|
||||
SourceType string
|
||||
ReleaseSelector string
|
||||
}
|
||||
|
||||
// PagesDueGitHubSourceCandidate is a scanner query DTO for due GitHub latest checks.
|
||||
type PagesDueGitHubSourceCandidate struct {
|
||||
SourceID uint
|
||||
ConfigVersion int
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestPagesSourceModelsMatchMigrationSchema(t *testing.T) {
|
||||
gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, gormDB.AutoMigrate(
|
||||
&PagesProject{},
|
||||
&PagesDeployment{},
|
||||
&PagesProjectSource{},
|
||||
&PagesProjectSourceRuntime{},
|
||||
))
|
||||
|
||||
assert.Equal(t, "of_pages_project_sources", (PagesProjectSource{}).TableName())
|
||||
assert.Equal(t, "of_pages_project_source_runtime", (PagesProjectSourceRuntime{}).TableName())
|
||||
assert.True(t, gormDB.Migrator().HasColumn(&PagesProjectSource{}, "github_repository"))
|
||||
assert.False(t, gormDB.Migrator().HasColumn(&PagesProjectSource{}, "git_hub_repository"))
|
||||
assert.True(t, gormDB.Migrator().HasColumn(&PagesProjectSourceRuntime{}, "etag"))
|
||||
assert.False(t, gormDB.Migrator().HasColumn(&PagesProjectSourceRuntime{}, "e_tag"))
|
||||
|
||||
var indexSQL string
|
||||
require.NoError(t, gormDB.Raw(
|
||||
"SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?",
|
||||
"idx_of_pages_deployments_source_revision",
|
||||
).Scan(&indexSQL).Error)
|
||||
assert.Contains(t, strings.ToUpper(indexSQL), "WHERE SOURCE_IDENTITY IS NOT NULL AND SOURCE_REVISION IS NOT NULL")
|
||||
}
|
||||
|
||||
func TestPagesSourceModelsDoNotSerializeSecretsOrFencingState(t *testing.T) {
|
||||
sourceJSON, err := json.Marshal(PagesProjectSource{
|
||||
ID: 1,
|
||||
ProjectID: 2,
|
||||
RemoteURL: "https://example.com/site.zip?token=secret",
|
||||
ConfigVersion: 3,
|
||||
SourceIdentity: strings.Repeat("a", 64),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, `{}`, string(sourceJSON))
|
||||
|
||||
runtimeJSON, err := json.Marshal(PagesProjectSourceRuntime{
|
||||
SourceID: 1,
|
||||
ETag: `"secret-etag"`,
|
||||
LeaseToken: "secret-lease",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, `{}`, string(runtimeJSON))
|
||||
|
||||
identity := strings.Repeat("b", 64)
|
||||
revision := strings.Repeat("c", 64)
|
||||
deploymentJSON, err := json.Marshal(PagesDeployment{
|
||||
SourceType: "remote_url",
|
||||
SourceIdentity: &identity,
|
||||
SourceRevision: &revision,
|
||||
SourceLabel: "site.zip",
|
||||
SourceMeta: `{"provider":"remote_url","private":"secret"}`,
|
||||
TriggerType: "manual_sync",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(deploymentJSON), identity)
|
||||
assert.NotContains(t, string(deploymentJSON), revision)
|
||||
assert.NotContains(t, string(deploymentJSON), "private")
|
||||
assert.Contains(t, string(deploymentJSON), `"source_type":"remote_url"`)
|
||||
assert.Contains(t, string(deploymentJSON), `"source_label":"site.zip"`)
|
||||
assert.Contains(t, string(deploymentJSON), `"trigger_type":"manual_sync"`)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const tableOfProxyRoutes = "of_proxy_routes"
|
||||
|
||||
// ProxyRoute OpenFlare 代理规则实体。
|
||||
// 域名与证书仅通过 of_zone_domains 关联,不再持久化在本表。
|
||||
type ProxyRoute struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
SiteName string `json:"site_name" gorm:"size:255;not null;default:''"`
|
||||
OriginID *uint `json:"origin_id" gorm:"index"`
|
||||
OriginURL string `json:"origin_url" gorm:"size:2048;not null"`
|
||||
OriginHost string `json:"origin_host" gorm:"size:255"`
|
||||
Upstreams string `json:"upstreams" gorm:"type:text;not null;default:'[]'"`
|
||||
Enabled bool `json:"enabled" gorm:"not null;default:true"`
|
||||
EnableHTTPS bool `json:"enable_https" gorm:"column:enable_https;not null;default:false"`
|
||||
RedirectHTTP bool `json:"redirect_http" gorm:"not null;default:false"`
|
||||
LimitConnPerServer int `json:"limit_conn_per_server" gorm:"not null;default:0"`
|
||||
LimitConnPerIP int `json:"limit_conn_per_ip" gorm:"not null;default:0"`
|
||||
LimitRate string `json:"limit_rate" gorm:"size:32;not null;default:''"`
|
||||
LimitReqPerIP string `json:"limit_req_per_ip" gorm:"size:32;not null;default:''"`
|
||||
CacheEnabled bool `json:"cache_enabled" gorm:"not null;default:false"`
|
||||
CachePolicy string `json:"cache_policy" gorm:"size:32;not null;default:''"`
|
||||
CacheRules string `json:"cache_rules" gorm:"type:text;not null;default:'[]'"`
|
||||
CustomHeaders string `json:"custom_headers" gorm:"type:text;not null;default:'[]'"`
|
||||
BasicAuthEnabled bool `json:"basic_auth_enabled" gorm:"not null;default:false"`
|
||||
BasicAuthUsername string `json:"basic_auth_username" gorm:"size:255;not null;default:''"`
|
||||
BasicAuthPassword string `json:"basic_auth_password" gorm:"size:255;not null;default:''"`
|
||||
UpstreamType string `json:"upstream_type" gorm:"size:32;not null;default:'direct'"`
|
||||
TunnelNodeID *uint `json:"tunnel_node_id" gorm:"index"`
|
||||
TunnelTargetAddr string `json:"tunnel_target_addr" gorm:"size:512"`
|
||||
TunnelTargetProtocol string `json:"tunnel_target_protocol" gorm:"size:16"`
|
||||
PagesProjectID *uint `json:"pages_project_id" gorm:"index"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
ZoneDomains []ZoneDomain `json:"zone_domains,omitempty" gorm:"foreignKey:ProxyRouteID"`
|
||||
}
|
||||
|
||||
// TableName 表名。
|
||||
func (ProxyRoute) TableName() string {
|
||||
return tableOfProxyRoutes
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TLSCertificate OpenFlare TLS 证书实体。
|
||||
type TLSCertificate struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"uniqueIndex;size:255;not null"`
|
||||
CertPEM string `json:"-" gorm:"type:text;not null"`
|
||||
KeyPEM string `json:"-" gorm:"type:text;not null"`
|
||||
NotBefore time.Time `json:"not_before"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
Remark string `json:"remark" gorm:"size:255"`
|
||||
Provider string `json:"provider" gorm:"size:64;default:upload"`
|
||||
AcmeAccountID uint `json:"acme_account_id"`
|
||||
DNSAccountID uint `json:"dns_account_id"`
|
||||
KeyAlgorithm string `json:"key_algorithm" gorm:"size:32"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
PrimaryDomain string `json:"primary_domain" gorm:"size:255"`
|
||||
OtherDomains string `json:"other_domains" gorm:"type:text"`
|
||||
DisableCNAME bool `json:"disable_cname"`
|
||||
SkipDNS bool `json:"skip_dns"`
|
||||
DNS1 string `json:"dns1" gorm:"size:128"`
|
||||
DNS2 string `json:"dns2" gorm:"size:128"`
|
||||
ApplyStatus string `json:"apply_status" gorm:"size:64;default:ready"`
|
||||
ApplyMessage string `json:"apply_message" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名。
|
||||
func (TLSCertificate) TableName() string {
|
||||
return "of_tls_certificates"
|
||||
}
|
||||
|
||||
// TLSProxyRouteRef 删除证书时检查代理规则引用的最小字段集。
|
||||
type TLSProxyRouteRef struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
CertID *uint `gorm:"column:cert_id"`
|
||||
CertIDs string `gorm:"column:cert_ids"`
|
||||
DomainCertIDs string `gorm:"column:domain_cert_ids"`
|
||||
}
|
||||
|
||||
// TableName 表名。
|
||||
func (TLSProxyRouteRef) TableName() string {
|
||||
return tableOfProxyRoutes
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OpenFlareWAFRuleGroup stores a WAF rule group.
|
||||
type OpenFlareWAFRuleGroup struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"size:255;not null"`
|
||||
Enabled bool `json:"enabled" gorm:"not null;default:true"`
|
||||
IsGlobal bool `json:"is_global" gorm:"not null;default:false;index"`
|
||||
Graph string `json:"graph" gorm:"type:text;not null;default:''"`
|
||||
Revision uint64 `json:"revision" gorm:"not null;default:1"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName returns the GORM table name.
|
||||
func (OpenFlareWAFRuleGroup) TableName() string {
|
||||
return "of_waf_rule_groups"
|
||||
}
|
||||
|
||||
// OpenFlareWAFIPGroup stores a WAF IP group.
|
||||
type OpenFlareWAFIPGroup struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"size:255;not null"`
|
||||
Type string `json:"type" gorm:"size:32;not null;index"`
|
||||
Enabled bool `json:"enabled" gorm:"not null;default:true"`
|
||||
IPList string `json:"ip_list" gorm:"type:text;not null;default:'[]'"`
|
||||
AutoConfig string `json:"auto_config" gorm:"type:text;not null;default:'{}'"`
|
||||
ExtIPs string `json:"ext_ips" gorm:"type:text;not null;default:'[]'"`
|
||||
SubscriptionURL string `json:"subscription_url" gorm:"size:2048;not null;default:''"`
|
||||
SubscriptionFormat string `json:"subscription_format" gorm:"size:32;not null;default:'text'"`
|
||||
SubscriptionMappingRule string `json:"subscription_mapping_rule" gorm:"size:255;not null;default:''"`
|
||||
SyncIntervalMinutes int `json:"sync_interval_minutes" gorm:"not null;default:1440"`
|
||||
LastSyncedAt *time.Time `json:"last_synced_at"`
|
||||
NextSyncAt *time.Time `json:"next_sync_at" gorm:"index"`
|
||||
LastSyncStatus string `json:"last_sync_status" gorm:"size:32;not null;default:''"`
|
||||
LastSyncMessage string `json:"last_sync_message" gorm:"type:text;not null;default:''"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName returns the GORM table name.
|
||||
func (OpenFlareWAFIPGroup) TableName() string {
|
||||
return "of_waf_ip_groups"
|
||||
}
|
||||
|
||||
// OpenFlareWAFRuleGroupBinding binds a rule group to a proxy route.
|
||||
type OpenFlareWAFRuleGroupBinding struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
RuleGroupID uint `json:"rule_group_id" gorm:"not null;uniqueIndex:idx_of_waf_group_route"`
|
||||
ProxyRouteID uint `json:"proxy_route_id" gorm:"not null;uniqueIndex:idx_of_waf_group_route;index"`
|
||||
Sequence int `json:"sequence" gorm:"not null;default:0"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// ErrWAFRuleRevisionConflict indicates that a rule graph was updated from a stale revision.
|
||||
var ErrWAFRuleRevisionConflict = errors.New("waf rule revision conflict")
|
||||
|
||||
// TableName returns the GORM table name.
|
||||
func (OpenFlareWAFRuleGroupBinding) TableName() string {
|
||||
return "of_waf_rule_group_bindings"
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
tableOfZones = "of_zones"
|
||||
tableOfZoneDomains = "of_zone_domains"
|
||||
)
|
||||
|
||||
// Zone OpenFlare 注册根域实体。
|
||||
type Zone struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Domain string `json:"domain" gorm:"uniqueIndex:idx_of_zones_domain;size:255;not null"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名。
|
||||
func (Zone) TableName() string {
|
||||
return tableOfZones
|
||||
}
|
||||
|
||||
// ZoneDomain OpenFlare Zone 下的明确域名实体。
|
||||
type ZoneDomain struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
ZoneID uint `json:"zone_id" gorm:"not null;index:idx_of_zone_domains_zone_id"`
|
||||
ProxyRouteID *uint `json:"proxy_route_id" gorm:"index:idx_of_zone_domains_proxy_route_id"`
|
||||
Domain string `json:"domain" gorm:"uniqueIndex:idx_of_zone_domains_domain;size:255;not null"`
|
||||
CertID *uint `json:"cert_id" gorm:"index:idx_of_zone_domains_cert_id"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名。
|
||||
func (ZoneDomain) TableName() string {
|
||||
return tableOfZoneDomains
|
||||
}
|
||||
|
||||
// ZoneDomainCount is the per-zone explicit domain count for list queries.
|
||||
type ZoneDomainCount struct {
|
||||
ZoneID uint `json:"zone_id" gorm:"column:zone_id"`
|
||||
Count int64 `json:"count" gorm:"column:count"`
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
adminmodel "Wavelet/plugins/domain/admin/model"
|
||||
authmodel "Wavelet/plugins/domain/auth"
|
||||
uploadmodels "Wavelet/plugins/domain/upload/models"
|
||||
usermodel "Wavelet/plugins/domain/user"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenByteLength = 24
|
||||
maskThreshold = 8
|
||||
)
|
||||
|
||||
// User is the Wavelet w_users entity.
|
||||
type User = usermodel.User
|
||||
|
||||
// AccessToken is the Wavelet w_access_tokens entity.
|
||||
type AccessToken = usermodel.AccessToken
|
||||
|
||||
// AuthSource is the Wavelet w_auth_sources entity.
|
||||
type AuthSource = authmodel.AuthSource
|
||||
|
||||
// ExternalAccount is the Wavelet w_external_accounts entity.
|
||||
type ExternalAccount = authmodel.ExternalAccount
|
||||
|
||||
// TaskExecution is the Wavelet w_task_executions entity.
|
||||
type TaskExecution = adminmodel.TaskExecution
|
||||
|
||||
// Template is the Wavelet w_templates entity.
|
||||
type Template = adminmodel.Template
|
||||
|
||||
// Schedule is the Wavelet w_schedules entity.
|
||||
type Schedule = adminmodel.Schedule
|
||||
|
||||
// Upload is the Wavelet w_uploads entity.
|
||||
type Upload = uploadmodels.Upload
|
||||
|
||||
// UploadMetadata is the Wavelet upload metadata JSON.
|
||||
type UploadMetadata = uploadmodels.UploadMetadata
|
||||
|
||||
// UploadStatus is the Wavelet upload status.
|
||||
type UploadStatus = uploadmodels.UploadStatus
|
||||
|
||||
// UploadStat is the Wavelet w_upload_stats entity.
|
||||
type UploadStat = uploadmodels.UploadStat
|
||||
|
||||
const (
|
||||
// UploadStatusPending is a newly stored unused upload.
|
||||
UploadStatusPending = uploadmodels.UploadStatusPending
|
||||
// UploadStatusUsed is an in-use upload.
|
||||
UploadStatusUsed = uploadmodels.UploadStatusUsed
|
||||
// UploadStatusDeleted is a soft-deleted upload.
|
||||
UploadStatusDeleted = uploadmodels.UploadStatusDeleted
|
||||
|
||||
// UploadStatDimensionTotal is the total stats dimension.
|
||||
UploadStatDimensionTotal = uploadmodels.UploadStatDimensionTotal
|
||||
// UploadStatDimensionType is the type stats dimension.
|
||||
UploadStatDimensionType = uploadmodels.UploadStatDimensionType
|
||||
// UploadStatDimensionCategory is the category stats dimension.
|
||||
UploadStatDimensionCategory = uploadmodels.UploadStatDimensionCategory
|
||||
// UploadStatDimensionTrend is the trend stats dimension.
|
||||
UploadStatDimensionTrend = uploadmodels.UploadStatDimensionTrend
|
||||
)
|
||||
|
||||
// GenerateTokenString 生成加密安全的随机 Token 值
|
||||
func GenerateTokenString() (string, error) {
|
||||
bytes := make([]byte, tokenByteLength)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "at_" + hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// HashToken 计算 Token 的 SHA-256 哈希值用于数据库存储与查询
|
||||
func HashToken(token string) string {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(token))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// MaskTokenString 生成脱敏显示的 Token,仅保留前缀和最后四位
|
||||
func MaskTokenString(token string) string {
|
||||
if len(token) <= maskThreshold {
|
||||
return "at_****"
|
||||
}
|
||||
return fmt.Sprintf("%s...%s", token[:7], token[len(token)-4:])
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
adminmodel "Wavelet/plugins/domain/admin/model"
|
||||
)
|
||||
|
||||
// 配置键常量 - 所有系统配置的 key 定义
|
||||
const (
|
||||
ConfigKeyUploadAllowedExtensions = "upload_allowed_extensions" // 允许上传的文件扩展名,逗号分隔
|
||||
ConfigKeySiteName = "site_name" // 站点名称
|
||||
ConfigKeyPasswordLoginEnabled = "password_login_enabled" // 是否允许密码登录
|
||||
ConfigKeyRegistrationEnabled = "registration_enabled" // 是否允许注册
|
||||
ConfigKeyPasswordRegisterEnabled = "password_register_enabled" // 是否允许密码注册
|
||||
ConfigKeyOIDCLoginEnabled = "oidc_login_enabled" // 是否允许 OIDC 登录
|
||||
ConfigKeyMaxAPIKeysPerUser = "max_api_keys_per_user" //nolint:gosec // false positive: config key name. 每个用户最大 API Key 数量
|
||||
ConfigKeyCapLoginEnabled = "cap_login_enabled" // 是否启用登录人机验证
|
||||
ConfigKeyCapAutoSolve = "cap_auto_solve" // 打开页面后是否自动开始计算(false 则需用户手动点击)
|
||||
ConfigKeyCapChallengeCount = "cap_challenge_count" // 客户端需求解的 PoW 难题总数,默认 1,推荐 1~5
|
||||
ConfigKeyCapChallengeSize = "cap_challenge_size" // 人机验证盐值长度
|
||||
ConfigKeyCapChallengeDifficulty = "cap_challenge_difficulty" // 人机验证 PoW 难度(目标前缀长度)
|
||||
ConfigKeyCapChallengeTTL = "cap_challenge_ttl_seconds" // 人机验证难题有效时间(秒)
|
||||
ConfigKeyCapTokenTTL = "cap_token_ttl_seconds" //nolint:gosec // false positive: config key name. 人机验证兑换凭证有效时间(秒)
|
||||
ConfigKeyServerAddress = "server_address" // 服务器地址
|
||||
ConfigKeySMTPHost = "smtp_host" // SMTP 服务器地址
|
||||
ConfigKeySMTPPort = "smtp_port" // SMTP 端口
|
||||
ConfigKeySMTPUsername = "smtp_username" // SMTP 账户
|
||||
ConfigKeySMTPPassword = "smtp_password" // SMTP 访问凭证
|
||||
ConfigKeyEmailLoginVerificationEnabled = "email_login_verification_enabled" // 是否启用邮箱登录验证
|
||||
ConfigKeyEmailRegisterVerificationEnabled = "email_register_verification_enabled" // 是否启用邮箱注册验证
|
||||
ConfigKeyMenuDisplayConfig = "menu_display_config" // 目录显示配置 (JSON 字符串)
|
||||
ConfigKeySearchEngineIndexingEnabled = "search_engine_indexing_enabled" // 是否允许搜索引擎检索
|
||||
ConfigKeyFileAccessWhitelist = "file_access_whitelist" // 免登录访问的文件业务类型白名单 (JSON 数组格式)
|
||||
ConfigKeyDiskCacheMaxSizeMB = "disk_cache_max_size_mb" // 磁盘缓存最大空间大小 (MB)
|
||||
ConfigKeyDiskCacheTTLMinutes = "disk_cache_ttl_minutes" // 磁盘缓存默认有效期 (分钟)
|
||||
ConfigKeyDiskCacheLRUEnabled = "disk_cache_lru_enabled" // 是否启用 LRU 淘汰机制
|
||||
ConfigKeyLoginSessionTTLHours = "login_session_ttl_hours" // 登录会话过期时间 (小时,0表示浏览器关闭后自动退出登录,-1表示永不过期)
|
||||
ConfigKeyUpdateUpstreamRepository = "update_upstream_repository" // GitHub Actions Release 上游仓库
|
||||
ConfigKeyStorageConfig = "storage_config" // 文件存储配置 (JSON)
|
||||
ConfigKeyRelayFRPSWebUIEnabled = "relay_frps_web_ui_enabled" // 是否启用 FRPS 内置 Web 界面
|
||||
ConfigKeyRelayFRPSWebUIPort = "relay_frps_web_ui_port" // FRPS 内置 Web 界面端口
|
||||
|
||||
// OpenFlare 业务配置(从 of_options 迁移)
|
||||
ConfigKeyAgentDiscoveryToken = "agent_discovery_token" //nolint:gosec // false positive: config key name. Agent 发现令牌
|
||||
ConfigKeyAgentHeartbeatInterval = "agent_heartbeat_interval" // Agent 心跳间隔(毫秒)
|
||||
ConfigKeyAgentWebsocketUpgradeEnabled = "agent_websocket_upgrade_enabled" // Agent WebSocket 升级开关
|
||||
ConfigKeyNodeOfflineThreshold = "node_offline_threshold" // 节点离线阈值(毫秒)
|
||||
ConfigKeyAgentUpdateRepo = "agent_update_repo" // Agent 更新仓库
|
||||
ConfigKeyGeoIPProvider = "geoip_provider" // GeoIP 服务商
|
||||
|
||||
// Pages 静态托管配置
|
||||
ConfigKeyPagesMaxPackageSizeMB = "pages_max_package_size_mb" // Pages 部署包上传大小上限(MiB)
|
||||
ConfigKeyPagesMaxHistoryCount = "pages_max_history_count" // Pages 每个项目最大历史部署保留数(0 表示不限制)
|
||||
|
||||
// UptimeKuma 集成配置
|
||||
ConfigKeyUptimeKumaEnabled = "uptime_kuma_enabled" // UptimeKuma 集成开关
|
||||
ConfigKeyUptimeKumaURL = "uptime_kuma_url" // UptimeKuma URL
|
||||
ConfigKeyUptimeKumaUsername = "uptime_kuma_username" // UptimeKuma 用户名
|
||||
ConfigKeyUptimeKumaPassword = "uptime_kuma_password" //nolint:gosec // false positive: config key name. UptimeKuma 密码
|
||||
ConfigKeyUptimeKumaMonitorScope = "uptime_kuma_monitor_scope" // UptimeKuma 监控范围
|
||||
ConfigKeyUptimeKumaSelectedSites = "uptime_kuma_selected_sites" // UptimeKuma 选定站点
|
||||
ConfigKeyUptimeKumaSyncInterval = "uptime_kuma_sync_interval" // UptimeKuma 同步间隔(分钟)
|
||||
ConfigKeyUptimeKumaInterval = "uptime_kuma_interval" // UptimeKuma 监控间隔(秒)
|
||||
ConfigKeyUptimeKumaRetry = "uptime_kuma_retry" // UptimeKuma 重试次数
|
||||
ConfigKeyUptimeKumaRetryInterval = "uptime_kuma_retry_interval" // UptimeKuma 重试间隔(秒)
|
||||
ConfigKeyUptimeKumaTimeout = "uptime_kuma_timeout" // UptimeKuma 超时(秒)
|
||||
|
||||
// OpenResty 配置
|
||||
ConfigKeyOpenRestyDefaultServerReturnStatus = "openresty_default_server_return_status" // 默认服务器返回状态码
|
||||
ConfigKeyOpenRestyWorkerProcesses = "openresty_worker_processes" // Worker 进程数
|
||||
ConfigKeyOpenRestyWorkerConnections = "openresty_worker_connections" // Worker 连接数
|
||||
ConfigKeyOpenRestyWorkerRlimitNofile = "openresty_worker_rlimit_nofile" // Worker 文件描述符限制
|
||||
ConfigKeyOpenRestyEventsUse = "openresty_events_use" // 事件模型
|
||||
ConfigKeyOpenRestyEventsMultiAcceptEnabled = "openresty_events_multi_accept_enabled" // 多路接受开关
|
||||
ConfigKeyOpenRestyKeepaliveTimeout = "openresty_keepalive_timeout" // Keepalive 超时(秒)
|
||||
ConfigKeyOpenRestyKeepaliveRequests = "openresty_keepalive_requests" // Keepalive 请求数
|
||||
ConfigKeyOpenRestyClientHeaderTimeout = "openresty_client_header_timeout" // 客户端头超时(秒)
|
||||
ConfigKeyOpenRestyClientBodyTimeout = "openresty_client_body_timeout" // 客户端体超时(秒)
|
||||
ConfigKeyOpenRestyClientMaxBodySize = "openresty_client_max_body_size" // 客户端最大体大小
|
||||
ConfigKeyOpenRestyLargeClientHeaderBuffers = "openresty_large_client_header_buffers" // 大客户端头缓冲区
|
||||
ConfigKeyOpenRestySendTimeout = "openresty_send_timeout" // 发送超时(秒)
|
||||
ConfigKeyOpenRestyResolvers = "openresty_resolvers" // DNS 解析器
|
||||
ConfigKeyOpenRestyProxyConnectTimeout = "openresty_proxy_connect_timeout" // 代理连接超时(秒)
|
||||
ConfigKeyOpenRestyProxySendTimeout = "openresty_proxy_send_timeout" // 代理发送超时(秒)
|
||||
ConfigKeyOpenRestyProxyReadTimeout = "openresty_proxy_read_timeout" // 代理读取超时(秒)
|
||||
ConfigKeyOpenRestyWebsocketEnabled = "openresty_websocket_enabled" // WebSocket 支持开关
|
||||
ConfigKeyOpenRestyHTTP3Enabled = "openresty_http3_enabled" // HTTP/3 支持开关
|
||||
ConfigKeyOpenRestyProxyRequestBufferingEnabled = "openresty_proxy_request_buffering_enabled" // 代理请求缓冲开关
|
||||
ConfigKeyOpenRestyProxyBufferingEnabled = "openresty_proxy_buffering_enabled" // 代理响应缓冲开关
|
||||
ConfigKeyOpenRestyProxyBuffers = "openresty_proxy_buffers" // 代理缓冲区
|
||||
ConfigKeyOpenRestyProxyBufferSize = "openresty_proxy_buffer_size" // 代理缓冲区大小
|
||||
ConfigKeyOpenRestyProxyBusyBuffersSize = "openresty_proxy_busy_buffers_size" // 代理繁忙缓冲区大小
|
||||
ConfigKeyOpenRestyGzipEnabled = "openresty_gzip_enabled" // Gzip 压缩开关
|
||||
ConfigKeyOpenRestyGzipMinLength = "openresty_gzip_min_length" // Gzip 最小长度
|
||||
ConfigKeyOpenRestyGzipCompLevel = "openresty_gzip_comp_level" // Gzip 压缩级别
|
||||
ConfigKeyOpenRestyCacheEnabled = "openresty_cache_enabled" // 缓存开关
|
||||
ConfigKeyOpenRestyCachePath = "openresty_cache_path" // 缓存路径
|
||||
ConfigKeyOpenRestyCacheLevels = "openresty_cache_levels" // 缓存层级
|
||||
ConfigKeyOpenRestyCacheInactive = "openresty_cache_inactive" // 缓存不活跃时间
|
||||
ConfigKeyOpenRestyCacheMaxSize = "openresty_cache_max_size" // 缓存最大大小
|
||||
ConfigKeyOpenRestyCacheKeyTemplate = "openresty_cache_key_template" // 缓存键模板
|
||||
ConfigKeyOpenRestyCacheLockEnabled = "openresty_cache_lock_enabled" // 缓存锁开关
|
||||
ConfigKeyOpenRestyCacheLockTimeout = "openresty_cache_lock_timeout" // 缓存锁超时
|
||||
ConfigKeyOpenRestyCacheUseStale = "openresty_cache_use_stale" // 缓存失效策略
|
||||
ConfigKeyOpenRestyMainConfigTemplate = "openresty_main_config_template" // 主配置模板
|
||||
ConfigKeyOpenRestyDefaultLimitConnPerServer = "openresty_default_limit_conn_per_server" // 默认站点并发连接
|
||||
ConfigKeyOpenRestyDefaultLimitConnPerIP = "openresty_default_limit_conn_per_ip" // 默认单 IP 并发连接
|
||||
ConfigKeyOpenRestyDefaultLimitRate = "openresty_default_limit_rate" // 默认单请求带宽
|
||||
ConfigKeyOpenRestyDefaultLimitReqPerIP = "openresty_default_limit_req_per_ip" // 默认单 IP 请求频率限制
|
||||
|
||||
// 源站错误页
|
||||
ConfigKeyOriginErrorPageEnabled = "origin_error_page_enabled" // 是否启用源站错误页
|
||||
ConfigKeyOriginErrorPageStatusCodes = "origin_error_page_status_codes" // 源站错误页触发状态码标签 JSON 数组
|
||||
ConfigKeyOriginErrorPageHTML = "origin_error_page_html" // 源站错误页自定义 HTML(空则内置默认)
|
||||
ConfigKeyOriginErrorPageGetOnly = "origin_error_page_get_only" // 是否仅对 GET 请求返回自定义错误页
|
||||
|
||||
// Service Worker 离线兜底
|
||||
ConfigKeySWOfflineEnabled = "sw_offline_enabled" // 是否启用 Service Worker 离线兜底
|
||||
ConfigKeySWOfflineHTML = "sw_offline_html" // 离线联系页自定义 HTML(空则内置默认)
|
||||
ConfigKeySWOfflineDomains = "sw_offline_domains" // 离线兜底生效域名列表(JSON 数组,空则仅总开关无效)
|
||||
)
|
||||
|
||||
// 日志数据库解耦
|
||||
const (
|
||||
ConfigKeyLogDatabase = "log_database" // 当前日志主库:postgres|sqlite|clickhouse(仅迁移任务写入)
|
||||
ConfigKeyLogDBMigration = "log_db_migration" // 迁移冻结标记:"migrating" 或空
|
||||
ConfigKeyLogRetentionDaysPostgres = "log_retention_days_postgres" // PostgreSQL 日志保留天数
|
||||
ConfigKeyLogRetentionDaysSQLite = "log_retention_days_sqlite" // SQLite 日志保留天数
|
||||
ConfigKeyLogRetentionDaysClickHouse = "log_retention_days_clickhouse" // ClickHouse 日志保留天数
|
||||
// ConfigKeyMetricRetentionDays 性能指标(CPU/内存/磁盘/网络)保留天数,三库共用;
|
||||
// 性能数据价值衰减快,默认短留存(3 天),不随访问日志保留配置。
|
||||
ConfigKeyMetricRetentionDays = "metric_retention_days"
|
||||
)
|
||||
|
||||
const (
|
||||
// ConfigVisibilityHidden 表示配置不通过公共配置接口暴露
|
||||
ConfigVisibilityHidden = adminmodel.ConfigVisibilityHidden
|
||||
// ConfigVisibilityVisible 表示配置通过公共配置接口暴露
|
||||
ConfigVisibilityVisible = adminmodel.ConfigVisibilityVisible
|
||||
)
|
||||
|
||||
// SystemConfig is the Wavelet w_system_configs entity.
|
||||
type SystemConfig = adminmodel.SystemConfig
|
||||
Reference in New Issue
Block a user