Merge pull request #2492 from DaydreamCoding/feat/dingtalk-login

feat(dingtalk): 钉钉 OAuth 登录接入 + internal_only 用户属性同步
This commit is contained in:
Wesley Liddick
2026-05-19 15:36:13 +08:00
committed by GitHub
54 changed files with 5545 additions and 365 deletions
+350
View File
@@ -2333,6 +2333,294 @@
</div>
</div>
<!-- DingTalk Connect OAuth 登录 -->
<div class="card">
<div
class="border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ t("admin.settings.dingtalk.title") }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ t("admin.settings.dingtalk.description") }}
</p>
</div>
<div class="space-y-5 p-6">
<div class="flex items-center justify-between">
<div>
<label class="font-medium text-gray-900 dark:text-white">{{
t("admin.settings.dingtalk.enable")
}}</label>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t("admin.settings.dingtalk.enableHint") }}
</p>
</div>
<Toggle v-model="form.dingtalk_connect_enabled" />
</div>
<div
v-if="form.dingtalk_connect_enabled"
class="border-t border-gray-100 pt-4 dark:border-dark-700"
>
<div class="grid grid-cols-1 gap-6">
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.settings.dingtalk.clientId") }}
</label>
<input
v-model="form.dingtalk_connect_client_id"
type="text"
class="input font-mono text-sm"
:placeholder="
t('admin.settings.dingtalk.clientIdPlaceholder')
"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.settings.dingtalk.clientIdHint") }}
</p>
</div>
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.settings.dingtalk.clientSecret") }}
</label>
<input
v-model="form.dingtalk_connect_client_secret"
type="password"
class="input font-mono text-sm"
:placeholder="
form.dingtalk_connect_client_secret_configured
? t(
'admin.settings.dingtalk.clientSecretConfiguredPlaceholder',
)
: t('admin.settings.dingtalk.clientSecretPlaceholder')
"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{
form.dingtalk_connect_client_secret_configured
? t(
"admin.settings.dingtalk.clientSecretConfiguredHint",
)
: t("admin.settings.dingtalk.clientSecretHint")
}}
</p>
</div>
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.settings.dingtalk.redirectUrl") }}
</label>
<input
v-model="form.dingtalk_connect_redirect_url"
type="url"
class="input font-mono text-sm"
:placeholder="
t('admin.settings.dingtalk.redirectUrlPlaceholder')
"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.settings.dingtalk.redirectUrlHint") }}
</p>
</div>
<!-- Corp Restriction Policy -->
<div class="border-t border-gray-100 pt-4 dark:border-dark-700">
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t("admin.settings.dingtalk.corpPolicy.label") }}
</label>
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.settings.dingtalk.corpPolicy.hint") }}
</p>
<div class="space-y-2">
<label class="flex cursor-pointer items-center gap-3">
<input
v-model="form.dingtalk_connect_corp_restriction_policy"
type="radio"
value="none"
class="h-4 w-4 text-primary-600"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">
{{ t("admin.settings.dingtalk.corpPolicy.none") }}
</span>
</label>
<label class="flex cursor-pointer items-center gap-3">
<input
v-model="form.dingtalk_connect_corp_restriction_policy"
type="radio"
value="internal_only"
class="h-4 w-4 text-primary-600"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">
{{ t("admin.settings.dingtalk.corpPolicy.internalOnly") }}
</span>
</label>
</div>
</div>
<!-- bypass_registration toggle(仅 internal_only 模式下可见可用) -->
<div
v-if="form.dingtalk_connect_corp_restriction_policy === 'internal_only'"
class="flex items-center justify-between pt-4 border-t border-gray-100 dark:border-dark-700"
>
<div>
<label class="font-medium text-gray-900 dark:text-white">{{
t("admin.settings.dingtalk.bypassRegistration")
}}</label>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t("admin.settings.dingtalk.bypassRegistrationHint") }}
</p>
</div>
<Toggle v-model="form.dingtalk_connect_bypass_registration" />
</div>
<!-- 身份同步开关(仅 internal_only 模式下可见) -->
<div
v-if="form.dingtalk_connect_corp_restriction_policy === 'internal_only'"
class="pt-4 border-t border-gray-100 dark:border-dark-700 space-y-2"
>
<div class="flex items-center justify-between">
<div>
<label class="font-medium text-gray-900 dark:text-white">{{
t("admin.settings.dingtalk.syncDisplayName")
}}</label>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t("admin.settings.dingtalk.syncDisplayNameHint") }}
</p>
</div>
<Toggle v-model="form.dingtalk_connect_sync_display_name" />
</div>
<div v-if="form.dingtalk_connect_sync_display_name" class="space-y-2">
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap min-w-[5rem]">
{{ t("admin.settings.dingtalk.syncDisplayNameTarget") }}
</label>
<input
v-model="form.dingtalk_connect_sync_display_name_attr_key"
type="text"
placeholder="dingtalk_name"
class="input text-sm flex-1 max-w-xs"
/>
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap min-w-[5rem]">
{{ t("admin.settings.dingtalk.syncAttrDisplayName") }}
</label>
<input
v-model="form.dingtalk_connect_sync_display_name_attr_name"
type="text"
placeholder="钉钉姓名"
class="input text-sm flex-1 max-w-xs"
/>
</div>
</div>
<p v-if="form.dingtalk_connect_sync_display_name" class="text-xs text-gray-400 dark:text-gray-500">
{{ t("admin.settings.dingtalk.syncDisplayNameTargetHint") }}
</p>
</div>
<div
v-if="form.dingtalk_connect_corp_restriction_policy === 'internal_only'"
class="pt-4 border-t border-gray-100 dark:border-dark-700 space-y-2"
>
<div class="flex items-center justify-between">
<div>
<label class="font-medium text-gray-900 dark:text-white">{{
t("admin.settings.dingtalk.syncCorpEmail")
}}</label>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t("admin.settings.dingtalk.syncCorpEmailHint") }}
</p>
<p class="text-xs text-amber-600 dark:text-amber-400 mt-1">
{{ t("admin.settings.dingtalk.syncCorpEmailPermissionHint") }}
</p>
</div>
<Toggle v-model="form.dingtalk_connect_sync_corp_email" />
</div>
<div v-if="form.dingtalk_connect_sync_corp_email" class="space-y-2">
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap min-w-[5rem]">
{{ t("admin.settings.dingtalk.syncCorpEmailTarget") }}
</label>
<input
v-model="form.dingtalk_connect_sync_corp_email_attr_key"
type="text"
placeholder="dingtalk_email"
class="input text-sm flex-1 max-w-xs"
/>
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap min-w-[5rem]">
{{ t("admin.settings.dingtalk.syncAttrDisplayName") }}
</label>
<input
v-model="form.dingtalk_connect_sync_corp_email_attr_name"
type="text"
placeholder="钉钉企业邮箱"
class="input text-sm flex-1 max-w-xs"
/>
</div>
</div>
<p v-if="form.dingtalk_connect_sync_corp_email" class="text-xs text-gray-400 dark:text-gray-500">
{{ t("admin.settings.dingtalk.syncCorpEmailTargetHint") }}
</p>
</div>
<div
v-if="form.dingtalk_connect_corp_restriction_policy === 'internal_only'"
class="pt-4 border-t border-gray-100 dark:border-dark-700 space-y-2"
>
<div class="flex items-center justify-between">
<div>
<label class="font-medium text-gray-900 dark:text-white">{{
t("admin.settings.dingtalk.syncDept")
}}</label>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t("admin.settings.dingtalk.syncDeptHint") }}
</p>
<p class="text-xs text-amber-600 dark:text-amber-400 mt-1">
{{ t("admin.settings.dingtalk.syncDeptPermissionHint") }}
</p>
</div>
<Toggle v-model="form.dingtalk_connect_sync_dept" />
</div>
<div v-if="form.dingtalk_connect_sync_dept" class="space-y-2">
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap min-w-[5rem]">
{{ t("admin.settings.dingtalk.syncDeptTarget") }}
</label>
<input
v-model="form.dingtalk_connect_sync_dept_attr_key"
type="text"
placeholder="dingtalk_department"
class="input text-sm flex-1 max-w-xs"
/>
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap min-w-[5rem]">
{{ t("admin.settings.dingtalk.syncAttrDisplayName") }}
</label>
<input
v-model="form.dingtalk_connect_sync_dept_attr_name"
type="text"
placeholder="钉钉部门"
class="input text-sm flex-1 max-w-xs"
/>
</div>
</div>
<p v-if="form.dingtalk_connect_sync_dept" class="text-xs text-gray-400 dark:text-gray-500">
{{ t("admin.settings.dingtalk.syncDeptTargetHint") }}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Generic OIDC OAuth 登录 -->
<div class="card">
<div
@@ -6417,6 +6705,7 @@ type SettingsForm = Omit<
smtp_password: string;
turnstile_secret_key: string;
linuxdo_connect_client_secret: string;
dingtalk_connect_client_secret: string;
wechat_connect_app_secret: string;
wechat_connect_open_app_secret: string;
wechat_connect_mp_app_secret: string;
@@ -6518,6 +6807,24 @@ const form = reactive<SettingsForm>({
linuxdo_connect_client_secret: "",
linuxdo_connect_client_secret_configured: false,
linuxdo_connect_redirect_url: "",
// DingTalk Connect OAuth 登录
dingtalk_connect_enabled: false,
dingtalk_connect_client_id: "",
dingtalk_connect_client_secret: "",
dingtalk_connect_client_secret_configured: false,
dingtalk_connect_redirect_url: "",
dingtalk_connect_corp_restriction_policy: "none",
dingtalk_connect_internal_corp_id: "",
dingtalk_connect_bypass_registration: false,
dingtalk_connect_sync_corp_email: false,
dingtalk_connect_sync_display_name: false,
dingtalk_connect_sync_dept: false,
dingtalk_connect_sync_corp_email_attr_key: "dingtalk_email",
dingtalk_connect_sync_display_name_attr_key: "dingtalk_name",
dingtalk_connect_sync_dept_attr_key: "dingtalk_department",
dingtalk_connect_sync_corp_email_attr_name: "钉钉企业邮箱",
dingtalk_connect_sync_display_name_attr_name: "钉钉姓名",
dingtalk_connect_sync_dept_attr_name: "钉钉部门",
wechat_connect_enabled: false,
wechat_connect_app_id: "",
wechat_connect_app_secret: "",
@@ -6658,6 +6965,14 @@ const authSourceDefaultsMeta = computed(() => [
"Applied on first signup or first bind through a verified Google email.",
),
},
{
source: "dingtalk" as AuthSourceType,
title: "钉钉",
description: localText(
"通过钉钉首次注册或首次绑定时应用。",
"Applied on first signup or first bind through DingTalk.",
),
},
]);
// Proxies for web search emulation ProxySelector
@@ -7241,6 +7556,7 @@ async function loadSettings() {
smtpPasswordManuallyEdited.value = false;
form.turnstile_secret_key = "";
form.linuxdo_connect_client_secret = "";
form.dingtalk_connect_client_secret = "";
form.github_oauth_client_secret = "";
form.google_oauth_client_secret = "";
form.wechat_connect_app_secret = "";
@@ -7593,6 +7909,24 @@ async function saveSettings() {
linuxdo_connect_client_secret:
form.linuxdo_connect_client_secret || undefined,
linuxdo_connect_redirect_url: form.linuxdo_connect_redirect_url,
dingtalk_connect_enabled: form.dingtalk_connect_enabled,
dingtalk_connect_client_id: form.dingtalk_connect_client_id,
dingtalk_connect_client_secret:
form.dingtalk_connect_client_secret || undefined,
dingtalk_connect_redirect_url: form.dingtalk_connect_redirect_url,
dingtalk_connect_corp_restriction_policy:
form.dingtalk_connect_corp_restriction_policy,
dingtalk_connect_internal_corp_id: form.dingtalk_connect_internal_corp_id,
dingtalk_connect_bypass_registration: form.dingtalk_connect_bypass_registration,
dingtalk_connect_sync_corp_email: form.dingtalk_connect_sync_corp_email,
dingtalk_connect_sync_display_name: form.dingtalk_connect_sync_display_name,
dingtalk_connect_sync_dept: form.dingtalk_connect_sync_dept,
dingtalk_connect_sync_corp_email_attr_key: form.dingtalk_connect_sync_corp_email_attr_key,
dingtalk_connect_sync_display_name_attr_key: form.dingtalk_connect_sync_display_name_attr_key,
dingtalk_connect_sync_dept_attr_key: form.dingtalk_connect_sync_dept_attr_key,
dingtalk_connect_sync_corp_email_attr_name: form.dingtalk_connect_sync_corp_email_attr_name,
dingtalk_connect_sync_display_name_attr_name: form.dingtalk_connect_sync_display_name_attr_name,
dingtalk_connect_sync_dept_attr_name: form.dingtalk_connect_sync_dept_attr_name,
wechat_connect_enabled: form.wechat_connect_enabled,
wechat_connect_app_id:
form.wechat_connect_open_app_id ||
@@ -7775,6 +8109,7 @@ async function saveSettings() {
smtpPasswordManuallyEdited.value = false;
form.turnstile_secret_key = "";
form.linuxdo_connect_client_secret = "";
form.dingtalk_connect_client_secret = "";
form.github_oauth_client_secret = "";
form.google_oauth_client_secret = "";
form.wechat_connect_app_secret = "";
@@ -8992,6 +9327,21 @@ watch(
}
},
);
// bypass_registration 与身份同步三开关仅在 internal_only 模式下生效。切换 policy 到其它值时,
// 立即把相关字段重置为 false,避免保存请求里残留旧值。后端 admin handler 与
// 配置加载层都有 coerce 兜底,这里是 UX 层的同步而非安全防线。
watch(
() => form.dingtalk_connect_corp_restriction_policy,
(policy) => {
if (policy !== "internal_only") {
if (form.dingtalk_connect_bypass_registration) form.dingtalk_connect_bypass_registration = false;
if (form.dingtalk_connect_sync_corp_email) form.dingtalk_connect_sync_corp_email = false;
if (form.dingtalk_connect_sync_display_name) form.dingtalk_connect_sync_display_name = false;
if (form.dingtalk_connect_sync_dept) form.dingtalk_connect_sync_dept = false;
}
},
);
</script>
<style scoped>