Merge branch 'dev' of https://git.118im.com/im_team/im_manager into dev
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
-- 消息加密密钥管理。
|
||||
-- 可重复执行。根 KEK 只在首次执行时生成,重复执行不会覆盖已有值。
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET @now := NOW();
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_tenant_config'
|
||||
AND COLUMN_NAME = 'message_encryption_enabled') = 0,
|
||||
'ALTER TABLE `im_tenant_config` ADD COLUMN `message_encryption_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否启用消息加密'' AFTER `local_moderation_enabled`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `im_message_encryption_keys` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` bigint NOT NULL COMMENT '商户ID',
|
||||
`key_version` int unsigned NOT NULL COMMENT '密钥版本',
|
||||
`algorithm` varchar(32) NOT NULL DEFAULT 'AES-256-GCM' COMMENT '数据加密算法',
|
||||
`encrypted_dek` blob NOT NULL COMMENT '由根KEK包裹后的商户DEK',
|
||||
`dek_nonce` varbinary(12) NOT NULL COMMENT '包裹DEK使用的随机nonce',
|
||||
`status` varchar(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/active/retired/disabled',
|
||||
`activated_at` datetime DEFAULT NULL,
|
||||
`retired_at` datetime DEFAULT NULL,
|
||||
`remark` varchar(255) NOT NULL DEFAULT '',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`created_by` bigint NOT NULL DEFAULT 0,
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`updated_by` bigint NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_im_message_key_tenant_version` (`tenant_id`, `key_version`),
|
||||
KEY `idx_im_message_key_tenant_status` (`tenant_id`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商户消息加密密钥版本';
|
||||
|
||||
INSERT INTO `hg_sys_dict_type`
|
||||
(`pid`, `name`, `type`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT 0, 'IM内部安全材料', 'im_internal_secret', 9999,
|
||||
'仅供服务端读取,通用字典接口禁止查询和维护', 1, @now, @now
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `hg_sys_dict_type` WHERE `type` = 'im_internal_secret'
|
||||
);
|
||||
|
||||
INSERT INTO `hg_sys_dict_data`
|
||||
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT 'core_material_01', TO_BASE64(RANDOM_BYTES(32)), 'string', 'im_internal_secret',
|
||||
'', 2, 1, '消息DEK包裹根密钥KEK,请勿通过后台展示或修改', 1, @now, @now
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `hg_sys_dict_data`
|
||||
WHERE `type` = 'im_internal_secret' AND `label` = 'core_material_01'
|
||||
);
|
||||
|
||||
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
|
||||
SET @settingsId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imSystemSettings' LIMIT 1);
|
||||
|
||||
INSERT INTO `hg_admin_menu`
|
||||
(`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`,
|
||||
`permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`,
|
||||
`frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`,
|
||||
`status`, `created_at`, `updated_at`)
|
||||
SELECT @settingsId, '消息密钥管理', 'imMessageEncryptionIndex', 'message-encryption/index',
|
||||
'KeyOutlined', 2, '', '/imMessageEncryption/list', '', '/imMessageEncryption/index',
|
||||
1, 'imManager', 0, 0, '', 0, 0, 0, 3,
|
||||
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' '), 80,
|
||||
'商户消息加密开关及密钥版本管理', 1, @now, @now
|
||||
WHERE @settingsId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionIndex');
|
||||
|
||||
SET @keyMenuId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionIndex' LIMIT 1);
|
||||
|
||||
UPDATE `hg_admin_menu`
|
||||
SET `pid` = @settingsId,
|
||||
`title` = '消息密钥管理',
|
||||
`path` = 'message-encryption/index',
|
||||
`icon` = 'KeyOutlined',
|
||||
`permissions` = '/imMessageEncryption/list',
|
||||
`component` = '/imMessageEncryption/index',
|
||||
`active_menu` = 'imManager',
|
||||
`level` = 3,
|
||||
`tree` = CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' '),
|
||||
`sort` = 80,
|
||||
`hidden` = 0,
|
||||
`status` = 1,
|
||||
`updated_at` = @now
|
||||
WHERE `id` = @keyMenuId;
|
||||
|
||||
INSERT INTO `hg_admin_menu`
|
||||
(`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`,
|
||||
`permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`,
|
||||
`frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`,
|
||||
`status`, `created_at`, `updated_at`)
|
||||
SELECT @keyMenuId, '初始化密钥', 'imMessageEncryptionInitialize', '', '', 3, '',
|
||||
'/imMessageEncryption/initialize', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
|
||||
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 10, '', 1, @now, @now
|
||||
WHERE @keyMenuId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionInitialize');
|
||||
|
||||
INSERT INTO `hg_admin_menu`
|
||||
(`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`,
|
||||
`permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`,
|
||||
`frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`,
|
||||
`status`, `created_at`, `updated_at`)
|
||||
SELECT @keyMenuId, '轮换密钥', 'imMessageEncryptionRotate', '', '', 3, '',
|
||||
'/imMessageEncryption/rotate', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
|
||||
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 20, '', 1, @now, @now
|
||||
WHERE @keyMenuId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionRotate');
|
||||
|
||||
INSERT INTO `hg_admin_menu`
|
||||
(`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`,
|
||||
`permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`,
|
||||
`frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`,
|
||||
`status`, `created_at`, `updated_at`)
|
||||
SELECT @keyMenuId, '切换消息加密', 'imMessageEncryptionSwitch', '', '', 3, '',
|
||||
'/imMessageEncryption/switch', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
|
||||
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 30, '', 1, @now, @now
|
||||
WHERE @keyMenuId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionSwitch');
|
||||
|
||||
UPDATE `hg_admin_menu`
|
||||
SET `level` = 4,
|
||||
`tree` = CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '),
|
||||
`updated_at` = @now
|
||||
WHERE `pid` = @keyMenuId AND `type` = 3;
|
||||
|
||||
INSERT IGNORE INTO `hg_admin_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT 1, `id` FROM `hg_admin_menu`
|
||||
WHERE `name` IN (
|
||||
'imManager',
|
||||
'imSystemSettings',
|
||||
'imMessageEncryptionIndex',
|
||||
'imMessageEncryptionInitialize',
|
||||
'imMessageEncryptionRotate',
|
||||
'imMessageEncryptionSwitch'
|
||||
);
|
||||
@@ -1,6 +1,10 @@
|
||||
package imconfiginternal
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
import (
|
||||
"hotgo/internal/model/input/sysin"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type TenantReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/by-code/{code}" method:"get" tags:"IM内部配置" summary:"按编码获取IM租户"`
|
||||
@@ -99,14 +103,24 @@ type SecurityPolicyReq struct {
|
||||
}
|
||||
|
||||
type SecurityPolicyRes struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
TenantCode string `json:"tenant_code"`
|
||||
PolicyVersion uint `json:"policy_version"`
|
||||
GlobalFriendEnabled bool `json:"global_friend_enabled"`
|
||||
FriendDisabledPolicy map[string]interface{} `json:"friend_disabled_policy"`
|
||||
MessageCurfewEnabled bool `json:"message_curfew_enabled"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
TenantCode string `json:"tenant_code"`
|
||||
PolicyVersion uint `json:"policy_version"`
|
||||
GlobalFriendEnabled bool `json:"global_friend_enabled"`
|
||||
FriendDisabledPolicy map[string]interface{} `json:"friend_disabled_policy"`
|
||||
MessageCurfewEnabled bool `json:"message_curfew_enabled"`
|
||||
LoginRateLimit *SecurityLoginRateLimitRes `json:"login_rate_limit"`
|
||||
CurfewRules []*SecurityCurfewRuleRes `json:"curfew_rules"`
|
||||
CurfewRules []*SecurityCurfewRuleRes `json:"curfew_rules"`
|
||||
}
|
||||
|
||||
type RealnamePolicyReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/by-id/{id}/realname-policy" method:"get" tags:"IM内部配置" summary:"获取租户实名认证策略"`
|
||||
Id int64 `json:"id" in:"path" dc:"租户ID"`
|
||||
}
|
||||
|
||||
type RealnamePolicyRes struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
RealnameReviewPolicy int `json:"realname_review_policy"`
|
||||
}
|
||||
|
||||
type SecurityLoginRateLimitRes struct {
|
||||
@@ -145,6 +159,33 @@ type SensitiveWordsRes struct {
|
||||
Items []*SensitiveWordModel `json:"items"`
|
||||
}
|
||||
|
||||
type MessageKeyActiveReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/{tenantCode}/message-keys/active" method:"get" tags:"IM内部配置" summary:"获取商户当前消息密钥"`
|
||||
TenantCode string `json:"tenantCode" in:"path"`
|
||||
}
|
||||
|
||||
type MessageKeyActiveRes struct {
|
||||
*sysin.ImMessageKeyModel
|
||||
}
|
||||
|
||||
type MessageKeyVersionReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/{tenantCode}/message-keys/{version}" method:"get" tags:"IM内部配置" summary:"获取商户指定版本消息密钥"`
|
||||
TenantCode string `json:"tenantCode" in:"path"`
|
||||
Version uint `json:"version" in:"path"`
|
||||
}
|
||||
|
||||
type MessageKeyVersionRes struct {
|
||||
*sysin.ImMessageKeyModel
|
||||
}
|
||||
|
||||
type MessageKeyAckReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/{tenantCode}/message-keys/ack" method:"post" tags:"IM内部配置" summary:"确认商户消息密钥可用"`
|
||||
TenantCode string `json:"tenantCode" in:"path"`
|
||||
Version uint `json:"version" v:"required|min:1#密钥版本不能为空|密钥版本不正确"`
|
||||
}
|
||||
|
||||
type MessageKeyAckRes struct{}
|
||||
|
||||
type SensitiveWordModel struct {
|
||||
ID int64 `json:"id"`
|
||||
Word string `json:"word"`
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package immessageencryption
|
||||
|
||||
import (
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type ListReq struct {
|
||||
g.Meta `path:"/imMessageEncryption/list" method:"get" tags:"消息加密密钥" summary:"获取商户消息加密状态"`
|
||||
sysin.ImMessageEncryptionListInp
|
||||
}
|
||||
|
||||
type ListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImMessageEncryptionListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
type InitializeReq struct {
|
||||
g.Meta `path:"/imMessageEncryption/initialize" method:"post" tags:"消息加密密钥" summary:"初始化商户消息密钥"`
|
||||
sysin.ImMessageEncryptionActionInp
|
||||
}
|
||||
type InitializeRes struct{}
|
||||
|
||||
type RotateReq struct {
|
||||
g.Meta `path:"/imMessageEncryption/rotate" method:"post" tags:"消息加密密钥" summary:"轮换商户消息密钥"`
|
||||
sysin.ImMessageEncryptionActionInp
|
||||
}
|
||||
type RotateRes struct{}
|
||||
|
||||
type SwitchReq struct {
|
||||
g.Meta `path:"/imMessageEncryption/switch" method:"post" tags:"消息加密密钥" summary:"切换商户消息加密"`
|
||||
sysin.ImMessageEncryptionSwitchInp
|
||||
}
|
||||
type SwitchRes struct{}
|
||||
@@ -13,6 +13,7 @@ const (
|
||||
RegionSpilt = " / " // 地区分隔符
|
||||
Unknown = "Unknown" // Unknown
|
||||
SuperRoleKey = "super" // 超管角色唯一标识符,通过角色验证超管
|
||||
ImPlatformRoleKey = "supreme_manager" // IM平台超管角色唯一标识符
|
||||
MaxServeLogContentLen = 2048 // 最大保留服务日志内容大小
|
||||
SysDefaultLanguage = "zh_CN" // 系统默认语言,当配置文件没有国际化配置时生效
|
||||
)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package consts
|
||||
|
||||
const (
|
||||
ImInternalSecretDictType = "im_internal_secret"
|
||||
ImMessageRootKEKLabel = "core_material_01"
|
||||
|
||||
ImMessageKeyAlgorithm = "AES-256-GCM"
|
||||
ImMessageKeyStatusPending = "pending"
|
||||
ImMessageKeyStatusActive = "active"
|
||||
ImMessageKeyStatusRetired = "retired"
|
||||
ImMessageKeyStatusDisabled = "disabled"
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/api/admin/immessageencryption"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
var ImMessageEncryption = cImMessageEncryption{}
|
||||
|
||||
type cImMessageEncryption struct{}
|
||||
|
||||
func (c *cImMessageEncryption) List(ctx context.Context, req *immessageencryption.ListReq) (res *immessageencryption.ListRes, err error) {
|
||||
list, total, err := service.SysImMessageEncryption().List(ctx, &req.ImMessageEncryptionListInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.ImMessageEncryptionListModel{}
|
||||
}
|
||||
res = new(immessageencryption.ListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, total)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *cImMessageEncryption) Initialize(ctx context.Context, req *immessageencryption.InitializeReq) (res *immessageencryption.InitializeRes, err error) {
|
||||
err = service.SysImMessageEncryption().Initialize(ctx, &req.ImMessageEncryptionActionInp)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImMessageEncryption) Rotate(ctx context.Context, req *immessageencryption.RotateReq) (res *immessageencryption.RotateRes, err error) {
|
||||
err = service.SysImMessageEncryption().Rotate(ctx, &req.ImMessageEncryptionActionInp)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImMessageEncryption) Switch(ctx context.Context, req *immessageencryption.SwitchReq) (res *immessageencryption.SwitchRes, err error) {
|
||||
err = service.SysImMessageEncryption().Switch(ctx, &req.ImMessageEncryptionSwitchInp)
|
||||
return
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"hotgo/internal/library/immoderation"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"strings"
|
||||
|
||||
@@ -302,6 +303,29 @@ func (c *cConfig) SecurityPolicy(ctx context.Context, req *api.SecurityPolicyReq
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) RealnamePolicy(ctx context.Context, req *api.RealnamePolicyReq) (res *api.RealnamePolicyRes, err error) {
|
||||
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Id, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = defaultRealnamePolicy(tenant.Id)
|
||||
var row *struct {
|
||||
RealnameReviewPolicy int `json:"realname_review_policy"`
|
||||
}
|
||||
if err = imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
|
||||
Fields("realname_review_policy").
|
||||
Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).Scan(&row); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取租户实名认证策略失败")
|
||||
}
|
||||
if row == nil {
|
||||
return res, nil
|
||||
}
|
||||
if row.RealnameReviewPolicy == 1 {
|
||||
res.RealnameReviewPolicy = 1
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func parseStringArray(raw string) []string {
|
||||
values := []string{}
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
@@ -359,6 +383,27 @@ func (c *cConfig) SensitiveWords(ctx context.Context, req *api.SensitiveWordsReq
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) MessageKeyActive(ctx context.Context, req *api.MessageKeyActiveReq) (res *api.MessageKeyActiveRes, err error) {
|
||||
key, err := service.SysImMessageEncryption().GetKey(ctx, &sysin.ImMessageKeyGetInp{TenantCode: req.TenantCode})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &api.MessageKeyActiveRes{ImMessageKeyModel: key}, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) MessageKeyVersion(ctx context.Context, req *api.MessageKeyVersionReq) (res *api.MessageKeyVersionRes, err error) {
|
||||
key, err := service.SysImMessageEncryption().GetKey(ctx, &sysin.ImMessageKeyGetInp{TenantCode: req.TenantCode, Version: req.Version})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &api.MessageKeyVersionRes{ImMessageKeyModel: key}, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) MessageKeyAck(ctx context.Context, req *api.MessageKeyAckReq) (res *api.MessageKeyAckRes, err error) {
|
||||
err = service.SysImMessageEncryption().Ack(ctx, &sysin.ImMessageKeyAckInp{TenantCode: req.TenantCode, Version: req.Version})
|
||||
return
|
||||
}
|
||||
|
||||
func defaultModerationPolicy(tenant *entity.ImTenant) *api.ModerationPolicyRes {
|
||||
return &api.ModerationPolicyRes{TenantID: tenant.Id, TenantCode: tenant.Code, Enabled: false,
|
||||
AliyunEnabled: false, LocalWordsEnabled: false, Scenes: cloneBoolMap(defaultModerationScenes),
|
||||
@@ -366,6 +411,13 @@ func defaultModerationPolicy(tenant *entity.ImTenant) *api.ModerationPolicyRes {
|
||||
BlockMessage: "内容不符合平台规范,请修改后重试"}
|
||||
}
|
||||
|
||||
func defaultRealnamePolicy(tenantID int64) *api.RealnamePolicyRes {
|
||||
return &api.RealnamePolicyRes{
|
||||
TenantID: tenantID,
|
||||
RealnameReviewPolicy: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func cloneBoolMap(src map[string]bool) map[string]bool {
|
||||
dst := make(map[string]bool, len(src))
|
||||
for k, v := range src {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package immessagecrypto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/utility/encrypt"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
func GenerateDEK() ([]byte, error) {
|
||||
key := make([]byte, encrypt.AESGCMKeySize)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return nil, gerror.Wrap(err, "生成商户消息密钥失败")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func WrapDEK(ctx context.Context, tenantID int64, version uint, dek []byte) (ciphertext, nonce []byte, err error) {
|
||||
kek, err := loadRootKEK(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return encrypt.AESGCMEncrypt(dek, kek, keyAAD(tenantID, version))
|
||||
}
|
||||
|
||||
func UnwrapDEK(ctx context.Context, tenantID int64, version uint, ciphertext, nonce []byte) ([]byte, error) {
|
||||
kek, err := loadRootKEK(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dek, err := encrypt.AESGCMDecrypt(ciphertext, nonce, kek, keyAAD(tenantID, version))
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "解包商户消息密钥失败")
|
||||
}
|
||||
if len(dek) != encrypt.AESGCMKeySize {
|
||||
return nil, gerror.New("商户消息密钥长度不正确")
|
||||
}
|
||||
return dek, nil
|
||||
}
|
||||
|
||||
func EncodeDEK(dek []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(dek)
|
||||
}
|
||||
|
||||
func loadRootKEK(ctx context.Context) ([]byte, error) {
|
||||
columns := dao.SysDictData.Columns()
|
||||
value, err := dao.SysDictData.Ctx(ctx).
|
||||
Fields(columns.Value).
|
||||
Where(columns.Type, consts.ImInternalSecretDictType).
|
||||
Where(columns.Label, consts.ImMessageRootKEKLabel).
|
||||
Where(columns.Status, consts.StatusEnabled).
|
||||
Value()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取消息根密钥失败")
|
||||
}
|
||||
if value.IsEmpty() {
|
||||
return nil, gerror.New("消息根密钥未初始化")
|
||||
}
|
||||
kek, err := base64.StdEncoding.DecodeString(value.String())
|
||||
if err != nil || len(kek) != encrypt.AESGCMKeySize {
|
||||
return nil, gerror.New("消息根密钥格式不正确")
|
||||
}
|
||||
return kek, nil
|
||||
}
|
||||
|
||||
func keyAAD(tenantID int64, version uint) []byte {
|
||||
aad := make([]byte, 16)
|
||||
binary.BigEndian.PutUint64(aad[:8], uint64(tenantID))
|
||||
binary.BigEndian.PutUint64(aad[8:], uint64(version))
|
||||
return aad
|
||||
}
|
||||
@@ -468,7 +468,7 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
|
||||
return
|
||||
}
|
||||
|
||||
if service.AdminMember().VerifySuperId(ctx, opMemberId) {
|
||||
if service.AdminMember().VerifySuperId(ctx, opMemberId) || contexts.GetRoleKey(ctx) == consts.ImPlatformRoleKey {
|
||||
if in.TenantId > 0 {
|
||||
count, queryErr := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
||||
Where(dao.ImTenant.Columns().Id, in.TenantId).
|
||||
@@ -508,6 +508,9 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
|
||||
if err = service.AdminRole().VerifyRoleId(ctx, in.RoleId); err != nil {
|
||||
return
|
||||
}
|
||||
if err = s.normalizeTenantByRole(ctx, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证部门ID
|
||||
if err = service.AdminDept().VerifyDeptId(ctx, in.DeptId); err != nil {
|
||||
@@ -606,6 +609,50 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sAdminMember) normalizeTenantByRole(ctx context.Context, in *adminin.MemberEditInp) error {
|
||||
var role *entity.AdminRole
|
||||
if err := dao.AdminRole.Ctx(ctx).
|
||||
Fields(dao.AdminRole.Columns().Id, dao.AdminRole.Columns().Key).
|
||||
WherePri(in.RoleId).
|
||||
Scan(&role); err != nil {
|
||||
return gerror.Wrap(err, "获取角色信息失败,请稍后重试!")
|
||||
}
|
||||
if role == nil || role.Id <= 0 {
|
||||
return gerror.New("角色不存在")
|
||||
}
|
||||
switch role.Key {
|
||||
case consts.ImPlatformRoleKey:
|
||||
deptId, err := s.getCompanyDeptId(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
in.TenantId = 0
|
||||
in.DeptId = deptId
|
||||
case "imManage":
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("IM管理员必须选择所属商户")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAdminMember) getCompanyDeptId(ctx context.Context) (int64, error) {
|
||||
value, err := dao.AdminDept.Ctx(ctx).
|
||||
Fields(dao.AdminDept.Columns().Id).
|
||||
Where(dao.AdminDept.Columns().Type, consts.DeptTypeCompany).
|
||||
Where(dao.AdminDept.Columns().Status, consts.StatusEnabled).
|
||||
OrderAsc(dao.AdminDept.Columns().Id).
|
||||
Limit(1).
|
||||
Value()
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "获取公司部门失败,请稍后重试!")
|
||||
}
|
||||
if value.IsEmpty() {
|
||||
return 0, gerror.New("请先配置公司部门")
|
||||
}
|
||||
return value.Int64(), nil
|
||||
}
|
||||
|
||||
// View 获取用户信息
|
||||
func (s *sAdminMember) View(ctx context.Context, in *adminin.MemberViewInp) (res *adminin.MemberViewModel, err error) {
|
||||
if err = s.FilterAuthModel(ctx, contexts.GetUserId(ctx)).Hook(hook.MemberInfo).WherePri(in.Id).Scan(&res); err != nil {
|
||||
@@ -912,9 +959,6 @@ func (s *sAdminMember) ClusterSyncSuperAdmin(ctx context.Context, message *gredi
|
||||
// 非超管用户只能操作自己的下级角色用户,并且需要满足自身角色的数据权限设置
|
||||
func (s *sAdminMember) FilterAuthModel(ctx context.Context, memberId int64) *gdb.Model {
|
||||
m := dao.AdminMember.Ctx(ctx)
|
||||
if s.VerifySuperId(ctx, memberId) {
|
||||
return m
|
||||
}
|
||||
|
||||
var roleId int64
|
||||
if contexts.GetUserId(ctx) == memberId {
|
||||
@@ -929,6 +973,10 @@ func (s *sAdminMember) FilterAuthModel(ctx context.Context, memberId int64) *gdb
|
||||
roleId = ro.Int64()
|
||||
}
|
||||
|
||||
if isAdminScopeSuper(ctx, memberId, roleId) {
|
||||
return m
|
||||
}
|
||||
|
||||
roleIds, err := service.AdminRole().GetSubRoleIds(ctx, roleId, false)
|
||||
if err != nil {
|
||||
g.Log().Panicf(ctx, "get the subordinate role permission exception, err:%+v", err)
|
||||
|
||||
@@ -37,6 +37,35 @@ func init() {
|
||||
service.RegisterAdminRole(NewAdminRole())
|
||||
}
|
||||
|
||||
func isAdminScopeSuper(ctx context.Context, memberId, roleId int64) bool {
|
||||
if service.AdminMember().VerifySuperId(ctx, memberId) {
|
||||
return true
|
||||
}
|
||||
|
||||
if memberId == contexts.GetUserId(ctx) && contexts.GetRoleKey(ctx) == consts.ImPlatformRoleKey {
|
||||
return true
|
||||
}
|
||||
|
||||
if roleId <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
value, err := dao.AdminRole.Ctx(ctx).Fields(dao.AdminRole.Columns().Key).WherePri(roleId).Value()
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "query admin role key failed, roleId:%d err:%+v", roleId, err)
|
||||
return false
|
||||
}
|
||||
return value.String() == consts.ImPlatformRoleKey
|
||||
}
|
||||
|
||||
func getSuperRoleId(ctx context.Context) (int64, error) {
|
||||
value, err := dao.AdminRole.Ctx(ctx).Fields(dao.AdminRole.Columns().Id).Where(dao.AdminRole.Columns().Key, consts.SuperRoleKey).Value()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return value.Int64(), nil
|
||||
}
|
||||
|
||||
// Verify 验证权限
|
||||
func (s *sAdminRole) Verify(ctx context.Context, path, method string) bool {
|
||||
var (
|
||||
@@ -64,13 +93,19 @@ func (s *sAdminRole) Verify(ctx context.Context, path, method string) bool {
|
||||
// List 获取列表
|
||||
func (s *sAdminRole) List(ctx context.Context, in *adminin.RoleListInp) (res *adminin.RoleListModel, totalCount int, err error) {
|
||||
var (
|
||||
mod = dao.AdminRole.Ctx(ctx)
|
||||
mod = dao.AdminRole.Ctx(ctx).WhereNot(dao.AdminRole.Columns().Key, consts.SuperRoleKey)
|
||||
models []*entity.AdminRole
|
||||
pid int64 = 0
|
||||
)
|
||||
|
||||
// 非超管只获取下级角色
|
||||
if !service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx)) {
|
||||
// 超管隐藏内置根角色,但从内置根角色的子级开始展示角色树。
|
||||
if isAdminScopeSuper(ctx, contexts.GetUserId(ctx), contexts.GetRoleId(ctx)) {
|
||||
pid, err = getSuperRoleId(ctx)
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
pid = contexts.GetRoleId(ctx)
|
||||
mod = mod.WhereLike(dao.AdminRole.Columns().Tree, "%"+tree.GetIdLabel(pid)+"%")
|
||||
}
|
||||
@@ -326,7 +361,7 @@ func (s *sAdminRole) VerifyRoleId(ctx context.Context, id int64) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
ids, err := s.GetSubRoleIds(ctx, mb.RoleId, service.AdminMember().VerifySuperId(ctx, mb.Id))
|
||||
ids, err := s.GetSubRoleIds(ctx, mb.RoleId, isAdminScopeSuper(ctx, mb.Id, mb.RoleId))
|
||||
if err != nil {
|
||||
err = gerror.New("验证角色信息失败!")
|
||||
return
|
||||
|
||||
@@ -30,6 +30,11 @@ func init() {
|
||||
|
||||
// Delete 删除
|
||||
func (s *sSysDictData) Delete(ctx context.Context, in *sysin.DictDataDeleteInp) error {
|
||||
if count, err := dao.SysDictData.Ctx(ctx).WherePri(in.Id).Where(dao.SysDictData.Columns().Type, consts.ImInternalSecretDictType).Count(); err != nil {
|
||||
return gerror.Wrap(err, consts.ErrorORM)
|
||||
} else if count > 0 {
|
||||
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||
}
|
||||
_, err := dao.SysDictData.Ctx(ctx).Where("id", in.Id).Delete()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
@@ -42,6 +47,11 @@ func (s *sSysDictData) Delete(ctx context.Context, in *sysin.DictDataDeleteInp)
|
||||
func (s *sSysDictData) Edit(ctx context.Context, in *sysin.DictDataEditInp) (err error) {
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
if count, queryErr := dao.SysDictData.Ctx(ctx).WherePri(in.Id).Where(dao.SysDictData.Columns().Type, consts.ImInternalSecretDictType).Count(); queryErr != nil {
|
||||
return gerror.Wrap(queryErr, consts.ErrorORM)
|
||||
} else if count > 0 {
|
||||
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||
}
|
||||
_, err = dao.SysDictData.Ctx(ctx).Fields(sysin.DictDataUpdateFields{}).WherePri(in.Id).Data(in).Update()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
@@ -60,6 +70,9 @@ func (s *sSysDictData) Edit(ctx context.Context, in *sysin.DictDataEditInp) (err
|
||||
if in.Type == "" {
|
||||
return gerror.Wrap(err, "类型选择无效,请检查")
|
||||
}
|
||||
if in.Type == consts.ImInternalSecretDictType {
|
||||
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||
}
|
||||
|
||||
_, err = dao.SysDictData.Ctx(ctx).Fields(sysin.DictDataInsertFields{}).Data(in).OmitEmptyData().Insert()
|
||||
if err != nil {
|
||||
@@ -71,7 +84,7 @@ func (s *sSysDictData) Edit(ctx context.Context, in *sysin.DictDataEditInp) (err
|
||||
|
||||
// List 获取列表
|
||||
func (s *sSysDictData) List(ctx context.Context, in *sysin.DictDataListInp) (list []*sysin.DictDataListModel, totalCount int, err error) {
|
||||
mod := dao.SysDictData.Ctx(ctx)
|
||||
mod := dao.SysDictData.Ctx(ctx).WhereNot(dao.SysDictData.Columns().Type, consts.ImInternalSecretDictType)
|
||||
// 类型ID
|
||||
if in.TypeID > 0 {
|
||||
types, err := s.GetTypes(ctx, in.TypeID)
|
||||
@@ -151,6 +164,9 @@ func (s *sSysDictData) GetTypes(ctx context.Context, id int64) (types []string,
|
||||
|
||||
// Select 获取列表
|
||||
func (s *sSysDictData) Select(ctx context.Context, in *sysin.DataSelectInp) (list sysin.DataSelectModel, err error) {
|
||||
if in.Type == consts.ImInternalSecretDictType {
|
||||
return nil, gerror.New("内部密钥字典禁止通过通用接口读取")
|
||||
}
|
||||
// 优先从内置字典中获取
|
||||
options, err := dict.GetOptions(ctx, in.Type)
|
||||
if err == nil {
|
||||
|
||||
@@ -31,7 +31,7 @@ func init() {
|
||||
// Tree 树
|
||||
func (s *sSysDictType) Tree(ctx context.Context) (list []*sysin.DictTypeTree, err error) {
|
||||
var (
|
||||
mod = dao.SysDictType.Ctx(ctx)
|
||||
mod = dao.SysDictType.Ctx(ctx).WhereNot(dao.SysDictType.Columns().Type, consts.ImInternalSecretDictType)
|
||||
models []*entity.SysDictType
|
||||
)
|
||||
|
||||
@@ -55,6 +55,9 @@ func (s *sSysDictType) Delete(ctx context.Context, in *sysin.DictTypeDeleteInp)
|
||||
err = gerror.New("数据不存在或已删除!")
|
||||
return
|
||||
}
|
||||
if models.Type == consts.ImInternalSecretDictType {
|
||||
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||
}
|
||||
|
||||
exist, err := dao.SysDictData.Ctx(ctx).Where("type", models.Type).One()
|
||||
if err != nil {
|
||||
@@ -83,6 +86,18 @@ func (s *sSysDictType) Delete(ctx context.Context, in *sysin.DictTypeDeleteInp)
|
||||
|
||||
// Edit 修改/新增
|
||||
func (s *sSysDictType) Edit(ctx context.Context, in *sysin.DictTypeEditInp) (err error) {
|
||||
if in.Type == consts.ImInternalSecretDictType {
|
||||
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||
}
|
||||
if in.Id > 0 {
|
||||
count, queryErr := dao.SysDictType.Ctx(ctx).WherePri(in.Id).Where(dao.SysDictType.Columns().Type, consts.ImInternalSecretDictType).Count()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, consts.ErrorORM)
|
||||
}
|
||||
if count > 0 {
|
||||
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||
}
|
||||
}
|
||||
if err = hgorm.IsUnique(ctx, &dao.SysDictType, g.Map{dao.SysDictType.Columns().Name: in.Name}, "名称已存在", in.Id); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -105,7 +120,7 @@ func (s *sSysDictType) Edit(ctx context.Context, in *sysin.DictTypeEditInp) (err
|
||||
// TreeSelect 获取类型关系树选项
|
||||
func (s *sSysDictType) TreeSelect(ctx context.Context, in *sysin.DictTreeSelectInp) (list []*sysin.DictTypeTree, err error) {
|
||||
var (
|
||||
mod = dao.SysDictType.Ctx(ctx)
|
||||
mod = dao.SysDictType.Ctx(ctx).WhereNot(dao.SysDictType.Columns().Type, consts.ImInternalSecretDictType)
|
||||
models []*entity.SysDictType
|
||||
)
|
||||
|
||||
|
||||
@@ -71,6 +71,9 @@ func isImPlatformAdmin(ctx context.Context, operatorId int64) bool {
|
||||
if service.AdminMember().VerifySuperId(ctx, operatorId) {
|
||||
return true
|
||||
}
|
||||
if contexts.GetRoleKey(ctx) == consts.ImPlatformRoleKey {
|
||||
return true
|
||||
}
|
||||
return contexts.IsCompanyDept(ctx) && contexts.GetTenantId(ctx) <= 0
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/immessagecrypto"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const (
|
||||
imMessageEncryptionKeyTable = "im_message_encryption_keys"
|
||||
imTenantMessageEncryptionColumn = "message_encryption_enabled"
|
||||
)
|
||||
|
||||
type sSysImMessageEncryption struct{}
|
||||
|
||||
func NewSysImMessageEncryption() *sSysImMessageEncryption { return &sSysImMessageEncryption{} }
|
||||
|
||||
func init() {
|
||||
service.RegisterSysImMessageEncryption(NewSysImMessageEncryption())
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) List(ctx context.Context, in *sysin.ImMessageEncryptionListInp) (list []*sysin.ImMessageEncryptionListModel, total int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
if err = s.requireSuperAdmin(ctx); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
db := imtenant.RegistryDB()
|
||||
mod := db.Model(dao.ImTenant.Table()+" t").Safe().Ctx(ctx).
|
||||
LeftJoin(dao.ImTenantConfig.Table()+" c", "c.tenant_id=t.id").
|
||||
Fields(
|
||||
"t.id tenant_id", "t.code tenant_code", "t.name tenant_name",
|
||||
"COALESCE(c."+imTenantMessageEncryptionColumn+",0) message_encryption_enabled",
|
||||
"COALESCE((SELECT MAX(k.key_version) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id AND k.status='active'),0) active_version",
|
||||
"COALESCE((SELECT MAX(k.key_version) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id AND k.status='pending'),0) pending_version",
|
||||
"(SELECT COUNT(1) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id) key_count",
|
||||
"(SELECT MAX(COALESCE(k.activated_at,k.created_at)) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id) last_rotated_at",
|
||||
)
|
||||
if in.TenantId > 0 {
|
||||
mod = mod.Where("t.id", in.TenantId)
|
||||
}
|
||||
if keyword := strings.TrimSpace(in.Keyword); keyword != "" {
|
||||
mod = mod.Where("t.code LIKE ? OR t.name LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
if in.Enabled != nil {
|
||||
mod = mod.Where("COALESCE(c."+imTenantMessageEncryptionColumn+",0)=?", *in.Enabled)
|
||||
}
|
||||
if err = mod.Page(in.Page, in.PerPage).OrderAsc("t.id").ScanAndCount(&list, &total, false); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取商户消息加密配置失败")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) Initialize(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.requireSuperAdmin(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
tenant, err := s.tenantByID(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dek, err := immessagecrypto.GenerateDEK()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ciphertext, nonce, err := immessagecrypto.WrapDEK(ctx, tenant.Id, 1, dek)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
count, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Where("tenant_id", tenant.Id).Count()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "检查商户消息密钥失败")
|
||||
}
|
||||
if count > 0 {
|
||||
return gerror.New("该商户已经初始化消息密钥")
|
||||
}
|
||||
_, insertErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Data(g.Map{
|
||||
"tenant_id": tenant.Id, "key_version": 1, "algorithm": consts.ImMessageKeyAlgorithm,
|
||||
"encrypted_dek": ciphertext, "dek_nonce": nonce, "status": consts.ImMessageKeyStatusActive,
|
||||
"created_at": gtime.Now(), "activated_at": gtime.Now(), "created_by": contexts.GetUserId(ctx),
|
||||
"remark": strings.TrimSpace(in.Remark),
|
||||
}).Insert()
|
||||
if insertErr != nil {
|
||||
return gerror.Wrap(insertErr, "初始化商户消息密钥失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenant.Id, "message_key_initialize", imMessageEncryptionKeyTable, 0, "success", g.Map{"version": 1})
|
||||
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.updated", "message_encryption", 1)
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) Rotate(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.requireSuperAdmin(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
tenant, err := s.tenantByID(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var version uint
|
||||
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
pending, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusPending).Count()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "检查待激活消息密钥失败")
|
||||
}
|
||||
if pending > 0 {
|
||||
return gerror.New("该商户已有待激活密钥,请等待IM Server确认")
|
||||
}
|
||||
value, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Fields("COALESCE(MAX(key_version),0)").Where("tenant_id", tenant.Id).Value()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "获取消息密钥版本失败")
|
||||
}
|
||||
if value.Uint() == 0 {
|
||||
return gerror.New("请先初始化商户消息密钥")
|
||||
}
|
||||
version = value.Uint() + 1
|
||||
dek, generateErr := immessagecrypto.GenerateDEK()
|
||||
if generateErr != nil {
|
||||
return generateErr
|
||||
}
|
||||
ciphertext, nonce, wrapErr := immessagecrypto.WrapDEK(ctx, tenant.Id, version, dek)
|
||||
if wrapErr != nil {
|
||||
return wrapErr
|
||||
}
|
||||
_, insertErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Data(g.Map{
|
||||
"tenant_id": tenant.Id, "key_version": version, "algorithm": consts.ImMessageKeyAlgorithm,
|
||||
"encrypted_dek": ciphertext, "dek_nonce": nonce, "status": consts.ImMessageKeyStatusPending,
|
||||
"created_at": gtime.Now(), "created_by": contexts.GetUserId(ctx), "remark": strings.TrimSpace(in.Remark),
|
||||
}).Insert()
|
||||
if insertErr != nil {
|
||||
return gerror.Wrap(insertErr, "创建待激活消息密钥失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenant.Id, "message_key_rotate", imMessageEncryptionKeyTable, 0, "success", g.Map{"version": version, "status": consts.ImMessageKeyStatusPending})
|
||||
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.updated", "message_encryption", version)
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) Switch(ctx context.Context, in *sysin.ImMessageEncryptionSwitchInp) error {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.requireSuperAdmin(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
tenant, err := s.tenantByID(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Enabled == 1 {
|
||||
count, queryErr := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusActive).Count()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "检查商户消息密钥失败")
|
||||
}
|
||||
if count == 0 {
|
||||
return gerror.New("启用消息加密前必须先初始化有效密钥")
|
||||
}
|
||||
}
|
||||
result, err := imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
|
||||
Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).
|
||||
Data(g.Map{imTenantMessageEncryptionColumn: in.Enabled, dao.ImTenantConfig.Columns().UpdatedBy: contexts.GetUserId(ctx)}).Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新商户消息加密开关失败")
|
||||
}
|
||||
if affected, _ := result.RowsAffected(); affected == 0 {
|
||||
return gerror.New("商户配置不存在")
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenant.Id, "message_encryption_switch", "im_tenant_config", tenant.Id, "success", g.Map{"enabled": in.Enabled})
|
||||
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "tenant.features.updated", "message_encryption", 0)
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) GetKey(ctx context.Context, in *sysin.ImMessageKeyGetInp) (*sysin.ImMessageKeyModel, error) {
|
||||
tenant, err := s.tenantByCode(ctx, in.TenantCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enabledValue, err := imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
|
||||
Fields(imTenantMessageEncryptionColumn).Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).Value()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户消息加密开关失败")
|
||||
}
|
||||
mod := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Where("tenant_id", tenant.Id)
|
||||
if in.Version > 0 {
|
||||
mod = mod.Where("key_version", in.Version).WhereIn("status", []string{consts.ImMessageKeyStatusPending, consts.ImMessageKeyStatusActive, consts.ImMessageKeyStatusRetired})
|
||||
} else {
|
||||
mod = mod.Where("status", consts.ImMessageKeyStatusActive).OrderDesc("key_version")
|
||||
}
|
||||
row, err := mod.One()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户消息密钥失败")
|
||||
}
|
||||
if row.IsEmpty() {
|
||||
return nil, gerror.New("商户消息密钥不存在")
|
||||
}
|
||||
version := row["key_version"].Uint()
|
||||
dek, err := immessagecrypto.UnwrapDEK(ctx, tenant.Id, version, row["encrypted_dek"].Bytes(), row["dek_nonce"].Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sysin.ImMessageKeyModel{
|
||||
TenantId: tenant.Id, TenantCode: tenant.Code, Enabled: enabledValue.Int() == 1,
|
||||
Version: version, Algorithm: row["algorithm"].String(), Status: row["status"].String(), Key: immessagecrypto.EncodeDEK(dek),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) Ack(ctx context.Context, in *sysin.ImMessageKeyAckInp) error {
|
||||
tenant, err := s.tenantByCode(ctx, in.TenantCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Version == 0 {
|
||||
return gerror.New("消息密钥版本不能为空")
|
||||
}
|
||||
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
row, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenant.Id).Where("key_version", in.Version).One()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "获取待激活消息密钥失败")
|
||||
}
|
||||
if row.IsEmpty() || row["status"].String() != consts.ImMessageKeyStatusPending {
|
||||
return gerror.New("待激活消息密钥不存在")
|
||||
}
|
||||
if _, updateErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusActive).
|
||||
Data(g.Map{"status": consts.ImMessageKeyStatusRetired, "retired_at": gtime.Now()}).Update(); updateErr != nil {
|
||||
return gerror.Wrap(updateErr, "退役旧消息密钥失败")
|
||||
}
|
||||
if _, updateErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenant.Id).Where("key_version", in.Version).
|
||||
Data(g.Map{"status": consts.ImMessageKeyStatusActive, "activated_at": gtime.Now()}).Update(); updateErr != nil {
|
||||
return gerror.Wrap(updateErr, "激活新消息密钥失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.activated", "message_encryption", in.Version)
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) requireSuperAdmin(ctx context.Context) error {
|
||||
if !service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx)) {
|
||||
return gerror.New("只有最高权限平台管理员可以管理消息密钥")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) tenantByID(ctx context.Context, tenantID int64) (*entity.ImTenant, error) {
|
||||
var tenant *entity.ImTenant
|
||||
if err := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantID).Scan(&tenant); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户失败")
|
||||
}
|
||||
if tenant == nil || tenant.Id == 0 {
|
||||
return nil, gerror.New("商户不存在")
|
||||
}
|
||||
return tenant, nil
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) tenantByCode(ctx context.Context, code string) (*entity.ImTenant, error) {
|
||||
var tenant *entity.ImTenant
|
||||
if err := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
||||
Where(dao.ImTenant.Columns().Code, strings.TrimSpace(code)).Scan(&tenant); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户失败")
|
||||
}
|
||||
if tenant == nil || tenant.Id == 0 {
|
||||
return nil, gerror.New("商户不存在")
|
||||
}
|
||||
return tenant, nil
|
||||
}
|
||||
@@ -26,18 +26,22 @@ import (
|
||||
type sSysImTenant struct{}
|
||||
|
||||
const (
|
||||
tenantConfigAliyunModerationEnabled = "aliyun_moderation_enabled"
|
||||
tenantConfigLocalModerationEnabled = "local_moderation_enabled"
|
||||
tenantConfigAliyunModerationEnabled = "aliyun_moderation_enabled"
|
||||
tenantConfigLocalModerationEnabled = "local_moderation_enabled"
|
||||
tenantConfigMessageEncryptionEnabled = "message_encryption_enabled"
|
||||
tenantConfigRealnameReviewPolicy = "realname_review_policy"
|
||||
)
|
||||
|
||||
type imTenantConfigModeration struct {
|
||||
Id int64 `json:"id"`
|
||||
TenantId int64 `json:"tenant_id"`
|
||||
MaxUsers uint64 `json:"max_users"`
|
||||
MaxGroups uint64 `json:"max_groups"`
|
||||
MaxGroupMembers uint `json:"max_group_members"`
|
||||
AliyunModerationEnabled int `json:"aliyun_moderation_enabled"`
|
||||
LocalModerationEnabled int `json:"local_moderation_enabled"`
|
||||
Id int64 `json:"id" orm:"id"`
|
||||
TenantId int64 `json:"tenant_id" orm:"tenant_id"`
|
||||
MaxUsers uint64 `json:"max_users" orm:"max_users"`
|
||||
MaxGroups uint64 `json:"max_groups" orm:"max_groups"`
|
||||
MaxGroupMembers uint `json:"max_group_members" orm:"max_group_members"`
|
||||
AliyunModerationEnabled int `json:"aliyun_moderation_enabled" orm:"aliyun_moderation_enabled"`
|
||||
LocalModerationEnabled int `json:"local_moderation_enabled" orm:"local_moderation_enabled"`
|
||||
MessageEncryptionEnabled int `json:"message_encryption_enabled" orm:"message_encryption_enabled"`
|
||||
RealnameReviewPolicy int `json:"realname_review_policy" orm:"realname_review_policy"`
|
||||
}
|
||||
|
||||
func NewSysImTenant() *sSysImTenant {
|
||||
@@ -157,6 +161,8 @@ func (s *sSysImTenant) fillListConfig(ctx context.Context, list []*sysin.ImTenan
|
||||
item.MaxGroupMembers = conf.MaxGroupMembers
|
||||
item.AliyunModerationEnabled = conf.AliyunModerationEnabled
|
||||
item.LocalModerationEnabled = conf.LocalModerationEnabled
|
||||
item.MessageEncryptionEnabled = conf.MessageEncryptionEnabled
|
||||
item.RealnameReviewPolicy = conf.RealnameReviewPolicy
|
||||
}
|
||||
item.DbDsnConfigured = dsnMap[item.Id] != ""
|
||||
}
|
||||
@@ -210,7 +216,7 @@ func (s *sSysImTenant) saveTenant(ctx context.Context, in *sysin.ImTenantEditInp
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
return gerror.Wrap(err, "修改IM租户失败,请稍后重试!")
|
||||
}
|
||||
return s.saveConfig(ctx, in.Id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, in.AliyunModerationEnabled, in.LocalModerationEnabled, userId)
|
||||
return s.saveConfig(ctx, in, userId)
|
||||
}
|
||||
|
||||
in.CreatedBy = userId
|
||||
@@ -222,7 +228,8 @@ func (s *sSysImTenant) saveTenant(ctx context.Context, in *sysin.ImTenantEditInp
|
||||
return gerror.Wrap(err, "新增IM租户失败,请稍后重试!")
|
||||
}
|
||||
tenantID = id
|
||||
return s.saveConfig(ctx, id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, in.AliyunModerationEnabled, in.LocalModerationEnabled, userId)
|
||||
in.Id = id
|
||||
return s.saveConfig(ctx, in, userId)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -246,20 +253,34 @@ func (s *sSysImTenant) checkCodeUnique(ctx context.Context, id int64, code strin
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers uint64, maxGroups uint64, maxGroupMembers uint, aliyunModerationEnabled, localModerationEnabled int, userId int64) (err error) {
|
||||
func (s *sSysImTenant) saveConfig(ctx context.Context, in *sysin.ImTenantEditInp, userId int64) (err error) {
|
||||
if in.MessageEncryptionEnabled == 1 {
|
||||
count, queryErr := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Where("tenant_id", in.Id).
|
||||
Where("status", consts.ImMessageKeyStatusActive).
|
||||
Count()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "检查商户消息密钥失败")
|
||||
}
|
||||
if count == 0 {
|
||||
return gerror.New("启用消息加密前必须先在消息密钥管理中初始化有效密钥")
|
||||
}
|
||||
}
|
||||
var conf *entity.ImTenantConfig
|
||||
if err = s.configModel(ctx).
|
||||
Where(dao.ImTenantConfig.Columns().TenantId, tenantId).
|
||||
Where(dao.ImTenantConfig.Columns().TenantId, in.Id).
|
||||
Scan(&conf); err != nil {
|
||||
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
|
||||
}
|
||||
|
||||
data := g.Map{
|
||||
dao.ImTenantConfig.Columns().MaxUsers: maxUsers,
|
||||
dao.ImTenantConfig.Columns().MaxGroups: maxGroups,
|
||||
dao.ImTenantConfig.Columns().MaxGroupMembers: maxGroupMembers,
|
||||
tenantConfigAliyunModerationEnabled: aliyunModerationEnabled,
|
||||
tenantConfigLocalModerationEnabled: localModerationEnabled,
|
||||
dao.ImTenantConfig.Columns().MaxUsers: in.MaxUsers,
|
||||
dao.ImTenantConfig.Columns().MaxGroups: in.MaxGroups,
|
||||
dao.ImTenantConfig.Columns().MaxGroupMembers: in.MaxGroupMembers,
|
||||
tenantConfigAliyunModerationEnabled: in.AliyunModerationEnabled,
|
||||
tenantConfigLocalModerationEnabled: in.LocalModerationEnabled,
|
||||
tenantConfigMessageEncryptionEnabled: in.MessageEncryptionEnabled,
|
||||
tenantConfigRealnameReviewPolicy: in.RealnameReviewPolicy,
|
||||
}
|
||||
if conf != nil && conf.Id > 0 {
|
||||
data[dao.ImTenantConfig.Columns().UpdatedBy] = userId
|
||||
@@ -270,6 +291,8 @@ func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers
|
||||
dao.ImTenantConfig.Columns().MaxGroupMembers,
|
||||
tenantConfigAliyunModerationEnabled,
|
||||
tenantConfigLocalModerationEnabled,
|
||||
tenantConfigMessageEncryptionEnabled,
|
||||
tenantConfigRealnameReviewPolicy,
|
||||
dao.ImTenantConfig.Columns().UpdatedBy,
|
||||
).
|
||||
WherePri(conf.Id).Data(data).Update(); err != nil {
|
||||
@@ -278,7 +301,7 @@ func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers
|
||||
return
|
||||
}
|
||||
|
||||
data[dao.ImTenantConfig.Columns().TenantId] = tenantId
|
||||
data[dao.ImTenantConfig.Columns().TenantId] = in.Id
|
||||
data[dao.ImTenantConfig.Columns().CreatedBy] = userId
|
||||
data[dao.ImTenantConfig.Columns().UpdatedBy] = userId
|
||||
if _, err = s.configModel(ctx).
|
||||
@@ -289,6 +312,8 @@ func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers
|
||||
dao.ImTenantConfig.Columns().MaxGroupMembers,
|
||||
tenantConfigAliyunModerationEnabled,
|
||||
tenantConfigLocalModerationEnabled,
|
||||
tenantConfigMessageEncryptionEnabled,
|
||||
tenantConfigRealnameReviewPolicy,
|
||||
dao.ImTenantConfig.Columns().CreatedBy,
|
||||
dao.ImTenantConfig.Columns().UpdatedBy,
|
||||
).
|
||||
@@ -324,6 +349,8 @@ func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res
|
||||
res.MaxGroupMembers = conf.MaxGroupMembers
|
||||
res.AliyunModerationEnabled = conf.AliyunModerationEnabled
|
||||
res.LocalModerationEnabled = conf.LocalModerationEnabled
|
||||
res.MessageEncryptionEnabled = conf.MessageEncryptionEnabled
|
||||
res.RealnameReviewPolicy = conf.RealnameReviewPolicy
|
||||
}
|
||||
res.DbDsnConfigured = res.DbDsn != ""
|
||||
return
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/model/input/form"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type ImMessageEncryptionListInp struct {
|
||||
form.PageReq
|
||||
TenantId int64 `json:"tenantId" dc:"商户ID"`
|
||||
Keyword string `json:"keyword" dc:"商户编码或名称"`
|
||||
Enabled *int `json:"enabled" dc:"消息加密开关"`
|
||||
}
|
||||
|
||||
func (in *ImMessageEncryptionListInp) Filter(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImMessageEncryptionListModel struct {
|
||||
TenantId int64 `json:"tenantId" dc:"商户ID"`
|
||||
TenantCode string `json:"tenantCode" dc:"商户编码"`
|
||||
TenantName string `json:"tenantName" dc:"商户名称"`
|
||||
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
|
||||
ActiveVersion uint `json:"activeVersion" dc:"当前密钥版本"`
|
||||
PendingVersion uint `json:"pendingVersion" dc:"待激活密钥版本"`
|
||||
KeyCount int `json:"keyCount" dc:"密钥版本数"`
|
||||
LastRotatedAt *gtime.Time `json:"lastRotatedAt" dc:"最后轮换时间"`
|
||||
}
|
||||
|
||||
type ImMessageEncryptionActionInp struct {
|
||||
TenantId int64 `json:"tenantId" v:"required|min:1#请选择商户|商户不正确" dc:"商户ID"`
|
||||
Remark string `json:"remark" dc:"操作备注"`
|
||||
}
|
||||
|
||||
func (in *ImMessageEncryptionActionInp) Filter(ctx context.Context) error {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择商户")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImMessageEncryptionSwitchInp struct {
|
||||
TenantId int64 `json:"tenantId" v:"required|min:1#请选择商户|商户不正确" dc:"商户ID"`
|
||||
Enabled int `json:"enabled" dc:"是否启用"`
|
||||
}
|
||||
|
||||
func (in *ImMessageEncryptionSwitchInp) Filter(ctx context.Context) error {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择商户")
|
||||
}
|
||||
if in.Enabled != 0 && in.Enabled != 1 {
|
||||
return gerror.New("消息加密开关值不正确")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImMessageKeyGetInp struct {
|
||||
TenantCode string `json:"tenantCode" dc:"商户编码"`
|
||||
Version uint `json:"version" dc:"密钥版本,0表示当前版本"`
|
||||
}
|
||||
|
||||
type ImMessageKeyModel struct {
|
||||
TenantId int64 `json:"tenantId" dc:"商户ID"`
|
||||
TenantCode string `json:"tenantCode" dc:"商户编码"`
|
||||
Enabled bool `json:"enabled" dc:"消息加密是否启用"`
|
||||
Version uint `json:"version" dc:"密钥版本"`
|
||||
Algorithm string `json:"algorithm" dc:"算法"`
|
||||
Status string `json:"status" dc:"密钥状态"`
|
||||
Key string `json:"key" dc:"Base64编码DEK,仅内部接口返回"`
|
||||
}
|
||||
|
||||
type ImMessageKeyAckInp struct {
|
||||
TenantCode string `json:"tenantCode" dc:"商户编码"`
|
||||
Version uint `json:"version" dc:"密钥版本"`
|
||||
}
|
||||
@@ -43,33 +43,39 @@ type ImTenantInsertFields struct {
|
||||
|
||||
// ImTenantConfigUpdateFields 修改IM租户配置字段过滤
|
||||
type ImTenantConfigUpdateFields struct {
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
||||
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
||||
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
|
||||
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
|
||||
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
|
||||
}
|
||||
|
||||
// ImTenantConfigInsertFields 新增IM租户配置字段过滤
|
||||
type ImTenantConfigInsertFields struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
||||
CreatedBy int64 `json:"createdBy" dc:"创建者"`
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
||||
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
|
||||
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
|
||||
CreatedBy int64 `json:"createdBy" dc:"创建者"`
|
||||
}
|
||||
|
||||
// ImTenantEditInp 修改/新增IM租户
|
||||
type ImTenantEditInp struct {
|
||||
entity.ImTenant
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
||||
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
|
||||
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
|
||||
}
|
||||
|
||||
func (in *ImTenantEditInp) Filter(ctx context.Context) (err error) {
|
||||
@@ -86,8 +92,12 @@ func (in *ImTenantEditInp) Filter(ctx context.Context) (err error) {
|
||||
return gerror.New("租户状态不正确")
|
||||
}
|
||||
if (in.AliyunModerationEnabled != 0 && in.AliyunModerationEnabled != 1) ||
|
||||
(in.LocalModerationEnabled != 0 && in.LocalModerationEnabled != 1) {
|
||||
return gerror.New("内容审核开关值不正确")
|
||||
(in.LocalModerationEnabled != 0 && in.LocalModerationEnabled != 1) ||
|
||||
(in.MessageEncryptionEnabled != 0 && in.MessageEncryptionEnabled != 1) {
|
||||
return gerror.New("商户功能开关值不正确")
|
||||
}
|
||||
if in.RealnameReviewPolicy != 0 && in.RealnameReviewPolicy != 1 {
|
||||
return gerror.New("实名审核策略不正确")
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -105,16 +115,18 @@ func (in *ImTenantViewInp) Filter(ctx context.Context) (err error) {
|
||||
|
||||
type ImTenantViewModel struct {
|
||||
entity.ImTenant
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
||||
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
|
||||
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
|
||||
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
|
||||
TenantDbReachable bool `json:"tenantDbReachable" dc:"业务库是否可达"`
|
||||
TenantDbCheckError string `json:"tenantDbCheckError" dc:"业务库检查错误"`
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
||||
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
|
||||
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
|
||||
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
|
||||
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
|
||||
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
|
||||
TenantDbReachable bool `json:"tenantDbReachable" dc:"业务库是否可达"`
|
||||
TenantDbCheckError string `json:"tenantDbCheckError" dc:"业务库检查错误"`
|
||||
}
|
||||
|
||||
// ImTenantListInp 获取IM租户列表
|
||||
@@ -133,24 +145,26 @@ func (in *ImTenantListInp) Filter(ctx context.Context) (err error) {
|
||||
}
|
||||
|
||||
type ImTenantListModel struct {
|
||||
Id int64 `json:"id" dc:"ID"`
|
||||
Code string `json:"code" dc:"租户编码"`
|
||||
Name string `json:"name" dc:"租户名称"`
|
||||
TenantDesc string `json:"tenantDesc" dc:"租户描述"`
|
||||
DbName string `json:"dbName" dc:"业务库名"`
|
||||
State int `json:"state" dc:"状态"`
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
||||
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
|
||||
CreatedBy int64 `json:"createdBy" dc:"创建者"`
|
||||
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
|
||||
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
|
||||
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
Id int64 `json:"id" dc:"ID"`
|
||||
Code string `json:"code" dc:"租户编码"`
|
||||
Name string `json:"name" dc:"租户名称"`
|
||||
TenantDesc string `json:"tenantDesc" dc:"租户描述"`
|
||||
DbName string `json:"dbName" dc:"业务库名"`
|
||||
State int `json:"state" dc:"状态"`
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
||||
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
|
||||
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
|
||||
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
|
||||
CreatedBy int64 `json:"createdBy" dc:"创建者"`
|
||||
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
|
||||
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
|
||||
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
}
|
||||
|
||||
// ImTenantStatusInp 更新IM租户状态
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package genrouter
|
||||
|
||||
import "hotgo/internal/controller/admin/sys"
|
||||
|
||||
func init() {
|
||||
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImMessageEncryption)
|
||||
}
|
||||
@@ -211,6 +211,14 @@ type (
|
||||
// Restore 恢复IM租户
|
||||
Restore(ctx context.Context, in *sysin.ImTenantLifecycleInp) (err error)
|
||||
}
|
||||
ISysImMessageEncryption interface {
|
||||
List(ctx context.Context, in *sysin.ImMessageEncryptionListInp) (list []*sysin.ImMessageEncryptionListModel, total int, err error)
|
||||
Initialize(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error
|
||||
Rotate(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error
|
||||
Switch(ctx context.Context, in *sysin.ImMessageEncryptionSwitchInp) error
|
||||
GetKey(ctx context.Context, in *sysin.ImMessageKeyGetInp) (*sysin.ImMessageKeyModel, error)
|
||||
Ack(ctx context.Context, in *sysin.ImMessageKeyAckInp) error
|
||||
}
|
||||
ISysImRegistration interface {
|
||||
// GetConfig 获取IM注册配置
|
||||
GetConfig(ctx context.Context) (res *sysin.ImRegistrationConfigModel, err error)
|
||||
@@ -781,60 +789,61 @@ type (
|
||||
)
|
||||
|
||||
var (
|
||||
localSysAddons ISysAddons
|
||||
localSysAddonsConfig ISysAddonsConfig
|
||||
localSysAttachment ISysAttachment
|
||||
localSysBlacklist ISysBlacklist
|
||||
localSysConfig ISysConfig
|
||||
localSysCron ISysCron
|
||||
localSysCronGroup ISysCronGroup
|
||||
localSysCurdDemo ISysCurdDemo
|
||||
localSysCultivateFlower ISysCultivateFlower
|
||||
localSysCultivateContent ISysCultivateContent
|
||||
localSysImTenant ISysImTenant
|
||||
localSysImRegistration ISysImRegistration
|
||||
localSysImInvite ISysImInvite
|
||||
localSysImUser ISysImUser
|
||||
localSysImGroup ISysImGroup
|
||||
localSysImRealname ISysImRealname
|
||||
localSysImWorkOrder ISysImWorkOrder
|
||||
localSysImOutbox ISysImOutbox
|
||||
localSysImOrg ISysImOrg
|
||||
localSysImFile ISysImFile
|
||||
localSysImFilePolicy ISysImFilePolicy
|
||||
localSysImUserPolicyGroup ISysImUserPolicyGroup
|
||||
localSysImSensitiveWord ISysImSensitiveWord
|
||||
localSysImSecurityPolicy ISysImSecurityPolicy
|
||||
localSysImDashboard ISysImDashboard
|
||||
localSysImPrivateMessage ISysImPrivateMessage
|
||||
localSysImGroupMessage ISysImGroupMessage
|
||||
localSysImUserLoginHistory ISysImUserLoginHistory
|
||||
localSysImLoginFailureLog ISysImLoginFailureLog
|
||||
localSysImExternalConfig ISysImExternalConfig
|
||||
localSysImNotification ISysImNotification
|
||||
localSysImAppRelease ISysImAppRelease
|
||||
localSysImAbuseReason ISysImAbuseReason
|
||||
localSysImMailboxConfig ISysImMailboxConfig
|
||||
localSysImFeedbackConfig ISysImFeedbackConfig
|
||||
localSysImWorkbench ISysImWorkbench
|
||||
localSysImTaskDashboard ISysImTaskDashboard
|
||||
localSysImAuditLog ISysImAuditLog
|
||||
localSysImKafkaAudit ISysImKafkaAudit
|
||||
localSysImKafkaCommand ISysImKafkaCommand
|
||||
localSysImDeadLetter ISysImDeadLetter
|
||||
localSysDictData ISysDictData
|
||||
localSysDictType ISysDictType
|
||||
localSysEmsLog ISysEmsLog
|
||||
localSysGenCodes ISysGenCodes
|
||||
localSysLog ISysLog
|
||||
localSysLoginLog ISysLoginLog
|
||||
localSysNormalTreeDemo ISysNormalTreeDemo
|
||||
localSysOptionTreeDemo ISysOptionTreeDemo
|
||||
localSysProvinces ISysProvinces
|
||||
localSysServeLicense ISysServeLicense
|
||||
localSysServeLog ISysServeLog
|
||||
localSysSmsLog ISysSmsLog
|
||||
localSysTestCategory ISysTestCategory
|
||||
localSysAddons ISysAddons
|
||||
localSysAddonsConfig ISysAddonsConfig
|
||||
localSysAttachment ISysAttachment
|
||||
localSysBlacklist ISysBlacklist
|
||||
localSysConfig ISysConfig
|
||||
localSysCron ISysCron
|
||||
localSysCronGroup ISysCronGroup
|
||||
localSysCurdDemo ISysCurdDemo
|
||||
localSysCultivateFlower ISysCultivateFlower
|
||||
localSysCultivateContent ISysCultivateContent
|
||||
localSysImTenant ISysImTenant
|
||||
localSysImMessageEncryption ISysImMessageEncryption
|
||||
localSysImRegistration ISysImRegistration
|
||||
localSysImInvite ISysImInvite
|
||||
localSysImUser ISysImUser
|
||||
localSysImGroup ISysImGroup
|
||||
localSysImRealname ISysImRealname
|
||||
localSysImWorkOrder ISysImWorkOrder
|
||||
localSysImOutbox ISysImOutbox
|
||||
localSysImOrg ISysImOrg
|
||||
localSysImFile ISysImFile
|
||||
localSysImFilePolicy ISysImFilePolicy
|
||||
localSysImUserPolicyGroup ISysImUserPolicyGroup
|
||||
localSysImSensitiveWord ISysImSensitiveWord
|
||||
localSysImSecurityPolicy ISysImSecurityPolicy
|
||||
localSysImDashboard ISysImDashboard
|
||||
localSysImPrivateMessage ISysImPrivateMessage
|
||||
localSysImGroupMessage ISysImGroupMessage
|
||||
localSysImUserLoginHistory ISysImUserLoginHistory
|
||||
localSysImLoginFailureLog ISysImLoginFailureLog
|
||||
localSysImExternalConfig ISysImExternalConfig
|
||||
localSysImNotification ISysImNotification
|
||||
localSysImAppRelease ISysImAppRelease
|
||||
localSysImAbuseReason ISysImAbuseReason
|
||||
localSysImMailboxConfig ISysImMailboxConfig
|
||||
localSysImFeedbackConfig ISysImFeedbackConfig
|
||||
localSysImWorkbench ISysImWorkbench
|
||||
localSysImTaskDashboard ISysImTaskDashboard
|
||||
localSysImAuditLog ISysImAuditLog
|
||||
localSysImKafkaAudit ISysImKafkaAudit
|
||||
localSysImKafkaCommand ISysImKafkaCommand
|
||||
localSysImDeadLetter ISysImDeadLetter
|
||||
localSysDictData ISysDictData
|
||||
localSysDictType ISysDictType
|
||||
localSysEmsLog ISysEmsLog
|
||||
localSysGenCodes ISysGenCodes
|
||||
localSysLog ISysLog
|
||||
localSysLoginLog ISysLoginLog
|
||||
localSysNormalTreeDemo ISysNormalTreeDemo
|
||||
localSysOptionTreeDemo ISysOptionTreeDemo
|
||||
localSysProvinces ISysProvinces
|
||||
localSysServeLicense ISysServeLicense
|
||||
localSysServeLog ISysServeLog
|
||||
localSysSmsLog ISysSmsLog
|
||||
localSysTestCategory ISysTestCategory
|
||||
)
|
||||
|
||||
func SysAddons() ISysAddons {
|
||||
@@ -956,6 +965,17 @@ func RegisterSysImTenant(i ISysImTenant) {
|
||||
localSysImTenant = i
|
||||
}
|
||||
|
||||
func SysImMessageEncryption() ISysImMessageEncryption {
|
||||
if localSysImMessageEncryption == nil {
|
||||
panic("implement not found for interface ISysImMessageEncryption, forgot register?")
|
||||
}
|
||||
return localSysImMessageEncryption
|
||||
}
|
||||
|
||||
func RegisterSysImMessageEncryption(i ISysImMessageEncryption) {
|
||||
localSysImMessageEncryption = i
|
||||
}
|
||||
|
||||
func SysImRegistration() ISysImRegistration {
|
||||
if localSysImRegistration == nil {
|
||||
panic("implement not found for interface ISysImRegistration, forgot register?")
|
||||
|
||||
@@ -18,7 +18,7 @@ SELECT @imRootId, '日志审计', 'imLogAudit', 'audit', 'AuditOutlined', 1, '/i
|
||||
WHERE @imRootId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imLogAudit');
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT @imRootId, '系统设置', 'imSystemSettings', 'settings', 'SettingOutlined', 1, '/im/settings/tenant/index', '', '', 'ParentLayout', 1, '', 0, 0, '', 0, 0, 0, 2, CONCAT('tr_', @imRootId, ' '), 40, '', 1, @now, @now
|
||||
SELECT @imRootId, '平台设置', 'imSystemSettings', 'settings', 'SettingOutlined', 1, '/im/settings/tenant/index', '', '', 'ParentLayout', 1, '', 0, 0, '', 0, 0, 0, 2, CONCAT('tr_', @imRootId, ' '), 40, '', 1, @now, @now
|
||||
WHERE @imRootId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imSystemSettings');
|
||||
|
||||
SET @peopleId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imUserAndGroup' LIMIT 1);
|
||||
@@ -29,7 +29,7 @@ SET @settingsId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imSystemSet
|
||||
UPDATE `hg_admin_menu` SET `pid`=@imRootId, `title`='用户与群组', `path`='people', `icon`='TeamOutlined', `type`=1, `redirect`='/im/people/user/index', `component`='ParentLayout', `always_show`=1, `hidden`=0, `status`=1, `level`=2, `tree`=CONCAT('tr_', @imRootId, ' '), `sort`=10, `updated_at`=@now WHERE `id`=@peopleId;
|
||||
UPDATE `hg_admin_menu` SET `pid`=@imRootId, `title`='内容审核', `path`='content', `icon`='FileSearchOutlined', `type`=1, `redirect`='/im/content/private-message/index', `component`='ParentLayout', `always_show`=1, `hidden`=0, `status`=1, `level`=2, `tree`=CONCAT('tr_', @imRootId, ' '), `sort`=20, `updated_at`=@now WHERE `id`=@contentId;
|
||||
UPDATE `hg_admin_menu` SET `pid`=@imRootId, `title`='日志审计', `path`='audit', `icon`='AuditOutlined', `type`=1, `redirect`='/im/audit/operation/index', `component`='ParentLayout', `always_show`=1, `hidden`=0, `status`=1, `level`=2, `tree`=CONCAT('tr_', @imRootId, ' '), `sort`=30, `updated_at`=@now WHERE `id`=@auditId;
|
||||
UPDATE `hg_admin_menu` SET `pid`=@imRootId, `title`='系统设置', `path`='settings', `icon`='SettingOutlined', `type`=1, `redirect`='/im/settings/tenant/index', `component`='ParentLayout', `always_show`=1, `hidden`=0, `status`=1, `level`=2, `tree`=CONCAT('tr_', @imRootId, ' '), `sort`=40, `updated_at`=@now WHERE `id`=@settingsId;
|
||||
UPDATE `hg_admin_menu` SET `pid`=@imRootId, `title`='平台设置', `path`='settings', `icon`='SettingOutlined', `type`=1, `redirect`='/im/settings/tenant/index', `component`='ParentLayout', `always_show`=1, `hidden`=0, `status`=1, `level`=2, `tree`=CONCAT('tr_', @imRootId, ' '), `sort`=40, `updated_at`=@now WHERE `id`=@settingsId;
|
||||
UPDATE `hg_admin_menu` SET `pid`=@imRootId, `icon`='DashboardOutlined', `type`=2, `level`=2, `tree`=CONCAT('tr_', @imRootId, ' '), `sort`=5, `updated_at`=@now WHERE `name`='imDashboardIndex';
|
||||
UPDATE `hg_admin_menu` SET `redirect`='/im/dashboard/index', `updated_at`=@now WHERE `id`=@imRootId;
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
-- 消息加密密钥管理。
|
||||
-- 可重复执行。根 KEK 只在首次执行时生成,重复执行不会覆盖已有值。
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET @now := NOW();
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_tenant_config'
|
||||
AND COLUMN_NAME = 'message_encryption_enabled') = 0,
|
||||
'ALTER TABLE `im_tenant_config` ADD COLUMN `message_encryption_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否启用消息加密'' AFTER `local_moderation_enabled`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `im_message_encryption_keys` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` bigint NOT NULL COMMENT '商户ID',
|
||||
`key_version` int unsigned NOT NULL COMMENT '密钥版本',
|
||||
`algorithm` varchar(32) NOT NULL DEFAULT 'AES-256-GCM' COMMENT '数据加密算法',
|
||||
`encrypted_dek` blob NOT NULL COMMENT '由根KEK包裹后的商户DEK',
|
||||
`dek_nonce` varbinary(12) NOT NULL COMMENT '包裹DEK使用的随机nonce',
|
||||
`status` varchar(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/active/retired/disabled',
|
||||
`activated_at` datetime DEFAULT NULL,
|
||||
`retired_at` datetime DEFAULT NULL,
|
||||
`remark` varchar(255) NOT NULL DEFAULT '',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`created_by` bigint NOT NULL DEFAULT 0,
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`updated_by` bigint NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_im_message_key_tenant_version` (`tenant_id`, `key_version`),
|
||||
KEY `idx_im_message_key_tenant_status` (`tenant_id`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商户消息加密密钥版本';
|
||||
|
||||
INSERT INTO `hg_sys_dict_type`
|
||||
(`pid`, `name`, `type`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT 0, 'IM内部安全材料', 'im_internal_secret', 9999,
|
||||
'仅供服务端读取,通用字典接口禁止查询和维护', 1, @now, @now
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `hg_sys_dict_type` WHERE `type` = 'im_internal_secret'
|
||||
);
|
||||
|
||||
INSERT INTO `hg_sys_dict_data`
|
||||
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT 'core_material_01', TO_BASE64(RANDOM_BYTES(32)), 'string', 'im_internal_secret',
|
||||
'', 2, 1, '消息DEK包裹根密钥KEK,请勿通过后台展示或修改', 1, @now, @now
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `hg_sys_dict_data`
|
||||
WHERE `type` = 'im_internal_secret' AND `label` = 'core_material_01'
|
||||
);
|
||||
|
||||
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
|
||||
SET @settingsId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imSystemSettings' LIMIT 1);
|
||||
|
||||
INSERT INTO `hg_admin_menu`
|
||||
(`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`,
|
||||
`permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`,
|
||||
`frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`,
|
||||
`status`, `created_at`, `updated_at`)
|
||||
SELECT @settingsId, '消息密钥管理', 'imMessageEncryptionIndex', 'message-encryption/index',
|
||||
'KeyOutlined', 2, '', '/imMessageEncryption/list', '', '/imMessageEncryption/index',
|
||||
1, 'imManager', 0, 0, '', 0, 0, 0, 3,
|
||||
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' '), 80,
|
||||
'商户消息加密开关及密钥版本管理', 1, @now, @now
|
||||
WHERE @settingsId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionIndex');
|
||||
|
||||
SET @keyMenuId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionIndex' LIMIT 1);
|
||||
|
||||
UPDATE `hg_admin_menu`
|
||||
SET `pid` = @settingsId,
|
||||
`title` = '消息密钥管理',
|
||||
`path` = 'message-encryption/index',
|
||||
`icon` = 'KeyOutlined',
|
||||
`permissions` = '/imMessageEncryption/list',
|
||||
`component` = '/imMessageEncryption/index',
|
||||
`active_menu` = 'imManager',
|
||||
`level` = 3,
|
||||
`tree` = CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' '),
|
||||
`sort` = 80,
|
||||
`hidden` = 0,
|
||||
`status` = 1,
|
||||
`updated_at` = @now
|
||||
WHERE `id` = @keyMenuId;
|
||||
|
||||
INSERT INTO `hg_admin_menu`
|
||||
(`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`,
|
||||
`permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`,
|
||||
`frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`,
|
||||
`status`, `created_at`, `updated_at`)
|
||||
SELECT @keyMenuId, '初始化密钥', 'imMessageEncryptionInitialize', '', '', 3, '',
|
||||
'/imMessageEncryption/initialize', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
|
||||
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 10, '', 1, @now, @now
|
||||
WHERE @keyMenuId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionInitialize');
|
||||
|
||||
INSERT INTO `hg_admin_menu`
|
||||
(`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`,
|
||||
`permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`,
|
||||
`frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`,
|
||||
`status`, `created_at`, `updated_at`)
|
||||
SELECT @keyMenuId, '轮换密钥', 'imMessageEncryptionRotate', '', '', 3, '',
|
||||
'/imMessageEncryption/rotate', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
|
||||
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 20, '', 1, @now, @now
|
||||
WHERE @keyMenuId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionRotate');
|
||||
|
||||
INSERT INTO `hg_admin_menu`
|
||||
(`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`,
|
||||
`permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`,
|
||||
`frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`,
|
||||
`status`, `created_at`, `updated_at`)
|
||||
SELECT @keyMenuId, '切换消息加密', 'imMessageEncryptionSwitch', '', '', 3, '',
|
||||
'/imMessageEncryption/switch', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
|
||||
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 30, '', 1, @now, @now
|
||||
WHERE @keyMenuId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionSwitch');
|
||||
|
||||
UPDATE `hg_admin_menu`
|
||||
SET `level` = 4,
|
||||
`tree` = CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '),
|
||||
`updated_at` = @now
|
||||
WHERE `pid` = @keyMenuId AND `type` = 3;
|
||||
|
||||
INSERT IGNORE INTO `hg_admin_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT 1, `id` FROM `hg_admin_menu`
|
||||
WHERE `name` IN (
|
||||
'imManager',
|
||||
'imSystemSettings',
|
||||
'imMessageEncryptionIndex',
|
||||
'imMessageEncryptionInitialize',
|
||||
'imMessageEncryptionRotate',
|
||||
'imMessageEncryptionSwitch'
|
||||
);
|
||||
@@ -6,10 +6,56 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/forgoer/openssl"
|
||||
"io"
|
||||
)
|
||||
|
||||
const AESGCMKeySize = 32
|
||||
|
||||
// AESGCMEncrypt encrypts plaintext with AES-256-GCM and a fresh random nonce.
|
||||
func AESGCMEncrypt(plaintext, key, additionalData []byte) (ciphertext, nonce []byte, err error) {
|
||||
if len(key) != AESGCMKeySize {
|
||||
return nil, nil, fmt.Errorf("AES-256-GCM key must be %d bytes", AESGCMKeySize)
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
nonce = make([]byte, gcm.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return gcm.Seal(nil, nonce, plaintext, additionalData), nonce, nil
|
||||
}
|
||||
|
||||
// AESGCMDecrypt authenticates and decrypts ciphertext encrypted by AESGCMEncrypt.
|
||||
func AESGCMDecrypt(ciphertext, nonce, key, additionalData []byte) ([]byte, error) {
|
||||
if len(key) != AESGCMKeySize {
|
||||
return nil, fmt.Errorf("AES-256-GCM key must be %d bytes", AESGCMKeySize)
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(nonce) != gcm.NonceSize() {
|
||||
return nil, fmt.Errorf("AES-GCM nonce must be %d bytes", gcm.NonceSize())
|
||||
}
|
||||
return gcm.Open(nil, nonce, ciphertext, additionalData)
|
||||
}
|
||||
|
||||
// AesECBEncrypt 加密
|
||||
func AesECBEncrypt(src, key []byte) (dst []byte, err error) {
|
||||
return openssl.AesECBEncrypt(src, key, openssl.PKCS7_PADDING)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAESGCMRoundTripAndRandomNonce(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{7}, AESGCMKeySize)
|
||||
plain := []byte("message encryption key")
|
||||
aad := []byte("tenant:1:version:1")
|
||||
first, firstNonce, err := AESGCMEncrypt(plain, key, aad)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, secondNonce, err := AESGCMEncrypt(plain, key, aad)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Equal(firstNonce, secondNonce) || bytes.Equal(first, second) {
|
||||
t.Fatal("repeated encryption must use a different nonce and ciphertext")
|
||||
}
|
||||
decoded, err := AESGCMDecrypt(first, firstNonce, key, aad)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(decoded, plain) {
|
||||
t.Fatalf("unexpected plaintext: %q", decoded)
|
||||
}
|
||||
first[0] ^= 1
|
||||
if _, err = AESGCMDecrypt(first, firstNonce, key, aad); err == nil {
|
||||
t.Fatal("tampered ciphertext must fail authentication")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
export function List(params) {
|
||||
return http.request({ url: '/imMessageEncryption/list', method: 'GET', params });
|
||||
}
|
||||
|
||||
export function Initialize(params) {
|
||||
return http.request({ url: '/imMessageEncryption/initialize', method: 'POST', params });
|
||||
}
|
||||
|
||||
export function Rotate(params) {
|
||||
return http.request({ url: '/imMessageEncryption/rotate', method: 'POST', params });
|
||||
}
|
||||
|
||||
export function Switch(params) {
|
||||
return http.request({ url: '/imMessageEncryption/switch', method: 'POST', params });
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="n-layout-page-header">
|
||||
<n-card :bordered="false" title="消息密钥管理" />
|
||||
</div>
|
||||
<n-alert type="warning" :bordered="false" class="mb-3">
|
||||
密钥明文不会在管理后台展示。启用消息加密前必须先初始化密钥;轮换密钥将在 IM Server
|
||||
确认加载后自动激活。
|
||||
</n-alert>
|
||||
<n-card :bordered="false" class="proCard">
|
||||
<BasicForm
|
||||
ref="searchFormRef"
|
||||
@register="register"
|
||||
@submit="reloadTable"
|
||||
@reset="reloadTable"
|
||||
@keyup.enter="reloadTable"
|
||||
/>
|
||||
<BasicTable
|
||||
ref="actionRef"
|
||||
:columns="columns"
|
||||
:request="loadDataTable"
|
||||
:row-key="(row) => row.tenantId"
|
||||
:actionColumn="actionColumn"
|
||||
:scroll-x="scrollX"
|
||||
:resizeHeightOffset="-10000"
|
||||
/>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, reactive, ref } from 'vue';
|
||||
import { useDialog, useMessage } from 'naive-ui';
|
||||
import { BasicForm, useForm } from '@/components/Form/index';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
import { List, Initialize, Rotate, Switch } from '@/api/imMessageEncryption';
|
||||
import { columns, schemas, State } from './model';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const actionRef = ref();
|
||||
const searchFormRef = ref<any>({});
|
||||
|
||||
const actionColumn = reactive({
|
||||
width: 240,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
render(record: State) {
|
||||
const actions: Record<string, any>[] = [];
|
||||
if (record.keyCount === 0) {
|
||||
actions.push({
|
||||
label: '初始化',
|
||||
type: 'primary',
|
||||
auth: ['/imMessageEncryption/initialize'],
|
||||
onClick: () => handleInitialize(record),
|
||||
});
|
||||
} else {
|
||||
actions.push({
|
||||
label: record.messageEncryptionEnabled ? '停用加密' : '启用加密',
|
||||
type: record.messageEncryptionEnabled ? 'warning' : 'primary',
|
||||
auth: ['/imMessageEncryption/switch'],
|
||||
onClick: () => handleSwitch(record),
|
||||
});
|
||||
if (record.pendingVersion === 0) {
|
||||
actions.push({
|
||||
label: '轮换密钥',
|
||||
auth: ['/imMessageEncryption/rotate'],
|
||||
onClick: () => handleRotate(record),
|
||||
});
|
||||
}
|
||||
}
|
||||
return h(TableAction as any, { style: 'button', actions });
|
||||
},
|
||||
});
|
||||
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
|
||||
|
||||
const [register] = useForm({
|
||||
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||||
labelWidth: 90,
|
||||
schemas,
|
||||
});
|
||||
|
||||
const loadDataTable = async (res) => {
|
||||
return await List({ ...searchFormRef.value?.formModel, ...res });
|
||||
};
|
||||
|
||||
function reloadTable() {
|
||||
actionRef.value?.reload();
|
||||
}
|
||||
|
||||
function handleInitialize(record: State) {
|
||||
dialog.warning({
|
||||
title: '初始化消息密钥',
|
||||
content: `将为商户“${record.tenantName}”生成首个 AES-256-GCM 数据密钥。密钥不会在页面展示,是否继续?`,
|
||||
positiveText: '确认初始化',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await Initialize({ tenantId: record.tenantId });
|
||||
message.success('消息密钥初始化成功');
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleSwitch(record: State) {
|
||||
const enabled = record.messageEncryptionEnabled ? 0 : 1;
|
||||
dialog.warning({
|
||||
title: enabled ? '启用消息加密' : '停用消息加密',
|
||||
content: enabled
|
||||
? `启用后,商户“${record.tenantName}”的新消息将由 IM Server 加密存储。是否继续?`
|
||||
: `停用后仅影响新消息,历史密文仍按原密钥解密。是否继续?`,
|
||||
positiveText: '确认',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await Switch({ tenantId: record.tenantId, enabled });
|
||||
message.success(enabled ? '消息加密已启用' : '消息加密已停用');
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleRotate(record: State) {
|
||||
dialog.warning({
|
||||
title: '轮换消息密钥',
|
||||
content: `将为商户“${record.tenantName}”生成新版本密钥。IM Server 确认加载前,当前密钥仍保持有效。是否继续?`,
|
||||
positiveText: '确认轮换',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await Rotate({ tenantId: record.tenantId });
|
||||
message.success('待激活密钥已生成');
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,87 @@
|
||||
import { h, ref } from 'vue';
|
||||
import { NTag } from 'naive-ui';
|
||||
import { FormSchema } from '@/components/Form';
|
||||
|
||||
export class State {
|
||||
public tenantId = 0;
|
||||
public tenantCode = '';
|
||||
public tenantName = '';
|
||||
public messageEncryptionEnabled = 0;
|
||||
public activeVersion = 0;
|
||||
public pendingVersion = 0;
|
||||
public keyCount = 0;
|
||||
public lastRotatedAt = '';
|
||||
}
|
||||
|
||||
export const schemas = ref<FormSchema[]>([
|
||||
{
|
||||
field: 'keyword',
|
||||
component: 'NInput',
|
||||
label: '商户',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '商户编码/名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'enabled',
|
||||
component: 'NSelect',
|
||||
label: '加密状态',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
options: [
|
||||
{ label: '已启用', value: 1 },
|
||||
{ label: '未启用', value: 0 },
|
||||
],
|
||||
placeholder: '全部',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
export const columns = [
|
||||
{ title: '商户编码', key: 'tenantCode', align: 'left', width: 150 },
|
||||
{ title: '商户名称', key: 'tenantName', align: 'left', width: 180 },
|
||||
{
|
||||
title: '消息加密',
|
||||
key: 'messageEncryptionEnabled',
|
||||
align: 'left',
|
||||
width: 110,
|
||||
render(row: State) {
|
||||
return h(
|
||||
NTag,
|
||||
{ type: row.messageEncryptionEnabled ? 'success' : 'default', bordered: false },
|
||||
{ default: () => (row.messageEncryptionEnabled ? '已启用' : '未启用') }
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '当前版本',
|
||||
key: 'activeVersion',
|
||||
align: 'left',
|
||||
width: 110,
|
||||
render(row: State) {
|
||||
return row.activeVersion > 0 ? `v${row.activeVersion}` : '--';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '待激活版本',
|
||||
key: 'pendingVersion',
|
||||
align: 'left',
|
||||
width: 120,
|
||||
render(row: State) {
|
||||
return row.pendingVersion > 0
|
||||
? h(NTag, { type: 'warning', bordered: false }, { default: () => `v${row.pendingVersion}` })
|
||||
: '--';
|
||||
},
|
||||
},
|
||||
{ title: '版本数', key: 'keyCount', align: 'left', width: 90 },
|
||||
{
|
||||
title: '最后轮换时间',
|
||||
key: 'lastRotatedAt',
|
||||
align: 'left',
|
||||
width: 180,
|
||||
render(row: State) {
|
||||
return row.lastRotatedAt || '--';
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -100,6 +100,24 @@
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi span="1">
|
||||
<n-form-item label="消息加密存储" path="messageEncryptionEnabled">
|
||||
<n-switch
|
||||
v-model:value="formValue.messageEncryptionEnabled"
|
||||
:checked-value="1"
|
||||
:unchecked-value="0"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi span="1">
|
||||
<n-form-item label="实名审核方式" path="realnameReviewPolicy">
|
||||
<n-select
|
||||
v-model:value="formValue.realnameReviewPolicy"
|
||||
:options="realnameReviewPolicyOptions"
|
||||
placeholder="请选择实名审核方式"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi span="2">
|
||||
<n-form-item label="商户描述" path="tenantDesc">
|
||||
<n-input
|
||||
@@ -145,6 +163,10 @@
|
||||
return adaModalWidth(840);
|
||||
});
|
||||
const stateOptions = dict.getOption('ImTenantStateOptions');
|
||||
const realnameReviewPolicyOptions = [
|
||||
{ label: '不自动审核', value: 0 },
|
||||
{ label: '自动审核,失败后转人工', value: 1 },
|
||||
];
|
||||
|
||||
// 提交表单
|
||||
function confirmForm(e) {
|
||||
|
||||
@@ -21,6 +21,8 @@ export class State {
|
||||
public maxGroupMembers = 0; // 最大群成员数
|
||||
public aliyunModerationEnabled = 0; // 是否启用阿里云内容审核
|
||||
public localModerationEnabled = 0; // 是否启用本地内容审核
|
||||
public messageEncryptionEnabled = 0; // 是否启用消息加密
|
||||
public realnameReviewPolicy = 0; // 实名审核策略
|
||||
public dbDsnConfigured = false; // 是否配置独立连接串
|
||||
public createdBy = 0; // 创建者
|
||||
public createdBySumma?: null | MemberSumma = null; // 创建者摘要信息
|
||||
@@ -236,6 +238,32 @@ export const columns = [
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '消息加密',
|
||||
key: 'messageEncryptionEnabled',
|
||||
align: 'left',
|
||||
width: 120,
|
||||
render(row: State) {
|
||||
return h(
|
||||
NTag,
|
||||
{ type: row.messageEncryptionEnabled === 1 ? 'success' : 'default', bordered: false },
|
||||
{ default: () => (row.messageEncryptionEnabled === 1 ? '已启用' : '未启用') }
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '实名审核',
|
||||
key: 'realnameReviewPolicy',
|
||||
align: 'left',
|
||||
width: 150,
|
||||
render(row: State) {
|
||||
return h(
|
||||
NTag,
|
||||
{ type: row.realnameReviewPolicy === 1 ? 'success' : 'default', bordered: false },
|
||||
{ default: () => (row.realnameReviewPolicy === 1 ? '自动审核' : '人工审核') }
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建者',
|
||||
key: 'createdBy',
|
||||
|
||||
Reference in New Issue
Block a user