diff --git a/README.md b/README.md index 8c2b1a7..87b02e3 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,13 @@ DATABASE_URL=postgresql://... NEXTAUTH_SECRET=replace-this-with-a-strong-secret ``` +启用素材平台 API Key 创建和后续查看时,还必须配置独立且稳定的加密密钥: + +```bash +# 至少 32 个字符;生产环境必须独立生成并稳定保存 +API_KEY_ENCRYPTION_KEY=replace-this-with-a-separate-strong-secret +``` + 常用配置: ```bash @@ -167,6 +174,11 @@ DINGTALK_PERFORMANCE_SURVEY_NOTIFICATION_CHANNEL=robot_oto DINGTALK_ROBOT_CODE=your-dingtalk-robot-code # 灰度阶段填写允许走机器人单聊的钉钉 userId,多个用逗号分隔;留空表示全量启用 DINGTALK_ROBOT_OTO_TEST_USER_IDS=user-id-1,user-id-2 +# 通知中心管理后台代理(仅服务端使用,绝不能下发到浏览器) +YYD_NOTIFICATION_CENTER_ADMIN_URL=https://notify-admin.example.com/notification-center/admin +YYD_NOTIFICATION_CENTER_ADMIN_API_SECRET=replace-with-server-side-admin-secret +# 可选,请求超时毫秒;默认 8000 +YYD_NOTIFICATION_CENTER_ADMIN_TIMEOUT_MS=8000 # 下载与外部接口 DOWNLOAD_TOKEN_SECRET=replace-this diff --git a/app/admin/api-keys/page.tsx b/app/admin/api-keys/page.tsx index 9c9d24c..be831b2 100644 --- a/app/admin/api-keys/page.tsx +++ b/app/admin/api-keys/page.tsx @@ -36,9 +36,8 @@ export default function ApiKeysPage() { if (status === 'unauthenticated') { router.push('/login'); } else if (status === 'authenticated' && session?.user) { - const userRole = (session.user as any).role; - if (userRole !== 'ADMIN' && userRole !== 'SYSTEM_ADMIN') { - router.push('/'); + if (!(session.user as any).isSuperAdmin) { + router.push('/admin'); } } }, [status, session, router]); @@ -207,7 +206,7 @@ export default function ApiKeysPage() {
{createdKey}
-

注意:此密钥只显示一次,关闭后将无法再次查看

+

关闭后仍可在超级管理员工作台的“权限与凭证”中按需查看。

