feat: 增加商户消息加密密钥管理
This commit is contained in:
@@ -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,15 @@
|
||||
/>
|
||||
</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
|
||||
|
||||
@@ -21,6 +21,7 @@ export class State {
|
||||
public maxGroupMembers = 0; // 最大群成员数
|
||||
public aliyunModerationEnabled = 0; // 是否启用阿里云内容审核
|
||||
public localModerationEnabled = 0; // 是否启用本地内容审核
|
||||
public messageEncryptionEnabled = 0; // 是否启用消息加密
|
||||
public realnameReviewPolicy = 0; // 实名审核策略
|
||||
public dbDsnConfigured = false; // 是否配置独立连接串
|
||||
public createdBy = 0; // 创建者
|
||||
@@ -237,6 +238,19 @@ 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',
|
||||
|
||||
Reference in New Issue
Block a user