mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-02 00:30:32 +00:00
feat(dingtalk): 钉钉 OAuth 登录接入与 internal_only 用户属性同步
⚠️ 应用类型约束:当前实现仅支持「钉钉登录-企业内部应用」(DingTalk 开放平台 internal_app 类型)。第三方个人应用、第三方企业应用类型暂不支持——OAuth 流程 相同但 corp 校验、跨企业行为不同。backend 通过 DingTalkAppKind 校验对非 internal_app 类型 fail-closed(硬约束)。 钉钉 OAuth 登录主链 - 4 步 OAuth 链:ExchangeCodeForUserToken / GetUnionIdByUserToken / GetUserIdByUnionId / GetStaffInfoByUserId;app token 缓存 - pending session 机制持久化 OAuth 中间态;cookie-only token 持久化 - 三种分流:bind_login_required / email_completion / choose_account_action - corp_restriction_policy 支持 none + internal_only;stale "whitelist" 在 加载层与写入层均静默 coerce 为 none + slog.Warn - bypass_registration 开关:企业内部模式豁免全局 REGISTRATION_DISABLED - isReservedEmail / signup_source / canUnbindProvider / OAuth pending flow 等横切点支持 dingtalk provider - migration 136:4 表 CHECK 约束加入 'dingtalk' provider 值 internal_only 模式同步企业邮箱/姓名/部门到用户属性 - SyncCorpEmail / SyncDisplayName / SyncDept 三个独立开关 + 对应 SyncXxxAttrKey 目标属性 key(默认 dingtalk_email / dingtalk_name / dingtalk_department);非 internal_only policy 在写入层与加载层均 coerce 为 false,admin handler 与 setting_service 双层兜底 - 同步语义:首次注册写 users.username(昵称优先 → 企业姓名 fallback), 之后每次登录刷新 3 个属性;空值也写入以覆盖旧值 - 邮箱三级 fallback:org_email > email > extension["企业邮箱"] (钉钉自定义字段 JSON) - 部门路径递归向上拼接,跳过 dept_id=1 选首个真实子部门,剥离根组织名 - GetUnionIdByUserToken 同时返回 OIDC /contact/users/me 的 nick 字段; 新增 GetDeptInfo 调用 OAPI /topapi/v2/department/get - AuthHandler 注入 UserAttributeService;OAuth pending flow 在 createPendingOAuthAccount / bindPendingOAuthLogin 分别派发到 AfterRegistration(syncUsername=true)/ AfterLogin - migration 137 seed dingtalk_email/name/department 三个用户属性定义 附带修复(同集成路径暴露的两个 OAuth 注册回归) - LoginOrRegisterOAuthWithTokenPair 新建用户分支用 inferLegacySignupSource 覆写 caller 显式传入的 signupSource,导致 dingtalk/linuxdo/oidc/wechat 渠道授权按 email 渠道读取;改为只在 caller 未显式传入时回退邮箱推断 - mergeProviderDefaultGrantSettings 把 parse fallback 默认值 (Concurrency=5 / Balance=0) 当作"未配置"哨兵,admin 显式设 5 时被误判 退回全局默认(复现:全局默认 1 + 渠道默认并发 5 + grant_on_signup → 新 用户实际 concurrency=1);去掉哨兵,admin 任何 >=0 值都覆盖 globalDefaults 前端 - DingTalk Login / Callback / EmailCompletion / ChoiceAccount / Error 视图;router + auth API client - admin SettingsView:corp policy radio(none / internal_only)+ bypass 注册开关 + i18n;internal_only 下展示三同步开关 + 目标 attr key 下拉 (拉取 user attribute definitions),展示 fieldEmail / qyapi_get_department_list 钉钉权限申请提示 - Profile:S1 主动绑定 / S5 解绑钉钉按钮 + 合成邮箱防自锁 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
committed by
QTom
co-authored by
Claude Opus 4.7
parent
b006e36af9
commit
b19da9c7fe
@@ -461,7 +461,7 @@ export default {
|
||||
invitationCodeInvalid: 'Invalid or used invitation code',
|
||||
invitationCodeValidating: 'Validating invitation code...',
|
||||
invitationCodeInvalidCannotRegister: 'Invalid invitation code. Please check and try again',
|
||||
oauthOrContinue: 'or continue with email',
|
||||
oauthOrContinue: 'or continue with others',
|
||||
linuxdo: {
|
||||
signIn: 'Continue with Linux.do',
|
||||
orContinue: 'or continue with email',
|
||||
@@ -476,6 +476,34 @@ export default {
|
||||
completing: 'Completing registration…',
|
||||
completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.'
|
||||
},
|
||||
dingtalk: {
|
||||
signIn: 'Continue with DingTalk',
|
||||
callbackTitle: 'Signing you in with DingTalk',
|
||||
callbackProcessing: 'Completing DingTalk login, please wait...',
|
||||
callbackHint: 'If you are not redirected automatically, go back to the login page and try again.',
|
||||
callbackMissingToken: 'Missing login token, please try again.',
|
||||
backToLogin: 'Back to Login',
|
||||
invitationRequired: 'This DingTalk account is not yet registered. The site requires an invitation code — please enter one to complete registration.',
|
||||
invalidPendingToken: 'The registration token has expired. Please sign in with DingTalk again.',
|
||||
completeRegistration: 'Complete Registration',
|
||||
completing: 'Completing registration…',
|
||||
completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.',
|
||||
createAccountTitle: 'Create DingTalk Account',
|
||||
registrationDisabledRedirectToBind: 'New account registration is currently disabled. Please bind to your existing account with its email and password.',
|
||||
error: {
|
||||
title: 'DingTalk Sign-in Failed',
|
||||
csrf: 'Login session expired, please scan again',
|
||||
corp_rejected: 'Your DingTalk account is not part of this organization. Please contact administrator',
|
||||
dingtalk_not_enabled: 'DingTalk login is not enabled',
|
||||
upstream_error: 'DingTalk service is temporarily unavailable. Please try again later',
|
||||
missing_browser_session: 'Browser session lost. Please login again',
|
||||
missing_params: 'Request parameters are incomplete',
|
||||
invalid_state: 'Invalid login state',
|
||||
provider_error: 'DingTalk authorization failed',
|
||||
session_error: 'Failed to create session. Please retry',
|
||||
retry: 'Retry Login'
|
||||
}
|
||||
},
|
||||
emailOAuth: {
|
||||
signIn: 'Continue with {providerName}'
|
||||
},
|
||||
@@ -524,6 +552,7 @@ export default {
|
||||
wechatNotConfigured: 'WeChat sign-in is not configured yet.'
|
||||
},
|
||||
linuxdoCallbackPageTitle: 'LinuxDo Sign-In Callback',
|
||||
dingtalkCallbackPageTitle: 'DingTalk Sign-In Callback',
|
||||
oidcCallbackPageTitle: 'OIDC Sign-In Callback',
|
||||
oauthCallbackPageTitle: 'OAuth Callback',
|
||||
wechatProviderName: 'WeChat',
|
||||
@@ -1244,6 +1273,7 @@ export default {
|
||||
providers: {
|
||||
email: 'Email',
|
||||
linuxdo: 'LinuxDo',
|
||||
dingtalk: 'DingTalk',
|
||||
oidc: '{providerName}',
|
||||
wechat: 'WeChat',
|
||||
},
|
||||
@@ -5268,6 +5298,47 @@ export default {
|
||||
quickSetCopy: 'Generate & Copy (current site)',
|
||||
redirectUrlSetAndCopied: 'Redirect URL generated and copied to clipboard'
|
||||
},
|
||||
dingtalk: {
|
||||
title: 'DingTalk Login',
|
||||
description: 'Configure DingTalk OAuth for Sub2API end-user login',
|
||||
enable: 'Enable DingTalk Login (Internal Corporate App)',
|
||||
enableHint: 'Show DingTalk login on the login/register pages',
|
||||
clientId: 'Client ID (AppKey)',
|
||||
clientIdPlaceholder: 'e.g., dingxxxxxxxxxxxxxxxx',
|
||||
clientIdHint: 'Get this from the DingTalk Open Platform app details',
|
||||
clientSecret: 'Client Secret (AppSecret)',
|
||||
clientSecretPlaceholder: '********',
|
||||
clientSecretHint: 'Used by backend to exchange tokens (keep it secret)',
|
||||
clientSecretConfiguredPlaceholder: '********',
|
||||
clientSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.',
|
||||
redirectUrl: 'Redirect URL',
|
||||
redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/dingtalk/callback',
|
||||
redirectUrlHint:
|
||||
'Must match the redirect URL configured in DingTalk Open Platform (must be an absolute http(s) URL)',
|
||||
corpPolicy: {
|
||||
label: 'Corp Restriction Policy',
|
||||
hint: 'Control which DingTalk accounts (orgs) are allowed to sign in',
|
||||
none: 'No restriction (all DingTalk accounts allowed)',
|
||||
internalOnly: 'Internal only (single corp)'
|
||||
},
|
||||
bypassRegistration: 'Enable DingTalk signup',
|
||||
bypassRegistrationHint: 'Allow new users to register via DingTalk even when public registration is disabled.',
|
||||
syncDisplayName: 'Sync DingTalk display name',
|
||||
syncDisplayNameHint: 'Overwrite username with the DingTalk staff name on each login (also stored in the dingtalk_name attribute).',
|
||||
syncCorpEmail: 'Sync corporate email',
|
||||
syncCorpEmailHint: 'Write the DingTalk corporate email to the dingtalk_email attribute on each login (does not change the login email).',
|
||||
syncCorpEmailPermissionHint: 'Requires the OAPI permission "Personal info incl. email (fieldEmail)" to be granted to the app on the DingTalk open platform, otherwise OAPI will not return the email field.',
|
||||
syncDept: 'Sync department',
|
||||
syncDeptHint: 'Write the full DingTalk department path to the dingtalk_department attribute on each login (fetched live each time).',
|
||||
syncDeptPermissionHint: 'Requires the OAPI "Department info read (qyapi_get_department_list)" permission to be granted to the app on the DingTalk open platform, otherwise the department path cannot be resolved.',
|
||||
syncDisplayNameTarget: 'Attribute key',
|
||||
syncDisplayNameTargetHint: 'Defaults to dingtalk_name / DingTalk Name. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).',
|
||||
syncCorpEmailTarget: 'Attribute key',
|
||||
syncCorpEmailTargetHint: 'Defaults to dingtalk_email / DingTalk Corporate Email. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).',
|
||||
syncDeptTarget: 'Attribute key',
|
||||
syncDeptTargetHint: 'Defaults to dingtalk_department / DingTalk Department. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).',
|
||||
syncAttrDisplayName: 'Display name'
|
||||
},
|
||||
oidc: {
|
||||
title: 'OIDC Login',
|
||||
description: 'Configure a standard OIDC provider (for example Keycloak)',
|
||||
|
||||
@@ -460,7 +460,7 @@ export default {
|
||||
invitationCodeInvalid: '邀请码无效或已被使用',
|
||||
invitationCodeValidating: '正在验证邀请码...',
|
||||
invitationCodeInvalidCannotRegister: '邀请码无效,请检查后重试',
|
||||
oauthOrContinue: '或使用邮箱密码继续',
|
||||
oauthOrContinue: '或使用其他继续',
|
||||
linuxdo: {
|
||||
signIn: '使用 Linux.do 登录',
|
||||
orContinue: '或使用邮箱密码继续',
|
||||
@@ -475,6 +475,34 @@ export default {
|
||||
completing: '正在完成注册...',
|
||||
completeRegistrationFailed: '注册失败,请检查邀请码后重试。'
|
||||
},
|
||||
dingtalk: {
|
||||
signIn: '钉钉登录',
|
||||
callbackTitle: '正在完成钉钉登录',
|
||||
callbackProcessing: '正在验证钉钉登录信息,请稍候...',
|
||||
callbackHint: '如果页面未自动跳转,请返回登录页重试。',
|
||||
callbackMissingToken: '登录信息缺失,请返回重试。',
|
||||
backToLogin: '返回登录',
|
||||
invitationRequired: '该钉钉账号尚未注册,站点已开启邀请码注册,请输入邀请码以完成注册。',
|
||||
invalidPendingToken: '注册凭证已失效,请重新使用钉钉登录。',
|
||||
completeRegistration: '完成注册',
|
||||
completing: '正在完成注册...',
|
||||
completeRegistrationFailed: '注册失败,请检查邀请码后重试。',
|
||||
createAccountTitle: '创建钉钉账户',
|
||||
registrationDisabledRedirectToBind: '当前已禁止注册新账户,请使用已有账户邮箱和密码绑定钉钉登录',
|
||||
error: {
|
||||
title: '钉钉登录失败',
|
||||
csrf: '登录会话已过期,请重新扫码登录',
|
||||
corp_rejected: '您的钉钉账号不属于本企业,请联系管理员',
|
||||
dingtalk_not_enabled: '钉钉登录暂未启用',
|
||||
upstream_error: '钉钉服务暂时不可用,请稍后重试',
|
||||
missing_browser_session: '浏览器会话丢失,请重新登录',
|
||||
missing_params: '请求参数不完整',
|
||||
invalid_state: '登录状态异常',
|
||||
provider_error: '钉钉授权失败',
|
||||
session_error: '会话创建失败,请重试',
|
||||
retry: '重新登录'
|
||||
}
|
||||
},
|
||||
emailOAuth: {
|
||||
signIn: '使用 {providerName} 登录'
|
||||
},
|
||||
@@ -522,6 +550,7 @@ export default {
|
||||
wechatNotConfigured: '微信登录尚未配置。'
|
||||
},
|
||||
linuxdoCallbackPageTitle: 'LinuxDo 登录回调',
|
||||
dingtalkCallbackPageTitle: '钉钉登录回调',
|
||||
oidcCallbackPageTitle: 'OIDC 登录回调',
|
||||
oauthCallbackPageTitle: 'OAuth 回调',
|
||||
wechatProviderName: '微信',
|
||||
@@ -1248,6 +1277,7 @@ export default {
|
||||
providers: {
|
||||
email: '邮箱',
|
||||
linuxdo: 'LinuxDo',
|
||||
dingtalk: '钉钉',
|
||||
oidc: '{providerName}',
|
||||
wechat: '微信',
|
||||
},
|
||||
@@ -5431,6 +5461,46 @@ export default {
|
||||
quickSetCopy: '使用当前站点生成并复制',
|
||||
redirectUrlSetAndCopied: '已使用当前站点生成回调地址并复制到剪贴板'
|
||||
},
|
||||
dingtalk: {
|
||||
title: '钉钉登录',
|
||||
description: '配置钉钉 OAuth,用于 Sub2API 用户登录',
|
||||
enable: '启用钉钉登录-企业内部应用',
|
||||
enableHint: '在登录/注册页面显示钉钉登录入口',
|
||||
clientId: 'Client ID(AppKey)',
|
||||
clientIdPlaceholder: '例如:dingxxxxxxxxxxxxxxxx',
|
||||
clientIdHint: '从钉钉开放平台应用详情页获取',
|
||||
clientSecret: 'Client Secret(AppSecret)',
|
||||
clientSecretPlaceholder: '********',
|
||||
clientSecretHint: '用于后端交换 token(请保密)',
|
||||
clientSecretConfiguredPlaceholder: '********',
|
||||
clientSecretConfiguredHint: '密钥已配置,留空以保留当前值。',
|
||||
redirectUrl: '回调地址(Redirect URL)',
|
||||
redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/dingtalk/callback',
|
||||
redirectUrlHint: '需与钉钉开放平台中配置的回调地址一致(必须是 http(s) 完整 URL)',
|
||||
corpPolicy: {
|
||||
label: '企业限制策略',
|
||||
hint: '控制哪些钉钉账号(企业)可以登录',
|
||||
none: '不限制(所有钉钉账号均可登录)',
|
||||
internalOnly: '仅本企业(Internal Only)'
|
||||
},
|
||||
bypassRegistration: '开放钉钉注册',
|
||||
bypassRegistrationHint: '即使「开放注册」关闭时也可以通过钉钉登录来注册',
|
||||
syncDisplayName: '同步钉钉姓名',
|
||||
syncDisplayNameHint: '登录时将钉钉姓名写入 username 字段(同时记录到 dingtalk_name 属性)',
|
||||
syncCorpEmail: '同步企业邮箱',
|
||||
syncCorpEmailHint: '登录时将钉钉企业邮箱写入 dingtalk_email 属性(不影响登录邮箱)',
|
||||
syncCorpEmailPermissionHint: '需在钉钉开放平台 → 应用 → 权限管理中为本应用申请「邮箱等个人信息(fieldEmail)」权限,否则 OAPI 不会返回企业邮箱字段',
|
||||
syncDept: '同步部门',
|
||||
syncDeptHint: '登录时将钉钉首个部门完整路径写入 dingtalk_department 属性(每次登录实时拉取)',
|
||||
syncDeptPermissionHint: '需在钉钉开放平台 → 应用 → 权限管理中为本应用申请「通讯录部门信息读权限(qyapi_get_department_list)」,否则无法递归出部门路径',
|
||||
syncDisplayNameTarget: '属性键',
|
||||
syncDisplayNameTargetHint: '默认 dingtalk_name / 钉钉姓名;保存设置时按上述属性键和显示名称自动创建用户属性(已存在则仅同步显示名称)',
|
||||
syncCorpEmailTarget: '属性键',
|
||||
syncCorpEmailTargetHint: '默认 dingtalk_email / 钉钉企业邮箱;保存设置时按上述属性键和显示名称自动创建用户属性(已存在则仅同步显示名称)',
|
||||
syncDeptTarget: '属性键',
|
||||
syncDeptTargetHint: '默认 dingtalk_department / 钉钉部门;保存设置时按上述属性键和显示名称自动创建用户属性(已存在则仅同步显示名称)',
|
||||
syncAttrDisplayName: '显示名称'
|
||||
},
|
||||
oidc: {
|
||||
title: 'OIDC 登录',
|
||||
description: '配置标准 OIDC Provider(例如 Keycloak)',
|
||||
|
||||
Reference in New Issue
Block a user