87c2b04174
- 统一审批中心:模型/资源/渠道/工具四类申请聚合审批,通过自动开通 (marketplace 安装/渠道授权),outbox 双向站内信;门户可发起/撤回。 - 工具治理:rate_limit_rpm(固定窗口原子 upsert,多实例共享)+ approval_required (首次调用自动发起审批,批准前一律拒绝)。 - 平台环境变量:平台级注入 skill/MCP 运行时,个人可覆盖;系统管理员可写。 - 数字员工会话入口:门户列表/对话/调用记录,复用用户运行时凭据。 - 个人渠道:webhook 入站令牌 SHA-256 摘要 + constant-time 校验,绑定已批准 模型,用量归属用户 Key。 - 报表多维:工具调用/审批授权/安全事件三组统计端点与页面。 - 租户配额:部门 Key/月 Token 上限,运行时凭据开通强制校验,概览展示用量。 - 迁移 000042-000045;修复渠道空 API Key NOT NULL 违约与 inet 扫描; 25 包测试通过,前后端构建通过,端到端验证完成。
671 lines
32 KiB
Vue
671 lines
32 KiB
Vue
<template>
|
||
<div class="page-content">
|
||
<div class="mb-5 flex items-center justify-between gap-4">
|
||
<div>
|
||
<h2 class="text-xl font-semibold">账号与权限</h2>
|
||
<p class="text-g-500 mt-1 text-sm">管理员和门户账号统一管理,权限变更即时生效</p>
|
||
</div>
|
||
<ElButton type="primary" @click="openCreate">
|
||
{{ activeTab === 'department' ? '新增部门' : activeTab === 'oidc' || activeTab === 'saml' || activeTab === 'social' ? '新增身份源' : '新增账号' }}
|
||
</ElButton>
|
||
</div>
|
||
|
||
<ElAlert
|
||
class="mb-4"
|
||
type="info"
|
||
:closable="false"
|
||
title="内置角色提供默认权限;直接权限使用 resource:action 格式并与角色权限合并。"
|
||
/>
|
||
|
||
<ElTabs v-model="activeTab" @tab-change="load">
|
||
<ElTabPane label="管理员" name="admin" />
|
||
<ElTabPane label="门户用户" name="portal" />
|
||
<ElTabPane label="部门" name="department" />
|
||
<ElTabPane label="OIDC 身份源" name="oidc" />
|
||
<ElTabPane label="SAML 身份源" name="saml" />
|
||
<ElTabPane label="扫码登录" name="social" />
|
||
</ElTabs>
|
||
|
||
<ElTable v-if="activeTab === 'admin' || activeTab === 'portal'" v-loading="loading" :data="records" row-key="id">
|
||
<ElTableColumn prop="login" label="账号" min-width="170" />
|
||
<ElTableColumn prop="display_name" label="显示名称" min-width="150">
|
||
<template #default="{ row }">{{ row.display_name || '-' }}</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn prop="role" label="角色" width="130" />
|
||
<ElTableColumn v-if="activeTab === 'portal'" prop="department_name" label="部门" min-width="150">
|
||
<template #default="{ row }">{{ row.department_name || '未分配' }}</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="有效权限" min-width="280">
|
||
<template #default="{ row }">
|
||
<div class="flex flex-wrap gap-1">
|
||
<ElTag v-for="permission in row.effective_permissions" :key="permission" size="small">
|
||
{{ permission }}
|
||
</ElTag>
|
||
<span v-if="row.effective_permissions.length === 0">-</span>
|
||
</div>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="TOTP" width="90">
|
||
<template #default="{ row }">
|
||
<ElTag :type="row.totp_enabled ? 'success' : 'info'">
|
||
{{ row.totp_enabled ? '已启用' : '未启用' }}
|
||
</ElTag>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="状态" width="90">
|
||
<template #default="{ row }">
|
||
<ElTag :type="row.active ? 'success' : 'danger'">
|
||
{{ row.active ? '启用' : '停用' }}
|
||
</ElTag>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="操作" width="90" fixed="right">
|
||
<template #default="{ row }">
|
||
<ElButton link type="primary" @click="openEdit(row)">编辑</ElButton>
|
||
</template>
|
||
</ElTableColumn>
|
||
</ElTable>
|
||
|
||
<ElTable v-else-if="activeTab === 'department'" v-loading="loading" :data="departments" row-key="id">
|
||
<ElTableColumn prop="code" label="代码" min-width="150" />
|
||
<ElTableColumn prop="name" label="名称" min-width="170" />
|
||
<ElTableColumn prop="parent_name" label="上级部门" min-width="160">
|
||
<template #default="{ row }">{{ row.parent_name || '根部门' }}</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn prop="description" label="描述" min-width="220" show-overflow-tooltip />
|
||
<ElTableColumn prop="user_count" label="启用用户" width="100" />
|
||
<ElTableColumn label="状态" width="90">
|
||
<template #default="{ row }">
|
||
<ElTag :type="row.active ? 'success' : 'info'">{{ row.active ? '启用' : '停用' }}</ElTag>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="操作" width="90" fixed="right">
|
||
<template #default="{ row }">
|
||
<ElButton link type="primary" @click="openDepartmentEdit(row)">编辑</ElButton>
|
||
</template>
|
||
</ElTableColumn>
|
||
</ElTable>
|
||
|
||
<ElTable v-else-if="activeTab === 'oidc'" v-loading="loading" :data="identityProviders" row-key="id">
|
||
<ElTableColumn prop="code" label="代码" min-width="130" />
|
||
<ElTableColumn prop="display_name" label="名称" min-width="150" />
|
||
<ElTableColumn prop="issuer_url" label="Issuer" min-width="260" show-overflow-tooltip />
|
||
<ElTableColumn prop="client_id" label="Client ID" min-width="170" show-overflow-tooltip />
|
||
<ElTableColumn label="自动开户" width="100">
|
||
<template #default="{ row }">{{ row.auto_provision ? '启用' : '关闭' }}</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="状态" width="90">
|
||
<template #default="{ row }"><ElTag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '启用' : '停用' }}</ElTag></template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="操作" width="90" fixed="right">
|
||
<template #default="{ row }"><ElButton link type="primary" @click="openIdentityProviderEdit(row)">编辑</ElButton></template>
|
||
</ElTableColumn>
|
||
</ElTable>
|
||
|
||
<ElTable v-else-if="activeTab === 'social'" v-loading="loading" :data="socialProviders" row-key="kind">
|
||
<ElTableColumn label="平台" width="110">
|
||
<template #default="{ row }">{{ socialKindName(row.kind) }}</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn prop="code" label="代码" min-width="120" />
|
||
<ElTableColumn prop="display_name" label="名称" min-width="140" />
|
||
<ElTableColumn prop="client_id" label="AppID" min-width="180" show-overflow-tooltip />
|
||
<ElTableColumn v-if="socialProviders.some((p: SocialProviderRecord) => p.kind === 'wecom')" prop="agent_id" label="AgentID" min-width="110" />
|
||
<ElTableColumn label="密钥" width="80">
|
||
<template #default="{ row }"><ElTag :type="row.secret_configured ? 'success' : 'danger'">{{ row.secret_configured ? '已配置' : '缺失' }}</ElTag></template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="自动开户" width="100">
|
||
<template #default="{ row }">{{ row.auto_provision ? '启用' : '关闭' }}</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="状态" width="90">
|
||
<template #default="{ row }"><ElTag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '启用' : '停用' }}</ElTag></template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="操作" width="140" fixed="right">
|
||
<template #default="{ row }">
|
||
<ElButton link type="primary" @click="openSocialProviderEdit(row)">编辑</ElButton>
|
||
<ElButton link type="danger" @click="removeSocialProvider(row)">删除</ElButton>
|
||
</template>
|
||
</ElTableColumn>
|
||
</ElTable>
|
||
|
||
<ElTable v-else v-loading="loading" :data="samlProviders" row-key="id">
|
||
<ElTableColumn prop="code" label="代码" min-width="130" />
|
||
<ElTableColumn prop="display_name" label="名称" min-width="150" />
|
||
<ElTableColumn prop="metadata_url" label="IdP Metadata" min-width="280" show-overflow-tooltip />
|
||
<ElTableColumn prop="sp_entity_id" label="SP Entity ID" min-width="220" show-overflow-tooltip />
|
||
<ElTableColumn label="自动开户" width="100">
|
||
<template #default="{ row }">{{ row.auto_provision ? '启用' : '关闭' }}</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="状态" width="90">
|
||
<template #default="{ row }"><ElTag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '启用' : '停用' }}</ElTag></template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="操作" width="90" fixed="right">
|
||
<template #default="{ row }"><ElButton link type="primary" @click="openSAMLProviderEdit(row)">编辑</ElButton></template>
|
||
</ElTableColumn>
|
||
</ElTable>
|
||
|
||
<ElDialog v-model="dialogVisible" :title="editingId ? '编辑账号' : '新增账号'" width="640px">
|
||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||
<ElFormItem label="账号" prop="login">
|
||
<ElInput v-model="form.login" placeholder="登录账号" />
|
||
</ElFormItem>
|
||
<ElFormItem label="显示名称">
|
||
<ElInput v-model="form.display_name" maxlength="64" />
|
||
</ElFormItem>
|
||
<ElFormItem label="角色" prop="role">
|
||
<ElSelect v-model="form.role" class="w-full">
|
||
<ElOption
|
||
v-for="role in roleOptions"
|
||
:key="role.value"
|
||
:label="role.label"
|
||
:value="role.value"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem :label="editingId ? '新口令' : '初始口令'" prop="password">
|
||
<ElInput
|
||
v-model="form.password"
|
||
type="password"
|
||
show-password
|
||
:placeholder="editingId ? '留空表示不修改' : '至少 12 个字符'"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="直接权限">
|
||
<ElSelect
|
||
v-model="form.permissions"
|
||
multiple
|
||
filterable
|
||
allow-create
|
||
default-first-option
|
||
class="w-full"
|
||
placeholder="选择或输入 resource:action"
|
||
>
|
||
<ElOption v-for="item in permissionOptions" :key="item" :label="item" :value="item" />
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem v-if="activeTab === 'portal'" label="所属部门">
|
||
<ElSelect v-model="form.department_id" clearable class="w-full" placeholder="可不分配">
|
||
<ElOption
|
||
v-for="department in activeDepartments"
|
||
:key="department.id"
|
||
:label="department.name"
|
||
:value="department.id"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem label="启用">
|
||
<ElSwitch v-model="form.active" />
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" :loading="saving" @click="submit">保存</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<ElDialog v-model="departmentDialogVisible" :title="departmentEditingId ? '编辑部门' : '新增部门'" width="600px">
|
||
<ElForm ref="departmentFormRef" :model="departmentForm" label-width="100px">
|
||
<ElFormItem label="代码" required>
|
||
<ElInput v-model="departmentForm.code" placeholder="例如 platform" />
|
||
</ElFormItem>
|
||
<ElFormItem label="名称" required>
|
||
<ElInput v-model="departmentForm.name" maxlength="128" />
|
||
</ElFormItem>
|
||
<ElFormItem label="上级部门">
|
||
<ElSelect v-model="departmentForm.parent_id" clearable class="w-full">
|
||
<ElOption
|
||
v-for="department in availableParents"
|
||
:key="department.id"
|
||
:label="department.name"
|
||
:value="department.id"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem label="描述">
|
||
<ElInput v-model="departmentForm.description" type="textarea" maxlength="1024" show-word-limit />
|
||
</ElFormItem>
|
||
<ElFormItem label="Key 配额">
|
||
<ElInputNumber v-model="departmentForm.max_api_keys" :min="0" :max="1000000" class="w-full" />
|
||
<div class="text-g-400 text-xs">该租户(部门)可绑定的网关 API Key 上限,0 = 不限</div>
|
||
</ElFormItem>
|
||
<ElFormItem label="月 Token 配额">
|
||
<ElInputNumber v-model="departmentForm.max_monthly_tokens" :min="0" :max="1000000000000000" class="w-full" />
|
||
<div class="text-g-400 text-xs">该租户每月 Token 用量上限,0 = 不限</div>
|
||
</ElFormItem>
|
||
<ElFormItem label="启用">
|
||
<ElSwitch v-model="departmentForm.active" />
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="departmentDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" :loading="saving" @click="submitDepartment">保存</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<ElDialog v-model="idpDialogVisible" :title="idpEditingId ? '编辑 OIDC 身份源' : '新增 OIDC 身份源'" width="720px">
|
||
<ElForm :model="idpForm" label-width="130px">
|
||
<ElFormItem label="代码" required><ElInput v-model="idpForm.code" placeholder="例如 corp_oidc" /></ElFormItem>
|
||
<ElFormItem label="显示名称" required><ElInput v-model="idpForm.display_name" /></ElFormItem>
|
||
<ElFormItem label="Issuer URL" required><ElInput v-model="idpForm.issuer_url" placeholder="https://id.example.com" /></ElFormItem>
|
||
<ElFormItem label="Client ID" required><ElInput v-model="idpForm.client_id" /></ElFormItem>
|
||
<ElFormItem label="Client Secret" :required="!idpEditingId">
|
||
<ElInput v-model="idpForm.client_secret" type="password" show-password :placeholder="idpEditingId ? '留空表示不更换' : ''" />
|
||
</ElFormItem>
|
||
<ElFormItem label="网关回调 URL" required><ElInput v-model="idpForm.redirect_uri" /></ElFormItem>
|
||
<ElFormItem label="门户返回 URL" required><ElInput v-model="idpForm.portal_return_url" /></ElFormItem>
|
||
<ElFormItem label="Scopes"><ElSelect v-model="idpForm.scopes" multiple allow-create filterable class="w-full" /></ElFormItem>
|
||
<ElFormItem label="默认部门">
|
||
<ElSelect v-model="idpForm.default_department_id" clearable class="w-full">
|
||
<ElOption v-for="department in activeDepartments" :key="department.id" :label="department.name" :value="department.id" />
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem label="自动开户"><ElSwitch v-model="idpForm.auto_provision" /></ElFormItem>
|
||
<ElFormItem label="启用"><ElSwitch v-model="idpForm.enabled" /></ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="idpDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" :loading="saving" @click="submitIdentityProvider">保存</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<ElDialog v-model="samlDialogVisible" :title="samlEditingId ? '编辑 SAML 身份源' : '新增 SAML 身份源'" width="760px">
|
||
<ElAlert class="mb-4" type="info" :closable="false" title="启用时服务端会拉取 IdP metadata,并验证 Redirect SSO 端点和当前有效的签名证书。" />
|
||
<ElForm :model="samlForm" label-width="150px">
|
||
<ElFormItem label="代码" required><ElInput v-model="samlForm.code" placeholder="例如 corp_saml" /></ElFormItem>
|
||
<ElFormItem label="显示名称" required><ElInput v-model="samlForm.display_name" /></ElFormItem>
|
||
<ElFormItem label="IdP Metadata URL" required><ElInput v-model="samlForm.metadata_url" placeholder="https://idp.example.com/metadata" /></ElFormItem>
|
||
<ElFormItem label="SP Entity ID" required><ElInput v-model="samlForm.sp_entity_id" placeholder="urn:example:ai-gateway" /></ElFormItem>
|
||
<ElFormItem label="ACS URL" required><ElInput v-model="samlForm.acs_url" placeholder="https://gateway.example.com/api/v1/portal/sso/corp_saml/callback" /></ElFormItem>
|
||
<ElFormItem label="门户返回 URL" required><ElInput v-model="samlForm.portal_return_url" /></ElFormItem>
|
||
<ElFormItem label="邮箱属性"><ElInput v-model="samlForm.email_attribute" placeholder="mail 或属性 URI" /></ElFormItem>
|
||
<ElFormItem label="姓名属性"><ElInput v-model="samlForm.name_attribute" placeholder="cn 或属性 URI" /></ElFormItem>
|
||
<ElFormItem label="默认部门">
|
||
<ElSelect v-model="samlForm.default_department_id" clearable class="w-full">
|
||
<ElOption v-for="department in activeDepartments" :key="department.id" :label="department.name" :value="department.id" />
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem label="自动开户"><ElSwitch v-model="samlForm.auto_provision" /></ElFormItem>
|
||
<ElFormItem label="启用"><ElSwitch v-model="samlForm.enabled" /></ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="samlDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" :loading="saving" @click="submitSAMLProvider">保存</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<ElDialog v-model="socialDialogVisible" :title="socialEditingKind ? '编辑扫码登录' : '新增扫码登录'" width="720px">
|
||
<ElAlert class="mb-4" type="info" :closable="false" title="在企业微信/钉钉/飞书开放平台创建应用后填写;回调地址需配置为下方「回调 URL」。" />
|
||
<ElForm :model="socialForm" label-width="150px">
|
||
<ElFormItem label="平台" required>
|
||
<ElSelect v-model="socialForm.kind" class="w-full" :disabled="!!socialEditingKind">
|
||
<ElOption label="企业微信" value="wecom" />
|
||
<ElOption label="钉钉" value="dingtalk" />
|
||
<ElOption label="飞书" value="feishu" />
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem label="代码" required><ElInput v-model="socialForm.code" :placeholder="`例如 corp_${socialForm.kind || 'wecom'}`" :disabled="!!socialEditingKind" /></ElFormItem>
|
||
<ElFormItem label="显示名称" required><ElInput v-model="socialForm.display_name" :placeholder="socialKindName(socialForm.kind)" /></ElFormItem>
|
||
<ElFormItem v-if="socialForm.kind === 'wecom'" label="CorpID" required><ElInput v-model="socialForm.client_id" placeholder="企业微信 CorpID" /></ElFormItem>
|
||
<ElFormItem v-else-if="socialForm.kind === 'dingtalk'" label="AppKey" required><ElInput v-model="socialForm.client_id" placeholder="钉钉应用 AppKey" /></ElFormItem>
|
||
<ElFormItem v-else label="AppID" required><ElInput v-model="socialForm.client_id" placeholder="飞书应用 AppID" /></ElFormItem>
|
||
<ElFormItem v-if="socialForm.kind === 'wecom'" label="AgentID" required><ElInput v-model="socialForm.agent_id" placeholder="企业微信应用 AgentID" /></ElFormItem>
|
||
<ElFormItem label="AppSecret" :required="!socialEditingKind">
|
||
<ElInput v-model="socialForm.secret" type="password" show-password :placeholder="socialEditingKind ? '留空则不修改' : '应用 AppSecret'" />
|
||
</ElFormItem>
|
||
<ElFormItem label="回调 URL" required>
|
||
<ElInput v-model="socialForm.redirect_uri" :placeholder="`https://你的域名/api/v1/portal/sso/${socialForm.code || 'corp_wecom'}/callback`" />
|
||
</ElFormItem>
|
||
<ElFormItem label="门户返回 URL" required><ElInput v-model="socialForm.portal_return_url" placeholder="例如 https://你的域名/#/auth/login" /></ElFormItem>
|
||
<ElFormItem label="默认部门">
|
||
<ElSelect v-model="socialForm.default_department_id" clearable class="w-full">
|
||
<ElOption v-for="department in activeDepartments" :key="department.id" :label="department.name" :value="department.id" />
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem label="自动开户"><ElSwitch v-model="socialForm.auto_provision" /></ElFormItem>
|
||
<ElFormItem label="启用"><ElSwitch v-model="socialForm.enabled" /></ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="socialDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" :loading="saving" @click="submitSocialProvider">保存</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ElMessage, ElMessageBox, FormInstance, FormRules, TabsPaneContext } from 'element-plus'
|
||
import {
|
||
createDepartment,
|
||
createIdentityProvider,
|
||
createSAMLProvider,
|
||
createIdentity,
|
||
createSocialProvider,
|
||
deleteSocialProvider,
|
||
DepartmentInput,
|
||
DepartmentRecord,
|
||
fetchDepartments,
|
||
fetchIdentityProviders,
|
||
fetchSAMLProviders,
|
||
fetchIdentities,
|
||
fetchSocialProviders,
|
||
IdentityInput,
|
||
IdentityKind,
|
||
IdentityRecord,
|
||
IdentityProviderInput,
|
||
IdentityProviderRecord,
|
||
SAMLProviderInput,
|
||
SAMLProviderRecord,
|
||
SocialProviderInput,
|
||
SocialProviderRecord,
|
||
updateDepartment,
|
||
updateIdentityProvider,
|
||
updateSAMLProvider,
|
||
updateIdentity,
|
||
updateSocialProvider
|
||
} from '@/api/identities'
|
||
|
||
defineOptions({ name: 'User' })
|
||
|
||
const permissionOptions = [
|
||
'identity:manage',
|
||
'provider:read',
|
||
'provider:manage',
|
||
'api_key:read',
|
||
'api_key:manage'
|
||
]
|
||
type ManagementTab = IdentityKind | 'department' | 'oidc' | 'saml' | 'social'
|
||
const activeTab = ref<ManagementTab>('admin')
|
||
const records = ref<IdentityRecord[]>([])
|
||
const loading = ref(false)
|
||
const saving = ref(false)
|
||
const dialogVisible = ref(false)
|
||
const editingId = ref('')
|
||
const departments = ref<DepartmentRecord[]>([])
|
||
const identityProviders = ref<IdentityProviderRecord[]>([])
|
||
const samlProviders = ref<SAMLProviderRecord[]>([])
|
||
const socialProviders = ref<SocialProviderRecord[]>([])
|
||
const departmentDialogVisible = ref(false)
|
||
const departmentEditingId = ref('')
|
||
const idpDialogVisible = ref(false)
|
||
const idpEditingId = ref('')
|
||
const samlDialogVisible = ref(false)
|
||
const samlEditingId = ref('')
|
||
const socialDialogVisible = ref(false)
|
||
const socialEditingKind = ref('')
|
||
const formRef = ref<FormInstance>()
|
||
const departmentFormRef = ref<FormInstance>()
|
||
const form = reactive<IdentityInput>({
|
||
login: '',
|
||
display_name: '',
|
||
role: 'operator',
|
||
password: '',
|
||
permissions: [],
|
||
active: true,
|
||
department_id: undefined
|
||
})
|
||
const departmentForm = reactive<DepartmentInput>({
|
||
code: '', name: '', description: '', parent_id: undefined, active: true, max_api_keys: 0, max_monthly_tokens: 0
|
||
})
|
||
const idpForm = reactive<IdentityProviderInput>({
|
||
code: '', display_name: '', issuer_url: '', client_id: '', client_secret: '',
|
||
redirect_uri: '', portal_return_url: '', scopes: ['openid', 'profile', 'email'],
|
||
auto_provision: false, default_department_id: undefined, enabled: false
|
||
})
|
||
const samlForm = reactive<SAMLProviderInput>({
|
||
code: '', display_name: '', metadata_url: '', sp_entity_id: '', acs_url: '',
|
||
portal_return_url: '', email_attribute: 'mail', name_attribute: 'cn',
|
||
auto_provision: false, default_department_id: undefined, enabled: false
|
||
})
|
||
const socialForm = reactive<SocialProviderInput & { kind: string }>({
|
||
kind: 'wecom', code: '', display_name: '', client_id: '', agent_id: '',
|
||
secret: '', redirect_uri: '', portal_return_url: '',
|
||
auto_provision: false, default_department_id: undefined, enabled: false
|
||
})
|
||
const activeDepartments = computed(() => departments.value.filter((item) => item.active))
|
||
const availableParents = computed(() =>
|
||
activeDepartments.value.filter((item) => item.id !== departmentEditingId.value)
|
||
)
|
||
const roleOptions = computed(() =>
|
||
activeTab.value === 'admin'
|
||
? [
|
||
{ label: '超级管理员', value: 'superadmin' },
|
||
{ label: '运维管理员', value: 'operator' },
|
||
{ label: '审计员', value: 'auditor' }
|
||
]
|
||
: [{ label: '普通成员', value: 'member' }]
|
||
)
|
||
const rules: FormRules = {
|
||
login: [
|
||
{ required: true, message: '请输入账号', trigger: 'blur' },
|
||
{ min: 2, max: 128, message: '长度必须为 2 至 128 个字符', trigger: 'blur' }
|
||
],
|
||
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
|
||
}
|
||
|
||
async function load(_pane?: TabsPaneContext | string | number) {
|
||
loading.value = true
|
||
try {
|
||
departments.value = await fetchDepartments()
|
||
if (activeTab.value === 'admin' || activeTab.value === 'portal') records.value = await fetchIdentities(activeTab.value)
|
||
if (activeTab.value === 'oidc') identityProviders.value = await fetchIdentityProviders()
|
||
if (activeTab.value === 'saml') samlProviders.value = await fetchSAMLProviders()
|
||
if (activeTab.value === 'social') socialProviders.value = await fetchSocialProviders()
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function resetForm() {
|
||
Object.assign(form, {
|
||
login: '',
|
||
display_name: '',
|
||
role: activeTab.value === 'admin' ? 'operator' : 'member',
|
||
password: '',
|
||
permissions: [],
|
||
active: true,
|
||
department_id: undefined
|
||
})
|
||
}
|
||
|
||
function openCreate() {
|
||
if (activeTab.value === 'department') {
|
||
departmentEditingId.value = ''
|
||
Object.assign(departmentForm, { code: '', name: '', description: '', parent_id: undefined, active: true, max_api_keys: 0, max_monthly_tokens: 0 })
|
||
departmentDialogVisible.value = true
|
||
return
|
||
}
|
||
if (activeTab.value === 'oidc') {
|
||
idpEditingId.value = ''
|
||
Object.assign(idpForm, { code: '', display_name: '', issuer_url: '', client_id: '', client_secret: '', redirect_uri: '', portal_return_url: '', scopes: ['openid', 'profile', 'email'], auto_provision: false, default_department_id: undefined, enabled: false })
|
||
idpDialogVisible.value = true
|
||
return
|
||
}
|
||
if (activeTab.value === 'saml') {
|
||
samlEditingId.value = ''
|
||
Object.assign(samlForm, { code: '', display_name: '', metadata_url: '', sp_entity_id: '', acs_url: '', portal_return_url: '', email_attribute: 'mail', name_attribute: 'cn', auto_provision: false, default_department_id: undefined, enabled: false })
|
||
samlDialogVisible.value = true
|
||
return
|
||
}
|
||
if (activeTab.value === 'social') {
|
||
socialEditingKind.value = ''
|
||
Object.assign(socialForm, { kind: 'wecom', code: '', display_name: '', client_id: '', agent_id: '', secret: '', redirect_uri: '', portal_return_url: '', auto_provision: false, default_department_id: undefined, enabled: false })
|
||
socialDialogVisible.value = true
|
||
return
|
||
}
|
||
editingId.value = ''
|
||
resetForm()
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
function openEdit(record: IdentityRecord) {
|
||
editingId.value = record.id
|
||
Object.assign(form, {
|
||
login: record.login,
|
||
display_name: record.display_name,
|
||
role: record.role,
|
||
password: '',
|
||
permissions: [...record.permissions],
|
||
active: record.active,
|
||
department_id: record.department_id
|
||
})
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
function openDepartmentEdit(record: DepartmentRecord) {
|
||
departmentEditingId.value = record.id
|
||
Object.assign(departmentForm, {
|
||
code: record.code, name: record.name, description: record.description,
|
||
parent_id: record.parent_id, active: record.active, max_api_keys: record.max_api_keys || 0, max_monthly_tokens: record.max_monthly_tokens || 0
|
||
})
|
||
departmentDialogVisible.value = true
|
||
}
|
||
|
||
async function submitDepartment() {
|
||
if (!departmentForm.code || !departmentForm.name) {
|
||
ElMessage.warning('部门代码和名称不能为空')
|
||
return
|
||
}
|
||
saving.value = true
|
||
try {
|
||
const payload: DepartmentInput = { ...departmentForm }
|
||
if (!payload.parent_id) delete payload.parent_id
|
||
if (departmentEditingId.value) await updateDepartment(departmentEditingId.value, payload)
|
||
else await createDepartment(payload)
|
||
ElMessage.success('部门保存成功')
|
||
departmentDialogVisible.value = false
|
||
await load()
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
function openIdentityProviderEdit(record: IdentityProviderRecord) {
|
||
idpEditingId.value = record.id
|
||
Object.assign(idpForm, { code: record.code, display_name: record.display_name, issuer_url: record.issuer_url, client_id: record.client_id, client_secret: '', redirect_uri: record.redirect_uri, portal_return_url: record.portal_return_url, scopes: [...record.scopes], auto_provision: record.auto_provision, default_department_id: record.default_department_id, enabled: record.enabled })
|
||
idpDialogVisible.value = true
|
||
}
|
||
|
||
async function submitIdentityProvider() {
|
||
if (!idpForm.code || !idpForm.display_name || !idpForm.issuer_url || !idpForm.client_id || !idpForm.redirect_uri || !idpForm.portal_return_url || (!idpEditingId.value && !idpForm.client_secret)) {
|
||
ElMessage.warning('请填写所有必填 OIDC 配置')
|
||
return
|
||
}
|
||
saving.value = true
|
||
try {
|
||
const payload: IdentityProviderInput = { ...idpForm, scopes: [...idpForm.scopes] }
|
||
if (!payload.client_secret) delete payload.client_secret
|
||
if (!payload.default_department_id) delete payload.default_department_id
|
||
if (idpEditingId.value) await updateIdentityProvider(idpEditingId.value, payload)
|
||
else await createIdentityProvider(payload)
|
||
ElMessage.success('OIDC 身份源保存成功')
|
||
idpDialogVisible.value = false
|
||
await load()
|
||
} finally { saving.value = false }
|
||
}
|
||
|
||
function openSAMLProviderEdit(record: SAMLProviderRecord) {
|
||
samlEditingId.value = record.id
|
||
Object.assign(samlForm, {
|
||
code: record.code, display_name: record.display_name, metadata_url: record.metadata_url,
|
||
sp_entity_id: record.sp_entity_id, acs_url: record.acs_url,
|
||
portal_return_url: record.portal_return_url, email_attribute: record.email_attribute,
|
||
name_attribute: record.name_attribute, auto_provision: record.auto_provision,
|
||
default_department_id: record.default_department_id, enabled: record.enabled
|
||
})
|
||
samlDialogVisible.value = true
|
||
}
|
||
|
||
async function submitSAMLProvider() {
|
||
if (!samlForm.code || !samlForm.display_name || !samlForm.metadata_url || !samlForm.sp_entity_id || !samlForm.acs_url || !samlForm.portal_return_url) {
|
||
ElMessage.warning('请填写所有必填 SAML 配置')
|
||
return
|
||
}
|
||
saving.value = true
|
||
try {
|
||
const payload: SAMLProviderInput = { ...samlForm }
|
||
if (!payload.default_department_id) delete payload.default_department_id
|
||
if (samlEditingId.value) await updateSAMLProvider(samlEditingId.value, payload)
|
||
else await createSAMLProvider(payload)
|
||
ElMessage.success('SAML 身份源保存成功')
|
||
samlDialogVisible.value = false
|
||
await load()
|
||
} finally { saving.value = false }
|
||
}
|
||
|
||
function socialKindName(kind: string) {
|
||
return ({ wecom: '企业微信', dingtalk: '钉钉', feishu: '飞书' } as Record<string, string>)[kind] || kind
|
||
}
|
||
|
||
function openSocialProviderEdit(record: SocialProviderRecord) {
|
||
socialEditingKind.value = record.kind
|
||
Object.assign(socialForm, {
|
||
kind: record.kind, code: record.code, display_name: record.display_name,
|
||
client_id: record.client_id, agent_id: record.agent_id || '',
|
||
secret: '', redirect_uri: record.redirect_uri, portal_return_url: record.portal_return_url,
|
||
auto_provision: record.auto_provision, default_department_id: record.default_department_id, enabled: record.enabled
|
||
})
|
||
socialDialogVisible.value = true
|
||
}
|
||
|
||
async function submitSocialProvider() {
|
||
const form = socialForm
|
||
if (!form.code || !form.display_name || !form.client_id || !form.redirect_uri || !form.portal_return_url || (form.kind === 'wecom' && !form.agent_id)) {
|
||
ElMessage.warning('请填写所有必填扫码登录配置')
|
||
return
|
||
}
|
||
saving.value = true
|
||
try {
|
||
const payload: SocialProviderInput = {
|
||
code: form.code, display_name: form.display_name, client_id: form.client_id,
|
||
agent_id: form.agent_id, secret: form.secret || undefined,
|
||
redirect_uri: form.redirect_uri, portal_return_url: form.portal_return_url,
|
||
auto_provision: form.auto_provision, default_department_id: form.default_department_id, enabled: form.enabled
|
||
}
|
||
if (!payload.default_department_id) delete payload.default_department_id
|
||
if (socialEditingKind.value) {
|
||
if (!payload.secret) delete payload.secret
|
||
await updateSocialProvider(socialEditingKind.value, payload)
|
||
} else {
|
||
await createSocialProvider(form.kind, payload)
|
||
}
|
||
ElMessage.success('扫码登录身份源保存成功')
|
||
socialDialogVisible.value = false
|
||
await load()
|
||
} finally { saving.value = false }
|
||
}
|
||
|
||
async function removeSocialProvider(record: SocialProviderRecord) {
|
||
await ElMessageBox.confirm(`删除后该平台的所有扫码绑定将失效,确定删除 ${socialKindName(record.kind)} 身份源?`, '删除身份源', { type: 'warning' })
|
||
await deleteSocialProvider(record.kind)
|
||
socialProviders.value = socialProviders.value.filter((item) => item.kind !== record.kind)
|
||
ElMessage.success('已删除')
|
||
}
|
||
|
||
async function submit() {
|
||
if (!(await formRef.value?.validate())) return
|
||
if (!editingId.value && (!form.password || form.password.length < 12)) {
|
||
ElMessage.warning('初始口令至少需要 12 个字符')
|
||
return
|
||
}
|
||
if (form.password && form.password.length < 12) {
|
||
ElMessage.warning('新口令至少需要 12 个字符')
|
||
return
|
||
}
|
||
saving.value = true
|
||
try {
|
||
const payload: IdentityInput = { ...form, permissions: [...form.permissions] }
|
||
if (editingId.value && !payload.password) delete payload.password
|
||
if (!payload.department_id) delete payload.department_id
|
||
const kind = activeTab.value as IdentityKind
|
||
if (editingId.value) await updateIdentity(kind, editingId.value, payload)
|
||
else await createIdentity(kind, payload)
|
||
ElMessage.success('保存成功,权限状态已即时生效')
|
||
dialogVisible.value = false
|
||
await load()
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(load)
|
||
</script>
|