任务执行1-19
This commit is contained in:
28
internal/models/audit_log.go
Normal file
28
internal/models/audit_log.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type AuditLog struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
TraceID string `gorm:"size:96;index" json:"trace_id"`
|
||||
SourceService string `gorm:"size:64;index" json:"source_service"`
|
||||
ActorID string `gorm:"size:128;index" json:"actor_id"`
|
||||
ActorName string `gorm:"size:128" json:"actor_name"`
|
||||
Action string `gorm:"size:128;index" json:"action"`
|
||||
ObjectType string `gorm:"size:128;index" json:"object_type"`
|
||||
ObjectID string `gorm:"size:128;index" json:"object_id"`
|
||||
OperationRisk string `gorm:"size:32;index" json:"operation_risk"`
|
||||
ApprovalID string `gorm:"size:128;index" json:"approval_id"`
|
||||
RequestMethod string `gorm:"size:16" json:"request_method"`
|
||||
RequestPath string `gorm:"size:512" json:"request_path"`
|
||||
ClientIP string `gorm:"size:64" json:"client_ip"`
|
||||
BeforeJSON string `gorm:"type:text" json:"before_json"`
|
||||
AfterJSON string `gorm:"type:text" json:"after_json"`
|
||||
Result string `gorm:"size:32;index" json:"result"`
|
||||
ErrorMessage string `gorm:"type:text" json:"error_message"`
|
||||
}
|
||||
|
||||
func (AuditLog) TableName() string {
|
||||
return "logs_audit_logs"
|
||||
}
|
||||
24
internal/models/blueprint_fields_test.go
Normal file
24
internal/models/blueprint_fields_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTrapDictionaryEntryHasBlueprintFields(t *testing.T) {
|
||||
typ := reflect.TypeOf(TrapDictionaryEntry{})
|
||||
for _, name := range []string{"Vendor", "OID", "Name", "SeverityMappingJSON", "ParseExpression"} {
|
||||
if _, ok := typ.FieldByName(name); !ok {
|
||||
t.Fatalf("TrapDictionaryEntry missing blueprint field %s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyslogRuleHasBlueprintFields(t *testing.T) {
|
||||
typ := reflect.TypeOf(SyslogRule{})
|
||||
for _, name := range []string{"SourceMatch", "MessageRegex", "SeverityMappingJSON", "ResourceUIDExtractRegex"} {
|
||||
if _, ok := typ.FieldByName(name); !ok {
|
||||
t.Fatalf("SyslogRule missing blueprint field %s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
28
internal/models/dangerous_operation_approval.go
Normal file
28
internal/models/dangerous_operation_approval.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type DangerousOperationApproval struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
RequestID string `gorm:"size:128;uniqueIndex" json:"request_id"`
|
||||
SourceService string `gorm:"size:64;index" json:"source_service"`
|
||||
Action string `gorm:"size:128;index" json:"action"`
|
||||
ObjectType string `gorm:"size:128;index" json:"object_type"`
|
||||
ObjectID string `gorm:"size:128;index" json:"object_id"`
|
||||
RequesterID string `gorm:"size:128;index" json:"requester_id"`
|
||||
RequesterName string `gorm:"size:128" json:"requester_name"`
|
||||
Reason string `gorm:"type:text" json:"reason"`
|
||||
BeforeJSON string `gorm:"type:text" json:"before_json"`
|
||||
AfterJSON string `gorm:"type:text" json:"after_json"`
|
||||
Status string `gorm:"size:32;index" json:"status"`
|
||||
ReviewerID string `gorm:"size:128;index" json:"reviewer_id"`
|
||||
ReviewerName string `gorm:"size:128" json:"reviewer_name"`
|
||||
ReviewComment string `gorm:"type:text" json:"review_comment"`
|
||||
ReviewedAt *time.Time `json:"reviewed_at"`
|
||||
}
|
||||
|
||||
func (DangerousOperationApproval) TableName() string {
|
||||
return "logs_dangerous_operation_approvals"
|
||||
}
|
||||
@@ -1,119 +1,147 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
// GetAllModels 数据库迁移用模型列表
|
||||
func GetAllModels() []interface{} {
|
||||
return []interface{}{
|
||||
&LogEvent{},
|
||||
&AlertOutbox{},
|
||||
&ResourceMapping{},
|
||||
&ResourceEventDedup{},
|
||||
&TrapDictionaryEntry{},
|
||||
&SyslogRule{},
|
||||
&TrapRule{},
|
||||
&TrapShield{},
|
||||
}
|
||||
}
|
||||
|
||||
// InitData 初始化默认规则数据(幂等)
|
||||
func InitData(db *gorm.DB) error {
|
||||
if db == nil {
|
||||
return nil
|
||||
}
|
||||
if err := seedDefaultSyslogRules(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedDefaultTrapRules(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedDefaultTrapDictionary(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedDefaultSyslogRules(db *gorm.DB) error {
|
||||
var cnt int64
|
||||
if err := db.Model(&SyslogRule{}).Count(&cnt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt > 0 {
|
||||
return nil
|
||||
}
|
||||
rows := []SyslogRule{
|
||||
{
|
||||
Name: "默认-系统严重错误",
|
||||
Enabled: true,
|
||||
Priority: 100,
|
||||
DeviceNameContains: "",
|
||||
KeywordRegex: "(?i)(panic|fatal|segmentation fault|kernel panic|out of memory|oom)",
|
||||
AlertName: "Syslog严重错误",
|
||||
SeverityCode: "critical",
|
||||
PolicyID: 0,
|
||||
},
|
||||
{
|
||||
Name: "默认-链路中断告警",
|
||||
Enabled: true,
|
||||
Priority: 90,
|
||||
DeviceNameContains: "",
|
||||
KeywordRegex: "(?i)(link down|interface .* down|port .* down)",
|
||||
AlertName: "Syslog链路中断",
|
||||
SeverityCode: "major",
|
||||
PolicyID: 0,
|
||||
},
|
||||
}
|
||||
return db.Create(&rows).Error
|
||||
}
|
||||
|
||||
func seedDefaultTrapRules(db *gorm.DB) error {
|
||||
var cnt int64
|
||||
if err := db.Model(&TrapRule{}).Count(&cnt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt > 0 {
|
||||
return nil
|
||||
}
|
||||
rows := []TrapRule{
|
||||
{
|
||||
Name: "默认-Trap链路中断",
|
||||
Enabled: true,
|
||||
Priority: 100,
|
||||
OIDPrefix: "1.3.6.1.6.3.1.1.5",
|
||||
VarbindMatchRegex: "(?i)(linkdown|ifdown|down)",
|
||||
AlertName: "SNMP Trap链路中断",
|
||||
SeverityCode: "major",
|
||||
PolicyID: 0,
|
||||
},
|
||||
}
|
||||
return db.Create(&rows).Error
|
||||
}
|
||||
|
||||
func seedDefaultTrapDictionary(db *gorm.DB) error {
|
||||
var cnt int64
|
||||
if err := db.Model(&TrapDictionaryEntry{}).Count(&cnt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt > 0 {
|
||||
return nil
|
||||
}
|
||||
rows := []TrapDictionaryEntry{
|
||||
{
|
||||
OIDPrefix: "1.3.6.1.6.3.1.1.5.3",
|
||||
Title: "ifDown 接口中断",
|
||||
Description: "检测到设备接口状态变为 down。",
|
||||
SeverityCode: "major",
|
||||
RecoveryMessage: "请检查链路、端口状态和对端设备。",
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
OIDPrefix: "1.3.6.1.6.3.1.1.5.4",
|
||||
Title: "ifUp 接口恢复",
|
||||
Description: "检测到设备接口状态恢复为 up。",
|
||||
SeverityCode: "info",
|
||||
RecoveryMessage: "接口已恢复,请确认业务连通性。",
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
return db.Create(&rows).Error
|
||||
}
|
||||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
// GetAllModels 数据库迁移用模型列表
|
||||
func GetAllModels() []interface{} {
|
||||
return []interface{}{
|
||||
&LogEvent{},
|
||||
&AlertOutbox{},
|
||||
&ResourceMapping{},
|
||||
&ResourceEventDedup{},
|
||||
&TrapDictionaryEntry{},
|
||||
&SyslogRule{},
|
||||
&TrapRule{},
|
||||
&TrapShield{},
|
||||
&AuditLog{},
|
||||
&DangerousOperationApproval{},
|
||||
}
|
||||
}
|
||||
|
||||
// InitData 初始化默认规则数据(幂等)
|
||||
func InitData(db *gorm.DB) error {
|
||||
if db == nil {
|
||||
return nil
|
||||
}
|
||||
if err := seedDefaultSyslogRules(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedDefaultTrapRules(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedDefaultTrapDictionary(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedDefaultSyslogRules(db *gorm.DB) error {
|
||||
var cnt int64
|
||||
if err := db.Model(&SyslogRule{}).Count(&cnt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt > 0 {
|
||||
return nil
|
||||
}
|
||||
rows := []SyslogRule{
|
||||
{
|
||||
Name: "默认-系统严重错误",
|
||||
Enabled: true,
|
||||
Priority: 100,
|
||||
DeviceNameContains: "",
|
||||
KeywordRegex: "(?i)(panic|fatal|segmentation fault|kernel panic|out of memory|oom)",
|
||||
AlertName: "Syslog严重错误",
|
||||
SeverityCode: "critical",
|
||||
PolicyID: 0,
|
||||
},
|
||||
{
|
||||
Name: "默认-链路中断告警",
|
||||
Enabled: true,
|
||||
Priority: 90,
|
||||
DeviceNameContains: "",
|
||||
KeywordRegex: "(?i)(link down|interface .* down|port .* down)",
|
||||
SourceMatch: "",
|
||||
MessageRegex: "(?i)(link down|interface .* down|port .* down|LINK_DOWN)",
|
||||
AlertName: "Syslog链路中断",
|
||||
SeverityCode: "major",
|
||||
SeverityMappingJSON: `{"(?i)(critical|fatal|emergency)":"critical","(?i)(error|LINK_DOWN|down)":"major","(?i)(warning|warn)":"warning"}`,
|
||||
ResourceUIDExtractRegex: `(?i)(?:resource_uid=|resource=)(?P<resource_uid>[a-z0-9_-]+:[a-z0-9_.:/-]+)|Interface (?P<iface>[A-Za-z0-9/._-]+)`,
|
||||
PolicyID: 0,
|
||||
},
|
||||
{
|
||||
Name: "H3C-Syslog-接口中断",
|
||||
Enabled: true,
|
||||
Priority: 120,
|
||||
SourceMatch: "h3c",
|
||||
MessageRegex: `(?i)(LINK_DOWN|Interface .* down|port .* down)`,
|
||||
AlertName: "H3C Syslog接口中断",
|
||||
SeverityCode: "major",
|
||||
SeverityMappingJSON: `{"(?i)(LINK_DOWN|down)":"major","(?i)(LINK_UP|up)":"info"}`,
|
||||
ResourceUIDExtractRegex: `(?i)(?:resource_uid=|resource=)(?P<resource_uid>network:[a-z0-9_.:/-]+)|Interface (?P<iface>[A-Za-z0-9/._-]+)`,
|
||||
PolicyID: 0,
|
||||
},
|
||||
}
|
||||
return db.Create(&rows).Error
|
||||
}
|
||||
|
||||
func seedDefaultTrapRules(db *gorm.DB) error {
|
||||
var cnt int64
|
||||
if err := db.Model(&TrapRule{}).Count(&cnt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt > 0 {
|
||||
return nil
|
||||
}
|
||||
rows := []TrapRule{
|
||||
{
|
||||
Name: "默认-Trap链路中断",
|
||||
Enabled: true,
|
||||
Priority: 100,
|
||||
OIDPrefix: "1.3.6.1.6.3.1.1.5",
|
||||
VarbindMatchRegex: "(?i)(linkdown|ifdown|down)",
|
||||
AlertName: "SNMP Trap链路中断",
|
||||
SeverityCode: "major",
|
||||
PolicyID: 0,
|
||||
},
|
||||
}
|
||||
return db.Create(&rows).Error
|
||||
}
|
||||
|
||||
func seedDefaultTrapDictionary(db *gorm.DB) error {
|
||||
var cnt int64
|
||||
if err := db.Model(&TrapDictionaryEntry{}).Count(&cnt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt > 0 {
|
||||
return nil
|
||||
}
|
||||
rows := []TrapDictionaryEntry{
|
||||
{
|
||||
Vendor: "H3C",
|
||||
OID: "1.3.6.1.6.3.1.1.5.3",
|
||||
OIDPrefix: "1.3.6.1.6.3.1.1.5.3",
|
||||
Name: "H3C ifDown 接口中断",
|
||||
Title: "ifDown 接口中断",
|
||||
Description: "检测到设备接口状态变为 down。",
|
||||
SeverityCode: "major",
|
||||
SeverityMappingJSON: `{"down":"major","up":"info"}`,
|
||||
ParseExpression: `(?i)(ifName|interface)=?(?P<interface>[A-Za-z0-9/._-]+)`,
|
||||
RecoveryMessage: "请检查链路、端口状态和对端设备。",
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
Vendor: "H3C",
|
||||
OID: "1.3.6.1.6.3.1.1.5.4",
|
||||
OIDPrefix: "1.3.6.1.6.3.1.1.5.4",
|
||||
Name: "H3C ifUp 接口恢复",
|
||||
Title: "ifUp 接口恢复",
|
||||
Description: "检测到设备接口状态恢复为 up。",
|
||||
SeverityCode: "info",
|
||||
SeverityMappingJSON: `{"up":"info"}`,
|
||||
ParseExpression: `(?i)(ifName|interface)=?(?P<interface>[A-Za-z0-9/._-]+)`,
|
||||
RecoveryMessage: "接口已恢复,请确认业务连通性。",
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
return db.Create(&rows).Error
|
||||
}
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// SyslogRule 表示一条 Syslog 规则,用于匹配设备日志并触发告警。
|
||||
type SyslogRule struct {
|
||||
// ID 是数据库主键。
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
// CreatedAt 记录创建时间(GORM 自动维护)。
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// UpdatedAt 记录更新时间(GORM 自动维护)。
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// Name 规则名称,用于展示/标识。
|
||||
Name string `gorm:"size:256" json:"name"`
|
||||
// Enabled 表示该规则是否启用。
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
// Priority 表示匹配优先级(数值越高/低需以业务约定为准)。
|
||||
Priority int `gorm:"index" json:"priority"`
|
||||
// DeviceNameContains 表示设备名称包含条件。
|
||||
DeviceNameContains string `gorm:"size:512" json:"device_name_contains"`
|
||||
// KeywordRegex 表示关键字/内容匹配的正则表达式。
|
||||
KeywordRegex string `gorm:"size:512" json:"keyword_regex"`
|
||||
// AlertName 表示告警名称。
|
||||
AlertName string `gorm:"size:256" json:"alert_name"`
|
||||
// SeverityCode 表示严重级别编码。
|
||||
SeverityCode string `gorm:"size:32" json:"severity_code"`
|
||||
// PolicyID 表示关联的告警/处理策略 ID。
|
||||
PolicyID uint `json:"policy_id"`
|
||||
}
|
||||
|
||||
func (SyslogRule) TableName() string {
|
||||
return "logs_syslog_rules"
|
||||
}
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// SyslogRule 表示一条 Syslog 规则,用于匹配设备日志并触发告警。
|
||||
type SyslogRule struct {
|
||||
// ID 是数据库主键。
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
// CreatedAt 记录创建时间(GORM 自动维护)。
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// UpdatedAt 记录更新时间(GORM 自动维护)。
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// Name 规则名称,用于展示/标识。
|
||||
Name string `gorm:"size:256" json:"name"`
|
||||
// Enabled 表示该规则是否启用。
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
// Priority 表示匹配优先级(数值越高/低需以业务约定为准)。
|
||||
Priority int `gorm:"index" json:"priority"`
|
||||
// DeviceNameContains 表示设备名称包含条件。
|
||||
DeviceNameContains string `gorm:"size:512" json:"device_name_contains"`
|
||||
// SourceMatch 表示来源匹配条件,可匹配来源 IP、主机名或原始行。
|
||||
SourceMatch string `gorm:"size:512" json:"source_match"`
|
||||
// KeywordRegex 表示关键字/内容匹配的正则表达式。
|
||||
KeywordRegex string `gorm:"size:512" json:"keyword_regex"`
|
||||
// MessageRegex 表示消息正文匹配的正则表达式。
|
||||
MessageRegex string `gorm:"size:1024" json:"message_regex"`
|
||||
// AlertName 表示告警名称。
|
||||
AlertName string `gorm:"size:256" json:"alert_name"`
|
||||
// SeverityCode 表示严重级别编码。
|
||||
SeverityCode string `gorm:"size:32" json:"severity_code"`
|
||||
// SeverityMappingJSON 保存按正则分组或厂商级别映射到平台级别的 JSON。
|
||||
SeverityMappingJSON string `gorm:"type:text" json:"severity_mapping_json"`
|
||||
// ResourceUIDExtractRegex 表示从消息中提取 resource_uid 的正则。
|
||||
ResourceUIDExtractRegex string `gorm:"size:1024" json:"resource_uid_extract_regex"`
|
||||
// PolicyID 表示关联的告警/处理策略 ID。
|
||||
PolicyID uint `json:"policy_id"`
|
||||
}
|
||||
|
||||
func (SyslogRule) TableName() string {
|
||||
return "logs_syslog_rules"
|
||||
}
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// TrapDictionaryEntry 表示 Trap 字典条目,用于描述某个 OID 前缀对应的告警元信息。
|
||||
type TrapDictionaryEntry struct {
|
||||
// ID 是数据库主键。
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
// CreatedAt 记录创建时间(GORM 自动维护)。
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// UpdatedAt 记录更新时间(GORM 自动维护)。
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// OIDPrefix 表示该字典条目对应的 OID 前缀(唯一)。
|
||||
OIDPrefix string `gorm:"size:512;uniqueIndex" json:"oid_prefix"`
|
||||
// Title 表示字典条目的标题。
|
||||
Title string `gorm:"size:512" json:"title"`
|
||||
// Description 表示字典条目的说明文本。
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
// SeverityCode 表示默认严重级别编码。
|
||||
SeverityCode string `gorm:"size:32" json:"severity_code"`
|
||||
// RecoveryMessage 表示恢复/消警时的消息模板内容。
|
||||
RecoveryMessage string `gorm:"type:text" json:"recovery_message"`
|
||||
// Enabled 表示该字典条目是否启用。
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
}
|
||||
|
||||
func (TrapDictionaryEntry) TableName() string {
|
||||
return "logs_trap_dictionary"
|
||||
}
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// TrapDictionaryEntry 表示 Trap 字典条目,用于描述某个 OID 前缀对应的告警元信息。
|
||||
type TrapDictionaryEntry struct {
|
||||
// ID 是数据库主键。
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
// CreatedAt 记录创建时间(GORM 自动维护)。
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// UpdatedAt 记录更新时间(GORM 自动维护)。
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// OIDPrefix 表示该字典条目对应的 OID 前缀(唯一)。
|
||||
OIDPrefix string `gorm:"size:512;uniqueIndex" json:"oid_prefix"`
|
||||
// Vendor 表示设备厂商,例如 H3C、Huawei、Cisco。
|
||||
Vendor string `gorm:"size:128;index" json:"vendor"`
|
||||
// OID 表示精确 Trap OID。为空时继续使用 OIDPrefix 做前缀匹配。
|
||||
OID string `gorm:"size:512;index" json:"oid"`
|
||||
// Name 表示 Trap 字典名称;保留 Title 作为旧页面兼容字段。
|
||||
Name string `gorm:"size:512" json:"name"`
|
||||
// Title 表示字典条目的标题。
|
||||
Title string `gorm:"size:512" json:"title"`
|
||||
// Description 表示字典条目的说明文本。
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
// SeverityCode 表示默认严重级别编码。
|
||||
SeverityCode string `gorm:"size:32" json:"severity_code"`
|
||||
// SeverityMappingJSON 保存按 varbind 或厂商级别映射到平台级别的 JSON。
|
||||
SeverityMappingJSON string `gorm:"type:text" json:"severity_mapping_json"`
|
||||
// ParseExpression 表示解析 varbind 的表达式或正则模板。
|
||||
ParseExpression string `gorm:"type:text" json:"parse_expression"`
|
||||
// RecoveryMessage 表示恢复/消警时的消息模板内容。
|
||||
RecoveryMessage string `gorm:"type:text" json:"recovery_message"`
|
||||
// Enabled 表示该字典条目是否启用。
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
}
|
||||
|
||||
func (TrapDictionaryEntry) TableName() string {
|
||||
return "logs_trap_dictionary"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user