- 安全策略登录限频由 JSON 文本改为结构化表单(次数/锁定分钟/窗口分钟),编辑校验同步扩展 - 失败日志新增解锁入口:/imLoginFailureLog/unlock 经 internal 通道调用 im-server 释放锁定 - 失败日志模型补充 accountHash 字段,解锁按钮按 risk_action=account_locked 展示 - 租户业务库初始化模板补充 im_login_lockouts 等表;新增解锁按钮菜单权限 SQL
370 lines
13 KiB
Go
370 lines
13 KiB
Go
package sysin
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"hotgo/internal/consts"
|
|
"hotgo/internal/library/dict"
|
|
"hotgo/internal/model/input/form"
|
|
|
|
"github.com/gogf/gf/v2/encoding/gjson"
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
"github.com/gogf/gf/v2/os/gtime"
|
|
"github.com/gogf/gf/v2/util/gconv"
|
|
)
|
|
|
|
type ImSecurityPolicyListInp struct {
|
|
form.PageReq
|
|
TenantId int64 `json:"tenantId"`
|
|
PublishStatus string `json:"publishStatus"`
|
|
}
|
|
|
|
func (in *ImSecurityPolicyListInp) Filter(ctx context.Context) error {
|
|
if in.PublishStatus != "" && !dict.HasOptionKey(consts.ImSecurityPublishStatusOptions, in.PublishStatus) {
|
|
return gerror.New("发布状态不合法")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ImMessageCurfewRuleModel struct {
|
|
Id int64 `json:"id"`
|
|
RuleCode string `json:"ruleCode"`
|
|
RuleName string `json:"ruleName"`
|
|
DateType string `json:"dateType"`
|
|
CustomDates string `json:"customDates"`
|
|
Weekdays string `json:"weekdays"`
|
|
Timezone string `json:"timezone"`
|
|
StartTime string `json:"startTime"`
|
|
EndTime string `json:"endTime"`
|
|
Scope string `json:"scope"`
|
|
WhitelistUserIds string `json:"whitelistUserIds"`
|
|
WhitelistGroupIds string `json:"whitelistGroupIds"`
|
|
Message string `json:"message"`
|
|
Status int `json:"status"`
|
|
Sort int `json:"sort"`
|
|
}
|
|
|
|
type ImSecurityPolicyModel struct {
|
|
Id int64 `json:"id"`
|
|
TenantId int64 `json:"tenantId"`
|
|
PolicyVersion uint `json:"policyVersion"`
|
|
PolicyName string `json:"policyName"`
|
|
PublishStatus string `json:"publishStatus"`
|
|
PasswordPolicy string `json:"passwordPolicy"`
|
|
LoginRateLimit string `json:"loginRateLimit"`
|
|
SensitiveWordPolicy string `json:"sensitiveWordPolicy"`
|
|
FileReviewPolicy string `json:"fileReviewPolicy"`
|
|
RealnamePolicy string `json:"realnamePolicy"`
|
|
RiskChallengePolicy string `json:"riskChallengePolicy"`
|
|
GlobalFriendEnabled int `json:"globalFriendEnabled"`
|
|
FriendDisabledPolicy string `json:"friendDisabledPolicy"`
|
|
MessageCurfewEnabled int `json:"messageCurfewEnabled"`
|
|
Remark string `json:"remark"`
|
|
PublishedAt *gtime.Time `json:"publishedAt"`
|
|
PublishedBy int64 `json:"publishedBy"`
|
|
CreatedAt *gtime.Time `json:"createdAt"`
|
|
UpdatedAt *gtime.Time `json:"updatedAt"`
|
|
CurfewRules []*ImMessageCurfewRuleModel `json:"curfewRules"`
|
|
}
|
|
|
|
type ImSecurityPolicyViewInp struct {
|
|
Id int64 `json:"id"`
|
|
TenantId int64 `json:"tenantId"`
|
|
}
|
|
|
|
func (in *ImSecurityPolicyViewInp) Filter(ctx context.Context) error {
|
|
if in.Id <= 0 && in.TenantId < 0 {
|
|
return gerror.New("策略ID或租户ID不合法")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ImMessageCurfewRuleInp struct {
|
|
RuleCode string `json:"ruleCode"`
|
|
RuleName string `json:"ruleName"`
|
|
DateType string `json:"dateType"`
|
|
CustomDates *gjson.Json `json:"customDates"`
|
|
Weekdays *gjson.Json `json:"weekdays"`
|
|
Timezone string `json:"timezone"`
|
|
StartTime string `json:"startTime"`
|
|
EndTime string `json:"endTime"`
|
|
Scope string `json:"scope"`
|
|
WhitelistUserIds *gjson.Json `json:"whitelistUserIds"`
|
|
WhitelistGroupIds *gjson.Json `json:"whitelistGroupIds"`
|
|
Message string `json:"message"`
|
|
Status int `json:"status"`
|
|
Sort int `json:"sort"`
|
|
}
|
|
|
|
func (in *ImMessageCurfewRuleInp) Filter(ctx context.Context) error {
|
|
in.RuleCode = strings.TrimSpace(in.RuleCode)
|
|
in.RuleName = strings.TrimSpace(in.RuleName)
|
|
in.Timezone = strings.TrimSpace(in.Timezone)
|
|
in.Message = strings.TrimSpace(in.Message)
|
|
if !imPolicyGroupCodePattern.MatchString(in.RuleCode) {
|
|
return gerror.New("宵禁规则编码格式不合法")
|
|
}
|
|
if in.RuleName == "" {
|
|
return gerror.New("宵禁规则名称不能为空")
|
|
}
|
|
if !dict.HasOptionKey(consts.ImCurfewDateTypeOptions, in.DateType) {
|
|
return gerror.New("宵禁日期类型不合法")
|
|
}
|
|
if !dict.HasOptionKey(consts.ImCurfewScopeOptions, in.Scope) {
|
|
return gerror.New("宵禁范围不合法")
|
|
}
|
|
if len(in.StartTime) != 5 || len(in.EndTime) != 5 {
|
|
return gerror.New("宵禁时间必须使用HH:mm格式")
|
|
}
|
|
if _, err := time.Parse("15:04", in.StartTime); err != nil {
|
|
return gerror.New("宵禁开始时间不合法")
|
|
}
|
|
if _, err := time.Parse("15:04", in.EndTime); err != nil {
|
|
return gerror.New("宵禁结束时间不合法")
|
|
}
|
|
if in.Timezone == "" {
|
|
in.Timezone = "Asia/Shanghai"
|
|
}
|
|
if in.Message == "" {
|
|
in.Message = "当前时段暂不能发送消息"
|
|
}
|
|
if in.Status == 0 {
|
|
in.Status = consts.StatusEnabled
|
|
}
|
|
if in.Status != consts.StatusEnabled && in.Status != consts.StatusDisable {
|
|
return gerror.New("宵禁规则状态不合法")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ImSecurityPolicySaveInp struct {
|
|
Id int64 `json:"id"`
|
|
TenantId int64 `json:"tenantId"`
|
|
PolicyName string `json:"policyName" v:"required#策略名称不能为空"`
|
|
PasswordPolicy *gjson.Json `json:"passwordPolicy"`
|
|
LoginRateLimit *gjson.Json `json:"loginRateLimit"`
|
|
SensitiveWordPolicy *gjson.Json `json:"sensitiveWordPolicy"`
|
|
FileReviewPolicy *gjson.Json `json:"fileReviewPolicy"`
|
|
RealnamePolicy *gjson.Json `json:"realnamePolicy"`
|
|
RiskChallengePolicy *gjson.Json `json:"riskChallengePolicy"`
|
|
GlobalFriendEnabled int `json:"globalFriendEnabled"`
|
|
FriendDisabledPolicy *gjson.Json `json:"friendDisabledPolicy"`
|
|
MessageCurfewEnabled int `json:"messageCurfewEnabled"`
|
|
Remark string `json:"remark"`
|
|
CurfewRules []*ImMessageCurfewRuleInp `json:"curfewRules"`
|
|
}
|
|
|
|
func (in *ImSecurityPolicySaveInp) Filter(ctx context.Context) error {
|
|
in.PolicyName = strings.TrimSpace(in.PolicyName)
|
|
in.Remark = strings.TrimSpace(in.Remark)
|
|
if in.PolicyName == "" || len([]rune(in.PolicyName)) > 128 {
|
|
return gerror.New("策略名称长度必须为1至128个字符")
|
|
}
|
|
if in.GlobalFriendEnabled != 0 && in.GlobalFriendEnabled != 1 {
|
|
return gerror.New("全局好友开关不合法")
|
|
}
|
|
if in.LoginRateLimit != nil {
|
|
normalized, err := normalizeLoginRateLimit(in.LoginRateLimit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
in.LoginRateLimit = normalized
|
|
}
|
|
if in.MessageCurfewEnabled != 0 && in.MessageCurfewEnabled != 1 {
|
|
return gerror.New("宵禁开关不合法")
|
|
}
|
|
if in.GlobalFriendEnabled == 0 {
|
|
policy := make(map[string]interface{})
|
|
if in.FriendDisabledPolicy != nil {
|
|
policy = in.FriendDisabledPolicy.Map()
|
|
}
|
|
policy["hide_profile"] = true
|
|
policy["block_qrcode"] = true
|
|
policy["block_group_member_entry"] = true
|
|
policy["blocked_entries"] = []string{
|
|
"search", "profile_card", "group_member", "qrcode", "invite_auto_add", "recommendation",
|
|
}
|
|
message, ok := policy["message"].(string)
|
|
if !ok || strings.TrimSpace(message) == "" {
|
|
policy["message"] = "当前时段暂不允许添加好友"
|
|
}
|
|
in.FriendDisabledPolicy = gjson.New(policy)
|
|
}
|
|
if in.MessageCurfewEnabled == 1 && len(in.CurfewRules) == 0 {
|
|
return gerror.New("启用宵禁时至少配置一条宵禁规则")
|
|
}
|
|
seen := make(map[string]struct{}, len(in.CurfewRules))
|
|
hasPrivateMessageRule := false
|
|
for _, rule := range in.CurfewRules {
|
|
if rule == nil {
|
|
return gerror.New("宵禁规则不能为空")
|
|
}
|
|
if err := rule.Filter(ctx); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := seen[rule.RuleCode]; ok {
|
|
return gerror.New("宵禁规则编码不能重复")
|
|
}
|
|
seen[rule.RuleCode] = struct{}{}
|
|
if rule.Scope == consts.ImCurfewScopePrivate && rule.Status == consts.StatusEnabled {
|
|
hasPrivateMessageRule = true
|
|
}
|
|
}
|
|
if in.MessageCurfewEnabled == 1 && !hasPrivateMessageRule {
|
|
return gerror.New("启用宵禁时至少配置一条启用的单聊/私聊规则")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// normalizeLoginRateLimit 校验并归一化登录失败锁定配置:
|
|
// {enabled, password_max_attempts, lock_seconds, window_seconds}。
|
|
// enabled=false 时输出仅含 enabled,行为与未配置一致。
|
|
func normalizeLoginRateLimit(value *gjson.Json) (*gjson.Json, error) {
|
|
policy := map[string]any{"enabled": false, "password_max_attempts": 5, "lock_seconds": 900, "window_seconds": 1800}
|
|
if value != nil && value.String() != "" && value.String() != "null" {
|
|
for key, item := range value.Map() {
|
|
policy[key] = item
|
|
}
|
|
}
|
|
if enabled, _ := policy["enabled"].(bool); !enabled {
|
|
return gjson.New(map[string]any{"enabled": false}), nil
|
|
}
|
|
maxAttempts := gconv.Int(policy["password_max_attempts"])
|
|
if maxAttempts == 0 {
|
|
maxAttempts = 5
|
|
}
|
|
if maxAttempts < 1 || maxAttempts > 20 {
|
|
return nil, gerror.New("登录失败最大次数必须在1至20之间")
|
|
}
|
|
lockSeconds := gconv.Int(policy["lock_seconds"])
|
|
if lockSeconds == 0 {
|
|
lockSeconds = 900
|
|
}
|
|
if lockSeconds < 60 || lockSeconds > 86400 {
|
|
return nil, gerror.New("账号锁定时长必须在1至1440分钟之间")
|
|
}
|
|
windowSeconds := gconv.Int(policy["window_seconds"])
|
|
if windowSeconds == 0 {
|
|
windowSeconds = 1800
|
|
}
|
|
if windowSeconds < 60 || windowSeconds > 14400 {
|
|
return nil, gerror.New("失败计数窗口必须在1至240分钟之间")
|
|
}
|
|
return gjson.New(map[string]any{"enabled": true, "password_max_attempts": maxAttempts, "lock_seconds": lockSeconds, "window_seconds": windowSeconds}), nil
|
|
}
|
|
|
|
type ImSecurityPolicyPublishInp struct {
|
|
Id int64 `json:"id" v:"required|min:1#策略ID不能为空|策略ID不合法"`
|
|
Reason string `json:"reason" v:"required#发布原因不能为空"`
|
|
}
|
|
|
|
func (in *ImSecurityPolicyPublishInp) Filter(ctx context.Context) error {
|
|
in.Reason = strings.TrimSpace(in.Reason)
|
|
if in.Id <= 0 {
|
|
return gerror.New("策略ID不合法")
|
|
}
|
|
if in.Reason == "" {
|
|
return gerror.New("发布原因不能为空")
|
|
}
|
|
if len([]rune(in.Reason)) > 500 {
|
|
return gerror.New("发布原因不能超过500个字符")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ImSecurityPolicyRestoreInp struct {
|
|
TenantId int64 `json:"tenantId"`
|
|
Reason string `json:"reason" v:"required#恢复原因不能为空"`
|
|
}
|
|
|
|
func (in *ImSecurityPolicyRestoreInp) Filter(ctx context.Context) error {
|
|
in.Reason = strings.TrimSpace(in.Reason)
|
|
if in.TenantId < 0 {
|
|
return gerror.New("租户ID不合法")
|
|
}
|
|
if in.Reason == "" {
|
|
return gerror.New("恢复原因不能为空")
|
|
}
|
|
if len([]rune(in.Reason)) > 500 {
|
|
return gerror.New("恢复原因不能超过500个字符")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ImSecurityIPRuleListInp struct {
|
|
form.PageReq
|
|
TenantId int64 `json:"tenantId"`
|
|
RuleType string `json:"ruleType"`
|
|
IpCidr string `json:"ipCidr"`
|
|
Status int `json:"status"`
|
|
}
|
|
|
|
func (in *ImSecurityIPRuleListInp) Filter(ctx context.Context) error {
|
|
in.IpCidr = strings.TrimSpace(in.IpCidr)
|
|
if in.RuleType != "" && in.RuleType != "whitelist" && in.RuleType != "blacklist" {
|
|
return gerror.New("IP规则类型不合法")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ImSecurityIPRuleModel struct {
|
|
Id int64 `json:"id"`
|
|
TenantId int64 `json:"tenantId"`
|
|
RuleType string `json:"ruleType"`
|
|
IpCidr string `json:"ipCidr"`
|
|
Action string `json:"action"`
|
|
SceneJson string `json:"sceneJson"`
|
|
RiskLevel string `json:"riskLevel"`
|
|
Status int `json:"status"`
|
|
ExpiresAt *gtime.Time `json:"expiresAt"`
|
|
Remark string `json:"remark"`
|
|
CreatedAt *gtime.Time `json:"createdAt"`
|
|
UpdatedAt *gtime.Time `json:"updatedAt"`
|
|
}
|
|
type ImSecurityIPRuleEditInp struct {
|
|
Id int64 `json:"id"`
|
|
TenantId int64 `json:"tenantId"`
|
|
RuleType string `json:"ruleType"`
|
|
IpCidr string `json:"ipCidr"`
|
|
Action string `json:"action"`
|
|
SceneJson *gjson.Json `json:"sceneJson"`
|
|
RiskLevel string `json:"riskLevel"`
|
|
Status int `json:"status"`
|
|
ExpiresAt *gtime.Time `json:"expiresAt"`
|
|
Remark string `json:"remark"`
|
|
}
|
|
|
|
func (in *ImSecurityIPRuleEditInp) Filter(ctx context.Context) error {
|
|
in.IpCidr = strings.TrimSpace(in.IpCidr)
|
|
if in.RuleType != "whitelist" && in.RuleType != "blacklist" {
|
|
return gerror.New("IP规则类型不合法")
|
|
}
|
|
if in.IpCidr == "" {
|
|
return gerror.New("IP或CIDR不能为空")
|
|
}
|
|
if !dict.HasOptionKey(consts.ImSecurityActionOptions, in.Action) {
|
|
return gerror.New("规则动作不合法")
|
|
}
|
|
if !dict.HasOptionKey(consts.ImRiskLevelOptions, in.RiskLevel) {
|
|
return gerror.New("风险等级不合法")
|
|
}
|
|
if in.Status == 0 {
|
|
in.Status = consts.StatusEnabled
|
|
}
|
|
if in.Status != consts.StatusEnabled && in.Status != consts.StatusDisable {
|
|
return gerror.New("状态不合法")
|
|
}
|
|
if in.SceneJson == nil || in.SceneJson.String() == "" || in.SceneJson.String() == "null" {
|
|
return gerror.New("生效场景不能为空")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ImSecurityIPRuleDeleteInp struct {
|
|
Id int64 `json:"id" v:"required|min:1#IP规则ID不能为空|IP规则ID不合法"`
|
|
}
|
|
|
|
func (in *ImSecurityIPRuleDeleteInp) Filter(ctx context.Context) error { return nil }
|