+ ) + })} + + + +
+ {activeWorkspace === 'access' && ( +
+
+
+
+

+ 当前 {count} / 4 个超级管理员{!canAdd ? ',已达到上限' : ''}。本区 API Key 仅属于素材平台外部接口。 +

+
{/* 添加超级管理员 */} {canAdd && (
@@ -680,17 +813,17 @@ export default function SuperAdminsTab({ showToast }: SuperAdminsTabProps) { )}
- {/* API Key 管理卡片 */} + {/* 素材平台 API Key 管理 */}

- API Key 管理 + 素材平台 API Key

@@ -700,7 +833,7 @@ export default function SuperAdminsTab({ showToast }: SuperAdminsTabProps) {
) : apiKeys.length === 0 ? (
-

暂无 API Key

+

暂无素材平台 API Key

点击上方按钮创建第一个 API Key

@@ -736,8 +869,28 @@ export default function SuperAdminsTab({ showToast }: SuperAdminsTabProps) { {key.name} - - {key.key.substring(0, 16)}... + +
+ + {revealedApiKeys[key.id] || key.key} + + {revealedApiKeys[key.id] && ( + + )} +
+ {!key.canReveal && ( +

+ 历史 Key 无法恢复,需创建新 Key 替换 +

+ )} +
)} -
+
+ + )} -
+ {activeWorkspace === 'notificationCenter' && ( + + )} + + {activeWorkspace === 'outbound' && ( +
+

- 外部系统通知目标 + 素材外发目标

- 这里配置素材库主动通知的目标系统。`task-detail.full-set.ready` 主要给自动上架平台,`task-detail.barcode-images.updated` 主要给产品平台。 + 配置素材库向其他业务系统发送 Webhook 的目标,与统一通知中心向员工发送钉钉单聊不同。

)} +
+
+ )}
{/* 创建 API Key 模态框 */} - {showCreateModal && ( + {activeWorkspace === 'access' && showCreateModal && (

- 创建 API Key + 创建素材平台 API Key

{createdKey ? (

- API Key 已创建,请妥善保存: + API Key 已创建,可复制给对应的外部系统:

{createdKey}
-

- 注意:此密钥只显示一次,关闭后将无法再次查看 + +

+ 关闭后仍可在列表中由超级管理员按需查看,请只通过安全渠道发送给外部系统负责人。

+
+ +
+ {[ + ['接入项目', String(projects.length), '当前已登记'], + ['正在使用', String(enabledProjectCount), '已启用项目'], + ['收件人花名', String(aliasCount), '已登记映射'], + ['近期失败', String(failedCount), failedCount > 0 ? '需要处理' : '未发现异常'], + ].map(([label, value, hint]) => ( +
+
{label}
+
+ {value} + 0 ? 'text-[#b42318]' : 'text-[#66758b]'}`}> + {hint} + +
+
+ ))} +
+
+ +
+ + +
+ {projectsError && ( +
+
+
+
+ )} + + {activeView === 'projects' && ( +
+
+
+

接入项目

+

新增项目后只会展示一次项目密钥,请立即保存到调用方环境变量。

+
+ +
+ + {projectsLoading ? ( +
+ {[0, 1, 2].map((item) =>
)} +
+ ) : projects.length === 0 ? ( +
+
+ ) : ( +
+
+ 项目允许跳转域名最近投递花名更新时间操作 +
+
+ {projects.map((project) => ( +
+
+
+ {project.name} + + {project.isActive ? '使用中' : '已停用'} + +
+ {project.projectCode} +
+
+ 允许域名: + {project.allowedHosts.length > 0 ? project.allowedHosts.slice(0, 2).join('、') : '未配置'} + {project.allowedHosts.length > 2 ? ` 等 ${project.allowedHosts.length} 个` : ''} +
+
+ + {getStatusLabel(project.deliverySummary.lastStatus)} + + {project.deliverySummary.recentFailedCount > 0 && ( +

近期失败 {project.deliverySummary.recentFailedCount} 条

+ )} +
+
花名:{project.recipientCount}
+
{formatDateTime(project.updatedAt)}
+ +
+ ))} +
+
+ )} +
+ )} + + {activeView === 'recipients' && ( +
+

收件人花名

+

花名必须由管理员明确登记,系统不会模糊猜测收件人。

+ {projects.length === 0 ? ( +
请先在“接入项目”中新建项目,再登记收件人花名。
+ ) : ( +
+ + +
+
花名钉钉 userId删除
+
+ {recipientRows.length === 0 &&

暂无花名。点击下方按钮登记明确的花名与钉钉 userId。

} + {recipientRows.map((recipient, index) => ( +
+ + setRecipientRows((current) => current.map((row, rowIndex) => rowIndex === index ? { ...row, alias: event.target.value } : row))} placeholder="例如:小王" className="min-h-10 min-w-0 rounded-[8px] border border-[#cfd8e3] px-3 text-[14px] focus:border-[#1677ff] focus:outline-none focus:ring-2 focus:ring-[#1677ff]/20" /> + + setRecipientRows((current) => current.map((row, rowIndex) => rowIndex === index ? { ...row, userId: event.target.value } : row))} placeholder="精确钉钉 userId" className="min-h-10 min-w-0 rounded-[8px] border border-[#cfd8e3] px-3 text-[14px] focus:border-[#1677ff] focus:outline-none focus:ring-2 focus:ring-[#1677ff]/20" /> + +
+ ))} +
+
+
+ + +
+
+ )} +
+ )} + + {activeView === 'test' && ( +
+

向指定员工发送测试通知

+

这是一次真实钉钉单聊。发送前会再次展示项目、收件人与通知内容。

+ {projects.length === 0 ? ( +
请先新增并启用接入项目,再发送测试通知。
+ ) : ( +
+
+ + +
+
+ + +
+
+ + {testRecipientMode === 'alias' ? ( + + ) : ( + setTestRecipient(event.target.value)} placeholder="输入精确钉钉 userId" className="mt-2 min-h-10 w-full rounded-[8px] border border-[#cfd8e3] px-3 text-[14px] focus:border-[#1677ff] focus:outline-none focus:ring-2 focus:ring-[#1677ff]/20" /> + )} +
+
setTestTitle(event.target.value)} className="mt-2 min-h-10 w-full rounded-[8px] border border-[#cfd8e3] px-3 text-[14px] focus:border-[#1677ff] focus:outline-none focus:ring-2 focus:ring-[#1677ff]/20" />
+