1. add APIs for login lockout config get and save 2. add backend service and controller for login lockout config 3. refactor frontend to separate login lockout config from main policy 4. update default login lockout policy values 5. add permission sql and docking documentation
417 lines
15 KiB
Go
417 lines
15 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": 86400, "window_seconds": 3600}
|
|
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 = 86400
|
|
}
|
|
if lockSeconds < 60 || lockSeconds > 86400 {
|
|
return nil, gerror.New("账号锁定时长必须在1至1440分钟之间")
|
|
}
|
|
windowSeconds := gconv.Int(policy["window_seconds"])
|
|
if windowSeconds == 0 {
|
|
windowSeconds = 3600
|
|
}
|
|
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 ImLoginLockoutConfigModel struct {
|
|
Enabled bool `json:"enabled"`
|
|
PasswordMaxAttempts int `json:"passwordMaxAttempts"`
|
|
LockMinutes int `json:"lockMinutes"`
|
|
WindowMinutes int `json:"windowMinutes"`
|
|
PolicyId int64 `json:"policyId"`
|
|
PolicyVersion uint `json:"policyVersion"`
|
|
}
|
|
|
|
type ImLoginLockoutConfigGetInp struct {
|
|
TenantId int64 `json:"tenantId"`
|
|
}
|
|
|
|
func (in *ImLoginLockoutConfigGetInp) Filter(ctx context.Context) error {
|
|
if in.TenantId < 0 {
|
|
return gerror.New("租户ID不合法")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ImLoginLockoutConfigSaveInp struct {
|
|
TenantId int64 `json:"tenantId"`
|
|
Enabled bool `json:"enabled"`
|
|
PasswordMaxAttempts int `json:"passwordMaxAttempts"`
|
|
LockMinutes int `json:"lockMinutes"`
|
|
WindowMinutes int `json:"windowMinutes"`
|
|
}
|
|
|
|
func (in *ImLoginLockoutConfigSaveInp) Filter(ctx context.Context) error {
|
|
if in.TenantId < 0 {
|
|
return gerror.New("租户ID不合法")
|
|
}
|
|
if !in.Enabled {
|
|
return nil
|
|
}
|
|
if in.PasswordMaxAttempts < 1 || in.PasswordMaxAttempts > 20 {
|
|
return gerror.New("登录失败最大次数必须在1至20之间")
|
|
}
|
|
if in.LockMinutes < 1 || in.LockMinutes > 1440 {
|
|
return gerror.New("账号锁定时长必须在1至1440分钟之间")
|
|
}
|
|
if in.WindowMinutes < 1 || in.WindowMinutes > 240 {
|
|
return gerror.New("失败计数窗口必须在1至240分钟之间")
|
|
}
|
|
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 }
|