批量更新
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
// 统一认证中心 - 接入应用(OIDC Client)管理
|
||||
|
||||
export function getAuthClientList(params) {
|
||||
return request({
|
||||
url: "/platform/authClient/list",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function getAuthClientDetail(id) {
|
||||
return request({
|
||||
url: `/platform/authClient/detail/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
export function createAuthClient(data) {
|
||||
return request({
|
||||
url: "/platform/authClient/create",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function editAuthClient(id, data) {
|
||||
return request({
|
||||
url: `/platform/authClient/edit/${id}`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 重置密钥:返回的新密钥仅此一次显示 */
|
||||
export function resetAuthClientSecret(id) {
|
||||
return request({
|
||||
url: `/platform/authClient/resetSecret/${id}`,
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
/** 停用应用(逻辑删除) */
|
||||
export function deleteAuthClient(id) {
|
||||
return request({
|
||||
url: `/platform/authClient/delete/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
// 统一认证中心 - 租户登录策略配置
|
||||
|
||||
export function getAuthConfig(tid) {
|
||||
return request({
|
||||
url: "/platform/authConfig/detail",
|
||||
method: "get",
|
||||
params: { tid },
|
||||
});
|
||||
}
|
||||
|
||||
export function saveAuthConfig(data) {
|
||||
return request({
|
||||
url: "/platform/authConfig/save",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 恢复默认值(删除该租户的自定义配置) */
|
||||
export function resetAuthConfig(tid) {
|
||||
return request({
|
||||
url: `/platform/authConfig/reset/${tid}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
// 统一认证中心 - 第三方登录配置(tid=0 全局 / tid>0 租户自带)
|
||||
|
||||
export function getAuthIdpList(tid) {
|
||||
return request({
|
||||
url: "/platform/authIdp/list",
|
||||
method: "get",
|
||||
params: { tid },
|
||||
});
|
||||
}
|
||||
|
||||
export function createAuthIdp(data) {
|
||||
return request({
|
||||
url: "/platform/authIdp/create",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function editAuthIdp(id, data) {
|
||||
return request({
|
||||
url: `/platform/authIdp/edit/${id}`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteAuthIdp(id) {
|
||||
return request({
|
||||
url: `/platform/authIdp/delete/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取某平台需要填写的回调地址 */
|
||||
export function getAuthIdpCallbackHint(provider) {
|
||||
return request({
|
||||
url: "/platform/authIdp/callbackHint",
|
||||
method: "get",
|
||||
params: { provider },
|
||||
});
|
||||
}
|
||||
@@ -21,6 +21,27 @@ const staticMainChildren = [
|
||||
component: () => import("@/views/system/email/index.vue"),
|
||||
meta: { requiresAuth: true, title: "邮箱管理" }
|
||||
},
|
||||
// 统一认证中心:接入应用(OIDC Client)注册管理
|
||||
{
|
||||
path: "/system/authClient",
|
||||
name: "AuthClientManage",
|
||||
component: () => import("@/views/system/authClient/index.vue"),
|
||||
meta: { requiresAuth: true, title: "应用注册" }
|
||||
},
|
||||
// 统一认证中心:第三方登录配置(全局 / 租户自带)
|
||||
{
|
||||
path: "/system/authIdp",
|
||||
name: "AuthIdpManage",
|
||||
component: () => import("@/views/system/authIdp/index.vue"),
|
||||
meta: { requiresAuth: true, title: "第三方登录" }
|
||||
},
|
||||
// 统一认证中心:租户登录策略(验证方式/会话时长/1号1机)
|
||||
{
|
||||
path: "/system/authConfig",
|
||||
name: "AuthConfigManage",
|
||||
component: () => import("@/views/system/authConfig/index.vue"),
|
||||
meta: { requiresAuth: true, title: "登录策略" }
|
||||
},
|
||||
{
|
||||
path: "/tools/passwordStore",
|
||||
name: "PlatformPasswordStore",
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<div class="auth-client-page">
|
||||
<div class="page-header">
|
||||
<div class="title">接入应用(统一认证)</div>
|
||||
<el-button type="primary" @click="openCreate">新增应用</el-button>
|
||||
</div>
|
||||
|
||||
<div class="tips">
|
||||
以后每开发一个新软件,只需在这里注册一条即可接入统一认证:填写 Client ID、应用编码和回跳地址白名单。
|
||||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="loading" border size="default">
|
||||
<el-table-column prop="client_id" label="Client ID" min-width="150" />
|
||||
<el-table-column prop="app_code" label="应用编码" width="110" />
|
||||
<el-table-column prop="name" label="应用名称" min-width="130" />
|
||||
<el-table-column label="类型" width="100">
|
||||
<template #default="{ row }">{{ appTypeText(row.app_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="realm" label="域" width="90" />
|
||||
<el-table-column label="回跳地址白名单" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.redirect_uris || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="令牌有效期" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ row.access_ttl }}s / {{ Math.floor(row.refresh_ttl / 86400) }}天
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'">
|
||||
{{ row.status === 1 ? "启用" : "停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
:disabled="row.app_type !== 1"
|
||||
@click="handleResetSecret(row)"
|
||||
>重置密钥</el-button
|
||||
>
|
||||
<el-button link type="danger" :disabled="row.status !== 1" @click="handleDelete(row)"
|
||||
>停用</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="editing ? '编辑应用' : '新增应用'"
|
||||
width="640px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form :model="form" label-width="130px">
|
||||
<el-form-item label="Client ID" required>
|
||||
<el-input
|
||||
v-model="form.client_id"
|
||||
placeholder="如 yz-crm"
|
||||
:disabled="editing"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="应用编码" required>
|
||||
<el-input v-model="form.app_code" placeholder="如 crm" :disabled="editing" />
|
||||
</el-form-item>
|
||||
<el-form-item label="应用名称" required>
|
||||
<el-input v-model="form.name" placeholder="如 客户关系管理" />
|
||||
</el-form-item>
|
||||
<el-form-item label="应用类型">
|
||||
<el-select v-model="form.app_type">
|
||||
<el-option :value="1" label="Web 后端(持有密钥)" />
|
||||
<el-option :value="2" label="单页应用 SPA(PKCE)" />
|
||||
<el-option :value="3" label="原生 APP(PKCE)" />
|
||||
<el-option :value="4" label="小程序(PKCE)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="域">
|
||||
<el-select v-model="form.realm">
|
||||
<el-option value="tenant" label="租户域" />
|
||||
<el-option value="platform" label="平台域" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="回跳地址白名单">
|
||||
<el-input
|
||||
v-model="form.redirect_uris"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder='JSON 数组,如 ["https://back.yunzer.cn/#/auth/callback"]'
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="登出回跳地址">
|
||||
<el-input
|
||||
v-model="form.post_logout_uris"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder='JSON 数组,如 ["https://back.yunzer.cn/#/login"]'
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="后端登出通知">
|
||||
<el-input
|
||||
v-model="form.backchannel_logout_uri"
|
||||
placeholder="单点登出回调地址(服务端通知,可留空)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="访问令牌有效期">
|
||||
<el-input-number v-model="form.access_ttl" :min="300" :step="300" /> 秒
|
||||
</el-form-item>
|
||||
<el-form-item label="刷新令牌有效期">
|
||||
<el-input-number v-model="form.refresh_ttl" :min="3600" :step="86400" /> 秒
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" v-if="editing">
|
||||
<el-switch v-model="form.statusOn" active-text="启用" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
getAuthClientList,
|
||||
createAuthClient,
|
||||
editAuthClient,
|
||||
resetAuthClientSecret,
|
||||
deleteAuthClient,
|
||||
} from "@/api/authClient";
|
||||
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const editing = ref(false);
|
||||
const submitting = ref(false);
|
||||
const currentId = ref(0);
|
||||
|
||||
const emptyForm = () => ({
|
||||
client_id: "",
|
||||
app_code: "",
|
||||
name: "",
|
||||
app_type: 2,
|
||||
realm: "tenant",
|
||||
redirect_uris: "",
|
||||
post_logout_uris: "",
|
||||
backchannel_logout_uri: "",
|
||||
access_ttl: 1800,
|
||||
refresh_ttl: 2592000,
|
||||
statusOn: true,
|
||||
});
|
||||
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
function appTypeText(t) {
|
||||
return { 1: "Web 后端", 2: "SPA", 3: "原生 APP", 4: "小程序" }[t] || "-";
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getAuthClientList();
|
||||
list.value = res?.data?.list || [];
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = false;
|
||||
Object.assign(form, emptyForm());
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editing.value = true;
|
||||
currentId.value = row.id;
|
||||
Object.assign(form, {
|
||||
client_id: row.client_id || "",
|
||||
app_code: row.app_code || "",
|
||||
name: row.name || "",
|
||||
app_type: row.app_type || 2,
|
||||
realm: row.realm || "tenant",
|
||||
redirect_uris: row.redirect_uris || "",
|
||||
post_logout_uris: row.post_logout_uris || "",
|
||||
backchannel_logout_uri: row.backchannel_logout_uri || "",
|
||||
access_ttl: row.access_ttl || 1800,
|
||||
refresh_ttl: row.refresh_ttl || 2592000,
|
||||
statusOn: row.status === 1,
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!form.client_id.trim() || !form.app_code.trim() || !form.name.trim()) {
|
||||
ElMessage.warning("Client ID、应用编码、名称不能为空");
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
name: form.name.trim(),
|
||||
app_type: form.app_type,
|
||||
realm: form.realm,
|
||||
redirect_uris: form.redirect_uris.trim(),
|
||||
post_logout_uris: form.post_logout_uris.trim(),
|
||||
backchannel_logout_uri: form.backchannel_logout_uri.trim(),
|
||||
access_ttl: form.access_ttl,
|
||||
refresh_ttl: form.refresh_ttl,
|
||||
};
|
||||
if (editing.value) {
|
||||
payload.status = form.statusOn ? 1 : 0;
|
||||
await editAuthClient(currentId.value, payload);
|
||||
ElMessage.success("保存成功");
|
||||
} else {
|
||||
payload.client_id = form.client_id.trim();
|
||||
payload.app_code = form.app_code.trim();
|
||||
const res = await createAuthClient(payload);
|
||||
ElMessage.success("创建成功");
|
||||
const secret = res?.data?.client_secret;
|
||||
if (secret) {
|
||||
await ElMessageBox.alert(
|
||||
`Client Secret:${secret}`,
|
||||
"请立即保存密钥(仅此一次显示)",
|
||||
{ confirmButtonText: "我已保存" }
|
||||
);
|
||||
}
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResetSecret(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`重置后旧密钥立即失效,使用旧密钥的应用需要更新配置。确定重置「${row.name}」的密钥吗?`,
|
||||
"重置密钥",
|
||||
{ type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await resetAuthClientSecret(row.id);
|
||||
await ElMessageBox.alert(
|
||||
`新的 Client Secret:${res?.data?.client_secret}`,
|
||||
"请立即保存密钥(仅此一次显示)",
|
||||
{ confirmButtonText: "我已保存" }
|
||||
);
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "重置失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`停用后该应用将无法登录,确定停用「${row.name}」吗?`,
|
||||
"停用应用",
|
||||
{ type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteAuthClient(row.id);
|
||||
ElMessage.success("已停用");
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "操作失败");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadList);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.auth-client-page {
|
||||
padding: 16px;
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tips {
|
||||
background: #f4f6ff;
|
||||
border: 1px solid #d9e0ff;
|
||||
color: #5a6072;
|
||||
font-size: 13px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<div class="auth-config-page">
|
||||
<div class="page-header">
|
||||
<div class="title">租户登录策略</div>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||
</div>
|
||||
|
||||
<div class="scope-bar">
|
||||
<span class="label">选择租户</span>
|
||||
<el-select
|
||||
v-model="tid"
|
||||
filterable
|
||||
style="width: 280px"
|
||||
placeholder="请选择租户"
|
||||
@change="loadConfig"
|
||||
>
|
||||
<el-option
|
||||
v-for="t in tenants"
|
||||
:key="t.id"
|
||||
:value="t.id"
|
||||
:label="t.tenant_name"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button link type="danger" :disabled="!tid || !configured" @click="handleReset">
|
||||
恢复默认
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="!tid" class="empty">请先选择一个租户</div>
|
||||
|
||||
<el-form v-else v-loading="loading" :model="form" label-width="160px" class="form-body">
|
||||
<el-divider content-position="left">登录验证</el-divider>
|
||||
|
||||
<el-form-item label="开启登录验证">
|
||||
<el-switch v-model="form.openVerifyOn" active-text="开启" inactive-text="关闭" />
|
||||
</el-form-item>
|
||||
<el-form-item label="验证方式">
|
||||
<el-select v-model="form.verify_type" style="width: 220px">
|
||||
<el-option value="captcha" label="图形验证码" />
|
||||
<el-option value="sms" label="短信验证码" />
|
||||
<el-option value="email" label="邮箱验证码" />
|
||||
<el-option value="geetest" label="极验人机验证" />
|
||||
<el-option value="none" label="不开启" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="开启二次验证(MFA)">
|
||||
<el-switch v-model="form.mfaOn" active-text="必须" inactive-text="不要求" />
|
||||
<span class="hint-inline">MFA 功能尚未上线,此配置暂不生效</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">密码策略</el-divider>
|
||||
|
||||
<el-form-item label="密码最小长度">
|
||||
<el-input-number v-model="form.pwd_min_len" :min="6" :max="32" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码复杂度">
|
||||
<el-select v-model="form.pwd_complexity" style="width: 220px">
|
||||
<el-option :value="0" label="不限制" />
|
||||
<el-option :value="1" label="字母 + 数字" />
|
||||
<el-option :value="2" label="大小写 + 数字 + 符号" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">会话与设备</el-divider>
|
||||
|
||||
<el-form-item label="会话有效期">
|
||||
<el-input-number v-model="form.session_ttl" :min="600" :step="600" /> 秒
|
||||
<span class="hint-inline">约 {{ Math.floor(form.session_ttl / 3600) }} 小时</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大同时在线">
|
||||
<el-input-number v-model="form.max_session" :min="1" :max="10" />
|
||||
<span class="hint-inline">1 = 一号一机(新登录踢掉旧设备)</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="超出限制时">
|
||||
<el-radio-group v-model="form.kick_strategy">
|
||||
<el-radio :value="1">踢掉最早的会话</el-radio>
|
||||
<el-radio :value="2">拒绝新登录</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">访问控制</el-divider>
|
||||
|
||||
<el-form-item label="IP 白名单">
|
||||
<el-input
|
||||
v-model="form.ip_whitelist"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder='JSON 数组,如 ["1.2.3.4","8.8.8.8"],留空表示不限制'
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="允许的第三方登录">
|
||||
<el-input
|
||||
v-model="form.allow_third"
|
||||
placeholder="逗号分隔,如 wechat,dingtalk;留空表示按全局配置"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { getAuthConfig, saveAuthConfig, resetAuthConfig } from "@/api/authConfig";
|
||||
import { getTenantList } from "@/api/tenant";
|
||||
|
||||
const tenants = ref([]);
|
||||
const tid = ref(0);
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const configured = ref(false);
|
||||
|
||||
const emptyForm = () => ({
|
||||
verify_type: "captcha",
|
||||
openVerifyOn: true,
|
||||
mfaOn: false,
|
||||
pwd_min_len: 8,
|
||||
pwd_complexity: 0,
|
||||
session_ttl: 7200,
|
||||
max_session: 1,
|
||||
kick_strategy: 1,
|
||||
ip_whitelist: "",
|
||||
allow_third: "",
|
||||
});
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
async function loadTenants() {
|
||||
try {
|
||||
const res = await getTenantList({ page: 1, pageSize: 200 });
|
||||
const data = res?.data || res || {};
|
||||
tenants.value = data.list || data.rows || [];
|
||||
} catch (e) {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
if (!tid.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getAuthConfig(tid.value);
|
||||
const d = res?.data || {};
|
||||
configured.value = !!d.configured;
|
||||
Object.assign(form, {
|
||||
verify_type: d.verify_type || "captcha",
|
||||
openVerifyOn: d.open_verify !== 0,
|
||||
mfaOn: d.mfa_required === 1,
|
||||
pwd_min_len: d.pwd_min_len || 8,
|
||||
pwd_complexity: d.pwd_complexity || 0,
|
||||
session_ttl: d.session_ttl || 7200,
|
||||
max_session: d.max_session || 1,
|
||||
kick_strategy: d.kick_strategy || 1,
|
||||
ip_whitelist: d.ip_whitelist || "",
|
||||
allow_third: d.allow_third || "",
|
||||
});
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!tid.value) {
|
||||
ElMessage.warning("请先选择租户");
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await saveAuthConfig({
|
||||
tid: tid.value,
|
||||
verify_type: form.verify_type,
|
||||
open_verify: form.openVerifyOn ? 1 : 0,
|
||||
mfa_required: form.mfaOn ? 1 : 0,
|
||||
pwd_min_len: form.pwd_min_len,
|
||||
pwd_complexity: form.pwd_complexity,
|
||||
session_ttl: form.session_ttl,
|
||||
max_session: form.max_session,
|
||||
kick_strategy: form.kick_strategy,
|
||||
ip_whitelist: form.ip_whitelist.trim(),
|
||||
allow_third: form.allow_third.trim(),
|
||||
});
|
||||
ElMessage.success("保存成功");
|
||||
configured.value = true;
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReset() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
"恢复默认后将删除该租户的自定义策略,使用系统默认值。确定吗?",
|
||||
"恢复默认",
|
||||
{ type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await resetAuthConfig(tid.value);
|
||||
ElMessage.success("已恢复默认");
|
||||
loadConfig();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "操作失败");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadTenants);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.auth-config-page {
|
||||
padding: 16px;
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.scope-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #f4f6ff;
|
||||
border: 1px solid #d9e0ff;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.label {
|
||||
font-size: 13px;
|
||||
color: #5a6072;
|
||||
}
|
||||
.form-body {
|
||||
max-width: 720px;
|
||||
}
|
||||
.hint-inline {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #8a8fa3;
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #8a8fa3;
|
||||
padding: 40px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,303 @@
|
||||
<template>
|
||||
<div class="auth-idp-page">
|
||||
<div class="page-header">
|
||||
<div class="title">第三方登录配置</div>
|
||||
<el-button type="primary" @click="openCreate">新增配置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="scope-bar">
|
||||
<span class="label">配置范围</span>
|
||||
<el-select v-model="tid" style="width: 260px" @change="loadList">
|
||||
<el-option :value="0" label="平台全局(所有租户共用)" />
|
||||
<el-option
|
||||
v-for="t in tenants"
|
||||
:key="t.id"
|
||||
:value="t.id"
|
||||
:label="`租户:${t.tenant_name}`"
|
||||
/>
|
||||
</el-select>
|
||||
<span class="hint">
|
||||
全局配置对所有租户生效;租户自带配置仅对该租户生效,且优先级更高。
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="loading" border>
|
||||
<el-table-column prop="provider" label="登录方式" width="120">
|
||||
<template #default="{ row }">{{ providerName(row.provider) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="名称" width="130" />
|
||||
<el-table-column prop="app_id" label="AppID" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="密钥" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.has_secret ? 'success' : 'info'">
|
||||
{{ row.has_secret ? "已配置" : "未配置" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="proxy_url" label="代理" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.proxy_url || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'">
|
||||
{{ row.status === 1 ? "启用" : "停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="info" @click="showCallback(row)">回调地址</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="editing ? '编辑配置' : '新增配置'"
|
||||
width="620px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="登录方式" required>
|
||||
<el-select v-model="form.provider" :disabled="editing" style="width: 100%">
|
||||
<el-option v-for="p in providers" :key="p.value" :value="p.value" :label="p.label" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="显示名称">
|
||||
<el-input v-model="form.name" placeholder="如 微信登录" />
|
||||
</el-form-item>
|
||||
<el-form-item label="AppID / ClientID" required>
|
||||
<el-input v-model="form.app_id" placeholder="第三方平台申请的 AppID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="AppSecret">
|
||||
<el-input
|
||||
v-model="form.app_secret"
|
||||
type="password"
|
||||
show-password
|
||||
:placeholder="editing && form.has_secret ? '留空表示不修改' : '第三方平台申请的密钥'"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="代理地址">
|
||||
<el-input
|
||||
v-model="form.proxy_url"
|
||||
placeholder="GitHub/Google 在国内服务器需要,如 http://127.0.0.1:7890"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="授权范围">
|
||||
<el-input v-model="form.scopes" placeholder="留空使用预设值,多个用逗号分隔" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" v-if="editing">
|
||||
<el-switch v-model="form.statusOn" active-text="启用" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
getAuthIdpList,
|
||||
createAuthIdp,
|
||||
editAuthIdp,
|
||||
deleteAuthIdp,
|
||||
getAuthIdpCallbackHint,
|
||||
} from "@/api/authIdp";
|
||||
import { getTenantList } from "@/api/tenant";
|
||||
|
||||
const providers = [
|
||||
{ value: "wechat", label: "微信" },
|
||||
{ value: "dingtalk", label: "钉钉" },
|
||||
{ value: "feishu", label: "飞书" },
|
||||
{ value: "qq", label: "QQ" },
|
||||
{ value: "github", label: "GitHub" },
|
||||
{ value: "google", label: "Google" },
|
||||
// 租户自带身份源(企业自有)
|
||||
{ value: "wework", label: "企业微信(租户自带)" },
|
||||
{ value: "oidc", label: "标准 OIDC(租户自带)" },
|
||||
{ value: "saml", label: "SAML(租户自带)" },
|
||||
];
|
||||
|
||||
const list = ref([]);
|
||||
const tenants = ref([]);
|
||||
const loading = ref(false);
|
||||
const tid = ref(0);
|
||||
const dialogVisible = ref(false);
|
||||
const editing = ref(false);
|
||||
const submitting = ref(false);
|
||||
const currentId = ref(0);
|
||||
|
||||
const emptyForm = () => ({
|
||||
provider: "wechat",
|
||||
name: "",
|
||||
app_id: "",
|
||||
app_secret: "",
|
||||
proxy_url: "",
|
||||
scopes: "",
|
||||
statusOn: true,
|
||||
has_secret: false,
|
||||
});
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
function providerName(p) {
|
||||
const hit = providers.find((i) => i.value === p);
|
||||
return hit ? hit.label : p;
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getAuthIdpList(tid.value);
|
||||
list.value = res?.data?.list || [];
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTenants() {
|
||||
try {
|
||||
const res = await getTenantList({ page: 1, pageSize: 200 });
|
||||
const data = res?.data || res || {};
|
||||
tenants.value = data.list || data.rows || [];
|
||||
} catch (e) {
|
||||
// 租户列表加载失败不影响主功能
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = false;
|
||||
Object.assign(form, emptyForm());
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editing.value = true;
|
||||
currentId.value = row.id;
|
||||
Object.assign(form, {
|
||||
provider: row.provider || "wechat",
|
||||
name: row.name || "",
|
||||
app_id: row.app_id || "",
|
||||
app_secret: "",
|
||||
proxy_url: row.proxy_url || "",
|
||||
scopes: row.scopes || "",
|
||||
statusOn: row.status === 1,
|
||||
has_secret: !!row.has_secret,
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!form.provider || !form.app_id.trim()) {
|
||||
ElMessage.warning("登录方式和 AppID 不能为空");
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
provider: form.provider,
|
||||
name: form.name.trim(),
|
||||
app_id: form.app_id.trim(),
|
||||
proxy_url: form.proxy_url.trim(),
|
||||
scopes: form.scopes.trim(),
|
||||
};
|
||||
if (form.app_secret.trim()) payload.app_secret = form.app_secret.trim();
|
||||
|
||||
if (editing.value) {
|
||||
payload.status = form.statusOn ? 1 : 0;
|
||||
await editAuthIdp(currentId.value, payload);
|
||||
} else {
|
||||
payload.tid = tid.value;
|
||||
await createAuthIdp(payload);
|
||||
}
|
||||
ElMessage.success("保存成功");
|
||||
dialogVisible.value = false;
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function showCallback(row) {
|
||||
try {
|
||||
const res = await getAuthIdpCallbackHint(row.provider);
|
||||
await ElMessageBox.alert(
|
||||
res?.data?.callback || "",
|
||||
`${providerName(row.provider)} 回调地址(填到第三方平台)`,
|
||||
{ confirmButtonText: "复制好了" }
|
||||
);
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "获取失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除「${providerName(row.provider)}」的配置吗?删除后该登录方式将不可用。`,
|
||||
"删除配置",
|
||||
{ type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteAuthIdp(row.id);
|
||||
ElMessage.success("已删除");
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTenants();
|
||||
loadList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.auth-idp-page {
|
||||
padding: 16px;
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.scope-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #f4f6ff;
|
||||
border: 1px solid #d9e0ff;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.label {
|
||||
font-size: 13px;
|
||||
color: #5a6072;
|
||||
}
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #8a8fa3;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user