diff --git a/es/aspects/AspectDict.d.ts b/es/aspects/AspectDict.d.ts index 7eafb8c4e..1d6d628cd 100644 --- a/es/aspects/AspectDict.d.ts +++ b/es/aspects/AspectDict.d.ts @@ -134,11 +134,13 @@ export type AspectDict = { * 微信小程序登录 * @param code 微信授权 code * @param env 小程序环境信息 + * @param wechatLoginId 可选的微信登录 ID(用于扫码登录场景) * @returns 返回登录 token */ - loginWechatMp: ({ code, env, }: { + loginWechatMp: ({ code, env, wechatLoginId, }: { code: string; env: WechatMpEnv; + wechatLoginId?: string; }, context: BackendRuntimeContext) => Promise; /** * 微信原生 APP 登录 @@ -299,6 +301,7 @@ export type AspectDict = { type: EntityDict['wechatLogin']['Schema']['type']; interval: number; router: EntityDict['wechatLogin']['Schema']['router']; + qrCodeType?: EntityDict['wechatLogin']['Schema']['qrCodeType']; }, context: BackendRuntimeContext) => Promise; /** * 解绑微信用户 @@ -758,5 +761,16 @@ export type AspectDict = { headers?: Record; formdata?: Record; }>; + /** + * 用户账号注册 + * @param loginName 账号 + * @param password 密码 + * @param context + * @returns + */ + registerUserByLoginName: (params: { + loginName: string; + password: string; + }, context: BackendRuntimeContext) => Promise; }; export default AspectDict; diff --git a/es/aspects/applicationPassport.js b/es/aspects/applicationPassport.js index 83fbec5c3..bf2f22b7b 100644 --- a/es/aspects/applicationPassport.js +++ b/es/aspects/applicationPassport.js @@ -12,9 +12,15 @@ export async function getApplicationPassports(params, context) { config: 1, }, isDefault: 1, + allowPwd: 1, }, filter: { applicationId, + passport: { + type: { + $ne: 'password', + } + } } }, {}); closeRoot(); diff --git a/es/aspects/index.d.ts b/es/aspects/index.d.ts index 297d2787a..430c9dc90 100644 --- a/es/aspects/index.d.ts +++ b/es/aspects/index.d.ts @@ -4,7 +4,7 @@ import { getApplication, signatureJsSDK, uploadWechatMedia, batchGetArticle, get import { updateConfig, updateApplicationConfig, updateStyle } from './config'; import { syncWechatTemplate, getMessageType } from './template'; import { syncSmsTemplate } from './sms'; -import { mergeUser, getChangePasswordChannels, updateUserPassword } from './user'; +import { mergeUser, getChangePasswordChannels, updateUserPassword, registerUserByLoginName } from './user'; import { createWechatLogin } from './wechatLogin'; import { unbindingWechat } from './wechatUser'; import { getMpUnlimitWxaCode } from './wechatQrCode'; @@ -88,6 +88,7 @@ declare const aspectDict: { setUserAvatarFromWechat: typeof setUserAvatarFromWechat; mergeChunkedUpload: typeof mergeChunkedUpload; presignFile: typeof presignFile; + registerUserByLoginName: typeof registerUserByLoginName; }; export default aspectDict; export { AspectDict } from './AspectDict'; diff --git a/es/aspects/index.js b/es/aspects/index.js index d28f3695e..2d6c34282 100644 --- a/es/aspects/index.js +++ b/es/aspects/index.js @@ -4,7 +4,7 @@ import { getApplication, signatureJsSDK, uploadWechatMedia, batchGetArticle, get import { updateConfig, updateApplicationConfig, updateStyle } from './config'; import { syncWechatTemplate, getMessageType } from './template'; import { syncSmsTemplate } from './sms'; -import { mergeUser, getChangePasswordChannels, updateUserPassword } from './user'; +import { mergeUser, getChangePasswordChannels, updateUserPassword, registerUserByLoginName } from './user'; import { createWechatLogin } from './wechatLogin'; import { unbindingWechat } from './wechatUser'; import { getMpUnlimitWxaCode } from './wechatQrCode'; @@ -90,5 +90,6 @@ const aspectDict = { // extraFile新增 mergeChunkedUpload, presignFile, + registerUserByLoginName, }; export default aspectDict; diff --git a/es/aspects/oauth.js b/es/aspects/oauth.js index 32d2d76d4..30d637380 100644 --- a/es/aspects/oauth.js +++ b/es/aspects/oauth.js @@ -16,6 +16,7 @@ export async function loginByOauth(params, context) { // 验证 state 并获取 OAuth 配置 const [state] = await context.select("oauthState", { data: { + providerId: 1, provider: { type: 1, clientId: 1, @@ -32,6 +33,31 @@ export async function loginByOauth(params, context) { state: stateCode, }, }, { dontCollect: true }); + const systemId = context.getSystemId(); + const [applicationPassport] = await context.select('applicationPassport', { + data: { + id: 1, + applicationId: 1, + passportId: 1, + passport: { + id: 1, + type: 1, + systemId: 1, + config: 1, + }, + }, + filter: { + passport: { + systemId, + type: 'oauth', + }, + applicationId, + } + }, { dontCollect: true }); + const allowOauth = !!(state.providerId && applicationPassport?.passport?.config?.oauthIds && applicationPassport?.passport?.config)?.oauthIds.includes(state.providerId); + if (!allowOauth) { + throw new OakUserException('error::user.loginWayDisabled'); + } assert(state, '无效的 state 参数'); assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用'); // 如果已经使用 diff --git a/es/aspects/token.d.ts b/es/aspects/token.d.ts index e2b32e6d0..0fe2b56bd 100644 --- a/es/aspects/token.d.ts +++ b/es/aspects/token.d.ts @@ -77,9 +77,10 @@ export declare function loginWechat({ code, env, wechatLo * @param context * @returns */ -export declare function loginWechatMp({ code, env, }: { +export declare function loginWechatMp({ code, env, wechatLoginId, }: { code: string; env: WechatMpEnv; + wechatLoginId?: string; }, context: BRC): Promise; /** * 同步从wx.getUserProfile拿到的用户信息 diff --git a/es/aspects/token.js b/es/aspects/token.js index 5431d03aa..347b83240 100644 --- a/es/aspects/token.js +++ b/es/aspects/token.js @@ -536,23 +536,9 @@ export async function loginByMobile(params, context) { } export async function verifyPassword(params, context) { const { password } = params; - const systemId = context.getSystemId(); - const [pwdPassport] = await context.select('passport', { - data: { - id: 1, - systemId: 1, - config: 1, - type: 1, - enabled: 1, - }, - filter: { - systemId, - enabled: true, - type: 'password', - } - }, { forUpdate: true }); - // assert(pwdPassport); - const pwdMode = pwdPassport?.config?.mode ?? 'all'; + const { system } = context.getApplication(); + const pwdConfig = system?.config.Password; + const pwdMode = pwdConfig?.mode ?? 'all'; let pwdFilter = {}; if (pwdMode === 'all') { pwdFilter = { @@ -607,8 +593,35 @@ export async function loginByAccount(params, context) { assert(password); assert(account); const accountType = isEmail(account) ? 'email' : (isMobile(account) ? 'mobile' : 'loginName'); + const applicationPassports = await context.select('applicationPassport', { + data: { + id: 1, + applicationId: 1, + passportId: 1, + passport: { + id: 1, + type: 1, + systemId: 1, + }, + allowPwd: 1, + }, + filter: { + passport: { + systemId, + }, + applicationId, + allowPwd: true, + } + }, { + dontCollect: true, + }); + const allowEmail = !!applicationPassports.find((ele) => ele.passport?.type === 'email'); + const allowSms = !!applicationPassports.find((ele) => ele.passport?.type === 'sms'); if (accountType === 'email') { const { config, emailConfig } = await getAndCheckPassportByEmail(context, account); + if (!allowEmail) { + throw new OakUserException('暂不支持邮箱登录'); + } const existEmail = await context.select('email', { data: { id: 1, @@ -655,49 +668,23 @@ export async function loginByAccount(params, context) { throw new OakUserException('error::user.passwordUnmath'); } case 1: { - const applicationPassports = await context.select('applicationPassport', { - data: { - id: 1, - applicationId: 1, - passportId: 1, - passport: { - id: 1, - type: 1, - systemId: 1, - } - }, - filter: { - passport: { - systemId, - }, - applicationId, - } - }, { - dontCollect: true, - }); - const allowEmail = !!applicationPassports.find((ele) => ele.passport?.type === 'email'); const [userRow] = result; const { email$user, id: userId, } = userRow; needUpdatePassword = !(userRow.password || userRow.passwordSha1); - if (allowEmail) { - const email = email$user?.find(ele => ele.email.toLowerCase() === account.toLowerCase()); - if (email) { - const ableState = email.ableState; - if (ableState === 'disabled') { - // 虽然密码和邮箱匹配,但邮箱已经禁用了,在可能的情况下提醒用户使用其它方法登录 - const exception = await tryMakeChangeLoginWay(userId, context); - if (exception) { - throw exception; - } + const email = email$user?.find(ele => ele.email.toLowerCase() === account.toLowerCase()); + if (email) { + const ableState = email.ableState; + if (ableState === 'disabled') { + // 虽然密码和邮箱匹配,但邮箱已经禁用了,在可能的情况下提醒用户使用其它方法登录 + const exception = await tryMakeChangeLoginWay(userId, context); + if (exception) { + throw exception; } - return await setupEmail(account, env, context); - } - else { - throw new OakUserException('error::user.emailUnexists'); } + return await setupEmail(account, env, context); } else { - throw new OakUserException('error::user.loginWayDisabled'); + throw new OakUserException('error::user.emailUnexists'); } } default: { @@ -706,6 +693,9 @@ export async function loginByAccount(params, context) { } } else if (accountType === 'mobile') { + if (!allowSms) { + throw new OakUserException('暂不支持手机号登录'); + } const existMobile = await context.select('mobile', { data: { id: 1, @@ -752,53 +742,27 @@ export async function loginByAccount(params, context) { throw new OakUserException('手机号与密码不匹配'); } case 1: { - const applicationPassports = await context.select('applicationPassport', { - data: { - id: 1, - applicationId: 1, - passportId: 1, - passport: { - id: 1, - type: 1, - systemId: 1, - } - }, - filter: { - passport: { - systemId, - }, - applicationId, - } - }, { - dontCollect: true, - }); const [userRow] = result; const { mobile$user, id: userId, } = userRow; needUpdatePassword = !(userRow.password || userRow.passwordSha1); - const allowSms = !!applicationPassports.find((ele) => ele.passport?.type === 'sms'); - if (allowSms) { - const mobile = mobile$user?.find(ele => ele.mobile === account); - if (mobile) { - const ableState = mobile.ableState; - if (ableState === 'disabled') { - // 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录 - const exception = await tryMakeChangeLoginWay(userId, context); - if (exception) { - throw exception; - } + const mobile = mobile$user?.find(ele => ele.mobile === account); + if (mobile) { + const ableState = mobile.ableState; + if (ableState === 'disabled') { + // 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录 + const exception = await tryMakeChangeLoginWay(userId, context); + if (exception) { + throw exception; } - return await setupMobile(account, env, context); - } - else { - throw new OakUserException('手机号未注册'); } + return await setupMobile(account, env, context); } else { - throw new OakUserException('暂不支持手机号登录'); + throw new OakUserException('手机号未注册'); } } default: { - throw new OakUserException('不支持的登录方式'); + throw new OakUserException('error::user.loginWayDisabled'); } } } @@ -869,35 +833,15 @@ export async function loginByAccount(params, context) { } } default: { - throw new OakUserException('不支持的登录方式'); + throw new OakUserException('error::user.loginWayDisabled'); } } } }; const closeRootMode = context.openRootMode(); - const application = context.getApplication(); - const [applicationPassport] = await context.select('applicationPassport', { - data: { - id: 1, - passportId: 1, - passport: { - id: 1, - config: 1, - type: 1, - }, - applicationId: 1, - }, - filter: { - applicationId: application?.id, - passport: { - type: 'password', - }, - } - }, { - dontCollect: true, - }); - // assert(applicationPassport?.passport); - const pwdMode = applicationPassport?.passport?.config?.mode ?? 'all'; + const { system } = context.getApplication(); + const pwdConfig = system?.config.Password; + const pwdMode = pwdConfig?.mode ?? 'all'; let pwdFilter = {}, updateData = {}; if (pwdMode === 'all') { pwdFilter = { @@ -1981,9 +1925,9 @@ export async function loginWechat({ code, env, wechatLoginId, }, context) { * @param context * @returns */ -export async function loginWechatMp({ code, env, }, context) { +export async function loginWechatMp({ code, env, wechatLoginId, }, context) { const closeRootMode = context.openRootMode(); - const tokenValue = await loginFromWechatEnv(code, env, context); + const tokenValue = await loginFromWechatEnv(code, env, context, wechatLoginId); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; diff --git a/es/aspects/user.d.ts b/es/aspects/user.d.ts index 092f5827f..3ffb67178 100644 --- a/es/aspects/user.d.ts +++ b/es/aspects/user.d.ts @@ -23,3 +23,12 @@ export declare function updateUserPassword(params: { result: string; times?: undefined; }>; +/** + * 用户账号注册 + * @param params + * @param context + */ +export declare function registerUserByLoginName(params: { + loginName: string; + password: string; +}, context: BRC): Promise; diff --git a/es/aspects/user.js b/es/aspects/user.js index 21d329212..6e291308c 100644 --- a/es/aspects/user.js +++ b/es/aspects/user.js @@ -1,4 +1,4 @@ -import { OakOperationUnpermittedException } from "oak-domain/lib/types"; +import { OakOperationUnpermittedException, OakPreConditionUnsetException } from "oak-domain/lib/types"; import { generateNewIdAsync } from "oak-domain/lib/utils/uuid"; import { encryptPasswordSha1 } from '../utils/password'; import { assert } from 'oak-domain/lib/utils/assert'; @@ -177,20 +177,17 @@ export async function updateUserPassword(params, context, innerLogic) { const systemId = context.getSystemId(); const closeRootMode = context.openRootMode(); try { - const [passport] = await context.select('passport', { + const [system] = await context.select('system', { data: { id: 1, - type: 1, config: 1, - systemId: 1, }, filter: { - systemId, - type: 'password', + id: systemId, } }, { forUpdate: true }); - assert(passport); - const config = passport.config; + assert(system); + const config = system.config?.Password; const mode = config?.mode ?? 'all'; const [user] = await context.select('user', { data: { @@ -257,13 +254,13 @@ export async function updateUserPassword(params, context, innerLogic) { }; } const allowUpdate = mode === 'sha1' ? user.passwordSha1 === prevPassword : user.password === prevPassword; //sha1密文模式判断密文是否相等 - let userDate = {}, changeCreateDate = {}; + let userData = {}, changeCreateData = {}; if (mode === 'all') { - userDate = { + userData = { password: newPassword, passwordSha1: encryptPasswordSha1(newPassword), }; - changeCreateDate = { + changeCreateData = { prevPassword, newPassword, prevPasswordSha1: encryptPasswordSha1(prevPassword), @@ -271,19 +268,19 @@ export async function updateUserPassword(params, context, innerLogic) { }; } else if (mode === 'plain') { - userDate = { + userData = { password: newPassword, }; - changeCreateDate = { + changeCreateData = { prevPassword, newPassword, }; } else if (mode === 'sha1') { - userDate = { + userData = { passwordSha1: newPassword, }; - changeCreateDate = { + changeCreateData = { prevPasswordSha1: prevPassword, newPasswordSha1: newPassword, }; @@ -292,7 +289,7 @@ export async function updateUserPassword(params, context, innerLogic) { await context.operate('user', { id: await generateNewIdAsync(), action: 'update', - data: userDate, + data: userData, filter: { id: userId, }, @@ -306,7 +303,7 @@ export async function updateUserPassword(params, context, innerLogic) { id: await generateNewIdAsync(), userId, result: 'success', - ...changeCreateDate, + ...changeCreateData, }, }, { dontCollect: true, @@ -324,7 +321,7 @@ export async function updateUserPassword(params, context, innerLogic) { id: await generateNewIdAsync(), userId, result: 'fail', - ...changeCreateDate, + ...changeCreateData, }, }, { dontCollect: true, @@ -353,13 +350,13 @@ export async function updateUserPassword(params, context, innerLogic) { dontCollect: true, }); if (aliveCaptcha) { - let userDate = {}, changeCreateDate = {}; + let userData = {}, changeCreateData = {}; if (mode === 'all') { - userDate = { + userData = { password: newPassword, passwordSha1: encryptPasswordSha1(newPassword), }; - changeCreateDate = { + changeCreateData = { prevPassword: user.password, newPassword, prevPasswordSha1: user.passwordSha1, @@ -367,19 +364,19 @@ export async function updateUserPassword(params, context, innerLogic) { }; } else if (mode === 'plain') { - userDate = { + userData = { password: newPassword, }; - changeCreateDate = { + changeCreateData = { prevPassword: user.password, newPassword, }; } else if (mode === 'sha1') { - userDate = { + userData = { passwordSha1: newPassword, }; - changeCreateDate = { + changeCreateData = { prevPasswordSha1: user.passwordSha1, newPasswordSha1: newPassword, }; @@ -387,7 +384,7 @@ export async function updateUserPassword(params, context, innerLogic) { await context.operate('user', { id: await generateNewIdAsync(), action: 'update', - data: userDate, + data: userData, filter: { id: userId, }, @@ -401,7 +398,7 @@ export async function updateUserPassword(params, context, innerLogic) { id: await generateNewIdAsync(), userId, result: 'success', - ...changeCreateDate, + ...changeCreateData, }, }, { dontCollect: true, @@ -428,3 +425,83 @@ export async function updateUserPassword(params, context, innerLogic) { throw err; } } +/** + * 用户账号注册 + * @param params + * @param context + */ +export async function registerUserByLoginName(params, context) { + const { loginName, password } = params; + const systemId = context.getSystemId(); + const closeRootMode = context.openRootMode(); + try { + // 检查loginName是否重复 + const [existLoginName] = await context.select('loginName', { + data: { + id: 1, + name: 1, + }, + filter: { + name: loginName, + ableState: 'enabled', + }, + }, { dontCollect: true, forUpdate: true }); + if (existLoginName) { + closeRootMode(); + throw new OakPreConditionUnsetException('账号已存在,请重新设置'); + } + // 创建user并附上密码,级联创建loginName + const [system] = await context.select('system', { + data: { + id: 1, + config: 1, + }, + filter: { + id: systemId, + } + }, { forUpdate: true }); + assert(system); + const config = system.config?.Password; + const mode = config?.mode ?? 'all'; + let passwordData = {}; + if (mode === 'all') { + passwordData = { + password: password, + passwordSha1: encryptPasswordSha1(password), + }; + } + else if (mode === 'plain') { + passwordData = { + password: password, + }; + } + else if (mode === 'sha1') { + passwordData = { + passwordSha1: password, + }; + } + const userData = { + id: await generateNewIdAsync(), + loginName$user: [ + { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + name: loginName, + } + } + ] + }; + Object.assign(userData, passwordData); + await context.operate('user', { + id: await generateNewIdAsync(), + action: 'create', + data: userData, + }, {}); + } + catch (err) { + closeRootMode(); + throw err; + } +} diff --git a/es/aspects/wechatLogin.d.ts b/es/aspects/wechatLogin.d.ts index c6bd46f98..1cc98449c 100644 --- a/es/aspects/wechatLogin.d.ts +++ b/es/aspects/wechatLogin.d.ts @@ -4,4 +4,5 @@ export declare function createWechatLogin(params: { type: EntityDict['wechatLogin']['Schema']['type']; interval: number; router: EntityDict['wechatLogin']['Schema']['router']; + qrCodeType?: EntityDict['wechatLogin']['Schema']['qrCodeType']; }, context: BRC): Promise; diff --git a/es/aspects/wechatLogin.js b/es/aspects/wechatLogin.js index f953a0277..a8f620ac8 100644 --- a/es/aspects/wechatLogin.js +++ b/es/aspects/wechatLogin.js @@ -1,6 +1,6 @@ import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; export async function createWechatLogin(params, context) { - const { type, interval, router } = params; + const { type, interval, qrCodeType = "wechatPublic", router } = params; let userId; if (type === 'bind') { userId = context.getCurrentUserId(); @@ -26,7 +26,7 @@ export async function createWechatLogin(params, context) { type, expiresAt: Date.now() + interval, expired: false, - qrCodeType: 'wechatPublic', + qrCodeType, successed: false, router: _router, }; diff --git a/es/components/applicationPassport/index.js b/es/components/applicationPassport/index.js index 4dff6c865..a5a7fab85 100644 --- a/es/components/applicationPassport/index.js +++ b/es/components/applicationPassport/index.js @@ -20,6 +20,7 @@ export default OakComponent({ enabled: 1, }, isDefault: 1, + allowPwd: 1, }, properties: { systemId: '', @@ -34,6 +35,9 @@ export default OakComponent({ }, passport: { systemId, + type: { + $ne: 'password' + } }, }; } @@ -83,8 +87,9 @@ export default OakComponent({ } else { const { disabled, disabledTip } = this.checkDisabled(a, r[0]); + const { showPwd, pwdDisabled, pwdDisabledTip } = this.checkPwd(r[0]); const apId = await generateNewIdAsync(); - Object.assign(typeRecords, { [key]: { render, pId: r[0].id, checked: false, disabled, disabledTip, apId, } }); + Object.assign(typeRecords, { [key]: { render, pId: r[0].id, checked: false, disabled, disabledTip, apId, showPwd, allowPwd: undefined, pwdDisabled, pwdDisabledTip } }); } } Object.assign(item, { typeRecords }); @@ -106,8 +111,15 @@ export default OakComponent({ if (apArray[aIdx].typeRecords[t].pId === p.id) { apArray[aIdx].typeRecords[t].checked = true; apArray[aIdx].typeRecords[t].apId = ap.id; + apArray[aIdx].typeRecords[t].allowPwd = ap.allowPwd; } } + if (t === 'loginName') { + apArray[aIdx].typeRecords[t].pwdDisabledTip = '账号登录必须使用密码方式'; + } + else { + apArray[aIdx].typeRecords[t].pwdDisabled = false; + } apArray[aIdx].defaultOptions.push({ label: this.t(`passport:v.type.${p.type}`), value: ap.id, @@ -157,6 +169,9 @@ export default OakComponent({ filter: { systemId, enabled: true, + type: { + $ne: 'password', + } }, sorter: [{ $attr: { @@ -193,36 +208,36 @@ export default OakComponent({ switch (pType) { case 'sms': if (!pConfig.mockSend) { - if (!pConfig.templateName || pConfig.templateName === '') { + if (!pConfig.templateName) { return { disabled: true, - disabledTip: '短信登录未配置验证码模板名称', + disabledTip: '手机号登录未配置验证码模板名称', }; } if (!pConfig.defaultOrigin) { return { disabled: true, - disabledTip: '短信登录未配置默认渠道', + disabledTip: '手机号登录未配置默认渠道', }; } } break; case 'email': if (!pConfig.mockSend) { - if (!pConfig.account || pConfig.account === '') { + if (!pConfig.account) { return { disabled: true, disabledTip: '邮箱登录未配置账号', }; } - else if (!pConfig.subject || pConfig.subject === '') { + else if (!pConfig.subject) { return { disabled: true, disabledTip: '邮箱登录未配置邮件主题', }; } - else if ((!pConfig.text || pConfig.text === '' || !pConfig.text?.includes('${code}')) && - (!pConfig.html || pConfig.html === '' || !pConfig.html?.includes('${code}'))) { + else if ((!pConfig.text || !pConfig.text?.includes('${code}')) && + (!pConfig.html || !pConfig.html?.includes('${code}'))) { return { disabled: true, disabledTip: '邮箱登录未配置邮件内容模板', @@ -231,7 +246,7 @@ export default OakComponent({ } break; case 'wechatPublicForWeb': - if (!pConfig.appId || pConfig.appId === '') { + if (!pConfig.appId) { return { disabled: true, disabledTip: '公众号授权登录未配置appId', @@ -239,13 +254,27 @@ export default OakComponent({ } break; case 'wechatMpForWeb': - if (!pConfig.appId || pConfig.appId === '') { + if (!pConfig.appId) { return { disabled: true, disabledTip: '小程序授权登录未配置appId', }; } break; + case 'oauth': + if (!(pConfig.oauthIds && pConfig.oauthIds.length > 0)) { + return { + disabled: true, + disabledTip: 'OAuth授权登录未配置oauth供应商', + }; + } + break; + case 'password': + return { + disabled: true, + disabledTip: '密码登录已调整', + }; + break; default: break; } @@ -269,7 +298,7 @@ export default OakComponent({ } break; case 'wechatMp': - if (['wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) { + if (['wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb', 'oauth'].includes(pType)) { return { disabled: true, disabledTip: '当前application不支持该登录方式', @@ -277,7 +306,7 @@ export default OakComponent({ } break; case 'wechatPublic': - if (['wechatMp', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) { + if (['wechatMp', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb', 'oauth'].includes(pType)) { return { disabled: true, disabledTip: '当前application不支持该登录方式', @@ -285,7 +314,7 @@ export default OakComponent({ } break; case 'native': - if (['wechatMp', 'wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) { + if (['wechatMp', 'wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb', 'oauth'].includes(pType)) { return { disabled: true, disabledTip: '当前application不支持该登录方式', @@ -301,13 +330,25 @@ export default OakComponent({ }; }, async onCheckedChange(aId, pId, checked, apId) { + const { passports } = this.state; + const passportType = passports?.find((ele) => ele.id === pId)?.type; if (checked) { //create applicationPassport - this.addItem({ - applicationId: aId, - passportId: pId, - isDefault: true, - }); + if (passportType === 'loginName') { + this.addItem({ + applicationId: aId, + passportId: pId, + isDefault: true, + allowPwd: true, + }); + } + else { + this.addItem({ + applicationId: aId, + passportId: pId, + isDefault: true, + }); + } } else { //remove id为apId的applicationPassport @@ -349,6 +390,20 @@ export default OakComponent({ } } return render; - } + }, + checkPwd(passport) { + const { type } = passport; + let showPwd = false, pwdDisabled = undefined, pwdDisabledTip = undefined; + if (['sms', 'email', 'loginName'].includes(type)) { + showPwd = true; + pwdDisabled = true; + pwdDisabledTip = '请先启用该登录方式'; + } + return { + showPwd, + pwdDisabled, + pwdDisabledTip, + }; + }, } }); diff --git a/es/components/applicationPassport/web.pc.d.ts b/es/components/applicationPassport/web.pc.d.ts index b87ceb9e6..799f59265 100644 --- a/es/components/applicationPassport/web.pc.d.ts +++ b/es/components/applicationPassport/web.pc.d.ts @@ -20,6 +20,10 @@ type TypeRecord = Record; type PassportOption = { label: string; diff --git a/es/components/applicationPassport/web.pc.js b/es/components/applicationPassport/web.pc.js index e9290452f..c2846d54e 100644 --- a/es/components/applicationPassport/web.pc.js +++ b/es/components/applicationPassport/web.pc.js @@ -56,8 +56,11 @@ export default function render(props) { }} disabled={typeRecords[type].disabled} options={typeRecords[type].passportOptions} optionRender={(option) => (
{option.data.label}
)} style={{ width: 140 }}/> - ) : ( - } unCheckedChildren={} checked={!!typeRecords[type].checked} onChange={(checked) => { + ) : (<> + + +
启用:
+ } unCheckedChildren={} checked={!!typeRecords[type].checked} onChange={(checked) => { if (!checked && checkLastOne(aId, typeRecords[type].pId)) { showConfirm(aId, typeRecords[type].pId, typeRecords[type].apId); } @@ -65,7 +68,20 @@ export default function render(props) { onCheckedChange(aId, typeRecords[type].pId, checked, typeRecords[type].apId); } }}/> -
)} + + + {typeRecords[type].showPwd && + +
允许密码登录:
+ + } unCheckedChildren={} checked={!!typeRecords[type].allowPwd} onChange={(checked) => { + methods.updateItem({ + allowPwd: checked + }, typeRecords[type].apId); + }}/> + +
} + )} }); diff --git a/es/components/changePassword/byMobile/index.js b/es/components/changePassword/byMobile/index.js index 0a7382f5d..6c8ba5328 100644 --- a/es/components/changePassword/byMobile/index.js +++ b/es/components/changePassword/byMobile/index.js @@ -54,9 +54,8 @@ export default OakComponent({ lifetimes: { async ready() { const lastSendAt = await this.load(SEND_KEY); - const application = this.features.application.getApplication(); - const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); - const passwordConfig = applicationPassports.find((ele) => ele.passport.type === 'password')?.passport.config; + const system = this.features.application.getApplication().system; + const passwordConfig = system?.config.Password; const mode = passwordConfig?.mode ?? 'all'; const pwdMin = passwordConfig?.min ?? 8; const pwdMax = passwordConfig?.max ?? 24; diff --git a/es/components/changePassword/byPassword/index.js b/es/components/changePassword/byPassword/index.js index 435ebdf71..023a58537 100644 --- a/es/components/changePassword/byPassword/index.js +++ b/es/components/changePassword/byPassword/index.js @@ -23,9 +23,8 @@ export default OakComponent({ }, lifetimes: { async ready() { - const application = this.features.application.getApplication(); - const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); - const passwordConfig = applicationPassports.find((ele) => ele.passport.type === 'password')?.passport.config; + const system = this.features.application.getApplication().system; + const passwordConfig = system?.config.Password; const mode = passwordConfig?.mode ?? 'all'; const pwdMin = passwordConfig?.min ?? 8; const pwdMax = passwordConfig?.max ?? 24; diff --git a/es/components/common/download/index.d.ts b/es/components/common/download/index.d.ts index 35b6ec953..46ed1b72b 100644 --- a/es/components/common/download/index.d.ts +++ b/es/components/common/download/index.d.ts @@ -11,6 +11,6 @@ declare namespace Download { var onDownload: (data: ArrayBuffer | ReadableStream, filename: string) => Promise; var base64ToBlob: (base64String: string) => Blob; var arrayBufferToBase64: (buffer: Buffer) => string; - var base64ToArrayBuffer: (base64String: string) => ArrayBuffer; + var base64ToArrayBuffer: (base64String: string) => ArrayBufferLike; } export default Download; diff --git a/es/components/config/upsert/password/index.d.ts b/es/components/config/upsert/password/index.d.ts new file mode 100644 index 000000000..60261773b --- /dev/null +++ b/es/components/config/upsert/password/index.d.ts @@ -0,0 +1,7 @@ +import React from 'react'; +import { Config } from '../../../../types/Config'; +export default function Password(props: { + password: Required['Password']; + setValue: (path: string, value: any) => void; + setValues: (value: Record) => void; +}): React.JSX.Element; diff --git a/es/components/config/upsert/password/index.js b/es/components/config/upsert/password/index.js new file mode 100644 index 000000000..3620afd9c --- /dev/null +++ b/es/components/config/upsert/password/index.js @@ -0,0 +1,78 @@ +import React, { useEffect, useState } from 'react'; +import { Col, Divider, Input, Form, Space, Radio, InputNumber, Switch, } from 'antd'; +import Styles from './web.module.less'; +import EditorRegexs from '../../../passport/password/editorRegexs'; +export default function Password(props) { + const { password, setValue, setValues } = props; + const { mode, min, max, verify, regexs, tip } = password || {}; + const [newTip, setNewTip] = useState(''); + useEffect(() => { + const { password } = props; + if (!password.mode) { + setValues({ + mode: 'all', + min: 8, + max: 24, + }); + } + }, [password]); + useEffect(() => { + if (tip && !newTip) { + setNewTip(tip); + } + }, [tip]); + return ( + + + 密码设置 + +
+ + { + const { value } = target; + setValue('mode', value); + }} value={mode}> + 明文与SHA1加密 + 仅明文 + 仅SHA1加密 + + + + + { + setValue('min', value); + }}/> +
~
+ { + setValue('max', value); + }}/> +
+
+ + + { + setValue('verigy', checked); + }}/> + + + <> + {!!verify ? (<> + { + setValue('regexs', regexs); + }}/> + ) : (
暂未启用正则校验,无需设置
)} + +
+ + { + setNewTip(e.target.value); + }} onBlur={() => { + if (newTip && newTip !== tip) { + setValue('tip', newTip); + } + }}/> + +
+ +
); +} diff --git a/es/components/config/upsert/password/web.module.less b/es/components/config/upsert/password/web.module.less new file mode 100644 index 000000000..7fa576eed --- /dev/null +++ b/es/components/config/upsert/password/web.module.less @@ -0,0 +1,16 @@ + +.label { + color: var(--oak-text-color-primary); + font-size: 28px; + line-height: 36px; +} + +.tips { + color: var(--oak-text-color-placeholder); + font-size: 12px; +} + +.title { + margin-bottom: 0px; + margin-top:36px; +} \ No newline at end of file diff --git a/es/components/config/upsert/web.pc.js b/es/components/config/upsert/web.pc.js index e9359adaf..0ab4a1023 100644 --- a/es/components/config/upsert/web.pc.js +++ b/es/components/config/upsert/web.pc.js @@ -9,10 +9,11 @@ import Sms from './sms/index'; import Email from './email/index'; import Basic from './basic/index'; import Security from './security/index'; +import Password from './password/index'; export default function Render(props) { const { entity, name, currentConfig, dirty } = props.data; const { resetConfig, updateConfig, setValue, setValues, removeItem, cleanKey, t } = props.methods; - const { Account: account, Cos: cos, Map: map, Live: live, Sms: sms, App: app, Emails: emails, Security: security, } = currentConfig || {}; + const { Account: account, Cos: cos, Map: map, Live: live, Sms: sms, App: app, Emails: emails, Security: security, Password: password, } = currentConfig || {}; return (<> @@ -84,6 +85,15 @@ export default function Render(props) { }); }}/>), }, + { + key: '密码设置', + label: '密码设置', + children: ( setValue(`Password.${path}`, value)} setValues={(value) => { + setValues({ + Password: value + }); + }}/>), + }, ]}> ); diff --git a/es/components/login/oauth/authorize/index.js b/es/components/login/oauth/authorize/index.js index de8d6374a..481f03371 100644 --- a/es/components/login/oauth/authorize/index.js +++ b/es/components/login/oauth/authorize/index.js @@ -26,13 +26,43 @@ export default OakComponent({ state: '', }, lifetimes: { - ready() { + async ready() { const searchParams = new URLSearchParams(window.location.search); const clientId = searchParams.get('client_id') || ''; const responseType = searchParams.get('response_type') || ''; const redirectUri = searchParams.get('redirect_uri') || ''; const scope = searchParams.get('scope') || ''; const state = searchParams.get('state') || ''; + //判断是否允许oauth登录 + const application = this.features.application.getApplication(); + const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); + const oauthPassport = applicationPassports?.find((ele) => ele.passport?.type === 'oauth'); + const oauthIds = oauthPassport?.config?.oauthIds; + let allowOauth = false; + if (clientId) { + const { data: [oauthProvider] } = await this.features.cache.refresh('oauthProvider', { + data: { + id: 1, + clientId: 1, + systemId: 1, + }, + filter: { + clientId, + systemId: application.systemId, + } + }); + if (oauthProvider?.id && oauthIds?.length > 0 && oauthIds.includes(oauthProvider?.id)) { + allowOauth = true; + } + } + if (!allowOauth) { + this.setState({ + hasError: true, + errorMsg: 'oauth.login', + }); + this.setState({ loading: false }); + return; + } this.setState({ client_id: clientId, response_type: responseType, diff --git a/es/components/login/oauth/authorize/locales/zh_CN.json b/es/components/login/oauth/authorize/locales/zh_CN.json index 025b0f41a..e88b0c428 100644 --- a/es/components/login/oauth/authorize/locales/zh_CN.json +++ b/es/components/login/oauth/authorize/locales/zh_CN.json @@ -16,6 +16,7 @@ "missing_client_id": "缺少 client_id 参数", "unknown": "未知错误,请稍后重试" } - } + }, + "login": "当前暂未支持该第三方应用授权登录" } } diff --git a/es/components/oauth/management/oauthProvider/upsert/web.pc.js b/es/components/oauth/management/oauthProvider/upsert/web.pc.js index 27cee1a61..5bc57529a 100644 --- a/es/components/oauth/management/oauthProvider/upsert/web.pc.js +++ b/es/components/oauth/management/oauthProvider/upsert/web.pc.js @@ -50,13 +50,13 @@ const Upsert = (props) => { return (
- + { update({ name: v.target.value }); }}/> - + 「{item.type}」不是预设类型,请自行注入 handler。 ) : undefined}> { update({ authorizationEndpoint: v.target.value }); }}/> - + { update({ tokenEndpoint: v.target.value }); }}/> @@ -106,13 +106,13 @@ const Upsert = (props) => { }}/> - + { update({ clientId: v.target.value }); }}/> - + { update({ clientSecret: v.target.value }); }}/> @@ -124,7 +124,7 @@ const Upsert = (props) => { }} tokenSeparators={[',']} open={false}/> - + { update({ redirectUri: v.target.value }); }}/> @@ -135,7 +135,7 @@ const Upsert = (props) => { - update({ ableState: checked ? "enabled" : "disabled" })}/> + update({ ableState: checked ? "enabled" : "disabled" })}/> diff --git a/es/components/passport/email/index.d.ts b/es/components/passport/email/index.d.ts index eb2eb7545..7b21f5773 100644 --- a/es/components/passport/email/index.d.ts +++ b/es/components/passport/email/index.d.ts @@ -1,5 +1,5 @@ import React from "react"; -import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig } from "../../../entities/Passport"; +import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig, NameConfig, OAuthConfig } from "../../../entities/Passport"; import { EntityDict } from "../../../oak-app-domain"; import '@wangeditor/editor/dist/css/style.css'; export default function Email(props: { @@ -8,5 +8,5 @@ export default function Email(props: { }; t: (k: string, params?: any) => string; changeEnabled: (enabled: boolean) => void; - updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig, path: string, value: any, type?: string) => void; + updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig, path: string, value: any, type?: string) => void; }): React.JSX.Element; diff --git a/es/components/passport/index.js b/es/components/passport/index.js index c053d4cc4..8bb96048f 100644 --- a/es/components/passport/index.js +++ b/es/components/passport/index.js @@ -36,9 +36,10 @@ export default OakComponent({ formData({ data }) { const passports = data.map((ele) => { const stateColor = ele.type ? this.features.style.getColor('passport', 'type', ele.type) : '#00BFFF'; - let appIdStr; + let appIdStr, hasQrCodePrefix = false; if (ele.type === 'wechatMpForWeb') { appIdStr = this.getAppIdStr('wechatMp', ele.config?.appId); + hasQrCodePrefix = this.checkMpQrCodePrefix(ele.config?.appId); } else if (ele.type === 'wechatPublicForWeb') { appIdStr = this.getAppIdStr('wechatPublic', ele.config?.appId); @@ -47,14 +48,44 @@ export default OakComponent({ ...ele, appIdStr, stateColor, + hasQrCodePrefix, }; }); return { passports, }; }, - data: {}, - lifetimes: {}, + data: { + oauthOptions: [], + }, + lifetimes: { + async ready() { + const { systemId } = this.props; + const { data: oauthProviders } = await this.features.cache.refresh('oauthProvider', { + data: { + id: 1, + name: 1, + systemId: 1, + ableState: 1, + }, + filter: { + systemId, + ableState: 'enabled' + } + }); + if (oauthProviders && oauthProviders?.length > 0) { + const oauthOptions = oauthProviders?.map((ele) => { + return { + label: ele.name, + value: ele.id, + }; + }); + this.setState({ + oauthOptions, + }); + } + } + }, methods: { updateConfig(id, config, path, value, type) { const newConfig = cloneDeep(config); @@ -64,13 +95,13 @@ export default OakComponent({ if (!newConfig.templateName || newConfig.templateName === '') { this.setMessage({ type: 'warning', - content: '短信登录未配置模板名称,将无法正常使用短信登录' + content: '手机号登录未配置模板名称,将无法正常使用手机号登录' }); } else if (!newConfig.defaultOrigin) { this.setMessage({ type: 'warning', - content: '短信登录未选择默认渠道,将无法正常使用短信登录' + content: '手机号登录未选择默认渠道,将无法正常使用手机号登录' }); } } @@ -78,20 +109,20 @@ export default OakComponent({ if (!newConfig.account || newConfig.account === '') { this.setMessage({ type: 'warning', - content: '邮箱登录未指定邮箱账号,将无法正常使用短信登录' + content: '邮箱登录未指定邮箱账号,将无法正常使用邮箱登录' }); } else if (!newConfig.subject || newConfig.subject === '') { this.setMessage({ type: 'warning', - content: '邮箱登录未配置邮件主题,将无法正常使用短信登录' + content: '邮箱登录未配置邮件主题,将无法正常使用邮箱登录' }); } else if ((!newConfig.text || newConfig.text === '' || !newConfig.text?.includes('${code}')) && (!newConfig.html || newConfig.html === '' || !newConfig.html?.includes('${code}'))) { this.setMessage({ type: 'warning', - content: '邮箱登录未配置邮件内容模板,将无法正常使用短信登录' + content: '邮箱登录未配置邮件内容模板,将无法正常使用邮箱登录' }); } } @@ -102,6 +133,12 @@ export default OakComponent({ content: '未填写appId,该登录方式将无法正常使用' }); } + else if (type === 'oauth ' && path === 'oauthId' && !(value && value.length > 0)) { + this.setMessage({ + type: 'warning', + content: '未选择oauth提供商,将无法正常使用OAuth授权登录' + }); + } this.updateItem({ config: newConfig, }, id); @@ -110,7 +147,7 @@ export default OakComponent({ const { passports } = this.state; let warnings = []; for (const passport of passports) { - const { type, config, enabled, id } = passport; + const { type, config = {}, enabled, id } = passport; if (enabled) { //检查启用的passport对应的config是否设置 switch (type) { @@ -120,7 +157,7 @@ export default OakComponent({ warnings.push({ id, type, - tip: '短信登录未配置验证码模板名称', + tip: '手机号登录未配置验证码模板名称', }); } if (!config.defaultOrigin) { @@ -132,7 +169,7 @@ export default OakComponent({ warnings.push({ id, type, - tip: '短信登录未选择默认渠道', + tip: '手机号登录未选择默认渠道', }); } } @@ -140,14 +177,14 @@ export default OakComponent({ break; case 'email': if (!config.mockSend) { - if (!config.account || config.account === '') { + if (!config.account) { warnings.push({ id, type, tip: '邮箱登录未指定邮箱账号', }); } - else if (!config.subject || config.subject === '') { + else if (!config.subject) { const emailWarning = warnings.find((ele) => ele.id === id); if (emailWarning) { Object.assign(emailWarning, { tip: emailWarning.tip + '、邮件主题' }); @@ -160,8 +197,8 @@ export default OakComponent({ }); } } - else if ((!config.text || config.text === '' || !config.text?.includes('${code}')) && - (!config.html || config.html === '' || !config.html?.includes('${code}'))) { + else if ((!config.text || !config.text?.includes('${code}')) && + (!config.html || !config.html?.includes('${code}'))) { const emailWarning = warnings.find((ele) => ele.id === id); if (emailWarning) { Object.assign(emailWarning, { tip: emailWarning.tip + '、邮件内容模板' }); @@ -177,7 +214,7 @@ export default OakComponent({ } break; case 'wechatPublicForWeb': - if (!config.appId || config.appId === '') { + if (!config.appId) { warnings.push({ id, type, @@ -186,7 +223,7 @@ export default OakComponent({ } break; case 'wechatMpForWeb': - if (!config.appId || config.appId === '') { + if (!config.appId) { warnings.push({ id, type, @@ -194,6 +231,15 @@ export default OakComponent({ }); } break; + case 'oauth': + if (!(config.oauthIds && config.oauthIds.length > 0)) { + warnings.push({ + id, + type, + tip: 'OAuth授权登录未选择oauth供应商', + }); + } + break; default: break; } @@ -233,6 +279,24 @@ export default OakComponent({ } }); return application?.name ? appId + ' (applicationName:' + application.name + ')' : appId; + }, + checkMpQrCodePrefix(appId) { + const systemId = this.features.application.getApplication().systemId; + const [application] = this.features.cache.get('application', { + data: { + id: 1, + config: 1, + }, + filter: { + systemId, + config: { + appId, + }, + type: 'wechatMp', + } + }); + const config = application?.config; + return !!config?.qrCodePrefix; } }, }); diff --git a/es/components/passport/loginName/index.d.ts b/es/components/passport/loginName/index.d.ts new file mode 100644 index 000000000..b98bf5ab1 --- /dev/null +++ b/es/components/passport/loginName/index.d.ts @@ -0,0 +1,11 @@ +import React from "react"; +import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig, NameConfig, OAuthConfig } from "../../../entities/Passport"; +import { EntityDict } from "../../../oak-app-domain"; +export default function LoginName(props: { + passport: EntityDict['passport']['OpSchema'] & { + stateColor: string; + }; + t: (k: string, params?: any) => string; + changeEnabled: (enabled: boolean) => void; + updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig, path: string, value: any, type?: string) => void; +}): React.JSX.Element; diff --git a/es/components/passport/loginName/index.js b/es/components/passport/loginName/index.js new file mode 100644 index 000000000..326adaca8 --- /dev/null +++ b/es/components/passport/loginName/index.js @@ -0,0 +1,73 @@ +import React, { useEffect, useState } from "react"; +import { Switch, Form, Input, Space, Tag, InputNumber, } from 'antd'; +import Styles from './web.module.less'; +import EditorRegexs from "../password/editorRegexs"; +export default function LoginName(props) { + const { passport, t, changeEnabled, updateConfig } = props; + const { id, type, enabled, stateColor } = passport; + const config = passport.config || {}; + const [min, setMin] = useState(config?.min); + const [max, setMax] = useState(config?.max); + const [regexs, setRegexs] = useState(config?.regexs || []); + const [register, setRegister] = useState(false); + const [tip, setTip] = useState(config?.tip || ''); + useEffect(() => { + setMin(config?.min || 2); + setMax(config?.max || 8); + setRegexs(config?.regexs || []); + setRegister(!!config?.register); + setTip(config?.tip || ''); + }, [config]); + return (
+
+ {t(`passport:v.type.${type}`)} + { + changeEnabled(checked); + }}/> +
+ {enabled && +
+
+ + + { + updateConfig(id, config, 'min', value, 'loginName'); + }}/> +
~
+ { + updateConfig(id, config, 'max', value, 'loginName'); + }}/> +
+
+ + { + updateConfig(id, config, 'verify', checked, 'loginName'); + }}/> + + + <> + {!!config?.verify ? (<> + { + updateConfig(id, config, 'regexs', regexs, 'loginName'); + }}/> + ) : (
暂未启用正则校验,无需设置
)} + +
+ + { + updateConfig(id, config, 'register', checked, 'loginName'); + }}/> + + + { + setTip(e.target.value); + }} onBlur={() => { + if (tip && tip !== config?.tip) { + updateConfig(id, config, 'tip', tip, 'loginName'); + } + }}/> + +
+
} +
); +} diff --git a/es/components/passport/loginName/web.module.less b/es/components/passport/loginName/web.module.less new file mode 100644 index 000000000..3af5758c2 --- /dev/null +++ b/es/components/passport/loginName/web.module.less @@ -0,0 +1,12 @@ +.item { + padding: 10px 16px; + border-radius: 12px; + border: 1px solid var(--oak-border-color); + margin: 10px 0px; +} + +.title { + display: flex; + align-items: center; + justify-content: space-between; +} \ No newline at end of file diff --git a/es/components/passport/oauth/index.d.ts b/es/components/passport/oauth/index.d.ts new file mode 100644 index 000000000..0e6e3ae1d --- /dev/null +++ b/es/components/passport/oauth/index.d.ts @@ -0,0 +1,15 @@ +import React from "react"; +import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig, NameConfig, OAuthConfig } from "../../../entities/Passport"; +import { EntityDict } from "../../../oak-app-domain"; +export default function Oauth(props: { + passport: EntityDict['passport']['OpSchema'] & { + stateColor: string; + }; + t: (k: string, params?: any) => string; + changeEnabled: (enabled: boolean) => void; + updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig, path: string, value: any, type?: string) => void; + oauthOptions: { + label: string; + value: string; + }[]; +}): React.JSX.Element; diff --git a/es/components/passport/oauth/index.js b/es/components/passport/oauth/index.js new file mode 100644 index 000000000..87a5db81f --- /dev/null +++ b/es/components/passport/oauth/index.js @@ -0,0 +1,32 @@ +import React, { useEffect, useState } from "react"; +import { Switch, Form, Select, Tag, Tooltip, } from 'antd'; +import Styles from './web.module.less'; +export default function Oauth(props) { + const { passport, t, changeEnabled, updateConfig, oauthOptions } = props; + const { id, type, enabled, stateColor } = passport; + const config = passport.config || {}; + const [oauthIds, setOauthIds] = useState(config?.oauthIds); + useEffect(() => { + setOauthIds(config?.oauthIds || []); + }, [config]); + return (
+
+ {t(`passport:v.type.${type}`)} + 0) ? '' : '请先启用oauth供应商'}> + { + changeEnabled(checked); + }} disabled={!(oauthOptions && oauthOptions?.length > 0)}/> + +
+ {enabled && +
+
+ + } placeholder={t('placeholder.Email')} onChange={(e) => { + } placeholder={t('placeholder.Email', { digit })} onChange={(e) => { inputChange('email', e.target.value); }} className={Style['loginbox-input']}/> - { + { inputChange('captcha', e.target.value); }} className={Style['loginbox-input']} suffix={ */} + +
)}
{Tip}
- {ScanMethods} + {(scanOptions?.length > 0 || oauthOptions?.length > 0) ? (
+ {t('otherMethods')} + }> + {ScanMethods} + {OauthMethods} + +
) : (<>)} ) : (<> {loginMode === 'wechatWeb' &&
@@ -123,7 +150,17 @@ export default function Render(props) { {Tip}
} - {InputMethods} + {loginMode === 'wechatMpForWeb' &&
+ + {Tip} +
} + {(inputOptions?.length > 0 || oauthOptions?.length > 0) ? (
+ {t('otherMethods')} + }> + {InputMethods} + {OauthMethods} + +
) : (<>)} )} )}
diff --git a/es/components/user/login/web.module.less b/es/components/user/login/web.module.less index 3408eebb0..02efe2e59 100644 --- a/es/components/user/login/web.module.less +++ b/es/components/user/login/web.module.less @@ -41,25 +41,27 @@ position: relative; padding: 32px; height: 220px; + padding-bottom: 0px; } &-password { position: relative; padding: 32px; height: 220px; + padding-bottom: 0px; } &-email { position: relative; padding: 32px; height: 220px; + padding-bottom: 0px; } &-qrcode { - padding: 0 32px; + padding: 16px 32px; font-size: 14px; - height: 268px; - padding-top: 16px; + min-height: 268px; &__sociallogin { text-align: center; @@ -126,4 +128,11 @@ font-size: 13px; color: #808080; } + + &-register { + display: flex; + align-items: center; + justify-content: flex-end; + } + } \ No newline at end of file diff --git a/es/components/user/password/update/index.js b/es/components/user/password/update/index.js index f532eb4fb..0cec4926e 100644 --- a/es/components/user/password/update/index.js +++ b/es/components/user/password/update/index.js @@ -34,9 +34,8 @@ export default OakComponent({ }, lifetimes: { async ready() { - const application = this.features.application.getApplication(); - const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); - const passwordConfig = applicationPassports.find((ele) => ele.passport.type === 'password')?.passport.config; + const system = this.features.application.getApplication().system; + const passwordConfig = system?.config.Password; const mode = passwordConfig?.mode ?? 'all'; const pwdMin = passwordConfig?.min ?? 8; const pwdMax = passwordConfig?.max ?? 24; diff --git a/es/components/user/password/verify/index.js b/es/components/user/password/verify/index.js index 1c7bd268e..752bed879 100644 --- a/es/components/user/password/verify/index.js +++ b/es/components/user/password/verify/index.js @@ -11,9 +11,8 @@ export default OakComponent({ lifetimes: { async ready() { this.features.token.getToken(); - const application = this.features.application.getApplication(); - const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); - const passwordConfig = applicationPassports.find((ele) => ele.passport.type === 'password')?.passport.config; + const system = this.features.application.getApplication().system; + const passwordConfig = system?.config.Password; const mode = passwordConfig?.mode ?? 'all'; this.setState({ mode, diff --git a/es/components/user/register/index.d.ts b/es/components/user/register/index.d.ts new file mode 100644 index 000000000..c7a58955e --- /dev/null +++ b/es/components/user/register/index.d.ts @@ -0,0 +1,6 @@ +import { EntityDict } from "../../../oak-app-domain"; +declare const _default: (props: import("oak-frontend-base").ReactComponentProps void) | undefined; + goBack: (() => void) | undefined; +}>) => React.ReactElement; +export default _default; diff --git a/es/components/user/register/index.js b/es/components/user/register/index.js new file mode 100644 index 000000000..678be5dc8 --- /dev/null +++ b/es/components/user/register/index.js @@ -0,0 +1,249 @@ +import { OakPreConditionUnsetException } from "oak-domain/lib/types"; +import { encryptPasswordSha1 } from "../../../utils/password"; +export default OakComponent({ + isList: false, + data: { + allowRegister: false, + loginNameMin: 2, + loginNameMax: 8, + loginNameNeedVerify: false, + loginNameRegexs: [], + loginNameTip: '', + mode: 'all', + pwdMin: 8, + pwdMax: 24, + pwdNeedVerify: false, + pwdRegexs: [], + pwdTip: '', + loginNameRulesMp: [], //小程序校验账号 + passwordRulesMp: [], //小程序校验密码 + confirmRulesMp: [], //小程序校验密码确认 + loginName: '', + password: '', + confirm: '', + loginNameHasErr: false, + passwordHasErr: false, + confirmHasErr: false, + }, + properties: { + goLogin: undefined, + goBack: undefined, + }, + lifetimes: { + async ready() { + const application = this.features.application.getApplication(); + const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); + const loginNameAP = applicationPassports.find((ele) => ele.passport.type === 'loginName'); + const loginNameConfig = loginNameAP?.passport?.config; + const loginNameMin = loginNameConfig?.min ?? 2; + const loginNameMax = loginNameConfig?.max ?? 8; + const loginNameNeedVerify = loginNameConfig?.verify; + const loginNameRegexs = (loginNameConfig?.regexs && loginNameConfig?.regexs.length > 0) ? loginNameConfig?.regexs : []; + const loginNameTip = loginNameConfig?.tip; + const allowRegister = !!loginNameConfig?.register; + const pwdConfig = application?.system?.config?.Password; + const mode = pwdConfig?.mode ?? 'all'; + const pwdMin = pwdConfig?.min ?? 8; + const pwdMax = pwdConfig?.max ?? 24; + const pwdNeedVerify = !!pwdConfig?.verify; + const pwdRegexs = (pwdConfig?.regexs && pwdConfig?.regexs.length > 0) ? pwdConfig?.regexs : []; + const pwdTip = pwdConfig?.tip ?? ''; + const loginNameRulesMp = [ + { required: true, message: this.t('placeholder.loginName'), trigger: 'blur' }, + // { min: loginNameMin, message: this.t('validator.loginNameMin', { loginNameMin }), trigger: 'change' }, + // { max: loginNameMax, message: this.t('validator.loginNameMax', { loginNameMax }), trigger: 'change' }, + { + validator: (rule, value, callback, source) => { + if (!value || value.length < loginNameMin) { + this.setState({ + loginNameHasErr: true + }); + callback(false); + return; + } + this.setState({ + loginNameHasErr: false + }); + callback(); + }, message: this.t('validator.loginNameMin', { loginNameMin }), trigger: 'change' + }, + { + validator: (rule, value, callback, source) => { + if (!value || value.length > loginNameMax) { + this.setState({ + loginNameHasErr: true + }); + callback(false); + return; + } + this.setState({ + loginNameHasErr: false + }); + callback(); + }, message: this.t('validator.loginNameMax', { loginNameMax }), trigger: 'change' + }, + { + validator: (rule, value, callback, source) => { + if (!!loginNameNeedVerify && loginNameRegexs && loginNameRegexs.length > 0) { + for (const regex of loginNameRegexs) { + const pattern = new RegExp(regex); + if (!pattern.test(value)) { + this.setState({ + loginNameHasErr: true + }); + callback(false); + return; + } + } + } + this.setState({ + loginNameHasErr: false + }); + callback(); + }, + message: this.t('validator.loginNameVerify'), + trigger: 'change' + } + ]; + const passwordRulesMp = [ + { required: true, message: this.t('placeholder.password'), trigger: 'blur' }, + // { min: pwdMin, message: this.t('validator.pwdMin', { pwdMin }), trigger: 'change' }, + // { max: pwdMax, message: this.t('validator.pwdMax', { pwdMax }), trigger: 'change' }, + { + validator: (rule, value, callback, source) => { + if (!value || value.length < pwdMin) { + this.setState({ + passwordHasErr: true + }); + callback(false); + return; + } + this.setState({ + passwordHasErr: false + }); + callback(); + }, message: this.t('validator.pwdMin', { pwdMin }), trigger: 'change' + }, + { + validator: (rule, value, callback, source) => { + if (!value || value.length > pwdMax) { + this.setState({ + passwordHasErr: true + }); + callback(false); + return; + } + this.setState({ + passwordHasErr: false + }); + callback(); + }, message: this.t('validator.pwdMax', { pwdMax }), trigger: 'change' + }, + { + validator: (rule, value, callback, source) => { + if (!!pwdNeedVerify && pwdRegexs && pwdRegexs.length > 0) { + for (const regex of pwdRegexs) { + const pattern = new RegExp(regex); + if (!pattern.test(value)) { + this.setState({ + hasErr: true + }); + callback(false); + return; + } + } + } + this.setState({ + hasErr: false + }); + callback(); + }, + message: this.t('validator.pwdVerify'), + trigger: 'change' + } + ]; + const confirmRulesMp = [ + { required: true, message: this.t('validator.noRepwd'), trigger: 'blur' }, + { + validator: (rule, value, callback, source) => { + const { password } = this.state; + if (password !== value) { + this.setState({ + confirmHasErr: true + }); + callback(false); + return; + } + this.setState({ + confirmHasErr: false + }); + callback(); + }, + message: this.t('validator.pwdDiff'), + trigger: 'change' + } + ]; + this.setState({ + allowRegister, + loginNameMin, + loginNameMax, + loginNameNeedVerify, + loginNameRegexs, + loginNameTip, + mode, + pwdMin, + pwdMax, + pwdNeedVerify, + pwdRegexs, + pwdTip, + loginNameRulesMp, + passwordRulesMp, + confirmRulesMp, + }, () => this.reRender()); + }, + }, + methods: { + async onConfirm(loginName, password) { + const { mode } = this.state; + let pwd = password; + if (mode === 'sha1') { + pwd = encryptPasswordSha1(password); + } + try { + await this.features.cache.exec('registerUserByLoginName', { + loginName, + password: pwd + }); + this.setMessage({ + type: 'success', + content: this.t('success') + }); + } + catch (err) { + if (err instanceof OakPreConditionUnsetException || err.name === 'OakPreConditionUnsetException') { + this.setMessage({ + type: 'error', + content: err.message + }); + } + else { + throw err; + } + } + }, + setValueMp(input) { + const { detail, target: { dataset }, } = input; + const { attr } = dataset; + const { value } = detail; + this.setState({ [attr]: value }); + }, + async onConfirmMp() { + const { loginName, password, } = this.state; + await this.onConfirm(loginName, password); + }, + goLoginMp() { + const { goLogin } = this.props; + goLogin && goLogin(); + } + }, +}); diff --git a/es/components/user/register/index.json b/es/components/user/register/index.json new file mode 100644 index 000000000..4feb55b3b --- /dev/null +++ b/es/components/user/register/index.json @@ -0,0 +1,10 @@ +{ + "navigationBarTitleText": "账号注册", + "enablePullDownRefresh": false, + "usingComponents": { + "l-button": "@oak-frontend-base/miniprogram_npm/lin-ui/button/index", + "l-form": "@oak-frontend-base/miniprogram_npm/lin-ui/form/index", + "l-form-item": "@oak-frontend-base/miniprogram_npm/lin-ui/form-item/index", + "l-input": "@oak-frontend-base/miniprogram_npm/lin-ui/input/index" + } +} diff --git a/es/components/user/register/index.less b/es/components/user/register/index.less new file mode 100644 index 000000000..8c80b59df --- /dev/null +++ b/es/components/user/register/index.less @@ -0,0 +1,80 @@ +/** index.wxss **/ +@import "../../../config/styles/mp/index.less"; +@import "../../../config/styles/mp/mixins.less"; + +.page-body { + height: 100%; + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + align-items: center; + box-sizing: border-box; + background-color: @oak-bg-color-container; + .safe-area-inset-bottom(); +} + +.register-box { + padding: 32rpx; + min-width: 80vw; +} + +.regitser-title { + padding-bottom: 24rpx; + font-size: 40rpx; + font-weight: 600; + text-align: center; +} + +.register-body { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + width: 100%; +} + +.my-input { + padding-right: 0rpx !important; + padding-left: 0rpx !important; + height: 80rpx !important; + line-height: 80rpx !important; + border: 2rpx solid #D9d9D9; + border-radius: 16rpx; +} + +.input { + padding-left: 12rpx !important; +} + +.formItem { + padding: 0rpx !important; + min-width: 80vw; +} + +.my-btn { + margin-top: 48rpx; +} + +.label { + display: flex; + align-items: flex-start; + justify-content: flex-start; + gap: 8rpx; + color: #000; +} + +.help { + font-size: 24rpx; +} + +.login { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + color: @oak-color-primary; + font-size: 28rpx; + margin-top: 24rpx; +} \ No newline at end of file diff --git a/es/components/user/register/index.xml b/es/components/user/register/index.xml new file mode 100644 index 000000000..3459ef4f4 --- /dev/null +++ b/es/components/user/register/index.xml @@ -0,0 +1,104 @@ + + + {{t('registerTitle')}} + + + + + + * + {{t('label.loginName')}} + + ({{loginNameTip}}) + + + + + + + * + {{t('label.password')}} + + ({{pwdTip}}) + + + + + + + * + {{t('label.rePwd')}} + + + + + + {{t('register')}} + + + + + + \ No newline at end of file diff --git a/es/components/user/register/locales/zh_CN.json b/es/components/user/register/locales/zh_CN.json new file mode 100644 index 000000000..d789970c9 --- /dev/null +++ b/es/components/user/register/locales/zh_CN.json @@ -0,0 +1,27 @@ +{ + "not allow register": "暂未支持自行注册,请联系管理员为您分配账号!", + "registerTitle": "账号注册", + "label": { + "loginName": "账号", + "password": "密码", + "rePwd": "密码确认" + }, + "placeholder": { + "loginName": "请输入账号", + "password": "请输入密码", + "rePwd": "请再次输入密码" + }, + "validator": { + "loginNameMin": "账号最短长度为%{loginNameMin}位", + "loginNameMax": "账号最大长度为%{loginNameMax}位", + "loginNameVerify": "当前账号未符合规范", + "pwdMin": "密码最短长度为%{pwdMin}位", + "pwdMax": "密码最短长度为%{pwdMax}位", + "pwdVerify": "当前密码较弱", + "pwdDiff": "两次输入的密码不一致,请检查", + "noRepwd": "请再次确认密码" + }, + "register": "立即注册", + "goLogin": "已有账号?立即登录", + "success": "注册成功,请前往登录页登录" +} diff --git a/es/components/user/register/web.d.ts b/es/components/user/register/web.d.ts new file mode 100644 index 000000000..9a8b9ff5e --- /dev/null +++ b/es/components/user/register/web.d.ts @@ -0,0 +1,21 @@ +import React from 'react'; +import { WebComponentProps } from 'oak-frontend-base'; +import { EntityDict } from '../../../oak-app-domain'; +export default function Render(props: WebComponentProps void; + goBack: () => void; +}, { + onConfirm: (loginName: string, password: string) => Promise; +}>): React.JSX.Element; diff --git a/es/components/user/register/web.js b/es/components/user/register/web.js new file mode 100644 index 000000000..cb95761d6 --- /dev/null +++ b/es/components/user/register/web.js @@ -0,0 +1,173 @@ +import React, { useState } from 'react'; +import { Form, Input, Button, } from 'antd'; +import { EyeTwoTone, EyeInvisibleOutlined, LeftOutlined, } from '@ant-design/icons'; +import classNames from 'classnames'; +import Style from './web.module.less'; +export default function Render(props) { + const { data, methods } = props; + const { width, allowRegister, loginNameMin, loginNameMax, loginNameNeedVerify, loginNameRegexs, loginNameTip, pwdMin, pwdMax, pwdNeedVerify, pwdRegexs, pwdTip, goLogin, goBack, oakExecuting, oakLoading, } = data; + const { t, onConfirm } = methods; + const [loginName, setLoginName] = useState(''); + const [loginNameHelp, setLoginNameHelp] = useState(''); + const [loginNameStatus, setLoginNameStatus] = useState(''); + const [password, setPassword] = useState(''); + const [password2, setPassword2] = useState(''); + const [validateHelp, setValidateHelp] = useState(''); + const [validateHelp2, setValidateHelp2] = useState(''); + const [validateStatus, setValidateStatus] = useState(''); + if (!allowRegister) { + return (
+
+ {t('not allow register')} +
+
); + } + return (
+ {!!goBack && goBack()}/>} +
+
{t('registerTitle')}
+
+
+ + { + if (value.length < loginNameMin) { + setLoginNameHelp(t('validator.loginNameMin', { loginNameMin })); + setLoginNameStatus('error'); + return; + } + else if (value.length > loginNameMax) { + setLoginNameHelp(t('validator.loginNameMax', { loginNameMax })); + setLoginNameStatus('error'); + return; + } + else if (!!loginNameNeedVerify && loginNameRegexs && loginNameRegexs.length > 0) { + for (const regex of loginNameRegexs) { + const pattern = new RegExp(regex); + if (!pattern.test(value)) { + setLoginNameHelp(t('validator.loginNameVerify')); + setLoginNameStatus('error'); + return; + } + } + } + setLoginNameHelp(''); + setLoginNameStatus('success'); + } + }, + ]}> + setLoginName(currentTarget.value)} placeholder={t('placeholder.loginName')} value={loginName}/> + + { + if (value.length < pwdMin) { + setValidateHelp(t('validator.pwdMin', { pwdMin })); + setValidateStatus('error'); + return; + } + else if (value.length > pwdMax) { + setValidateHelp(t('validator.pwdMax', { pwdMax })); + setValidateStatus('error'); + return; + } + else if (!!pwdNeedVerify && pwdRegexs && pwdRegexs.length > 0) { + for (const regex of pwdRegexs) { + const pattern = new RegExp(regex); + if (!pattern.test(value)) { + setValidateHelp(t('validator.pwdVerify')); + setValidateHelp2(''); + setValidateStatus('error'); + return; + } + } + if (password2) { + setValidateHelp(''); + setValidateHelp2(value === password2 + ? '' + : t('validator.pwdDiff')); + setValidateStatus(value === password2 + ? 'success' + : 'error'); + } + else { + setValidateHelp2(t('noRepwd')); + setValidateHelp(''); + setValidateStatus('error'); + } + } + else { + if (password2) { + setValidateHelp(''); + setValidateHelp2(value === password2 + ? '' + : t('validator.pwdDiff')); + setValidateStatus(value === password2 + ? 'success' + : 'error'); + } + else { + setValidateHelp2(t('validator.noRepwd')); + setValidateHelp(''); + setValidateStatus('error'); + } + } + } + }, + ]} hasFeedback validateStatus={validateStatus}> + { + const strValue = e.target.value; + setPassword(strValue); + }} iconRender={(visible) => (visible ? : )} placeholder={t('placeholder.password')}/> + + { + if (password.length < pwdMin || password.length > pwdMax) { + return; + } + else if (!!pwdNeedVerify && pwdRegexs && pwdRegexs.length > 0) { + for (const regex of pwdRegexs) { + const pattern = new RegExp(regex); + if (!pattern.test(password)) { + return; + } + } + } + setValidateHelp2(value === password ? '' : t('validator.pwdDiff')); + setValidateStatus(value === password ? 'success' : 'error'); + } + }, + ]} validateTrigger="onChange" help={validateHelp2} validateStatus={validateStatus} hasFeedback> + { + const strValue = e.target.value; + setPassword2(strValue); + }} iconRender={(visible) => (visible ? : )} placeholder={t('placeholder.rePwd')}/> + + + + + {!!goLogin && (
+ +
)} +
+
); +} diff --git a/es/components/user/register/web.module.less b/es/components/user/register/web.module.less new file mode 100644 index 000000000..393bf205c --- /dev/null +++ b/es/components/user/register/web.module.less @@ -0,0 +1,145 @@ +.registerbox { + &-main { + height: 100%; + display: flex; + flex: 1; + align-items: center; + flex-direction: column; + justify-content: center; + background: var(--oak-bg-color-container); + } + + &-logo { + width: 194px; + margin-bottom: 20px; + } + + &-wrap { + width: 400px; + display: block; + background: var(--oak-bg-color-container); + border-radius: 4px; + overflow: hidden; + box-shadow: 0 2px 4px rgb(0 0 0 / 8%), 0 0 4px rgb(0 0 0 / 8%); + transition: all 0.5s; + position: relative; + } + + &-hd { + padding: 32px; + padding-bottom: 0px; + font-size: 20px; + font-weight: 600; + text-align: center; + } + + &-bd { + padding: 32px; + padding-top: 24px; + } + + &-only { + padding-top: 32px !important; + } + + &-mobile { + position: relative; + padding: 32px; + height: 220px; + padding-bottom: 0px; + } + + &-password { + position: relative; + padding: 32px; + height: 220px; + padding-bottom: 0px; + } + + &-email { + position: relative; + padding: 32px; + height: 220px; + padding-bottom: 0px; + } + + &-qrcode { + padding: 16px 32px; + font-size: 14px; + height: 268px; + + &__sociallogin { + text-align: center; + color: #999; + } + + + &__refresh { + color: var(--oak-text-color-brand); + margin-left: 10px; + cursor: pointer; + + &-icon { + color: var(--oak-text-color-brand); + font-size: 14px; + margin-left: 4px; + } + } + + &__iframe { + position: relative; + width: 300px; + margin: 0 auto; + } + } + + &-input { + // background-color: rgba(0, 0, 0, .04) !important; + } + + &-ft { + height: 54px; + border-top: 1px solid #f2f3f5; + font-size: 14px; + + &__btn {} + } + + &-protocal { + padding: 20px 32px; + } + + &-current { + color: var(--oak-text-color-brand) !important; + cursor: default; + background-color: #fff; + } + + &-methods { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0px 24px 16px 24px; + font-size: 13px; + color: #6c7d8f; + } + + &-tip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 13px; + color: #808080; + } + + &-login { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + padding-top: 18px; + } + +} \ No newline at end of file diff --git a/es/components/userRelation/upsert/onUser/index.js b/es/components/userRelation/upsert/onUser/index.js index 370179a1d..88bae726e 100644 --- a/es/components/userRelation/upsert/onUser/index.js +++ b/es/components/userRelation/upsert/onUser/index.js @@ -84,9 +84,8 @@ export default OakComponent({ }, lifetimes: { async ready() { - const application = this.features.application.getApplication(); - const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); - const passwordConfig = applicationPassports.find((ele) => ele.passport.type === 'password')?.passport.config; + const system = this.features.application.getApplication().system; + const passwordConfig = system?.config.Password; const mode = passwordConfig?.mode ?? 'all'; const pwdMin = passwordConfig?.min ?? 8; const pwdMax = passwordConfig?.max ?? 24; diff --git a/es/components/wechatLogin/confirm/index.js b/es/components/wechatLogin/confirm/index.js index 0f8623aed..d7f1613b3 100644 --- a/es/components/wechatLogin/confirm/index.js +++ b/es/components/wechatLogin/confirm/index.js @@ -41,6 +41,15 @@ export default OakComponent({ const protocol = window.location.protocol === 'https:' ? 'https' : 'http'; const redirectUri = encodeURIComponent(`${protocol}://${host}${wechatUserLoginPage}?wechatLoginId=${wechatLoginId}`); window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`; + }, + async loginByWechatMp() { + const { loginUserId } = this.state; + if (!loginUserId) { + // 先小程序登录 + await this.features.token.loginWechatMp(); + } + await this.features.token.loginWechatMp({ wechatLoginId: this.props.oakId }); + this.refresh(); } }, }); diff --git a/es/components/wechatLogin/confirm/index.less b/es/components/wechatLogin/confirm/index.less index 5bb28b408..c388362d7 100644 --- a/es/components/wechatLogin/confirm/index.less +++ b/es/components/wechatLogin/confirm/index.less @@ -9,6 +9,8 @@ flex-direction: column; background-color: @oak-bg-color-container; box-sizing: border-box; + align-items: center; + justify-content: center; .safe-area-inset-bottom(); } @@ -19,19 +21,6 @@ margin-bottom: 60rpx; } -.circle-view { - margin-top: 30rpx; - padding: 10rpx; - width: 200rpx; - height: 200rpx; - border-radius: 50%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background-color: #fff; -} - .title { font-size: 32rpx; color: @oak-text-color-primary; @@ -41,4 +30,23 @@ margin-top: 16rpx; font-size: 28rpx; color: @oak-text-color-secondary; +} + +.circle-view { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.text { + font-size: 36rpx; + color: @oak-text-color-primary; + margin-top: 16rpx; +} + +.desc { + font-size: 24rpx; + color: @oak-text-color-secondary; + margin-top: 16rpx; } \ No newline at end of file diff --git a/es/components/wechatLogin/confirm/index.xml b/es/components/wechatLogin/confirm/index.xml index 9eea2a130..4f415e633 100644 --- a/es/components/wechatLogin/confirm/index.xml +++ b/es/components/wechatLogin/confirm/index.xml @@ -1,4 +1,30 @@ - 绑定小程序尚未实现 + + + + 二维码已过期,请重新扫码 + 抱歉,该码已过期 + + + + + + 登录成功 + + + + 一键登录 + + + + + 绑定小程序尚未实现 + \ No newline at end of file diff --git a/es/components/wechatLogin/qrCode/index.d.ts b/es/components/wechatLogin/qrCode/index.d.ts index 45b011d38..94eb43a76 100644 --- a/es/components/wechatLogin/qrCode/index.d.ts +++ b/es/components/wechatLogin/qrCode/index.d.ts @@ -5,5 +5,6 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps< size: undefined; disableBack: boolean; wechatLoginConfirmPage: string; + qrCodeType: EntityDict["wechatLogin"]["Schema"]["qrCodeType"]; }>) => React.ReactElement; export default _default; diff --git a/es/components/wechatLogin/qrCode/index.js b/es/components/wechatLogin/qrCode/index.js index 7e075fbbf..7b2e9476b 100644 --- a/es/components/wechatLogin/qrCode/index.js +++ b/es/components/wechatLogin/qrCode/index.js @@ -30,17 +30,19 @@ export default OakComponent({ url: '', // 扫码登录/绑定成功跳转的页面 size: undefined, disableBack: false, // 扫码登录/绑定成功后 是否禁用返回 - wechatLoginConfirmPage: '/wechatLogin/confirm' + wechatLoginConfirmPage: '/wechatLogin/confirm', + qrCodeType: 'wechatPublic', }, methods: { async createWechatLogin() { - const { type = 'bind', wechatLoginConfirmPage } = this.props; + const { type = 'bind', wechatLoginConfirmPage, qrCodeType } = this.props; const { result: wechatLoginId } = await this.features.cache.exec('createWechatLogin', { type, interval: Interval, router: { pathname: wechatLoginConfirmPage - } + }, + qrCodeType, }); this.setState({ wechatLoginId, diff --git a/es/components/wechatUser/login/index.js b/es/components/wechatUser/login/index.js index 811bbedba..0e5985fd5 100644 --- a/es/components/wechatUser/login/index.js +++ b/es/components/wechatUser/login/index.js @@ -6,10 +6,10 @@ export default OakComponent({ }, lifetimes: { attached() { - if (process.env.OAK_PLATFORM === 'web') { - //处理微信授权登录 - this.login(); - } + // if (process.env.OAK_PLATFORM === 'web') { + //处理微信授权登录 + this.login(); + // } }, }, methods: { diff --git a/es/data/i18n.js b/es/data/i18n.js index fd3e96c77..919f614bc 100644 --- a/es/data/i18n.js +++ b/es/data/i18n.js @@ -266,7 +266,8 @@ const i18ns = [ "missing_client_id": "缺少 client_id 参数", "unknown": "未知错误,请稍后重试" } - } + }, + "login": "当前暂未支持该第三方应用授权登录" } } }, @@ -665,10 +666,15 @@ const i18ns = [ "Login": "登录", "Send": "发送验证码", "placeholder": { - "Captcha": "输入4位验证码", + "Captcha": "输入%{digit}位验证码", "Email": "请输入邮箱" }, - "resendAfter": "秒后可重发" + "resendAfter": "秒后可重发", + "loginMode": { + "wechatMp": "一键登录", + "sms": "短信登录", + "password": "密码登录" + } } }, { @@ -692,7 +698,12 @@ const i18ns = [ "resendAfter": "秒后可重发", "otherMethods": "其他登录方式", "scanLogin": "扫码登录", - "tip": "未注册用户首次登录将自动注册" + "tip": "未注册用户首次登录将自动注册", + "goRegister": "去注册", + "loginMode": { + "wechatMp": "授权登录", + "other": "其他方式登录" + } } }, { @@ -704,10 +715,17 @@ const i18ns = [ data: { "Login": "登录", "placeholder": { - "Account": "请输入账号", - "Mobile": "/手机号", - "Email": "/邮箱", + "Account": "请输入", + "LoginName": "账号", + "Mobile": "手机号", + "Email": "邮箱", "Password": "请输入密码" + }, + "register": "去注册", + "loginMode": { + "wechatMp": "一键登录", + "sms": "短信登录", + "email": "邮箱登录" } } }, @@ -724,7 +742,12 @@ const i18ns = [ "Captcha": "输入%{digit}位短信验证码", "Mobile": "请输入手机号" }, - "resendAfter": "秒后可重发" + "resendAfter": "秒后可重发", + "loginMode": { + "wechatMp": "一键登录", + "password": "密码登录", + "email": "邮箱登录" + } } }, { @@ -786,6 +809,40 @@ const i18ns = [ } } }, + { + id: "36c643dbcc19c3258f6077a6684236ff", + namespace: "oak-general-business-c-user-register", + language: "zh-CN", + module: "oak-general-business", + position: "src/components/user/register", + data: { + "not allow register": "暂未支持自行注册,请联系管理员为您分配账号!", + "registerTitle": "账号注册", + "label": { + "loginName": "账号", + "password": "密码", + "rePwd": "密码确认" + }, + "placeholder": { + "loginName": "请输入账号", + "password": "请输入密码", + "rePwd": "请再次输入密码" + }, + "validator": { + "loginNameMin": "账号最短长度为%{loginNameMin}位", + "loginNameMax": "账号最大长度为%{loginNameMax}位", + "loginNameVerify": "当前账号未符合规范", + "pwdMin": "密码最短长度为%{pwdMin}位", + "pwdMax": "密码最短长度为%{pwdMax}位", + "pwdVerify": "当前密码较弱", + "pwdDiff": "两次输入的密码不一致,请检查", + "noRepwd": "请再次确认密码" + }, + "register": "立即注册", + "goLogin": "已有账号?立即登录", + "success": "注册成功,请前往登录页登录" + } + }, { id: "5bf96a3e054b8d73c76d7bb45ea90a80", namespace: "oak-general-business-c-userEntityGrant-claim", diff --git a/es/entities/ApplicationPassport.d.ts b/es/entities/ApplicationPassport.d.ts index 858cef186..5b0901ac8 100644 --- a/es/entities/ApplicationPassport.d.ts +++ b/es/entities/ApplicationPassport.d.ts @@ -7,5 +7,6 @@ export interface Schema extends EntityShape { application: Application; passport: Passport; isDefault: Boolean; + allowPwd?: Boolean; } export declare const entityDesc: EntityDesc; diff --git a/es/entities/ApplicationPassport.js b/es/entities/ApplicationPassport.js index 8309e9580..a1d2fdda4 100644 --- a/es/entities/ApplicationPassport.js +++ b/es/entities/ApplicationPassport.js @@ -7,6 +7,7 @@ export const entityDesc = { application: '应用', passport: '登录方式', isDefault: '是否默认', + allowPwd: '是否支持密码登录', }, }, }, diff --git a/es/entities/Passport.d.ts b/es/entities/Passport.d.ts index a68fee51c..d7c4c8f4a 100644 --- a/es/entities/Passport.d.ts +++ b/es/entities/Passport.d.ts @@ -2,7 +2,7 @@ import { Boolean } from 'oak-domain/lib/types/DataType'; import { EntityShape } from 'oak-domain/lib/types/Entity'; import { Schema as System } from './System'; import { EntityDesc } from 'oak-domain/lib/types/EntityDesc'; -export type Type = 'password' | 'sms' | 'email' | 'wechatWeb' | 'wechatMp' | 'wechatPublic' | 'wechatPublicForWeb' | 'wechatMpForWeb' | 'wechatNative'; +export type Type = 'password' | 'sms' | 'email' | 'wechatWeb' | 'wechatMp' | 'wechatPublic' | 'wechatPublicForWeb' | 'wechatMpForWeb' | 'wechatNative' | 'loginName' | 'oauth'; export type SmsConfig = { mockSend?: boolean; defaultOrigin?: 'ali' | 'tencent' | 'ctyun'; @@ -34,10 +34,21 @@ export type PwdConfig = { regexs?: string[]; tip?: string; }; +export type NameConfig = { + min?: number; + max?: number; + verify?: boolean; + regexs?: string[]; + register?: boolean; + tip?: string; +}; +export type OAuthConfig = { + oauthIds: string[]; +}; export interface Schema extends EntityShape { system: System; type: Type; - config?: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig; + config?: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig; enabled: Boolean; } export declare const entityDesc: EntityDesc extends Feature { loginWechat(code: string, params?: { wechatLoginId?: string; }): Promise; - loginWechatMp(): Promise; + loginWechatMp(params?: { + wechatLoginId?: string; + }): Promise; loginWechatNative(code: string): Promise; syncUserInfoWechatMp(): Promise; logout(dontPublish?: boolean): Promise; diff --git a/es/features/token.js b/es/features/token.js index eab7c5302..07cf5c88b 100644 --- a/es/features/token.js +++ b/es/features/token.js @@ -194,12 +194,13 @@ export class Token extends Feature { this.publish(); this.checkNeedSetPassword(); } - async loginWechatMp() { + async loginWechatMp(params) { const { code } = await wx.login(); const env = await this.environment.getEnv(); const { result } = await this.cache.exec('loginWechatMp', { code, env: env, + wechatLoginId: params?.wechatLoginId, }); this.tokenValue = result; await this.storage.save(LOCAL_STORAGE_KEYS.token, result); diff --git a/es/oak-app-domain/ApplicationPassport/Storage.js b/es/oak-app-domain/ApplicationPassport/Storage.js index 2d26caf75..498546be8 100644 --- a/es/oak-app-domain/ApplicationPassport/Storage.js +++ b/es/oak-app-domain/ApplicationPassport/Storage.js @@ -14,6 +14,9 @@ export const desc = { isDefault: { notNull: true, type: "boolean" + }, + allowPwd: { + type: "boolean" } }, actionType: "crud", diff --git a/es/oak-app-domain/ApplicationPassport/_baseSchema.d.ts b/es/oak-app-domain/ApplicationPassport/_baseSchema.d.ts index e79c309df..ab88629a6 100644 --- a/es/oak-app-domain/ApplicationPassport/_baseSchema.d.ts +++ b/es/oak-app-domain/ApplicationPassport/_baseSchema.d.ts @@ -7,6 +7,7 @@ export type OpSchema = EntityShape & { applicationId: ForeignKey<"application">; passportId: ForeignKey<"passport">; isDefault: Boolean; + allowPwd?: Boolean | null; } & { [A in ExpressionKey]?: any; }; @@ -19,6 +20,7 @@ export type OpFilter = { applicationId: Q_StringValue; passportId: Q_StringValue; isDefault: Q_BooleanValue; + allowPwd: Q_BooleanValue; } & ExprOp; export type OpProjection = { "#id"?: NodeId; @@ -30,6 +32,7 @@ export type OpProjection = { applicationId?: number; passportId?: number; isDefault?: number; + allowPwd?: number; } & Partial>; export type OpSortAttr = Partial<{ id: number; @@ -39,6 +42,7 @@ export type OpSortAttr = Partial<{ applicationId: number; passportId: number; isDefault: number; + allowPwd: number; [k: string]: any; } | ExprOp>; export type OpAction = OakMakeAction; diff --git a/es/oak-app-domain/ApplicationPassport/locales/zh_CN.json b/es/oak-app-domain/ApplicationPassport/locales/zh_CN.json index 0b65a14ed..7ef6175f2 100644 --- a/es/oak-app-domain/ApplicationPassport/locales/zh_CN.json +++ b/es/oak-app-domain/ApplicationPassport/locales/zh_CN.json @@ -3,6 +3,7 @@ "attr": { "application": "应用", "passport": "登录方式", - "isDefault": "是否默认" + "isDefault": "是否默认", + "allowPwd": "是否支持密码登录" } } diff --git a/es/oak-app-domain/Passport/Storage.js b/es/oak-app-domain/Passport/Storage.js index df09cf31a..58bc49335 100644 --- a/es/oak-app-domain/Passport/Storage.js +++ b/es/oak-app-domain/Passport/Storage.js @@ -9,7 +9,7 @@ export const desc = { type: { notNull: true, type: "enum", - enumeration: ["password", "sms", "email", "wechatWeb", "wechatMp", "wechatPublic", "wechatPublicForWeb", "wechatMpForWeb", "wechatNative"] + enumeration: ["password", "sms", "email", "wechatWeb", "wechatMp", "wechatPublic", "wechatPublicForWeb", "wechatMpForWeb", "wechatNative", "loginName", "oauth"] }, config: { type: "object" diff --git a/es/oak-app-domain/Passport/Style.js b/es/oak-app-domain/Passport/Style.js index 3e9fbbccb..14d6fcdcc 100644 --- a/es/oak-app-domain/Passport/Style.js +++ b/es/oak-app-domain/Passport/Style.js @@ -9,7 +9,9 @@ export const style = { wechatMp: '#ADDCCA', wechatMpForWeb: '#FDC454', wechatPublicForWeb: '#C0A27C', - wechatNative: '#C0A27C' + wechatNative: '#C0A27C', + loginName: '#456B3C', + oauth: '#3C4655', } } }; diff --git a/es/oak-app-domain/Passport/_baseSchema.d.ts b/es/oak-app-domain/Passport/_baseSchema.d.ts index 72cfb8b53..393c4d556 100644 --- a/es/oak-app-domain/Passport/_baseSchema.d.ts +++ b/es/oak-app-domain/Passport/_baseSchema.d.ts @@ -3,7 +3,7 @@ import { Q_DateValue, Q_BooleanValue, Q_NumberValue, Q_StringValue, Q_EnumValue, import { MakeAction as OakMakeAction, EntityShape } from "oak-domain/lib/types/Entity"; import { GenericAction } from "oak-domain/lib/actions/action"; import { Boolean } from "oak-domain/lib/types/DataType"; -export type Type = "password" | "sms" | "email" | "wechatWeb" | "wechatMp" | "wechatPublic" | "wechatPublicForWeb" | "wechatMpForWeb" | "wechatNative"; +export type Type = "password" | "sms" | "email" | "wechatWeb" | "wechatMp" | "wechatPublic" | "wechatPublicForWeb" | "wechatMpForWeb" | "wechatNative" | "loginName" | "oauth"; export type SmsConfig = { mockSend?: boolean; defaultOrigin?: "ali" | "tencent" | "ctyun"; @@ -35,10 +35,21 @@ export type PwdConfig = { regexs?: string[]; tip?: string; }; +export type NameConfig = { + min?: number; + max?: number; + verify?: boolean; + regexs?: string[]; + register?: boolean; + tip?: string; +}; +export type OAuthConfig = { + oauthIds: string[]; +}; export type OpSchema = EntityShape & { systemId: ForeignKey<"system">; type: Type; - config?: (SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig) | null; + config?: (SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig) | null; enabled: Boolean; } & { [A in ExpressionKey]?: any; @@ -51,7 +62,7 @@ export type OpFilter = { $$updateAt$$: Q_DateValue; systemId: Q_StringValue; type: Q_EnumValue; - config: JsonFilter; + config: JsonFilter; enabled: Q_BooleanValue; } & ExprOp; export type OpProjection = { @@ -63,7 +74,7 @@ export type OpProjection = { $$seq$$?: number; systemId?: number; type?: number; - config?: number | JsonProjection; + config?: number | JsonProjection; enabled?: number; } & Partial>; export type OpSortAttr = Partial<{ diff --git a/es/oak-app-domain/Passport/locales/zh_CN.json b/es/oak-app-domain/Passport/locales/zh_CN.json index 5c16e8f73..cd4865518 100644 --- a/es/oak-app-domain/Passport/locales/zh_CN.json +++ b/es/oak-app-domain/Passport/locales/zh_CN.json @@ -9,14 +9,16 @@ "v": { "type": { "email": "邮箱", - "sms": "短信", + "sms": "手机号", "password": "密码", "wechatMp": "小程序", "wechatPublic": "公众号", "wechatWeb": "微信网站", "wechatMpForWeb": "小程序授权网页", "wechatPublicForWeb": "公众号授权网页", - "wechatNative": "微信APP授权" + "wechatNative": "微信APP授权", + "loginName": "账号", + "oauth": "OAuth授权" } } } diff --git a/es/triggers/application.js b/es/triggers/application.js index e5311d188..8512d536a 100644 --- a/es/triggers/application.js +++ b/es/triggers/application.js @@ -30,7 +30,7 @@ const triggers = [ return !!data.config; }, fn: async ({ operation }, context, option) => { - const { filter } = operation; + const { filter, data } = operation; const applications = await context.select('application', { data: { id: 1, @@ -43,9 +43,8 @@ const triggers = [ let count = 0; for (const application of applications) { if (application.type === 'web') { - const { wechat } = application.config || {}; - const { appId, appSecret } = wechat || {}; - if (!(appId && appId !== '' && appSecret && appSecret !== '')) { + const { appId: newAppId, appSecret: newAppSecret } = data?.config?.wechat || {}; + if (!newAppId || !newAppSecret) { const [passport] = await context.select('passport', { data: { id: 1, @@ -100,7 +99,7 @@ const triggers = [ for (const application of applications) { if (application.type === 'wechatPublic') { const { appId, appSecret, isService } = application.config || {}; - if (appId && appId !== '') { + if (appId) { const [passport] = await context.select('passport', { data: { id: 1, @@ -116,7 +115,7 @@ const triggers = [ count: 1, indexFrom: 0, }, { forUpdate: true }); - if (appSecret && appSecret !== '' && isService) { + if (appSecret && isService) { if (!passport) { await context.operate('passport', { id: await generateNewIdAsync(), @@ -154,7 +153,7 @@ const triggers = [ } else if (application.type === 'wechatMp') { const { appId, appSecret, } = application.config || {}; - if (appId && appId !== '') { + if (appId) { const [passport] = await context.select('passport', { data: { id: 1, @@ -169,7 +168,7 @@ const triggers = [ count: 1, indexFrom: 0, }, { forUpdate: true }); - if (appSecret && appSecret !== '') { + if (appSecret) { if (!passport) { await context.operate('passport', { id: await generateNewIdAsync(), @@ -265,6 +264,66 @@ const triggers = [ return count; } }, + { + name: 'wechatMp applicaiton清空普通链接二维码规则配置时,将相应的passport禁用', + entity: 'application', + action: 'update', + when: 'after', + check: (operation) => { + const { data } = operation; + return !!data.config; + }, + fn: async ({ operation }, context, option) => { + const { filter } = operation; + const applications = await context.select('application', { + data: { + id: 1, + config: 1, + type: 1, + systemId: 1, + }, + filter, + }, {}); + let count = 0; + for (const application of applications) { + if (application.type === 'wechatMp') { + const { qrCodePrefix, appId } = application.config || {}; + if (appId && !qrCodePrefix) { + const [passport] = await context.select('passport', { + data: { + id: 1, + }, + filter: { + enabled: true, + systemId: application.systemId, + type: 'wechatMpForWeb', + config: { + appId, + } + }, + count: 1, + indexFrom: 0, + }, { forUpdate: true }); + if (passport) { + await context.operate('passport', { + id: await generateNewIdAsync(), + action: 'update', + data: { + enabled: false, + config: {}, + }, + filter: { + id: passport.id, + } + }, option); + count++; + } + } + } + } + return count; + } + }, { name: '删除application前,将相关的passport删除', entity: 'application', diff --git a/es/triggers/applicationPassport.d.ts b/es/triggers/applicationPassport.d.ts new file mode 100644 index 000000000..696745594 --- /dev/null +++ b/es/triggers/applicationPassport.d.ts @@ -0,0 +1,5 @@ +import { Trigger } from 'oak-domain/lib/types/Trigger'; +import { EntityDict } from '../oak-app-domain/EntityDict'; +import { BRC } from '../types/RuntimeCxt'; +declare const triggers: Trigger>[]; +export default triggers; diff --git a/es/triggers/applicationPassport.js b/es/triggers/applicationPassport.js new file mode 100644 index 000000000..2a8eb088b --- /dev/null +++ b/es/triggers/applicationPassport.js @@ -0,0 +1,30 @@ +import { assert } from 'oak-domain/lib/utils/assert'; +const triggers = [ + { + name: '当loginName类型的applicationPassport创建前,将其allowPwd置为ture', + entity: 'applicationPassport', + action: 'create', + when: 'before', + fn: async ({ operation }, context, option) => { + const { data } = operation; + assert(!(data instanceof Array)); + const [passport] = await context.select('passport', { + data: { + id: 1, + type: 1, + }, + filter: { + id: data?.passportId, + } + }, { + forUpdate: true, + }); + const { type } = passport || {}; + if (type === 'loginName' && !data.allowPwd) { + data.allowPwd = true; + } + return 1; + } + }, +]; +export default triggers; diff --git a/es/triggers/index.d.ts b/es/triggers/index.d.ts index 9f5cbb943..5bcc4547f 100644 --- a/es/triggers/index.d.ts +++ b/es/triggers/index.d.ts @@ -1,2 +1,2 @@ -declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; +declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; export default _default; diff --git a/es/triggers/index.js b/es/triggers/index.js index 10b6d3060..8b5f7e936 100644 --- a/es/triggers/index.js +++ b/es/triggers/index.js @@ -21,6 +21,7 @@ import oauthProviderTriggers from './oauthProvider'; import oauthUserTriggers from './oauthUser'; import oauthUserAuthTriggers from './oauthUserAuth'; import mobileTriggers from './mobile'; +import applicationPassportTriggers from './applicationPassport'; // import accountTriggers from './account'; export default [ // ...accountTriggers, @@ -47,4 +48,5 @@ export default [ ...oauthUserTriggers, ...oauthUserAuthTriggers, ...mobileTriggers, + ...applicationPassportTriggers, ]; diff --git a/es/triggers/oauthProvider.js b/es/triggers/oauthProvider.js index 0ed0bb9ce..122f0ba32 100644 --- a/es/triggers/oauthProvider.js +++ b/es/triggers/oauthProvider.js @@ -1,4 +1,6 @@ import assert from 'assert'; +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import { cloneDeep, pull } from 'oak-domain/lib/utils/lodash'; const triggers = [ { name: "创建provider时,填充数据", @@ -12,5 +14,193 @@ const triggers = [ return 0; } }, + { + name: "创建provider时,新增passport", + action: "create", + when: "after", + entity: "oauthProvider", + asRoot: true, + fn: async ({ operation }, context, option) => { + const { data } = operation; + let count = 0; + if (data?.systemId) { + const [passport] = await context.select('passport', { + data: { + id: 1, + systemId: 1, + type: 1, + }, + filter: { + systemId: data?.systemId, + type: 'oauth', + }, + indexFrom: 0, + count: 1, + }, { forUpdate: true }); + if (!passport) { + await context.operate('passport', { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + type: 'oauth', + enabled: false, + systemId: data?.systemId, + config: { + oauthIds: [data?.id], + } + } + }, option); + count++; + } + } + return count; + } + }, + { + name: "删除provider时,删除passport", + action: "remove", + when: "before", + entity: "oauthProvider", + asRoot: true, + fn: async ({ operation }, context, option) => { + const { filter } = operation; + const [oauthProvider] = await context.select('oauthProvider', { + data: { + id: 1, + systemId: 1, + }, + filter: filter, + }, { forUpdate: true }); + let count = 0; + if (oauthProvider) { + const { id, systemId } = oauthProvider; + const [other] = await context.select('oauthProvider', { + data: { + id: 1, + systemId: 1, + }, + filter: { + systemId, + id: { + $ne: id, + } + }, + indexFrom: 0, + count: 1, + }, { forUpdate: true }); + if (!other) { + await context.operate('passport', { + id: await generateNewIdAsync(), + action: 'remove', + data: {}, + filter: { + systemId, + type: 'oauth' + } + }, option); + count++; + } + else { + const [passport] = await context.select('passport', { + data: { + id: 1, + type: 1, + config: 1, + systemId: 1, + }, + filter: { + type: 'oauth', + systemId, + config: { + oauthIds: { + $contains: [id], + } + } + } + }, { forUpdate: true }); + if (passport) { + const { id: passportId, config } = passport; + let newConfig = cloneDeep(config); + pull(newConfig?.oauthIds, id); + await context.operate('passport', { + id: await generateNewIdAsync(), + action: 'update', + data: { + config: newConfig, + }, + filter: { + id: passportId, + } + }, option); + } + } + } + return count; + } + }, + { + name: '当provider禁用时,更新passport', + entity: 'oauthProvider', + action: 'update', + when: 'after', + check: (operation) => { + const { data } = operation; + return data.hasOwnProperty('ableState') && data.ableState === 'disabled'; + }, + fn: async ({ operation }, context, option) => { + const { filter } = operation; + const [oauthProvider] = await context.select('oauthProvider', { + data: { + id: 1, + systemId: 1, + ableState: 1, + }, + filter: filter, + }, { forUpdate: true }); + assert(oauthProvider, '禁用oauthProvider的filter请勿包含abledState'); + let count = 0; + const { id, systemId } = oauthProvider; + const [passport] = await context.select('passport', { + data: { + id: 1, + type: 1, + config: 1, + systemId: 1, + enabled: 1, + }, + filter: { + type: 'oauth', + systemId, + config: { + oauthIds: { + $contains: [id], + } + } + } + }, { forUpdate: true }); + if (passport && passport.enabled) { + const { id: passportId, config } = passport; + let newConfig = cloneDeep(config); + pull(newConfig?.oauthIds, id); + if (newConfig?.oauthIds?.length <= 0) { + //无可支持的oauthProvider,将启用了的passport关闭 + await context.operate('passport', { + id: await generateNewIdAsync(), + action: 'update', + data: { + enabled: false, + config: newConfig, + }, + filter: { + id: passport.id, + } + }, option); + count++; + } + } + return count; + } + } ]; export default triggers; diff --git a/es/triggers/system.js b/es/triggers/system.js index 575099ae0..dfae23821 100644 --- a/es/triggers/system.js +++ b/es/triggers/system.js @@ -5,7 +5,7 @@ import { isEmail } from 'oak-domain/lib/utils/validator'; const { attributes: { type: passportType } } = desc; const triggers = [ { - name: '添加system时,添加短信与密码的passport', + name: '添加system时,添加手机号、账号的passport', entity: 'system', action: 'create', when: 'before', @@ -13,9 +13,9 @@ const triggers = [ const { data } = operation; assert(!(data instanceof Array)); data.passport$system = [ - //仅创建短信与密码的passport,其他type由相应的application/Email的更新来创建 + //仅创建手机号、账号、密码的passport,其他type由相应的application/Email的更新来创建 { - //短信登录默认为模拟发送 + //手机号登录默认为模拟发送 id: await generateNewIdAsync(), action: 'create', data: { @@ -28,15 +28,26 @@ const triggers = [ } }, { - //密码登录 + //账号登录 id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), - type: 'password', - enabled: false, + type: 'loginName', + config: {}, + enabled: true, } - } + }, + // { + // //密码登录 + // id: await generateNewIdAsync(), + // action: 'create', + // data: { + // id: await generateNewIdAsync(), + // type: 'password', + // enabled: false, + // } + // } ]; return 1; } diff --git a/es/types/Config.d.ts b/es/types/Config.d.ts index be1c3fb96..c1ed29f4f 100644 --- a/es/types/Config.d.ts +++ b/es/types/Config.d.ts @@ -154,6 +154,14 @@ export type EmailConfig = { name?: string; secure?: boolean; }; +export type PasswordConfig = { + mode?: 'all' | 'plain' | 'sha1'; + min?: number; + max?: number; + verify?: boolean; + regexs?: string[]; + tip?: string; +}; export type QrCodeType = 'wechatMpDomainUrl' | 'wechatMpWxaCode' | 'wechatPublic' | 'wechatPublicForMp' | 'webForWechatPublic'; export type Config = { Account?: { @@ -205,6 +213,7 @@ export type Config = { level?: 'weak' | 'medium' | 'strong'; passwordVerifyGap?: number; }; + Password?: PasswordConfig; }; export type AccountOrigin = 'ali' | 'tencent' | 'qiniu' | 'amap' | 'ctyun' | 'local' | 's3'; export type CosOrigin = 'qiniu' | 'wechat' | 'ctyun' | 'aliyun' | 'tencent' | 'local' | 'unknown' | 's3'; diff --git a/lib/aspects/AspectDict.d.ts b/lib/aspects/AspectDict.d.ts index 7eafb8c4e..1d6d628cd 100644 --- a/lib/aspects/AspectDict.d.ts +++ b/lib/aspects/AspectDict.d.ts @@ -134,11 +134,13 @@ export type AspectDict = { * 微信小程序登录 * @param code 微信授权 code * @param env 小程序环境信息 + * @param wechatLoginId 可选的微信登录 ID(用于扫码登录场景) * @returns 返回登录 token */ - loginWechatMp: ({ code, env, }: { + loginWechatMp: ({ code, env, wechatLoginId, }: { code: string; env: WechatMpEnv; + wechatLoginId?: string; }, context: BackendRuntimeContext) => Promise; /** * 微信原生 APP 登录 @@ -299,6 +301,7 @@ export type AspectDict = { type: EntityDict['wechatLogin']['Schema']['type']; interval: number; router: EntityDict['wechatLogin']['Schema']['router']; + qrCodeType?: EntityDict['wechatLogin']['Schema']['qrCodeType']; }, context: BackendRuntimeContext) => Promise; /** * 解绑微信用户 @@ -758,5 +761,16 @@ export type AspectDict = { headers?: Record; formdata?: Record; }>; + /** + * 用户账号注册 + * @param loginName 账号 + * @param password 密码 + * @param context + * @returns + */ + registerUserByLoginName: (params: { + loginName: string; + password: string; + }, context: BackendRuntimeContext) => Promise; }; export default AspectDict; diff --git a/lib/aspects/applicationPassport.js b/lib/aspects/applicationPassport.js index 333824e6e..f190c2ee5 100644 --- a/lib/aspects/applicationPassport.js +++ b/lib/aspects/applicationPassport.js @@ -1,6 +1,7 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.removeApplicationPassportsByPIds = exports.getApplicationPassports = void 0; +exports.getApplicationPassports = getApplicationPassports; +exports.removeApplicationPassportsByPIds = removeApplicationPassportsByPIds; const uuid_1 = require("oak-domain/lib/utils/uuid"); async function getApplicationPassports(params, context) { const { applicationId } = params; @@ -15,15 +16,20 @@ async function getApplicationPassports(params, context) { config: 1, }, isDefault: 1, + allowPwd: 1, }, filter: { applicationId, + passport: { + type: { + $ne: 'password', + } + } } }, {}); closeRoot(); return applicationPassports; } -exports.getApplicationPassports = getApplicationPassports; async function removeApplicationPassportsByPIds(params, context) { const { passportIds } = params; const applicationPassports = await context.select('applicationPassport', { @@ -51,4 +57,3 @@ async function removeApplicationPassportsByPIds(params, context) { }, {}); } } -exports.removeApplicationPassportsByPIds = removeApplicationPassportsByPIds; diff --git a/lib/aspects/index.d.ts b/lib/aspects/index.d.ts index 297d2787a..430c9dc90 100644 --- a/lib/aspects/index.d.ts +++ b/lib/aspects/index.d.ts @@ -4,7 +4,7 @@ import { getApplication, signatureJsSDK, uploadWechatMedia, batchGetArticle, get import { updateConfig, updateApplicationConfig, updateStyle } from './config'; import { syncWechatTemplate, getMessageType } from './template'; import { syncSmsTemplate } from './sms'; -import { mergeUser, getChangePasswordChannels, updateUserPassword } from './user'; +import { mergeUser, getChangePasswordChannels, updateUserPassword, registerUserByLoginName } from './user'; import { createWechatLogin } from './wechatLogin'; import { unbindingWechat } from './wechatUser'; import { getMpUnlimitWxaCode } from './wechatQrCode'; @@ -88,6 +88,7 @@ declare const aspectDict: { setUserAvatarFromWechat: typeof setUserAvatarFromWechat; mergeChunkedUpload: typeof mergeChunkedUpload; presignFile: typeof presignFile; + registerUserByLoginName: typeof registerUserByLoginName; }; export default aspectDict; export { AspectDict } from './AspectDict'; diff --git a/lib/aspects/index.js b/lib/aspects/index.js index 280ecbaaf..59bba2646 100644 --- a/lib/aspects/index.js +++ b/lib/aspects/index.js @@ -92,5 +92,6 @@ const aspectDict = { // extraFile新增 mergeChunkedUpload: extraFile_1.mergeChunkedUpload, presignFile: extraFile_1.presignFile, + registerUserByLoginName: user_1.registerUserByLoginName, }; exports.default = aspectDict; diff --git a/lib/aspects/oauth.js b/lib/aspects/oauth.js index 57a04f278..cfde86094 100644 --- a/lib/aspects/oauth.js +++ b/lib/aspects/oauth.js @@ -23,6 +23,7 @@ async function loginByOauth(params, context) { // 验证 state 并获取 OAuth 配置 const [state] = await context.select("oauthState", { data: { + providerId: 1, provider: { type: 1, clientId: 1, @@ -39,6 +40,31 @@ async function loginByOauth(params, context) { state: stateCode, }, }, { dontCollect: true }); + const systemId = context.getSystemId(); + const [applicationPassport] = await context.select('applicationPassport', { + data: { + id: 1, + applicationId: 1, + passportId: 1, + passport: { + id: 1, + type: 1, + systemId: 1, + config: 1, + }, + }, + filter: { + passport: { + systemId, + type: 'oauth', + }, + applicationId, + } + }, { dontCollect: true }); + const allowOauth = !!(state.providerId && applicationPassport?.passport?.config?.oauthIds && applicationPassport?.passport?.config)?.oauthIds.includes(state.providerId); + if (!allowOauth) { + throw new types_1.OakUserException('error::user.loginWayDisabled'); + } (0, assert_1.default)(state, '无效的 state 参数'); (0, assert_1.default)(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用'); // 如果已经使用 @@ -127,7 +153,7 @@ async function loginByOauth(params, context) { const { user } = existingOAuthUser; const targetUser = user?.userState === 'merged' ? user.ref : user; const tokenValue = await (0, token_1.setUpTokenAndUser)(env, context, 'oauthUser', existingOAuthUser.id, // 使用已存在的 oauthUser ID - undefined, targetUser // 关联的用户 + undefined, targetUser // 关联的用户 ); // 更新登录信息 await context.operate("oauthUser", { @@ -172,7 +198,7 @@ async function loginByOauth(params, context) { }; // 不传 user 参数,会自动创建新用户 const tokenValue = await (0, token_1.setUpTokenAndUser)(env, context, 'oauthUser', undefined, oauthUserCreateData, // 创建新的 oauthUser - undefined // 不传 user,自动创建新用户 + undefined // 不传 user,自动创建新用户 ); await context.operate("oauthUser", { id: await (0, uuid_1.generateNewIdAsync)(), @@ -367,21 +393,19 @@ async function authorize(params, context) { throw new Error('unknown action'); } const fetchOAuthUserInfo = async (code, providerConfig) => { - const params = { - grant_type: 'authorization_code', - code: code, - client_id: providerConfig.clientId, - client_secret: providerConfig.clientSecret, - redirect_uri: providerConfig.redirectUri, - } - console.log("使用 OAuth Code 获取 Access Token:", providerConfig.tokenEndpoint, params); // 1. 使用 code 换取 access_token const tokenResponse = await fetch(providerConfig.tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, - body: new URLSearchParams(params), + body: new URLSearchParams({ + grant_type: 'authorization_code', + code: code, + client_id: providerConfig.clientId, + client_secret: providerConfig.clientSecret, + redirect_uri: providerConfig.redirectUri, + }), }); if (!tokenResponse.ok) { const errorjson = await tokenResponse.json(); diff --git a/lib/aspects/token.d.ts b/lib/aspects/token.d.ts index e2b32e6d0..0fe2b56bd 100644 --- a/lib/aspects/token.d.ts +++ b/lib/aspects/token.d.ts @@ -77,9 +77,10 @@ export declare function loginWechat({ code, env, wechatLo * @param context * @returns */ -export declare function loginWechatMp({ code, env, }: { +export declare function loginWechatMp({ code, env, wechatLoginId, }: { code: string; env: WechatMpEnv; + wechatLoginId?: string; }, context: BRC): Promise; /** * 同步从wx.getUserProfile拿到的用户信息 diff --git a/lib/aspects/token.js b/lib/aspects/token.js index 8c5dfc23c..aa4d5b81c 100644 --- a/lib/aspects/token.js +++ b/lib/aspects/token.js @@ -562,23 +562,9 @@ async function loginByMobile(params, context) { } async function verifyPassword(params, context) { const { password } = params; - const systemId = context.getSystemId(); - const [pwdPassport] = await context.select('passport', { - data: { - id: 1, - systemId: 1, - config: 1, - type: 1, - enabled: 1, - }, - filter: { - systemId, - enabled: true, - type: 'password', - } - }, { forUpdate: true }); - // assert(pwdPassport); - const pwdMode = pwdPassport?.config?.mode ?? 'all'; + const { system } = context.getApplication(); + const pwdConfig = system?.config.Password; + const pwdMode = pwdConfig?.mode ?? 'all'; let pwdFilter = {}; if (pwdMode === 'all') { pwdFilter = { @@ -633,8 +619,35 @@ async function loginByAccount(params, context) { (0, assert_1.assert)(password); (0, assert_1.assert)(account); const accountType = (0, validator_1.isEmail)(account) ? 'email' : ((0, validator_1.isMobile)(account) ? 'mobile' : 'loginName'); + const applicationPassports = await context.select('applicationPassport', { + data: { + id: 1, + applicationId: 1, + passportId: 1, + passport: { + id: 1, + type: 1, + systemId: 1, + }, + allowPwd: 1, + }, + filter: { + passport: { + systemId, + }, + applicationId, + allowPwd: true, + } + }, { + dontCollect: true, + }); + const allowEmail = !!applicationPassports.find((ele) => ele.passport?.type === 'email'); + const allowSms = !!applicationPassports.find((ele) => ele.passport?.type === 'sms'); if (accountType === 'email') { const { config, emailConfig } = await (0, passport_1.getAndCheckPassportByEmail)(context, account); + if (!allowEmail) { + throw new types_1.OakUserException('暂不支持邮箱登录'); + } const existEmail = await context.select('email', { data: { id: 1, @@ -681,49 +694,23 @@ async function loginByAccount(params, context) { throw new types_1.OakUserException('error::user.passwordUnmath'); } case 1: { - const applicationPassports = await context.select('applicationPassport', { - data: { - id: 1, - applicationId: 1, - passportId: 1, - passport: { - id: 1, - type: 1, - systemId: 1, - } - }, - filter: { - passport: { - systemId, - }, - applicationId, - } - }, { - dontCollect: true, - }); - const allowEmail = !!applicationPassports.find((ele) => ele.passport?.type === 'email'); const [userRow] = result; const { email$user, id: userId, } = userRow; needUpdatePassword = !(userRow.password || userRow.passwordSha1); - if (allowEmail) { - const email = email$user?.find(ele => ele.email.toLowerCase() === account.toLowerCase()); - if (email) { - const ableState = email.ableState; - if (ableState === 'disabled') { - // 虽然密码和邮箱匹配,但邮箱已经禁用了,在可能的情况下提醒用户使用其它方法登录 - const exception = await tryMakeChangeLoginWay(userId, context); - if (exception) { - throw exception; - } + const email = email$user?.find(ele => ele.email.toLowerCase() === account.toLowerCase()); + if (email) { + const ableState = email.ableState; + if (ableState === 'disabled') { + // 虽然密码和邮箱匹配,但邮箱已经禁用了,在可能的情况下提醒用户使用其它方法登录 + const exception = await tryMakeChangeLoginWay(userId, context); + if (exception) { + throw exception; } - return await setupEmail(account, env, context); - } - else { - throw new types_1.OakUserException('error::user.emailUnexists'); } + return await setupEmail(account, env, context); } else { - throw new types_1.OakUserException('error::user.loginWayDisabled'); + throw new types_1.OakUserException('error::user.emailUnexists'); } } default: { @@ -732,6 +719,9 @@ async function loginByAccount(params, context) { } } else if (accountType === 'mobile') { + if (!allowSms) { + throw new types_1.OakUserException('暂不支持手机号登录'); + } const existMobile = await context.select('mobile', { data: { id: 1, @@ -778,53 +768,27 @@ async function loginByAccount(params, context) { throw new types_1.OakUserException('手机号与密码不匹配'); } case 1: { - const applicationPassports = await context.select('applicationPassport', { - data: { - id: 1, - applicationId: 1, - passportId: 1, - passport: { - id: 1, - type: 1, - systemId: 1, - } - }, - filter: { - passport: { - systemId, - }, - applicationId, - } - }, { - dontCollect: true, - }); const [userRow] = result; const { mobile$user, id: userId, } = userRow; needUpdatePassword = !(userRow.password || userRow.passwordSha1); - const allowSms = !!applicationPassports.find((ele) => ele.passport?.type === 'sms'); - if (allowSms) { - const mobile = mobile$user?.find(ele => ele.mobile === account); - if (mobile) { - const ableState = mobile.ableState; - if (ableState === 'disabled') { - // 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录 - const exception = await tryMakeChangeLoginWay(userId, context); - if (exception) { - throw exception; - } + const mobile = mobile$user?.find(ele => ele.mobile === account); + if (mobile) { + const ableState = mobile.ableState; + if (ableState === 'disabled') { + // 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录 + const exception = await tryMakeChangeLoginWay(userId, context); + if (exception) { + throw exception; } - return await setupMobile(account, env, context); - } - else { - throw new types_1.OakUserException('手机号未注册'); } + return await setupMobile(account, env, context); } else { - throw new types_1.OakUserException('暂不支持手机号登录'); + throw new types_1.OakUserException('手机号未注册'); } } default: { - throw new types_1.OakUserException('不支持的登录方式'); + throw new types_1.OakUserException('error::user.loginWayDisabled'); } } } @@ -895,35 +859,15 @@ async function loginByAccount(params, context) { } } default: { - throw new types_1.OakUserException('不支持的登录方式'); + throw new types_1.OakUserException('error::user.loginWayDisabled'); } } } }; const closeRootMode = context.openRootMode(); - const application = context.getApplication(); - const [applicationPassport] = await context.select('applicationPassport', { - data: { - id: 1, - passportId: 1, - passport: { - id: 1, - config: 1, - type: 1, - }, - applicationId: 1, - }, - filter: { - applicationId: application?.id, - passport: { - type: 'password', - }, - } - }, { - dontCollect: true, - }); - // assert(applicationPassport?.passport); - const pwdMode = applicationPassport?.passport?.config?.mode ?? 'all'; + const { system } = context.getApplication(); + const pwdConfig = system?.config.Password; + const pwdMode = pwdConfig?.mode ?? 'all'; let pwdFilter = {}, updateData = {}; if (pwdMode === 'all') { pwdFilter = { @@ -2007,9 +1951,9 @@ async function loginWechat({ code, env, wechatLoginId, }, context) { * @param context * @returns */ -async function loginWechatMp({ code, env, }, context) { +async function loginWechatMp({ code, env, wechatLoginId, }, context) { const closeRootMode = context.openRootMode(); - const tokenValue = await loginFromWechatEnv(code, env, context); + const tokenValue = await loginFromWechatEnv(code, env, context, wechatLoginId); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; diff --git a/lib/aspects/user.d.ts b/lib/aspects/user.d.ts index 092f5827f..3ffb67178 100644 --- a/lib/aspects/user.d.ts +++ b/lib/aspects/user.d.ts @@ -23,3 +23,12 @@ export declare function updateUserPassword(params: { result: string; times?: undefined; }>; +/** + * 用户账号注册 + * @param params + * @param context + */ +export declare function registerUserByLoginName(params: { + loginName: string; + password: string; +}, context: BRC): Promise; diff --git a/lib/aspects/user.js b/lib/aspects/user.js index 4f4e40f06..170ef22e4 100644 --- a/lib/aspects/user.js +++ b/lib/aspects/user.js @@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.mergeUser = mergeUser; exports.getChangePasswordChannels = getChangePasswordChannels; exports.updateUserPassword = updateUserPassword; +exports.registerUserByLoginName = registerUserByLoginName; const tslib_1 = require("tslib"); const types_1 = require("oak-domain/lib/types"); const uuid_1 = require("oak-domain/lib/utils/uuid"); @@ -183,20 +184,17 @@ async function updateUserPassword(params, context, innerLogic) { const systemId = context.getSystemId(); const closeRootMode = context.openRootMode(); try { - const [passport] = await context.select('passport', { + const [system] = await context.select('system', { data: { id: 1, - type: 1, config: 1, - systemId: 1, }, filter: { - systemId, - type: 'password', + id: systemId, } }, { forUpdate: true }); - (0, assert_1.assert)(passport); - const config = passport.config; + (0, assert_1.assert)(system); + const config = system.config?.Password; const mode = config?.mode ?? 'all'; const [user] = await context.select('user', { data: { @@ -263,13 +261,13 @@ async function updateUserPassword(params, context, innerLogic) { }; } const allowUpdate = mode === 'sha1' ? user.passwordSha1 === prevPassword : user.password === prevPassword; //sha1密文模式判断密文是否相等 - let userDate = {}, changeCreateDate = {}; + let userData = {}, changeCreateData = {}; if (mode === 'all') { - userDate = { + userData = { password: newPassword, passwordSha1: (0, password_1.encryptPasswordSha1)(newPassword), }; - changeCreateDate = { + changeCreateData = { prevPassword, newPassword, prevPasswordSha1: (0, password_1.encryptPasswordSha1)(prevPassword), @@ -277,19 +275,19 @@ async function updateUserPassword(params, context, innerLogic) { }; } else if (mode === 'plain') { - userDate = { + userData = { password: newPassword, }; - changeCreateDate = { + changeCreateData = { prevPassword, newPassword, }; } else if (mode === 'sha1') { - userDate = { + userData = { passwordSha1: newPassword, }; - changeCreateDate = { + changeCreateData = { prevPasswordSha1: prevPassword, newPasswordSha1: newPassword, }; @@ -298,7 +296,7 @@ async function updateUserPassword(params, context, innerLogic) { await context.operate('user', { id: await (0, uuid_1.generateNewIdAsync)(), action: 'update', - data: userDate, + data: userData, filter: { id: userId, }, @@ -312,7 +310,7 @@ async function updateUserPassword(params, context, innerLogic) { id: await (0, uuid_1.generateNewIdAsync)(), userId, result: 'success', - ...changeCreateDate, + ...changeCreateData, }, }, { dontCollect: true, @@ -330,7 +328,7 @@ async function updateUserPassword(params, context, innerLogic) { id: await (0, uuid_1.generateNewIdAsync)(), userId, result: 'fail', - ...changeCreateDate, + ...changeCreateData, }, }, { dontCollect: true, @@ -359,13 +357,13 @@ async function updateUserPassword(params, context, innerLogic) { dontCollect: true, }); if (aliveCaptcha) { - let userDate = {}, changeCreateDate = {}; + let userData = {}, changeCreateData = {}; if (mode === 'all') { - userDate = { + userData = { password: newPassword, passwordSha1: (0, password_1.encryptPasswordSha1)(newPassword), }; - changeCreateDate = { + changeCreateData = { prevPassword: user.password, newPassword, prevPasswordSha1: user.passwordSha1, @@ -373,19 +371,19 @@ async function updateUserPassword(params, context, innerLogic) { }; } else if (mode === 'plain') { - userDate = { + userData = { password: newPassword, }; - changeCreateDate = { + changeCreateData = { prevPassword: user.password, newPassword, }; } else if (mode === 'sha1') { - userDate = { + userData = { passwordSha1: newPassword, }; - changeCreateDate = { + changeCreateData = { prevPasswordSha1: user.passwordSha1, newPasswordSha1: newPassword, }; @@ -393,7 +391,7 @@ async function updateUserPassword(params, context, innerLogic) { await context.operate('user', { id: await (0, uuid_1.generateNewIdAsync)(), action: 'update', - data: userDate, + data: userData, filter: { id: userId, }, @@ -407,7 +405,7 @@ async function updateUserPassword(params, context, innerLogic) { id: await (0, uuid_1.generateNewIdAsync)(), userId, result: 'success', - ...changeCreateDate, + ...changeCreateData, }, }, { dontCollect: true, @@ -434,3 +432,83 @@ async function updateUserPassword(params, context, innerLogic) { throw err; } } +/** + * 用户账号注册 + * @param params + * @param context + */ +async function registerUserByLoginName(params, context) { + const { loginName, password } = params; + const systemId = context.getSystemId(); + const closeRootMode = context.openRootMode(); + try { + // 检查loginName是否重复 + const [existLoginName] = await context.select('loginName', { + data: { + id: 1, + name: 1, + }, + filter: { + name: loginName, + ableState: 'enabled', + }, + }, { dontCollect: true, forUpdate: true }); + if (existLoginName) { + closeRootMode(); + throw new types_1.OakPreConditionUnsetException('账号已存在,请重新设置'); + } + // 创建user并附上密码,级联创建loginName + const [system] = await context.select('system', { + data: { + id: 1, + config: 1, + }, + filter: { + id: systemId, + } + }, { forUpdate: true }); + (0, assert_1.assert)(system); + const config = system.config?.Password; + const mode = config?.mode ?? 'all'; + let passwordData = {}; + if (mode === 'all') { + passwordData = { + password: password, + passwordSha1: (0, password_1.encryptPasswordSha1)(password), + }; + } + else if (mode === 'plain') { + passwordData = { + password: password, + }; + } + else if (mode === 'sha1') { + passwordData = { + passwordSha1: password, + }; + } + const userData = { + id: await (0, uuid_1.generateNewIdAsync)(), + loginName$user: [ + { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'create', + data: { + id: await (0, uuid_1.generateNewIdAsync)(), + name: loginName, + } + } + ] + }; + Object.assign(userData, passwordData); + await context.operate('user', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'create', + data: userData, + }, {}); + } + catch (err) { + closeRootMode(); + throw err; + } +} diff --git a/lib/aspects/wechatLogin.d.ts b/lib/aspects/wechatLogin.d.ts index c6bd46f98..1cc98449c 100644 --- a/lib/aspects/wechatLogin.d.ts +++ b/lib/aspects/wechatLogin.d.ts @@ -4,4 +4,5 @@ export declare function createWechatLogin(params: { type: EntityDict['wechatLogin']['Schema']['type']; interval: number; router: EntityDict['wechatLogin']['Schema']['router']; + qrCodeType?: EntityDict['wechatLogin']['Schema']['qrCodeType']; }, context: BRC): Promise; diff --git a/lib/aspects/wechatLogin.js b/lib/aspects/wechatLogin.js index d9d3dd418..a77fd4912 100644 --- a/lib/aspects/wechatLogin.js +++ b/lib/aspects/wechatLogin.js @@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.createWechatLogin = createWechatLogin; const uuid_1 = require("oak-domain/lib/utils/uuid"); async function createWechatLogin(params, context) { - const { type, interval, router } = params; + const { type, interval, qrCodeType = "wechatPublic", router } = params; let userId; if (type === 'bind') { userId = context.getCurrentUserId(); @@ -29,7 +29,7 @@ async function createWechatLogin(params, context) { type, expiresAt: Date.now() + interval, expired: false, - qrCodeType: 'wechatPublic', + qrCodeType, successed: false, router: _router, }; diff --git a/lib/data/i18n.js b/lib/data/i18n.js index 5c7eb392c..0bd174b90 100644 --- a/lib/data/i18n.js +++ b/lib/data/i18n.js @@ -268,7 +268,8 @@ const i18ns = [ "missing_client_id": "缺少 client_id 参数", "unknown": "未知错误,请稍后重试" } - } + }, + "login": "当前暂未支持该第三方应用授权登录" } } }, @@ -667,10 +668,15 @@ const i18ns = [ "Login": "登录", "Send": "发送验证码", "placeholder": { - "Captcha": "输入4位验证码", + "Captcha": "输入%{digit}位验证码", "Email": "请输入邮箱" }, - "resendAfter": "秒后可重发" + "resendAfter": "秒后可重发", + "loginMode": { + "wechatMp": "一键登录", + "sms": "短信登录", + "password": "密码登录" + } } }, { @@ -694,7 +700,12 @@ const i18ns = [ "resendAfter": "秒后可重发", "otherMethods": "其他登录方式", "scanLogin": "扫码登录", - "tip": "未注册用户首次登录将自动注册" + "tip": "未注册用户首次登录将自动注册", + "goRegister": "去注册", + "loginMode": { + "wechatMp": "授权登录", + "other": "其他方式登录" + } } }, { @@ -706,10 +717,17 @@ const i18ns = [ data: { "Login": "登录", "placeholder": { - "Account": "请输入账号", - "Mobile": "/手机号", - "Email": "/邮箱", + "Account": "请输入", + "LoginName": "账号", + "Mobile": "手机号", + "Email": "邮箱", "Password": "请输入密码" + }, + "register": "去注册", + "loginMode": { + "wechatMp": "一键登录", + "sms": "短信登录", + "email": "邮箱登录" } } }, @@ -726,7 +744,12 @@ const i18ns = [ "Captcha": "输入%{digit}位短信验证码", "Mobile": "请输入手机号" }, - "resendAfter": "秒后可重发" + "resendAfter": "秒后可重发", + "loginMode": { + "wechatMp": "一键登录", + "password": "密码登录", + "email": "邮箱登录" + } } }, { @@ -788,6 +811,40 @@ const i18ns = [ } } }, + { + id: "36c643dbcc19c3258f6077a6684236ff", + namespace: "oak-general-business-c-user-register", + language: "zh-CN", + module: "oak-general-business", + position: "src/components/user/register", + data: { + "not allow register": "暂未支持自行注册,请联系管理员为您分配账号!", + "registerTitle": "账号注册", + "label": { + "loginName": "账号", + "password": "密码", + "rePwd": "密码确认" + }, + "placeholder": { + "loginName": "请输入账号", + "password": "请输入密码", + "rePwd": "请再次输入密码" + }, + "validator": { + "loginNameMin": "账号最短长度为%{loginNameMin}位", + "loginNameMax": "账号最大长度为%{loginNameMax}位", + "loginNameVerify": "当前账号未符合规范", + "pwdMin": "密码最短长度为%{pwdMin}位", + "pwdMax": "密码最短长度为%{pwdMax}位", + "pwdVerify": "当前密码较弱", + "pwdDiff": "两次输入的密码不一致,请检查", + "noRepwd": "请再次确认密码" + }, + "register": "立即注册", + "goLogin": "已有账号?立即登录", + "success": "注册成功,请前往登录页登录" + } + }, { id: "5bf96a3e054b8d73c76d7bb45ea90a80", namespace: "oak-general-business-c-userEntityGrant-claim", diff --git a/lib/entities/ApplicationPassport.d.ts b/lib/entities/ApplicationPassport.d.ts index 858cef186..5b0901ac8 100644 --- a/lib/entities/ApplicationPassport.d.ts +++ b/lib/entities/ApplicationPassport.d.ts @@ -7,5 +7,6 @@ export interface Schema extends EntityShape { application: Application; passport: Passport; isDefault: Boolean; + allowPwd?: Boolean; } export declare const entityDesc: EntityDesc; diff --git a/lib/entities/ApplicationPassport.js b/lib/entities/ApplicationPassport.js index 67b153514..a4c0121c3 100644 --- a/lib/entities/ApplicationPassport.js +++ b/lib/entities/ApplicationPassport.js @@ -10,6 +10,7 @@ exports.entityDesc = { application: '应用', passport: '登录方式', isDefault: '是否默认', + allowPwd: '是否支持密码登录', }, }, }, diff --git a/lib/entities/Passport.d.ts b/lib/entities/Passport.d.ts index a68fee51c..d7c4c8f4a 100644 --- a/lib/entities/Passport.d.ts +++ b/lib/entities/Passport.d.ts @@ -2,7 +2,7 @@ import { Boolean } from 'oak-domain/lib/types/DataType'; import { EntityShape } from 'oak-domain/lib/types/Entity'; import { Schema as System } from './System'; import { EntityDesc } from 'oak-domain/lib/types/EntityDesc'; -export type Type = 'password' | 'sms' | 'email' | 'wechatWeb' | 'wechatMp' | 'wechatPublic' | 'wechatPublicForWeb' | 'wechatMpForWeb' | 'wechatNative'; +export type Type = 'password' | 'sms' | 'email' | 'wechatWeb' | 'wechatMp' | 'wechatPublic' | 'wechatPublicForWeb' | 'wechatMpForWeb' | 'wechatNative' | 'loginName' | 'oauth'; export type SmsConfig = { mockSend?: boolean; defaultOrigin?: 'ali' | 'tencent' | 'ctyun'; @@ -34,10 +34,21 @@ export type PwdConfig = { regexs?: string[]; tip?: string; }; +export type NameConfig = { + min?: number; + max?: number; + verify?: boolean; + regexs?: string[]; + register?: boolean; + tip?: string; +}; +export type OAuthConfig = { + oauthIds: string[]; +}; export interface Schema extends EntityShape { system: System; type: Type; - config?: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig; + config?: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig; enabled: Boolean; } export declare const entityDesc: EntityDesc extends Feature { loginWechat(code: string, params?: { wechatLoginId?: string; }): Promise; - loginWechatMp(): Promise; + loginWechatMp(params?: { + wechatLoginId?: string; + }): Promise; loginWechatNative(code: string): Promise; syncUserInfoWechatMp(): Promise; logout(dontPublish?: boolean): Promise; diff --git a/lib/features/token.js b/lib/features/token.js index 89fc8c6e4..bc30633d4 100644 --- a/lib/features/token.js +++ b/lib/features/token.js @@ -197,12 +197,13 @@ class Token extends Feature_1.Feature { this.publish(); this.checkNeedSetPassword(); } - async loginWechatMp() { + async loginWechatMp(params) { const { code } = await wx.login(); const env = await this.environment.getEnv(); const { result } = await this.cache.exec('loginWechatMp', { code, env: env, + wechatLoginId: params?.wechatLoginId, }); this.tokenValue = result; await this.storage.save(constants_1.LOCAL_STORAGE_KEYS.token, result); diff --git a/lib/oak-app-domain/ApplicationPassport/Storage.js b/lib/oak-app-domain/ApplicationPassport/Storage.js index 075c1cefc..df7dc972d 100644 --- a/lib/oak-app-domain/ApplicationPassport/Storage.js +++ b/lib/oak-app-domain/ApplicationPassport/Storage.js @@ -17,6 +17,9 @@ exports.desc = { isDefault: { notNull: true, type: "boolean" + }, + allowPwd: { + type: "boolean" } }, actionType: "crud", diff --git a/lib/oak-app-domain/ApplicationPassport/_baseSchema.d.ts b/lib/oak-app-domain/ApplicationPassport/_baseSchema.d.ts index e79c309df..ab88629a6 100644 --- a/lib/oak-app-domain/ApplicationPassport/_baseSchema.d.ts +++ b/lib/oak-app-domain/ApplicationPassport/_baseSchema.d.ts @@ -7,6 +7,7 @@ export type OpSchema = EntityShape & { applicationId: ForeignKey<"application">; passportId: ForeignKey<"passport">; isDefault: Boolean; + allowPwd?: Boolean | null; } & { [A in ExpressionKey]?: any; }; @@ -19,6 +20,7 @@ export type OpFilter = { applicationId: Q_StringValue; passportId: Q_StringValue; isDefault: Q_BooleanValue; + allowPwd: Q_BooleanValue; } & ExprOp; export type OpProjection = { "#id"?: NodeId; @@ -30,6 +32,7 @@ export type OpProjection = { applicationId?: number; passportId?: number; isDefault?: number; + allowPwd?: number; } & Partial>; export type OpSortAttr = Partial<{ id: number; @@ -39,6 +42,7 @@ export type OpSortAttr = Partial<{ applicationId: number; passportId: number; isDefault: number; + allowPwd: number; [k: string]: any; } | ExprOp>; export type OpAction = OakMakeAction; diff --git a/lib/oak-app-domain/ApplicationPassport/locales/zh_CN.json b/lib/oak-app-domain/ApplicationPassport/locales/zh_CN.json index 0b65a14ed..7ef6175f2 100644 --- a/lib/oak-app-domain/ApplicationPassport/locales/zh_CN.json +++ b/lib/oak-app-domain/ApplicationPassport/locales/zh_CN.json @@ -3,6 +3,7 @@ "attr": { "application": "应用", "passport": "登录方式", - "isDefault": "是否默认" + "isDefault": "是否默认", + "allowPwd": "是否支持密码登录" } } diff --git a/lib/oak-app-domain/Passport/Storage.js b/lib/oak-app-domain/Passport/Storage.js index b7171a8b1..7056177ed 100644 --- a/lib/oak-app-domain/Passport/Storage.js +++ b/lib/oak-app-domain/Passport/Storage.js @@ -12,7 +12,7 @@ exports.desc = { type: { notNull: true, type: "enum", - enumeration: ["password", "sms", "email", "wechatWeb", "wechatMp", "wechatPublic", "wechatPublicForWeb", "wechatMpForWeb", "wechatNative"] + enumeration: ["password", "sms", "email", "wechatWeb", "wechatMp", "wechatPublic", "wechatPublicForWeb", "wechatMpForWeb", "wechatNative", "loginName", "oauth"] }, config: { type: "object" diff --git a/lib/oak-app-domain/Passport/Style.js b/lib/oak-app-domain/Passport/Style.js index 4d2d31471..87a33af8d 100644 --- a/lib/oak-app-domain/Passport/Style.js +++ b/lib/oak-app-domain/Passport/Style.js @@ -12,7 +12,9 @@ exports.style = { wechatMp: '#ADDCCA', wechatMpForWeb: '#FDC454', wechatPublicForWeb: '#C0A27C', - wechatNative: '#C0A27C' + wechatNative: '#C0A27C', + loginName: '#456B3C', + oauth: '#3C4655', } } }; diff --git a/lib/oak-app-domain/Passport/_baseSchema.d.ts b/lib/oak-app-domain/Passport/_baseSchema.d.ts index 72cfb8b53..393c4d556 100644 --- a/lib/oak-app-domain/Passport/_baseSchema.d.ts +++ b/lib/oak-app-domain/Passport/_baseSchema.d.ts @@ -3,7 +3,7 @@ import { Q_DateValue, Q_BooleanValue, Q_NumberValue, Q_StringValue, Q_EnumValue, import { MakeAction as OakMakeAction, EntityShape } from "oak-domain/lib/types/Entity"; import { GenericAction } from "oak-domain/lib/actions/action"; import { Boolean } from "oak-domain/lib/types/DataType"; -export type Type = "password" | "sms" | "email" | "wechatWeb" | "wechatMp" | "wechatPublic" | "wechatPublicForWeb" | "wechatMpForWeb" | "wechatNative"; +export type Type = "password" | "sms" | "email" | "wechatWeb" | "wechatMp" | "wechatPublic" | "wechatPublicForWeb" | "wechatMpForWeb" | "wechatNative" | "loginName" | "oauth"; export type SmsConfig = { mockSend?: boolean; defaultOrigin?: "ali" | "tencent" | "ctyun"; @@ -35,10 +35,21 @@ export type PwdConfig = { regexs?: string[]; tip?: string; }; +export type NameConfig = { + min?: number; + max?: number; + verify?: boolean; + regexs?: string[]; + register?: boolean; + tip?: string; +}; +export type OAuthConfig = { + oauthIds: string[]; +}; export type OpSchema = EntityShape & { systemId: ForeignKey<"system">; type: Type; - config?: (SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig) | null; + config?: (SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig) | null; enabled: Boolean; } & { [A in ExpressionKey]?: any; @@ -51,7 +62,7 @@ export type OpFilter = { $$updateAt$$: Q_DateValue; systemId: Q_StringValue; type: Q_EnumValue; - config: JsonFilter; + config: JsonFilter; enabled: Q_BooleanValue; } & ExprOp; export type OpProjection = { @@ -63,7 +74,7 @@ export type OpProjection = { $$seq$$?: number; systemId?: number; type?: number; - config?: number | JsonProjection; + config?: number | JsonProjection; enabled?: number; } & Partial>; export type OpSortAttr = Partial<{ diff --git a/lib/oak-app-domain/Passport/locales/zh_CN.json b/lib/oak-app-domain/Passport/locales/zh_CN.json index 5c16e8f73..cd4865518 100644 --- a/lib/oak-app-domain/Passport/locales/zh_CN.json +++ b/lib/oak-app-domain/Passport/locales/zh_CN.json @@ -9,14 +9,16 @@ "v": { "type": { "email": "邮箱", - "sms": "短信", + "sms": "手机号", "password": "密码", "wechatMp": "小程序", "wechatPublic": "公众号", "wechatWeb": "微信网站", "wechatMpForWeb": "小程序授权网页", "wechatPublicForWeb": "公众号授权网页", - "wechatNative": "微信APP授权" + "wechatNative": "微信APP授权", + "loginName": "账号", + "oauth": "OAuth授权" } } } diff --git a/lib/triggers/application.js b/lib/triggers/application.js index f67287444..d3d42186b 100644 --- a/lib/triggers/application.js +++ b/lib/triggers/application.js @@ -32,7 +32,7 @@ const triggers = [ return !!data.config; }, fn: async ({ operation }, context, option) => { - const { filter } = operation; + const { filter, data } = operation; const applications = await context.select('application', { data: { id: 1, @@ -45,9 +45,8 @@ const triggers = [ let count = 0; for (const application of applications) { if (application.type === 'web') { - const { wechat } = application.config || {}; - const { appId, appSecret } = wechat || {}; - if (!(appId && appId !== '' && appSecret && appSecret !== '')) { + const { appId: newAppId, appSecret: newAppSecret } = data?.config?.wechat || {}; + if (!newAppId || !newAppSecret) { const [passport] = await context.select('passport', { data: { id: 1, @@ -102,7 +101,7 @@ const triggers = [ for (const application of applications) { if (application.type === 'wechatPublic') { const { appId, appSecret, isService } = application.config || {}; - if (appId && appId !== '') { + if (appId) { const [passport] = await context.select('passport', { data: { id: 1, @@ -118,7 +117,7 @@ const triggers = [ count: 1, indexFrom: 0, }, { forUpdate: true }); - if (appSecret && appSecret !== '' && isService) { + if (appSecret && isService) { if (!passport) { await context.operate('passport', { id: await (0, uuid_1.generateNewIdAsync)(), @@ -156,7 +155,7 @@ const triggers = [ } else if (application.type === 'wechatMp') { const { appId, appSecret, } = application.config || {}; - if (appId && appId !== '') { + if (appId) { const [passport] = await context.select('passport', { data: { id: 1, @@ -171,7 +170,7 @@ const triggers = [ count: 1, indexFrom: 0, }, { forUpdate: true }); - if (appSecret && appSecret !== '') { + if (appSecret) { if (!passport) { await context.operate('passport', { id: await (0, uuid_1.generateNewIdAsync)(), @@ -267,6 +266,66 @@ const triggers = [ return count; } }, + { + name: 'wechatMp applicaiton清空普通链接二维码规则配置时,将相应的passport禁用', + entity: 'application', + action: 'update', + when: 'after', + check: (operation) => { + const { data } = operation; + return !!data.config; + }, + fn: async ({ operation }, context, option) => { + const { filter } = operation; + const applications = await context.select('application', { + data: { + id: 1, + config: 1, + type: 1, + systemId: 1, + }, + filter, + }, {}); + let count = 0; + for (const application of applications) { + if (application.type === 'wechatMp') { + const { qrCodePrefix, appId } = application.config || {}; + if (appId && !qrCodePrefix) { + const [passport] = await context.select('passport', { + data: { + id: 1, + }, + filter: { + enabled: true, + systemId: application.systemId, + type: 'wechatMpForWeb', + config: { + appId, + } + }, + count: 1, + indexFrom: 0, + }, { forUpdate: true }); + if (passport) { + await context.operate('passport', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'update', + data: { + enabled: false, + config: {}, + }, + filter: { + id: passport.id, + } + }, option); + count++; + } + } + } + } + return count; + } + }, { name: '删除application前,将相关的passport删除', entity: 'application', diff --git a/lib/triggers/applicationPassport.d.ts b/lib/triggers/applicationPassport.d.ts new file mode 100644 index 000000000..696745594 --- /dev/null +++ b/lib/triggers/applicationPassport.d.ts @@ -0,0 +1,5 @@ +import { Trigger } from 'oak-domain/lib/types/Trigger'; +import { EntityDict } from '../oak-app-domain/EntityDict'; +import { BRC } from '../types/RuntimeCxt'; +declare const triggers: Trigger>[]; +export default triggers; diff --git a/lib/triggers/applicationPassport.js b/lib/triggers/applicationPassport.js new file mode 100644 index 000000000..0bd45277b --- /dev/null +++ b/lib/triggers/applicationPassport.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const assert_1 = require("oak-domain/lib/utils/assert"); +const triggers = [ + { + name: '当loginName类型的applicationPassport创建前,将其allowPwd置为ture', + entity: 'applicationPassport', + action: 'create', + when: 'before', + fn: async ({ operation }, context, option) => { + const { data } = operation; + (0, assert_1.assert)(!(data instanceof Array)); + const [passport] = await context.select('passport', { + data: { + id: 1, + type: 1, + }, + filter: { + id: data?.passportId, + } + }, { + forUpdate: true, + }); + const { type } = passport || {}; + if (type === 'loginName' && !data.allowPwd) { + data.allowPwd = true; + } + return 1; + } + }, +]; +exports.default = triggers; diff --git a/lib/triggers/index.d.ts b/lib/triggers/index.d.ts index 9f5cbb943..f37b9d34d 100644 --- a/lib/triggers/index.d.ts +++ b/lib/triggers/index.d.ts @@ -1,2 +1,2 @@ -declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; +declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; export default _default; diff --git a/lib/triggers/index.js b/lib/triggers/index.js index 05e54698c..6aabfc715 100644 --- a/lib/triggers/index.js +++ b/lib/triggers/index.js @@ -24,6 +24,7 @@ const oauthProvider_1 = tslib_1.__importDefault(require("./oauthProvider")); const oauthUser_1 = tslib_1.__importDefault(require("./oauthUser")); const oauthUserAuth_1 = tslib_1.__importDefault(require("./oauthUserAuth")); const mobile_1 = tslib_1.__importDefault(require("./mobile")); +const applicationPassport_1 = tslib_1.__importDefault(require("./applicationPassport")); // import accountTriggers from './account'; exports.default = [ // ...accountTriggers, @@ -50,4 +51,5 @@ exports.default = [ ...oauthUser_1.default, ...oauthUserAuth_1.default, ...mobile_1.default, + ...applicationPassport_1.default, ]; diff --git a/lib/triggers/oauthProvider.js b/lib/triggers/oauthProvider.js index 7287f7040..76e617262 100644 --- a/lib/triggers/oauthProvider.js +++ b/lib/triggers/oauthProvider.js @@ -2,6 +2,8 @@ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const assert_1 = tslib_1.__importDefault(require("assert")); +const uuid_1 = require("oak-domain/lib/utils/uuid"); +const lodash_1 = require("oak-domain/lib/utils/lodash"); const triggers = [ { name: "创建provider时,填充数据", @@ -15,5 +17,193 @@ const triggers = [ return 0; } }, + { + name: "创建provider时,新增passport", + action: "create", + when: "after", + entity: "oauthProvider", + asRoot: true, + fn: async ({ operation }, context, option) => { + const { data } = operation; + let count = 0; + if (data?.systemId) { + const [passport] = await context.select('passport', { + data: { + id: 1, + systemId: 1, + type: 1, + }, + filter: { + systemId: data?.systemId, + type: 'oauth', + }, + indexFrom: 0, + count: 1, + }, { forUpdate: true }); + if (!passport) { + await context.operate('passport', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'create', + data: { + id: await (0, uuid_1.generateNewIdAsync)(), + type: 'oauth', + enabled: false, + systemId: data?.systemId, + config: { + oauthIds: [data?.id], + } + } + }, option); + count++; + } + } + return count; + } + }, + { + name: "删除provider时,删除passport", + action: "remove", + when: "before", + entity: "oauthProvider", + asRoot: true, + fn: async ({ operation }, context, option) => { + const { filter } = operation; + const [oauthProvider] = await context.select('oauthProvider', { + data: { + id: 1, + systemId: 1, + }, + filter: filter, + }, { forUpdate: true }); + let count = 0; + if (oauthProvider) { + const { id, systemId } = oauthProvider; + const [other] = await context.select('oauthProvider', { + data: { + id: 1, + systemId: 1, + }, + filter: { + systemId, + id: { + $ne: id, + } + }, + indexFrom: 0, + count: 1, + }, { forUpdate: true }); + if (!other) { + await context.operate('passport', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'remove', + data: {}, + filter: { + systemId, + type: 'oauth' + } + }, option); + count++; + } + else { + const [passport] = await context.select('passport', { + data: { + id: 1, + type: 1, + config: 1, + systemId: 1, + }, + filter: { + type: 'oauth', + systemId, + config: { + oauthIds: { + $contains: [id], + } + } + } + }, { forUpdate: true }); + if (passport) { + const { id: passportId, config } = passport; + let newConfig = (0, lodash_1.cloneDeep)(config); + (0, lodash_1.pull)(newConfig?.oauthIds, id); + await context.operate('passport', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'update', + data: { + config: newConfig, + }, + filter: { + id: passportId, + } + }, option); + } + } + } + return count; + } + }, + { + name: '当provider禁用时,更新passport', + entity: 'oauthProvider', + action: 'update', + when: 'after', + check: (operation) => { + const { data } = operation; + return data.hasOwnProperty('ableState') && data.ableState === 'disabled'; + }, + fn: async ({ operation }, context, option) => { + const { filter } = operation; + const [oauthProvider] = await context.select('oauthProvider', { + data: { + id: 1, + systemId: 1, + ableState: 1, + }, + filter: filter, + }, { forUpdate: true }); + (0, assert_1.default)(oauthProvider, '禁用oauthProvider的filter请勿包含abledState'); + let count = 0; + const { id, systemId } = oauthProvider; + const [passport] = await context.select('passport', { + data: { + id: 1, + type: 1, + config: 1, + systemId: 1, + enabled: 1, + }, + filter: { + type: 'oauth', + systemId, + config: { + oauthIds: { + $contains: [id], + } + } + } + }, { forUpdate: true }); + if (passport && passport.enabled) { + const { id: passportId, config } = passport; + let newConfig = (0, lodash_1.cloneDeep)(config); + (0, lodash_1.pull)(newConfig?.oauthIds, id); + if (newConfig?.oauthIds?.length <= 0) { + //无可支持的oauthProvider,将启用了的passport关闭 + await context.operate('passport', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'update', + data: { + enabled: false, + config: newConfig, + }, + filter: { + id: passport.id, + } + }, option); + count++; + } + } + return count; + } + } ]; exports.default = triggers; diff --git a/lib/triggers/system.js b/lib/triggers/system.js index b7dcd13f2..f4e997ff5 100644 --- a/lib/triggers/system.js +++ b/lib/triggers/system.js @@ -7,7 +7,7 @@ const validator_1 = require("oak-domain/lib/utils/validator"); const { attributes: { type: passportType } } = Storage_1.desc; const triggers = [ { - name: '添加system时,添加短信与密码的passport', + name: '添加system时,添加手机号、账号的passport', entity: 'system', action: 'create', when: 'before', @@ -15,9 +15,9 @@ const triggers = [ const { data } = operation; (0, assert_1.assert)(!(data instanceof Array)); data.passport$system = [ - //仅创建短信与密码的passport,其他type由相应的application/Email的更新来创建 + //仅创建手机号、账号、密码的passport,其他type由相应的application/Email的更新来创建 { - //短信登录默认为模拟发送 + //手机号登录默认为模拟发送 id: await (0, uuid_1.generateNewIdAsync)(), action: 'create', data: { @@ -30,15 +30,26 @@ const triggers = [ } }, { - //密码登录 + //账号登录 id: await (0, uuid_1.generateNewIdAsync)(), action: 'create', data: { id: await (0, uuid_1.generateNewIdAsync)(), - type: 'password', - enabled: false, + type: 'loginName', + config: {}, + enabled: true, } - } + }, + // { + // //密码登录 + // id: await generateNewIdAsync(), + // action: 'create', + // data: { + // id: await generateNewIdAsync(), + // type: 'password', + // enabled: false, + // } + // } ]; return 1; } diff --git a/lib/types/Config.d.ts b/lib/types/Config.d.ts index be1c3fb96..c1ed29f4f 100644 --- a/lib/types/Config.d.ts +++ b/lib/types/Config.d.ts @@ -154,6 +154,14 @@ export type EmailConfig = { name?: string; secure?: boolean; }; +export type PasswordConfig = { + mode?: 'all' | 'plain' | 'sha1'; + min?: number; + max?: number; + verify?: boolean; + regexs?: string[]; + tip?: string; +}; export type QrCodeType = 'wechatMpDomainUrl' | 'wechatMpWxaCode' | 'wechatPublic' | 'wechatPublicForMp' | 'webForWechatPublic'; export type Config = { Account?: { @@ -205,6 +213,7 @@ export type Config = { level?: 'weak' | 'medium' | 'strong'; passwordVerifyGap?: number; }; + Password?: PasswordConfig; }; export type AccountOrigin = 'ali' | 'tencent' | 'qiniu' | 'amap' | 'ctyun' | 'local' | 's3'; export type CosOrigin = 'qiniu' | 'wechat' | 'ctyun' | 'aliyun' | 'tencent' | 'local' | 'unknown' | 's3'; diff --git a/lib/utils/user.js b/lib/utils/user.js index 0866734d7..f407104b0 100644 --- a/lib/utils/user.js +++ b/lib/utils/user.js @@ -1,6 +1,8 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.checkPassword = exports.maskPassword = exports.getUserSafetyFilter = void 0; +exports.getUserSafetyFilter = getUserSafetyFilter; +exports.maskPassword = maskPassword; +exports.checkPassword = checkPassword; const types_1 = require("oak-domain/lib/types"); function getUserSafetyFilter(context) { const application = context.getApplication(); @@ -23,12 +25,10 @@ function getUserSafetyFilter(context) { }; } } -exports.getUserSafetyFilter = getUserSafetyFilter; function maskPassword(password) { const encStr = "****************************************".slice(0, password.length - 2); return password[0] + encStr + password[password.length - 1]; } -exports.maskPassword = maskPassword; async function checkPassword(context, password) { const systemId = context.getSystemId(); const [passport] = await context.select('passport', { @@ -65,4 +65,3 @@ async function checkPassword(context, password) { } } } -exports.checkPassword = checkPassword; diff --git a/package.json b/package.json index 2f964fd6c..eee5125e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oak-general-business", - "version": "5.10.9", + "version": "5.11.0", "description": "oak框架中公共业务逻辑的实现", "author": { "name": "XuChang" diff --git a/src/aspects/AspectDict.ts b/src/aspects/AspectDict.ts index 8c353ab6f..e2dc04a18 100644 --- a/src/aspects/AspectDict.ts +++ b/src/aspects/AspectDict.ts @@ -179,15 +179,18 @@ export type AspectDict = { * 微信小程序登录 * @param code 微信授权 code * @param env 小程序环境信息 + * @param wechatLoginId 可选的微信登录 ID(用于扫码登录场景) * @returns 返回登录 token */ loginWechatMp: ( { code, env, + wechatLoginId, }: { code: string; env: WechatMpEnv; + wechatLoginId?: string; }, context: BackendRuntimeContext ) => Promise; @@ -413,6 +416,7 @@ export type AspectDict = { type: EntityDict['wechatLogin']['Schema']['type']; interval: number; router: EntityDict['wechatLogin']['Schema']['router']; + qrCodeType?: EntityDict['wechatLogin']['Schema']['qrCodeType']; }, context: BackendRuntimeContext ) => Promise; @@ -1039,6 +1043,20 @@ export type AspectDict = { headers?: Record; formdata?: Record; }>; + /** + * 用户账号注册 + * @param loginName 账号 + * @param password 密码 + * @param context + * @returns + */ + registerUserByLoginName: ( + params: { + loginName: string, + password: string, + }, + context: BackendRuntimeContext + ) => Promise; }; export default AspectDict; diff --git a/src/aspects/applicationPassport.ts b/src/aspects/applicationPassport.ts index 72a169e89..32ec91993 100644 --- a/src/aspects/applicationPassport.ts +++ b/src/aspects/applicationPassport.ts @@ -1,4 +1,3 @@ -import { pipeline } from "oak-domain/lib/utils/executor"; import { EntityDict } from "../oak-app-domain"; import { BRC } from '../types/RuntimeCxt'; import { generateNewIdAsync } from "oak-domain/lib/utils/uuid"; @@ -23,9 +22,15 @@ export async function getApplicationPassports( config: 1, }, isDefault: 1, + allowPwd: 1, }, filter: { applicationId, + passport: { + type: { + $ne: 'password', + } + } } }, {}); diff --git a/src/aspects/index.ts b/src/aspects/index.ts index 200256455..ae1f090bb 100644 --- a/src/aspects/index.ts +++ b/src/aspects/index.ts @@ -35,7 +35,7 @@ import { import { updateConfig, updateApplicationConfig, updateStyle } from './config'; import { syncWechatTemplate, getMessageType } from './template'; import { syncSmsTemplate } from './sms'; -import { mergeUser, getChangePasswordChannels, updateUserPassword } from './user'; +import { mergeUser, getChangePasswordChannels, updateUserPassword, registerUserByLoginName } from './user'; import { createWechatLogin } from './wechatLogin'; import { unbindingWechat } from './wechatUser'; import { getMpUnlimitWxaCode } from './wechatQrCode'; @@ -147,6 +147,7 @@ const aspectDict = { // extraFile新增 mergeChunkedUpload, presignFile, + registerUserByLoginName, }; export default aspectDict; diff --git a/src/aspects/oauth.ts b/src/aspects/oauth.ts index f59466ccf..d2c4200af 100644 --- a/src/aspects/oauth.ts +++ b/src/aspects/oauth.ts @@ -6,6 +6,7 @@ import { generateNewIdAsync } from "oak-domain/lib/utils/uuid"; import { loadTokenInfo, setUpTokenAndUser } from "./token"; import { randomUUID } from "crypto"; import { processUserInfo } from "../utils/oauth"; +import { OAuthConfig } from "../entities/Passport"; export async function loginByOauth(params: { code: string; @@ -26,6 +27,7 @@ export async function loginByOauth(params: { // 验证 state 并获取 OAuth 配置 const [state] = await context.select("oauthState", { data: { + providerId: 1, provider: { type: 1, clientId: 1, @@ -41,7 +43,33 @@ export async function loginByOauth(params: { filter: { state: stateCode, }, - }, { dontCollect: true }) + }, { dontCollect: true }); + + const systemId = context.getSystemId(); + const [applicationPassport] = await context.select('applicationPassport', { + data: { + id: 1, + applicationId: 1, + passportId: 1, + passport: { + id: 1, + type: 1, + systemId: 1, + config: 1, + }, + }, + filter: { + passport: { + systemId, + type: 'oauth', + }, + applicationId, + } + }, { dontCollect: true }); + const allowOauth = !!(state.providerId && (applicationPassport?.passport?.config as OAuthConfig)?.oauthIds && applicationPassport?.passport?.config as OAuthConfig)?.oauthIds.includes(state.providerId); + if (!allowOauth) { + throw new OakUserException('error::user.loginWayDisabled'); + } assert(state, '无效的 state 参数'); assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用'); @@ -414,7 +442,7 @@ export async function authorize(params: { // PKCE 支持 codeChallenge: code_challenge, - codeChallengeMethod: code_challenge_method || 'plain', + codeChallengeMethod: code_challenge_method || 'plain', } }, { dontCollect: true }) diff --git a/src/aspects/token.ts b/src/aspects/token.ts index a92f60848..70a3e8dfa 100644 --- a/src/aspects/token.ts +++ b/src/aspects/token.ts @@ -40,7 +40,7 @@ import { sendSms } from '../utils/sms'; import { mergeUser } from './user'; import { cloneDeep, pick } from 'oak-domain/lib/utils/lodash'; import { BRC } from '../types/RuntimeCxt'; -import { PwdConfig, SmsConfig } from '../entities/Passport'; +import { SmsConfig } from '../entities/Passport'; import { sendEmail } from '../utils/email'; import { EmailConfig } from '../oak-app-domain/Passport/Schema'; import { isEmail, isMobile } from 'oak-domain/lib/utils/validator'; @@ -730,23 +730,9 @@ export async function verifyPassword( context: BRC ) { const { password } = params; - const systemId = context.getSystemId(); - const [pwdPassport] = await context.select('passport', { - data: { - id: 1, - systemId: 1, - config: 1, - type: 1, - enabled: 1, - }, - filter: { - systemId, - enabled: true, - type: 'password', - } - }, { forUpdate: true }); - // assert(pwdPassport); - const pwdMode = (pwdPassport?.config as PwdConfig)?.mode ?? 'all'; + const { system } = context.getApplication()!; + const pwdConfig = system?.config.Password; + const pwdMode = pwdConfig?.mode ?? 'all'; let pwdFilter = {}; if (pwdMode === 'all') { pwdFilter = { @@ -812,10 +798,40 @@ export async function loginByAccount( assert(password); assert(account); const accountType = isEmail(account) ? 'email' : (isMobile(account) ? 'mobile' : 'loginName'); + const applicationPassports = await context.select( + 'applicationPassport', + { + data: { + id: 1, + applicationId: 1, + passportId: 1, + passport: { + id: 1, + type: 1, + systemId: 1, + }, + allowPwd: 1, + }, + filter: { + passport: { + systemId, + }, + applicationId, + allowPwd: true, + } + }, + { + dontCollect: true, + } + ); + const allowEmail = !!applicationPassports.find((ele) => ele.passport?.type === 'email'); + const allowSms = !!applicationPassports.find((ele) => ele.passport?.type === 'sms'); + if (accountType === 'email') { - const { config, emailConfig } = await getAndCheckPassportByEmail(context, account); - + if (!allowEmail) { + throw new OakUserException('暂不支持邮箱登录'); + } const existEmail = await context.select( 'email', { @@ -870,54 +886,25 @@ export async function loginByAccount( throw new OakUserException('error::user.passwordUnmath'); } case 1: { - const applicationPassports = await context.select( - 'applicationPassport', - { - data: { - id: 1, - applicationId: 1, - passportId: 1, - passport: { - id: 1, - type: 1, - systemId: 1, - } - }, - filter: { - passport: { - systemId, - }, - applicationId, - } - }, - { - dontCollect: true, - } - ); - const allowEmail = !!applicationPassports.find((ele) => ele.passport?.type === 'email'); const [userRow] = result; const { email$user, id: userId, } = userRow; needUpdatePassword = !(userRow.password || userRow.passwordSha1); - if (allowEmail) { - const email = email$user?.find(ele => ele.email.toLowerCase() === account.toLowerCase()); - if (email) { - const ableState = email.ableState; - if (ableState === 'disabled') { - // 虽然密码和邮箱匹配,但邮箱已经禁用了,在可能的情况下提醒用户使用其它方法登录 - const exception = await tryMakeChangeLoginWay( - userId as string, - context - ); - if (exception) { - throw exception; - } + const email = email$user?.find(ele => ele.email.toLowerCase() === account.toLowerCase()); + if (email) { + const ableState = email.ableState; + if (ableState === 'disabled') { + // 虽然密码和邮箱匹配,但邮箱已经禁用了,在可能的情况下提醒用户使用其它方法登录 + const exception = await tryMakeChangeLoginWay( + userId as string, + context + ); + if (exception) { + throw exception; } - return await setupEmail(account, env, context); - } else { - throw new OakUserException('error::user.emailUnexists'); } + return await setupEmail(account, env, context); } else { - throw new OakUserException('error::user.loginWayDisabled'); + throw new OakUserException('error::user.emailUnexists'); } } default: { @@ -925,6 +912,9 @@ export async function loginByAccount( } } } else if (accountType === 'mobile') { + if (!allowSms) { + throw new OakUserException('暂不支持手机号登录'); + } const existMobile = await context.select( 'mobile', { @@ -980,59 +970,29 @@ export async function loginByAccount( throw new OakUserException('手机号与密码不匹配'); } case 1: { - const applicationPassports = await context.select( - 'applicationPassport', - { - data: { - id: 1, - applicationId: 1, - passportId: 1, - passport: { - id: 1, - type: 1, - systemId: 1, - } - }, - filter: { - passport: { - systemId, - }, - applicationId, - } - }, - { - dontCollect: true, - } - ); const [userRow] = result; const { mobile$user, id: userId, } = userRow; needUpdatePassword = !(userRow.password || userRow.passwordSha1); - const allowSms = !!applicationPassports.find((ele) => ele.passport?.type === 'sms'); - - if (allowSms) { - const mobile = mobile$user?.find(ele => ele.mobile === account); - if (mobile) { - const ableState = mobile.ableState; - if (ableState === 'disabled') { - // 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录 - const exception = await tryMakeChangeLoginWay( - userId as string, - context - ); - if (exception) { - throw exception; - } + const mobile = mobile$user?.find(ele => ele.mobile === account); + if (mobile) { + const ableState = mobile.ableState; + if (ableState === 'disabled') { + // 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录 + const exception = await tryMakeChangeLoginWay( + userId as string, + context + ); + if (exception) { + throw exception; } - return await setupMobile(account, env, context); - } else { - throw new OakUserException('手机号未注册'); } + return await setupMobile(account, env, context); } else { - throw new OakUserException('暂不支持手机号登录'); + throw new OakUserException('手机号未注册'); } } default: { - throw new OakUserException('不支持的登录方式'); + throw new OakUserException('error::user.loginWayDisabled'); } } } else { @@ -1113,38 +1073,15 @@ export async function loginByAccount( } } default: { - throw new OakUserException('不支持的登录方式'); + throw new OakUserException('error::user.loginWayDisabled'); } } } }; const closeRootMode = context.openRootMode(); - const application = context.getApplication(); - const [applicationPassport] = await context.select('applicationPassport', - { - data: { - id: 1, - passportId: 1, - passport: { - id: 1, - config: 1, - type: 1, - }, - applicationId: 1, - }, - filter: { - applicationId: application?.id!, - passport: { - type: 'password', - }, - } - }, - { - dontCollect: true, - } - ); - // assert(applicationPassport?.passport); - const pwdMode = (applicationPassport?.passport?.config as PwdConfig)?.mode ?? 'all'; + const { system } = context.getApplication()!; + const pwdConfig = system?.config.Password; + const pwdMode = pwdConfig?.mode ?? 'all'; let pwdFilter = {}, updateData = {}; if (pwdMode === 'all') { pwdFilter = { @@ -2601,14 +2538,16 @@ export async function loginWechatMp( { code, env, + wechatLoginId, }: { code: string; env: WechatMpEnv; + wechatLoginId?: string; }, context: BRC ): Promise { const closeRootMode = context.openRootMode(); - const tokenValue = await loginFromWechatEnv(code, env, context); + const tokenValue = await loginFromWechatEnv(code, env, context, wechatLoginId); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; diff --git a/src/aspects/user.ts b/src/aspects/user.ts index 1c8963b4e..9c61b7e26 100644 --- a/src/aspects/user.ts +++ b/src/aspects/user.ts @@ -1,4 +1,4 @@ -import { OakOperationUnpermittedException } from "oak-domain/lib/types"; +import { OakOperationUnpermittedException, OakPreConditionUnsetException } from "oak-domain/lib/types"; import { generateNewIdAsync } from "oak-domain/lib/utils/uuid"; import { BackendRuntimeContext } from "../context/BackendRuntimeContext"; import { EntityDict } from "../oak-app-domain"; @@ -6,7 +6,6 @@ import { encryptPasswordSha1 } from '../utils/password'; import { assert } from 'oak-domain/lib/utils/assert'; import dayjs from 'dayjs'; import { BRC } from "../types/RuntimeCxt"; -import { PwdConfig } from "../entities/Passport"; export async function mergeUser( params: { @@ -204,22 +203,19 @@ export async function updateUserPassword(params: { userId const systemId = context.getSystemId(); const closeRootMode = context.openRootMode(); try { - const [passport] = await context.select('passport', + const [system] = await context.select('system', { data: { id: 1, - type: 1, config: 1, - systemId: 1, }, filter: { - systemId, - type: 'password', + id: systemId, } }, { forUpdate: true } ); - assert(passport); - const config = passport.config as PwdConfig; + assert(system); + const config = system.config?.Password; const mode = config?.mode ?? 'all'; const [user] = await context.select( 'user', @@ -299,31 +295,31 @@ export async function updateUserPassword(params: { userId } const allowUpdate = mode === 'sha1' ? user.passwordSha1 === prevPassword : user.password === prevPassword; //sha1密文模式判断密文是否相等 - let userDate = {}, changeCreateDate = {}; + let userData = {}, changeCreateData = {}; if (mode === 'all') { - userDate = { + userData = { password: newPassword, passwordSha1: encryptPasswordSha1(newPassword), }; - changeCreateDate = { + changeCreateData = { prevPassword, newPassword, prevPasswordSha1: encryptPasswordSha1(prevPassword), newPasswordSha1: encryptPasswordSha1(newPassword), }; } else if (mode === 'plain') { - userDate = { + userData = { password: newPassword, }; - changeCreateDate = { + changeCreateData = { prevPassword, newPassword, }; } else if (mode === 'sha1') { - userDate = { + userData = { passwordSha1: newPassword, }; - changeCreateDate = { + changeCreateData = { prevPasswordSha1: prevPassword, newPasswordSha1: newPassword, }; @@ -335,7 +331,7 @@ export async function updateUserPassword(params: { userId { id: await generateNewIdAsync(), action: 'update', - data: userDate, + data: userData, filter: { id: userId, }, @@ -353,7 +349,7 @@ export async function updateUserPassword(params: { userId id: await generateNewIdAsync(), userId, result: 'success', - ...changeCreateDate, + ...changeCreateData, }, }, { @@ -374,7 +370,7 @@ export async function updateUserPassword(params: { userId id: await generateNewIdAsync(), userId, result: 'fail', - ...changeCreateDate, + ...changeCreateData, }, }, { @@ -409,31 +405,31 @@ export async function updateUserPassword(params: { userId } ); if (aliveCaptcha) { - let userDate = {}, changeCreateDate = {}; + let userData = {}, changeCreateData = {}; if (mode === 'all') { - userDate = { + userData = { password: newPassword, passwordSha1: encryptPasswordSha1(newPassword), }; - changeCreateDate = { + changeCreateData = { prevPassword: user.password, newPassword, prevPasswordSha1: user.passwordSha1, newPasswordSha1: encryptPasswordSha1(newPassword), }; } else if (mode === 'plain') { - userDate = { + userData = { password: newPassword, }; - changeCreateDate = { + changeCreateData = { prevPassword: user.password, newPassword, }; } else if (mode === 'sha1') { - userDate = { + userData = { passwordSha1: newPassword, }; - changeCreateDate = { + changeCreateData = { prevPasswordSha1: user.passwordSha1, newPasswordSha1: newPassword, }; @@ -443,7 +439,7 @@ export async function updateUserPassword(params: { userId { id: await generateNewIdAsync(), action: 'update', - data: userDate, + data: userData, filter: { id: userId, }, @@ -461,7 +457,7 @@ export async function updateUserPassword(params: { userId id: await generateNewIdAsync(), userId, result: 'success', - ...changeCreateDate, + ...changeCreateData, }, }, { @@ -488,3 +484,92 @@ export async function updateUserPassword(params: { userId throw err; } } + +/** + * 用户账号注册 + * @param params + * @param context + */ +export async function registerUserByLoginName( + params: { + loginName: string, + password: string, + }, context: BRC, +) { + const { loginName, password } = params; + const systemId = context.getSystemId(); + const closeRootMode = context.openRootMode(); + try { + // 检查loginName是否重复 + const [existLoginName] = await context.select( + 'loginName', + { + data: { + id: 1, + name: 1, + }, + filter: { + name: loginName!, + ableState: 'enabled', + }, + }, + { dontCollect: true, forUpdate: true } + ); + if (existLoginName) { + closeRootMode(); + throw new OakPreConditionUnsetException('账号已存在,请重新设置'); + } + // 创建user并附上密码,级联创建loginName + const [system] = await context.select('system', + { + data: { + id: 1, + config: 1, + }, + filter: { + id: systemId, + } + }, { forUpdate: true } + ); + assert(system); + const config = system.config?.Password; + const mode = config?.mode ?? 'all'; + let passwordData = {}; + if (mode === 'all') { + passwordData = { + password: password, + passwordSha1: encryptPasswordSha1(password), + }; + } else if (mode === 'plain') { + passwordData = { + password: password, + }; + } else if (mode === 'sha1') { + passwordData = { + passwordSha1: password, + }; + } + const userData: EntityDict['user']['CreateSingle']['data'] = { + id: await generateNewIdAsync(), + loginName$user: [ + { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + name: loginName, + } + } + ] + } + Object.assign(userData, passwordData) + await context.operate('user', { + id: await generateNewIdAsync(), + action: 'create', + data: userData, + }, {}); + } catch (err) { + closeRootMode(); + throw err; + } +} diff --git a/src/aspects/wechatLogin.ts b/src/aspects/wechatLogin.ts index 0f2ae9688..2a4d0ce86 100644 --- a/src/aspects/wechatLogin.ts +++ b/src/aspects/wechatLogin.ts @@ -9,10 +9,11 @@ export async function createWechatLogin( type: EntityDict['wechatLogin']['Schema']['type']; interval: number; router: EntityDict['wechatLogin']['Schema']['router']; + qrCodeType?: EntityDict['wechatLogin']['Schema']['qrCodeType']; }, context: BRC ) { - const { type, interval, router } = params; + const { type, interval, qrCodeType = "wechatPublic", router } = params; let userId; if (type === 'bind') { userId = context.getCurrentUserId(); @@ -38,7 +39,7 @@ export async function createWechatLogin( type, expiresAt: Date.now() + interval, expired: false, - qrCodeType: 'wechatPublic', + qrCodeType, successed: false, router: _router, }; diff --git a/src/components/applicationPassport/index.ts b/src/components/applicationPassport/index.ts index 430c084c6..2ae8a5c51 100644 --- a/src/components/applicationPassport/index.ts +++ b/src/components/applicationPassport/index.ts @@ -1,6 +1,6 @@ import { groupBy, isEqual, uniq } from "oak-domain/lib/utils/lodash"; import { EntityDict } from "../../oak-app-domain"; -import { EmailConfig, PfwConfig, SmsConfig, MfwConfig } from "../../entities/Passport"; +import { EmailConfig, PfwConfig, SmsConfig, MfwConfig, OAuthConfig } from "../../entities/Passport"; import { WebConfig } from "../../entities/Application"; import { assert } from "oak-domain/lib/utils/assert"; import { generateNewId, generateNewIdAsync } from "oak-domain/lib/utils/uuid"; @@ -27,6 +27,10 @@ type TypeRecord = Record; type PassportOption = { @@ -57,6 +61,7 @@ export default OakComponent({ enabled: 1, }, isDefault: 1, + allowPwd: 1, }, properties: { systemId: '', @@ -71,6 +76,9 @@ export default OakComponent({ }, passport: { systemId, + type: { + $ne: 'password' + } }, } } @@ -120,8 +128,9 @@ export default OakComponent({ } else { const { disabled, disabledTip } = this.checkDisabled(a, r[0]); + const { showPwd, pwdDisabled, pwdDisabledTip } = this.checkPwd(r[0]); const apId = await generateNewIdAsync(); - Object.assign(typeRecords, { [key]: { render, pId: r[0].id, checked: false, disabled, disabledTip, apId, } }); + Object.assign(typeRecords, { [key]: { render, pId: r[0].id, checked: false, disabled, disabledTip, apId, showPwd, allowPwd: undefined, pwdDisabled, pwdDisabledTip } }); } } Object.assign(item, { typeRecords }); @@ -142,8 +151,14 @@ export default OakComponent({ if (apArray[aIdx].typeRecords[t].pId === p.id) { apArray[aIdx].typeRecords[t].checked = true; apArray[aIdx].typeRecords[t].apId = ap.id; + apArray[aIdx].typeRecords[t].allowPwd = ap.allowPwd; } } + if (t === 'loginName') { + apArray[aIdx].typeRecords[t].pwdDisabledTip = '账号登录必须使用密码方式' + } else { + apArray[aIdx].typeRecords[t].pwdDisabled = false; + } apArray[aIdx].defaultOptions.push({ label: this.t(`passport:v.type.${p.type}`), value: ap.id, @@ -196,6 +211,9 @@ export default OakComponent({ filter: { systemId, enabled: true, + type: { + $ne: 'password', + } }, sorter: [{ $attr: { @@ -236,36 +254,36 @@ export default OakComponent({ switch (pType) { case 'sms': if (!(pConfig as SmsConfig).mockSend) { - if (!(pConfig as SmsConfig).templateName || (pConfig as SmsConfig).templateName === '') { + if (!(pConfig as SmsConfig).templateName) { return { disabled: true, - disabledTip: '短信登录未配置验证码模板名称', + disabledTip: '手机号登录未配置验证码模板名称', } } if (!(pConfig as SmsConfig).defaultOrigin) { return { disabled: true, - disabledTip: '短信登录未配置默认渠道', + disabledTip: '手机号登录未配置默认渠道', } } } break; case 'email': if (!(pConfig as EmailConfig).mockSend) { - if (!(pConfig as EmailConfig).account || (pConfig as EmailConfig).account === '') { + if (!(pConfig as EmailConfig).account) { return { disabled: true, disabledTip: '邮箱登录未配置账号', } } - else if (!(pConfig as EmailConfig).subject || (pConfig as EmailConfig).subject === '') { + else if (!(pConfig as EmailConfig).subject) { return { disabled: true, disabledTip: '邮箱登录未配置邮件主题', } } else if ( - (!(pConfig as EmailConfig).text || (pConfig as EmailConfig).text === '' || !(pConfig as EmailConfig).text?.includes('${code}')) && - (!(pConfig as EmailConfig).html || (pConfig as EmailConfig).html === '' || !(pConfig as EmailConfig).html?.includes('${code}')) + (!(pConfig as EmailConfig).text || !(pConfig as EmailConfig).text?.includes('${code}')) && + (!(pConfig as EmailConfig).html || !(pConfig as EmailConfig).html?.includes('${code}')) ) { return { disabled: true, @@ -275,7 +293,7 @@ export default OakComponent({ } break; case 'wechatPublicForWeb': - if (!(pConfig as PfwConfig).appId || (pConfig as PfwConfig).appId === '') { + if (!(pConfig as PfwConfig).appId) { return { disabled: true, disabledTip: '公众号授权登录未配置appId', @@ -283,13 +301,27 @@ export default OakComponent({ } break; case 'wechatMpForWeb': - if (!(pConfig as MfwConfig).appId || (pConfig as MfwConfig).appId === '') { + if (!(pConfig as MfwConfig).appId) { return { disabled: true, disabledTip: '小程序授权登录未配置appId', } } break; + case 'oauth': + if (!((pConfig as OAuthConfig).oauthIds && (pConfig as OAuthConfig).oauthIds.length > 0)) { + return { + disabled: true, + disabledTip: 'OAuth授权登录未配置oauth供应商', + } + } + break; + case 'password': + return { + disabled: true, + disabledTip: '密码登录已调整', + } + break; default: break; } @@ -312,7 +344,7 @@ export default OakComponent({ } break; case 'wechatMp': - if (['wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) { + if (['wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb', 'oauth'].includes(pType)) { return { disabled: true, disabledTip: '当前application不支持该登录方式', @@ -320,7 +352,7 @@ export default OakComponent({ } break; case 'wechatPublic': - if (['wechatMp', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) { + if (['wechatMp', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb', 'oauth'].includes(pType)) { return { disabled: true, disabledTip: '当前application不支持该登录方式', @@ -328,7 +360,7 @@ export default OakComponent({ } break; case 'native': - if (['wechatMp', 'wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) { + if (['wechatMp', 'wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb', 'oauth'].includes(pType)) { return { disabled: true, disabledTip: '当前application不支持该登录方式', @@ -344,13 +376,24 @@ export default OakComponent({ } }, async onCheckedChange(aId: string, pId: string, checked: boolean, apId?: string) { + const { passports } = this.state; + const passportType = passports?.find((ele) => ele.id === pId)?.type; if (checked) { //create applicationPassport - this.addItem({ - applicationId: aId, - passportId: pId, - isDefault: true, - }) + if (passportType === 'loginName') { + this.addItem({ + applicationId: aId, + passportId: pId, + isDefault: true, + allowPwd: true, + }) + } else { + this.addItem({ + applicationId: aId, + passportId: pId, + isDefault: true, + }) + } } else { //remove id为apId的applicationPassport apId && this.removeItem(apId); @@ -391,7 +434,21 @@ export default OakComponent({ } } return render; - } + }, + checkPwd(passport: EntityDict['passport']['Schema']) { + const { type } = passport; + let showPwd = false, pwdDisabled = undefined, pwdDisabledTip = undefined; + if (['sms', 'email', 'loginName'].includes(type)) { + showPwd = true; + pwdDisabled = true; + pwdDisabledTip = '请先启用该登录方式' + } + return { + showPwd, + pwdDisabled, + pwdDisabledTip, + } + }, } }) as ( props: ReactComponentProps< diff --git a/src/components/applicationPassport/web.pc.tsx b/src/components/applicationPassport/web.pc.tsx index 726f18491..e80a6237e 100644 --- a/src/components/applicationPassport/web.pc.tsx +++ b/src/components/applicationPassport/web.pc.tsx @@ -29,6 +29,10 @@ type TypeRecord = Record; type PassportOption = { @@ -131,21 +135,45 @@ export default function render(props: WebComponentProps< /> ) : ( - - } - unCheckedChildren={} - checked={!!typeRecords[type].checked} - onChange={(checked) => { - if (!checked && checkLastOne(aId, typeRecords[type].pId!)) { - showConfirm(aId, typeRecords[type].pId!, typeRecords[type].apId!); - } else { - onCheckedChange(aId, typeRecords[type].pId!, checked, typeRecords[type].apId!) - } - }} - /> - + <> + + +
启用:
+ } + unCheckedChildren={} + checked={!!typeRecords[type].checked} + onChange={(checked) => { + if (!checked && checkLastOne(aId, typeRecords[type].pId!)) { + showConfirm(aId, typeRecords[type].pId!, typeRecords[type].apId!); + } else { + onCheckedChange(aId, typeRecords[type].pId!, checked, typeRecords[type].apId!); + } + }} + /> +
+
+ {typeRecords[type].showPwd && + +
允许密码登录:
+ + } + unCheckedChildren={} + checked={!!typeRecords[type].allowPwd} + onChange={(checked) => { + methods.updateItem({ + allowPwd: checked + }, typeRecords[type].apId!); + }} + /> + +
+ } + )} diff --git a/src/components/changePassword/byMobile/index.ts b/src/components/changePassword/byMobile/index.ts index 12b5c323f..e86791f97 100644 --- a/src/components/changePassword/byMobile/index.ts +++ b/src/components/changePassword/byMobile/index.ts @@ -63,9 +63,8 @@ export default OakComponent({ lifetimes: { async ready() { const lastSendAt = await this.load(SEND_KEY); - const application = this.features.application.getApplication(); - const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); - const passwordConfig: PwdConfig = applicationPassports.find((ele: EntityDict['applicationPassport']['Schema']) => ele.passport.type === 'password')?.passport.config; + const system = this.features.application.getApplication().system; + const passwordConfig = system?.config.Password; const mode = passwordConfig?.mode ?? 'all'; const pwdMin = passwordConfig?.min ?? 8; const pwdMax = passwordConfig?.max ?? 24; diff --git a/src/components/changePassword/byPassword/index.ts b/src/components/changePassword/byPassword/index.ts index 3afe1f8a9..eace07d57 100644 --- a/src/components/changePassword/byPassword/index.ts +++ b/src/components/changePassword/byPassword/index.ts @@ -26,9 +26,8 @@ export default OakComponent({ }, lifetimes: { async ready() { - const application = this.features.application.getApplication(); - const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); - const passwordConfig: PwdConfig = applicationPassports.find((ele: EntityDict['applicationPassport']['Schema']) => ele.passport.type === 'password')?.passport.config; + const system = this.features.application.getApplication().system; + const passwordConfig = system?.config.Password; const mode = passwordConfig?.mode ?? 'all'; const pwdMin = passwordConfig?.min ?? 8; const pwdMax = passwordConfig?.max ?? 24; diff --git a/src/components/config/upsert/password/index.tsx b/src/components/config/upsert/password/index.tsx new file mode 100644 index 000000000..df7bb2020 --- /dev/null +++ b/src/components/config/upsert/password/index.tsx @@ -0,0 +1,136 @@ +import React, { useEffect, useState } from 'react'; +import { + Tabs, + Row, + Col, + Card, + Divider, + Input, + Form, + Space, + Select, + Radio, + InputNumber, + Switch, +} from 'antd'; +import Styles from './web.module.less'; +import { Config } from '../../../../types/Config'; +import EditorRegexs from '../../../passport/password/editorRegexs'; + +export default function Password(props: { + password: Required['Password']; + setValue: (path: string, value: any) => void; + setValues: (value: Record) => void; +}) { + const { password, setValue, setValues } = props; + const { mode, min, max, verify, regexs, tip } = password || {}; + const [newTip, setNewTip] = useState(''); + + useEffect(() => { + const { password } = props; + if (!password.mode) { + setValues({ + mode: 'all', + min: 8, + max: 24, + }); + } + }, [password]); + + useEffect(() => { + if (tip && !newTip) { + setNewTip(tip) + } + }, [tip]); + + return ( + + + + 密码设置 + +
+ + { + const { value } = target; + setValue('mode', value); + }} + value={mode} + > + 明文与SHA1加密 + 仅明文 + 仅SHA1加密 + + + + + { + setValue('min', value); + }} /> +
~
+ { + setValue('max', value); + }} /> +
+
+ + + { + setValue('verigy', checked) + }} + /> + + + <> + {!!verify ? ( + <> + { + setValue('regexs', regexs); + }} /> + + ) : ( +
暂未启用正则校验,无需设置
+ )} + +
+ + { + setNewTip(e.target.value); + }} + onBlur={() => { + if (newTip && newTip !== tip) { + setValue('tip', newTip); + } + }} + /> + +
+ +
+ ); +} \ No newline at end of file diff --git a/src/components/config/upsert/password/web.module.less b/src/components/config/upsert/password/web.module.less new file mode 100644 index 000000000..7fa576eed --- /dev/null +++ b/src/components/config/upsert/password/web.module.less @@ -0,0 +1,16 @@ + +.label { + color: var(--oak-text-color-primary); + font-size: 28px; + line-height: 36px; +} + +.tips { + color: var(--oak-text-color-placeholder); + font-size: 12px; +} + +.title { + margin-bottom: 0px; + margin-top:36px; +} \ No newline at end of file diff --git a/src/components/config/upsert/web.pc.tsx b/src/components/config/upsert/web.pc.tsx index 9a27de34d..c37e84a92 100644 --- a/src/components/config/upsert/web.pc.tsx +++ b/src/components/config/upsert/web.pc.tsx @@ -9,6 +9,7 @@ import Sms from './sms/index'; import Email from './email/index'; import Basic from './basic/index'; import Security from './security/index'; +import Password from './password/index'; import { Config } from '../../../types/Config'; import { EntityDict } from '../../../oak-app-domain'; @@ -47,6 +48,7 @@ export default function Render( App: app, Emails: emails, Security: security, + Password: password, } = currentConfig || {}; return ( <> @@ -228,6 +230,23 @@ export default function Render( /> ), }, + { + key: '密码设置', + label: '密码设置', + children: ( + + setValue(`Password.${path}`, value) + } + setValues={(value) => { + setValues({ + Password: value + }); + }} + /> + ), + }, ]} > diff --git a/src/components/login/oauth/authorize/index.ts b/src/components/login/oauth/authorize/index.ts index 62b068c8f..79bc173be 100644 --- a/src/components/login/oauth/authorize/index.ts +++ b/src/components/login/oauth/authorize/index.ts @@ -36,7 +36,7 @@ export default OakComponent({ state: '', }, lifetimes: { - ready() { + async ready() { const searchParams = new URLSearchParams(window.location.search); const clientId = searchParams.get('client_id') || ''; const responseType = searchParams.get('response_type') || ''; @@ -44,6 +44,38 @@ export default OakComponent({ const scope = searchParams.get('scope') || ''; const state = searchParams.get('state') || ''; + //判断是否允许oauth登录 + const application = this.features.application.getApplication(); + const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); + const oauthPassport = applicationPassports?.find((ele: EntityDict['applicationPassport']['Schema']) => ele.passport?.type === 'oauth'); + const oauthIds = oauthPassport?.config?.oauthIds; + let allowOauth = false; + if (clientId) { + const { data: [oauthProvider] } = await this.features.cache.refresh('oauthProvider', { + data: { + id: 1, + clientId: 1, + systemId: 1, + }, + filter: { + clientId, + systemId: application.systemId, + } + }); + if (oauthProvider?.id && oauthIds?.length > 0 && oauthIds.includes(oauthProvider?.id)) { + allowOauth = true; + } + } + if (!allowOauth) { + this.setState({ + hasError: true, + errorMsg: 'oauth.login', + }); + + this.setState({ loading: false }); + return; + } + this.setState({ client_id: clientId, response_type: responseType, diff --git a/src/components/login/oauth/authorize/locales/zh_CN.json b/src/components/login/oauth/authorize/locales/zh_CN.json index 34d0f8371..8f14c69e3 100644 --- a/src/components/login/oauth/authorize/locales/zh_CN.json +++ b/src/components/login/oauth/authorize/locales/zh_CN.json @@ -16,6 +16,7 @@ "missing_client_id": "缺少 client_id 参数", "unknown": "未知错误,请稍后重试" } - } + }, + "login": "当前暂未支持该第三方应用授权登录" } } \ No newline at end of file diff --git a/src/components/oauth/management/oauthProvider/upsert/web.pc.tsx b/src/components/oauth/management/oauthProvider/upsert/web.pc.tsx index 5a5443e38..85bf48e0d 100644 --- a/src/components/oauth/management/oauthProvider/upsert/web.pc.tsx +++ b/src/components/oauth/management/oauthProvider/upsert/web.pc.tsx @@ -92,6 +92,7 @@ const Upsert = ( - update({ ableState: checked ? "enabled" : "disabled" })} /> + update({ ableState: checked ? "enabled" : "disabled" })} /> diff --git a/src/components/passport/email/index.tsx b/src/components/passport/email/index.tsx index b316f636d..2ebd46f72 100644 --- a/src/components/passport/email/index.tsx +++ b/src/components/passport/email/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useRef } from "react"; -import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig } from "../../../entities/Passport"; +import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig, NameConfig, OAuthConfig } from "../../../entities/Passport"; import { EntityDict } from "../../../oak-app-domain"; import { Space, Switch, Alert, Typography, Form, Input, Radio, Tag, Select, Tooltip, InputRef, Flex } from 'antd'; import Styles from './web.module.less'; @@ -100,7 +100,7 @@ export default function Email(props: { passport: EntityDict['passport']['OpSchema'] & { stateColor: string }; t: (k: string, params?: any) => string; changeEnabled: (enabled: boolean) => void; - updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig, path: string, value: any, type?: string) => void; + updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig, path: string, value: any, type?: string) => void; }) { const { passport, t, changeEnabled, updateConfig } = props; const { id, type, enabled, stateColor } = passport; diff --git a/src/components/passport/index.ts b/src/components/passport/index.ts index c36b38c77..72cd93d34 100644 --- a/src/components/passport/index.ts +++ b/src/components/passport/index.ts @@ -1,7 +1,7 @@ import { cloneDeep, isEqual, set, } from "oak-domain/lib/utils/lodash"; -import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, Type } from "../../entities/Passport"; -import { WebConfig, WechatMpConfig, WechatPublicConfig } from "../../entities/Application"; +import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, NameConfig, OAuthConfig } from "../../entities/Passport"; import { EntityDict } from "../../oak-app-domain"; +import { WechatMpConfig } from "../../entities/Application"; export default OakComponent({ entity: 'passport', @@ -40,9 +40,10 @@ export default OakComponent({ formData({ data }) { const passports = data.map((ele) => { const stateColor = ele.type ? this.features.style.getColor('passport', 'type', ele.type) : '#00BFFF'; - let appIdStr; + let appIdStr, hasQrCodePrefix = false; if (ele.type === 'wechatMpForWeb') { appIdStr = this.getAppIdStr('wechatMp', (ele.config as MfwConfig)?.appId); + hasQrCodePrefix = this.checkMpQrCodePrefix((ele.config as MfwConfig)?.appId); } else if (ele.type === 'wechatPublicForWeb') { appIdStr = this.getAppIdStr('wechatPublic', (ele.config as PfwConfig)?.appId); } @@ -50,6 +51,7 @@ export default OakComponent({ ...ele, appIdStr, stateColor, + hasQrCodePrefix, } }) return { @@ -57,11 +59,41 @@ export default OakComponent({ }; }, data: { + oauthOptions: [] as { + label: string; + value: string; + }[], }, lifetimes: { + async ready() { + const { systemId } = this.props; + const { data: oauthProviders } = await this.features.cache.refresh('oauthProvider', { + data: { + id: 1, + name: 1, + systemId: 1, + ableState: 1, + }, + filter: { + systemId, + ableState: 'enabled' + } + }); + if (oauthProviders && oauthProviders?.length > 0) { + const oauthOptions = oauthProviders?.map((ele) => { + return { + label: ele.name!, + value: ele.id!, + } + }) + this.setState({ + oauthOptions, + }) + } + } }, methods: { - updateConfig(id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig, path: string, value: any, type?: string) { + updateConfig(id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | NameConfig | OAuthConfig, path: string, value: any, type?: string) { const newConfig = cloneDeep(config); set(newConfig, path, value); if (path === 'mockSend' && !value) { @@ -69,24 +101,24 @@ export default OakComponent({ if (!(newConfig as SmsConfig).templateName || (newConfig as SmsConfig).templateName === '') { this.setMessage({ type: 'warning', - content: '短信登录未配置模板名称,将无法正常使用短信登录' + content: '手机号登录未配置模板名称,将无法正常使用手机号登录' }) } else if (!(newConfig as SmsConfig).defaultOrigin) { this.setMessage({ type: 'warning', - content: '短信登录未选择默认渠道,将无法正常使用短信登录' + content: '手机号登录未选择默认渠道,将无法正常使用手机号登录' }) } } else if (type === 'email') { if (!(newConfig as EmailConfig).account || (newConfig as EmailConfig).account === '') { this.setMessage({ type: 'warning', - content: '邮箱登录未指定邮箱账号,将无法正常使用短信登录' + content: '邮箱登录未指定邮箱账号,将无法正常使用邮箱登录' }) } else if (!(newConfig as EmailConfig).subject || (newConfig as EmailConfig).subject === '') { this.setMessage({ type: 'warning', - content: '邮箱登录未配置邮件主题,将无法正常使用短信登录' + content: '邮箱登录未配置邮件主题,将无法正常使用邮箱登录' }) } else if ( (!(newConfig as EmailConfig).text || (newConfig as EmailConfig).text === '' || !(newConfig as EmailConfig).text?.includes('${code}')) && @@ -94,7 +126,7 @@ export default OakComponent({ ) { this.setMessage({ type: 'warning', - content: '邮箱登录未配置邮件内容模板,将无法正常使用短信登录' + content: '邮箱登录未配置邮件内容模板,将无法正常使用邮箱登录' }) } } @@ -103,6 +135,11 @@ export default OakComponent({ type: 'warning', content: '未填写appId,该登录方式将无法正常使用' }) + } else if (type === 'oauth ' && path === 'oauthId' && !(value && value.length > 0)) { + this.setMessage({ + type: 'warning', + content: '未选择oauth提供商,将无法正常使用OAuth授权登录' + }) } this.updateItem({ config: newConfig, @@ -117,7 +154,7 @@ export default OakComponent({ tip: string, }[] = []; for (const passport of passports) { - const { type, config, enabled, id } = passport; + const { type, config = {}, enabled, id } = passport; if (enabled) { //检查启用的passport对应的config是否设置 switch (type) { @@ -127,7 +164,7 @@ export default OakComponent({ warnings.push({ id, type, - tip: '短信登录未配置验证码模板名称', + tip: '手机号登录未配置验证码模板名称', }); } if (!(config as SmsConfig).defaultOrigin) { @@ -138,7 +175,7 @@ export default OakComponent({ warnings.push({ id, type, - tip: '短信登录未选择默认渠道', + tip: '手机号登录未选择默认渠道', }); } } @@ -146,13 +183,13 @@ export default OakComponent({ break; case 'email': if (!(config as EmailConfig).mockSend) { - if (!(config as EmailConfig).account || (config as EmailConfig).account === '') { + if (!(config as EmailConfig).account) { warnings.push({ id, type, tip: '邮箱登录未指定邮箱账号', }); - } else if (!(config as EmailConfig).subject || (config as EmailConfig).subject === '') { + } else if (!(config as EmailConfig).subject) { const emailWarning = warnings.find((ele) => ele.id === id); if (emailWarning) { Object.assign(emailWarning, { tip: emailWarning.tip + '、邮件主题' }); @@ -164,8 +201,8 @@ export default OakComponent({ }); } } else if ( - (!(config as EmailConfig).text || (config as EmailConfig).text === '' || !(config as EmailConfig).text?.includes('${code}')) && - (!(config as EmailConfig).html || (config as EmailConfig).html === '' || !(config as EmailConfig).html?.includes('${code}')) + (!(config as EmailConfig).text || !(config as EmailConfig).text?.includes('${code}')) && + (!(config as EmailConfig).html || !(config as EmailConfig).html?.includes('${code}')) ) { const emailWarning = warnings.find((ele) => ele.id === id); if (emailWarning) { @@ -181,7 +218,7 @@ export default OakComponent({ } break; case 'wechatPublicForWeb': - if (!(config as PfwConfig).appId || (config as PfwConfig).appId === '') { + if (!(config as PfwConfig).appId) { warnings.push({ id, type, @@ -190,7 +227,7 @@ export default OakComponent({ } break; case 'wechatMpForWeb': - if (!(config as MfwConfig).appId || (config as MfwConfig).appId === '') { + if (!(config as MfwConfig).appId) { warnings.push({ id, type, @@ -198,6 +235,15 @@ export default OakComponent({ }); } break; + case 'oauth': + if (!((config as OAuthConfig).oauthIds && (config as OAuthConfig).oauthIds.length > 0)) { + warnings.push({ + id, + type, + tip: 'OAuth授权登录未选择oauth供应商', + }); + } + break; default: break; } @@ -239,6 +285,26 @@ export default OakComponent({ } ) return application?.name ? appId + ' (applicationName:' + application.name + ')' : appId + }, + checkMpQrCodePrefix(appId: string) { + const systemId = this.features.application.getApplication().systemId; + const [application] = this.features.cache.get('application', + { + data: { + id: 1, + config: 1, + }, + filter: { + systemId, + config: { + appId, + }, + type: 'wechatMp', + } + } + ); + const config = application?.config as WechatMpConfig; + return !!config?.qrCodePrefix; } }, diff --git a/src/components/passport/loginName/index.tsx b/src/components/passport/loginName/index.tsx new file mode 100644 index 000000000..9a2a5ed5a --- /dev/null +++ b/src/components/passport/loginName/index.tsx @@ -0,0 +1,127 @@ +import React, { useEffect, useState } from "react"; +import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig, NameConfig, OAuthConfig } from "../../../entities/Passport"; +import { EntityDict } from "../../../oak-app-domain"; +import { Switch, Form, Input, Select, Space, Tag, InputNumber, Radio, } from 'antd'; +import Styles from './web.module.less'; +import EditorRegexs from "../password/editorRegexs"; + +export default function LoginName(props: { + passport: EntityDict['passport']['OpSchema'] & { stateColor: string }; + t: (k: string, params?: any) => string; + changeEnabled: (enabled: boolean) => void; + updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig, path: string, value: any, type?: string) => void; +}) { + const { passport, t, changeEnabled, updateConfig } = props; + const { id, type, enabled, stateColor } = passport; + const config = passport.config as NameConfig || {}; + const [min, setMin] = useState(config?.min); + const [max, setMax] = useState(config?.max); + const [regexs, setRegexs] = useState(config?.regexs || []); + const [register, setRegister] = useState(false); + const [tip, setTip] = useState(config?.tip || ''); + + useEffect(() => { + setMin(config?.min || 2); + setMax(config?.max || 8); + setRegexs(config?.regexs || []); + setRegister(!!config?.register); + setTip(config?.tip || ''); + }, [config]); + + return ( +
+
+ {t(`passport:v.type.${type}`)} + { + changeEnabled(checked) + }} + /> +
+ {enabled && +
+
+ + + { + updateConfig(id, config!, 'min', value, 'loginName'); + }} /> +
~
+ { + updateConfig(id, config!, 'max', value, 'loginName'); + }} /> +
+
+ + { + updateConfig(id, config!, 'verify', checked, 'loginName'); + }} + /> + + + <> + {!!(config as PwdConfig)?.verify ? ( + <> + { + updateConfig(id, config!, 'regexs', regexs, 'loginName'); + }} /> + + ) : ( +
暂未启用正则校验,无需设置
+ )} + +
+ + { + updateConfig(id, config!, 'register', checked, 'loginName'); + }} + /> + + + { + setTip(e.target.value); + }} + onBlur={() => { + if (tip && tip !== (config as PwdConfig)?.tip) { + updateConfig(id, config!, 'tip', tip, 'loginName'); + } + }} + /> + +
+
+ } +
+ ) +} \ No newline at end of file diff --git a/src/components/passport/loginName/web.module.less b/src/components/passport/loginName/web.module.less new file mode 100644 index 000000000..3af5758c2 --- /dev/null +++ b/src/components/passport/loginName/web.module.less @@ -0,0 +1,12 @@ +.item { + padding: 10px 16px; + border-radius: 12px; + border: 1px solid var(--oak-border-color); + margin: 10px 0px; +} + +.title { + display: flex; + align-items: center; + justify-content: space-between; +} \ No newline at end of file diff --git a/src/components/passport/oauth/index.tsx b/src/components/passport/oauth/index.tsx new file mode 100644 index 000000000..f82ef14ac --- /dev/null +++ b/src/components/passport/oauth/index.tsx @@ -0,0 +1,65 @@ +import React, { useEffect, useState } from "react"; +import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig, NameConfig, OAuthConfig } from "../../../entities/Passport"; +import { EntityDict } from "../../../oak-app-domain"; +import { Switch, Form, Select, Tag, Tooltip, } from 'antd'; +import Styles from './web.module.less'; + +export default function Oauth(props: { + passport: EntityDict['passport']['OpSchema'] & { stateColor: string }; + t: (k: string, params?: any) => string; + changeEnabled: (enabled: boolean) => void; + updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig, path: string, value: any, type?: string) => void; + oauthOptions: { label: string, value: string }[]; +}) { + const { passport, t, changeEnabled, updateConfig, oauthOptions } = props; + const { id, type, enabled, stateColor } = passport; + const config = passport.config as OAuthConfig || {}; + const [oauthIds, setOauthIds] = useState(config?.oauthIds); + + useEffect(() => { + setOauthIds(config?.oauthIds || []); + }, [config]); + + return ( +
+
+ {t(`passport:v.type.${type}`)} + 0) ? '' : '请先启用oauth供应商'}> + { + changeEnabled(checked) + }} + disabled={!(oauthOptions && oauthOptions?.length > 0)} + /> + +
+ {enabled && +
+
+ + setLoginName(currentTarget.value)} + placeholder={t('placeholder.loginName')} + value={loginName} + /> + + { + if (value.length < pwdMin) { + setValidateHelp(t('validator.pwdMin', { pwdMin })); + setValidateStatus('error'); + return; + } else if (value.length > pwdMax) { + setValidateHelp(t('validator.pwdMax', { pwdMax })); + setValidateStatus('error'); + return; + } else if (!!pwdNeedVerify && pwdRegexs && pwdRegexs.length > 0) { + for (const regex of pwdRegexs) { + const pattern = new RegExp(regex); + if (!pattern.test(value)) { + setValidateHelp(t('validator.pwdVerify')); + setValidateHelp2('') + setValidateStatus('error'); + return; + } + } + if (password2) { + setValidateHelp('') + setValidateHelp2( + value === password2 + ? '' + : t('validator.pwdDiff') + ); + setValidateStatus( + value === password2 + ? 'success' + : 'error' + ); + } else { + setValidateHelp2(t('noRepwd')); + setValidateHelp(''); + setValidateStatus('error'); + } + } + else { + if (password2) { + setValidateHelp('') + setValidateHelp2( + value === password2 + ? '' + : t('validator.pwdDiff') + ); + setValidateStatus( + value === password2 + ? 'success' + : 'error' + ); + } else { + setValidateHelp2(t('validator.noRepwd')); + setValidateHelp(''); + setValidateStatus('error'); + } + } + } + }, + ]} + hasFeedback + validateStatus={validateStatus} + > + { + const strValue = e.target.value; + setPassword(strValue); + }} + iconRender={(visible) => (visible ? : )} + placeholder={t('placeholder.password')} + /> + + { + if (password.length < pwdMin || password.length > pwdMax) { + return; + } + else if (!!pwdNeedVerify && pwdRegexs && pwdRegexs.length > 0) { + for (const regex of pwdRegexs) { + const pattern = new RegExp(regex); + if (!pattern.test(password)) { + return; + } + } + } + setValidateHelp2(value === password ? '' : t('validator.pwdDiff')) + setValidateStatus(value === password ? 'success' : 'error') + } + }, + ]} + validateTrigger="onChange" + help={validateHelp2} + validateStatus={validateStatus} + hasFeedback + > + { + const strValue = e.target.value; + setPassword2(strValue) + }} + iconRender={(visible) => (visible ? : )} + placeholder={t('placeholder.rePwd')} + /> + + +
+ + {!!goLogin && ( +
+ +
+ )} +
+
+ ); +} diff --git a/src/components/userRelation/upsert/onUser/index.ts b/src/components/userRelation/upsert/onUser/index.ts index 711d0e50c..011c62689 100644 --- a/src/components/userRelation/upsert/onUser/index.ts +++ b/src/components/userRelation/upsert/onUser/index.ts @@ -90,9 +90,8 @@ export default OakComponent({ }, lifetimes: { async ready() { - const application = this.features.application.getApplication(); - const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); - const passwordConfig: PwdConfig = applicationPassports.find((ele: EntityDict['applicationPassport']['Schema']) => ele.passport.type === 'password')?.passport.config; + const system = this.features.application.getApplication().system; + const passwordConfig = system?.config.Password; const mode = passwordConfig?.mode ?? 'all'; const pwdMin = passwordConfig?.min ?? 8; const pwdMax = passwordConfig?.max ?? 24; diff --git a/src/components/wechatLogin/confirm/index.less b/src/components/wechatLogin/confirm/index.less index 5bb28b408..c388362d7 100644 --- a/src/components/wechatLogin/confirm/index.less +++ b/src/components/wechatLogin/confirm/index.less @@ -9,6 +9,8 @@ flex-direction: column; background-color: @oak-bg-color-container; box-sizing: border-box; + align-items: center; + justify-content: center; .safe-area-inset-bottom(); } @@ -19,19 +21,6 @@ margin-bottom: 60rpx; } -.circle-view { - margin-top: 30rpx; - padding: 10rpx; - width: 200rpx; - height: 200rpx; - border-radius: 50%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background-color: #fff; -} - .title { font-size: 32rpx; color: @oak-text-color-primary; @@ -41,4 +30,23 @@ margin-top: 16rpx; font-size: 28rpx; color: @oak-text-color-secondary; +} + +.circle-view { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.text { + font-size: 36rpx; + color: @oak-text-color-primary; + margin-top: 16rpx; +} + +.desc { + font-size: 24rpx; + color: @oak-text-color-secondary; + margin-top: 16rpx; } \ No newline at end of file diff --git a/src/components/wechatLogin/confirm/index.ts b/src/components/wechatLogin/confirm/index.ts index adbcc764c..a015a7cf9 100644 --- a/src/components/wechatLogin/confirm/index.ts +++ b/src/components/wechatLogin/confirm/index.ts @@ -44,6 +44,16 @@ export default OakComponent({ const protocol = window.location.protocol === 'https:' ? 'https' : 'http'; const redirectUri = encodeURIComponent(`${protocol}://${host}${wechatUserLoginPage}?wechatLoginId=${wechatLoginId}`); window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect` + }, + async loginByWechatMp() { + const { loginUserId } = this.state; + if (!loginUserId) { + // 先小程序登录 + await this.features.token.loginWechatMp(); + } + await this.features.token.loginWechatMp({ wechatLoginId: this.props.oakId }); + this.refresh(); } + }, }); diff --git a/src/components/wechatLogin/confirm/index.xml b/src/components/wechatLogin/confirm/index.xml index 9eea2a130..4f415e633 100644 --- a/src/components/wechatLogin/confirm/index.xml +++ b/src/components/wechatLogin/confirm/index.xml @@ -1,4 +1,30 @@ - 绑定小程序尚未实现 + + + + 二维码已过期,请重新扫码 + 抱歉,该码已过期 + + + + + + 登录成功 + + + + 一键登录 + + + + + 绑定小程序尚未实现 + \ No newline at end of file diff --git a/src/components/wechatLogin/qrCode/index.ts b/src/components/wechatLogin/qrCode/index.ts index 16f7f4ea8..f6a9e5946 100644 --- a/src/components/wechatLogin/qrCode/index.ts +++ b/src/components/wechatLogin/qrCode/index.ts @@ -33,11 +33,12 @@ export default OakComponent({ url: '', // 扫码登录/绑定成功跳转的页面 size: undefined, disableBack: false, // 扫码登录/绑定成功后 是否禁用返回 - wechatLoginConfirmPage: '/wechatLogin/confirm' + wechatLoginConfirmPage: '/wechatLogin/confirm', + qrCodeType: 'wechatPublic' as EntityDict['wechatLogin']['Schema']['qrCodeType'], }, methods: { async createWechatLogin() { - const { type = 'bind', wechatLoginConfirmPage } = this.props; + const { type = 'bind', wechatLoginConfirmPage, qrCodeType } = this.props; const { result: wechatLoginId } = await this.features.cache.exec( 'createWechatLogin', { @@ -45,7 +46,8 @@ export default OakComponent({ interval: Interval, router: { pathname: wechatLoginConfirmPage - } + }, + qrCodeType, } ); this.setState( @@ -166,7 +168,7 @@ export default OakComponent({ wechatLoginId ); } - + // url存在则跳转 if (url) { this.redirectTo({ diff --git a/src/components/wechatUser/login/index.ts b/src/components/wechatUser/login/index.ts index b2c75da42..4bdd165c9 100644 --- a/src/components/wechatUser/login/index.ts +++ b/src/components/wechatUser/login/index.ts @@ -7,10 +7,10 @@ export default OakComponent({ }, lifetimes: { attached() { - if (process.env.OAK_PLATFORM === 'web') { + // if (process.env.OAK_PLATFORM === 'web') { //处理微信授权登录 this.login(); - } + // } }, }, methods: { diff --git a/src/data/i18n.ts b/src/data/i18n.ts index a6e3da8d4..e522ea3b5 100644 --- a/src/data/i18n.ts +++ b/src/data/i18n.ts @@ -268,7 +268,8 @@ const i18ns: I18n[] = [ "missing_client_id": "缺少 client_id 参数", "unknown": "未知错误,请稍后重试" } - } + }, + "login": "当前暂未支持该第三方应用授权登录" } } }, @@ -667,10 +668,15 @@ const i18ns: I18n[] = [ "Login": "登录", "Send": "发送验证码", "placeholder": { - "Captcha": "输入4位验证码", + "Captcha": "输入%{digit}位验证码", "Email": "请输入邮箱" }, - "resendAfter": "秒后可重发" + "resendAfter": "秒后可重发", + "loginMode": { + "wechatMp": "一键登录", + "sms": "短信登录", + "password": "密码登录" + } } }, { @@ -694,7 +700,12 @@ const i18ns: I18n[] = [ "resendAfter": "秒后可重发", "otherMethods": "其他登录方式", "scanLogin": "扫码登录", - "tip": "未注册用户首次登录将自动注册" + "tip": "未注册用户首次登录将自动注册", + "goRegister": "去注册", + "loginMode": { + "wechatMp": "授权登录", + "other": "其他方式登录" + } } }, { @@ -706,10 +717,17 @@ const i18ns: I18n[] = [ data: { "Login": "登录", "placeholder": { - "Account": "请输入账号", - "Mobile": "/手机号", - "Email": "/邮箱", + "Account": "请输入", + "LoginName": "账号", + "Mobile": "手机号", + "Email": "邮箱", "Password": "请输入密码" + }, + "register": "去注册", + "loginMode": { + "wechatMp": "一键登录", + "sms": "短信登录", + "email": "邮箱登录" } } }, @@ -726,7 +744,12 @@ const i18ns: I18n[] = [ "Captcha": "输入%{digit}位短信验证码", "Mobile": "请输入手机号" }, - "resendAfter": "秒后可重发" + "resendAfter": "秒后可重发", + "loginMode": { + "wechatMp": "一键登录", + "password": "密码登录", + "email": "邮箱登录" + } } }, { @@ -788,6 +811,40 @@ const i18ns: I18n[] = [ } } }, + { + id: "36c643dbcc19c3258f6077a6684236ff", + namespace: "oak-general-business-c-user-register", + language: "zh-CN", + module: "oak-general-business", + position: "src/components/user/register", + data: { + "not allow register": "暂未支持自行注册,请联系管理员为您分配账号!", + "registerTitle": "账号注册", + "label": { + "loginName": "账号", + "password": "密码", + "rePwd": "密码确认" + }, + "placeholder": { + "loginName": "请输入账号", + "password": "请输入密码", + "rePwd": "请再次输入密码" + }, + "validator": { + "loginNameMin": "账号最短长度为%{loginNameMin}位", + "loginNameMax": "账号最大长度为%{loginNameMax}位", + "loginNameVerify": "当前账号未符合规范", + "pwdMin": "密码最短长度为%{pwdMin}位", + "pwdMax": "密码最短长度为%{pwdMax}位", + "pwdVerify": "当前密码较弱", + "pwdDiff": "两次输入的密码不一致,请检查", + "noRepwd": "请再次确认密码" + }, + "register": "立即注册", + "goLogin": "已有账号?立即登录", + "success": "注册成功,请前往登录页登录" + } + }, { id: "5bf96a3e054b8d73c76d7bb45ea90a80", namespace: "oak-general-business-c-userEntityGrant-claim", diff --git a/src/entities/ApplicationPassport.ts b/src/entities/ApplicationPassport.ts index b6dbcbfbd..8fcefdc5f 100644 --- a/src/entities/ApplicationPassport.ts +++ b/src/entities/ApplicationPassport.ts @@ -8,6 +8,7 @@ export interface Schema extends EntityShape { application: Application; passport: Passport; isDefault: Boolean; + allowPwd?: Boolean; }; @@ -19,6 +20,7 @@ export const entityDesc: EntityDesc = { application: '应用', passport: '登录方式', isDefault: '是否默认', + allowPwd: '是否支持密码登录', }, }, }, diff --git a/src/entities/Passport.ts b/src/entities/Passport.ts index ccf60c8ea..fee7be9c8 100644 --- a/src/entities/Passport.ts +++ b/src/entities/Passport.ts @@ -3,7 +3,7 @@ import { EntityShape } from 'oak-domain/lib/types/Entity'; import { Schema as System } from './System'; import { EntityDesc } from 'oak-domain/lib/types/EntityDesc'; -export type Type = 'password' | 'sms' | 'email' | 'wechatWeb' | 'wechatMp' | 'wechatPublic' | 'wechatPublicForWeb' | 'wechatMpForWeb' | 'wechatNative'; +export type Type = 'password' | 'sms' | 'email' | 'wechatWeb' | 'wechatMp' | 'wechatPublic' | 'wechatPublicForWeb' | 'wechatMpForWeb' | 'wechatNative' | 'loginName' | 'oauth'; export type SmsConfig = { mockSend?: boolean; defaultOrigin?: 'ali' | 'tencent' | 'ctyun'; //默认渠道 @@ -31,7 +31,7 @@ export type MfwConfig = { appId: string; }; - +// 暂时先保留 export type PwdConfig = { mode: 'all' | 'plain' | 'sha1' //密码存储模式,默认为all min: number; //位数最小值,默认为8 @@ -41,11 +41,24 @@ export type PwdConfig = { tip?: string; //登录提示语 } +export type NameConfig = { + min?: number; //位数最小值,默认为2 + max?: number; //位数最大值,默认为8 + verify?: boolean; //开启正则校验,默认不开启 + regexs?: string[]; + register?: boolean; //允许注册,默认不开启 + tip?: string; //注册提示语 +} + +export type OAuthConfig = { + oauthIds: string[]; +} + export interface Schema extends EntityShape { system: System; type: Type; - config?: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig; + config?: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig; enabled: Boolean; }; @@ -70,14 +83,16 @@ export const entityDesc: EntityDesc< v: { type: { email: '邮箱', - sms: '短信', + sms: '手机号', password: '密码', wechatMp: '小程序', wechatPublic: '公众号', wechatWeb: '微信网站', wechatMpForWeb: '小程序授权网页', wechatPublicForWeb: '公众号授权网页', - wechatNative: '微信APP授权' + wechatNative: '微信APP授权', + loginName: '账号', + oauth: 'OAuth授权' }, }, }, @@ -93,7 +108,9 @@ export const entityDesc: EntityDesc< wechatMp: '#ADDCCA', wechatMpForWeb: '#FDC454', wechatPublicForWeb: '#C0A27C', - wechatNative: '#C0A27C' + wechatNative: '#C0A27C', + loginName: '#456B3C', + oauth: '#3C4655', } } } diff --git a/src/features/token.ts b/src/features/token.ts index 5f0a50819..0a34ea718 100644 --- a/src/features/token.ts +++ b/src/features/token.ts @@ -287,13 +287,14 @@ export class Token extends Feature { this.checkNeedSetPassword(); } - async loginWechatMp() { + async loginWechatMp(params?: { wechatLoginId?: string }) { const { code } = await wx.login(); const env = await this.environment.getEnv(); const { result } = await this.cache.exec('loginWechatMp', { code, env: env as WechatMpEnv, + wechatLoginId: params?.wechatLoginId, }); this.tokenValue = result; await this.storage.save(LOCAL_STORAGE_KEYS.token, result); diff --git a/src/triggers/application.ts b/src/triggers/application.ts index ecbf0e779..10ebb1db4 100644 --- a/src/triggers/application.ts +++ b/src/triggers/application.ts @@ -39,7 +39,7 @@ const triggers: Trigger>[] = [ return !!data.config; }, fn: async ({ operation }, context, option) => { - const { filter } = operation; + const { filter, data } = operation; const applications = await context.select('application', { data: { id: 1, @@ -52,9 +52,8 @@ const triggers: Trigger>[] = [ let count = 0; for (const application of applications) { if (application.type === 'web') { - const { wechat } = application.config as WebConfig || {}; - const { appId, appSecret } = wechat || {}; - if (!(appId && appId !== '' && appSecret && appSecret !== '')) { + const { appId: newAppId, appSecret: newAppSecret } = (data?.config as WebConfig)?.wechat || {}; + if (!newAppId || !newAppSecret) { const [passport] = await context.select('passport', { data: { id: 1, @@ -109,7 +108,7 @@ const triggers: Trigger>[] = [ for (const application of applications) { if (application.type === 'wechatPublic') { const { appId, appSecret, isService } = application.config as WechatPublicConfig || {}; - if (appId && appId !== '') { + if (appId) { const [passport] = await context.select('passport', { data: { id: 1, @@ -125,7 +124,7 @@ const triggers: Trigger>[] = [ count: 1, indexFrom: 0, }, { forUpdate: true }); - if (appSecret && appSecret !== '' && isService) { + if (appSecret && isService) { if (!passport) { await context.operate('passport', { id: await generateNewIdAsync(), @@ -161,7 +160,7 @@ const triggers: Trigger>[] = [ } } else if (application.type === 'wechatMp') { const { appId, appSecret, } = application.config as WechatMpConfig || {}; - if (appId && appId !== '') { + if (appId) { const [passport] = await context.select('passport', { data: { id: 1, @@ -176,7 +175,7 @@ const triggers: Trigger>[] = [ count: 1, indexFrom: 0, }, { forUpdate: true }); - if (appSecret && appSecret !== '') { + if (appSecret) { if (!passport) { await context.operate('passport', { id: await generateNewIdAsync(), @@ -273,6 +272,66 @@ const triggers: Trigger>[] = [ return count; } }, + { + name: 'wechatMp applicaiton清空普通链接二维码规则配置时,将相应的passport禁用', + entity: 'application', + action: 'update', + when: 'after', + check: (operation) => { + const { data } = operation as EntityDict['application']['Update']; + return !!data.config; + }, + fn: async ({ operation }, context, option) => { + const { filter } = operation; + const applications = await context.select('application', { + data: { + id: 1, + config: 1, + type: 1, + systemId: 1, + }, + filter, + }, {}); + let count = 0; + for (const application of applications) { + if (application.type === 'wechatMp') { + const { qrCodePrefix, appId } = application.config as WechatMpConfig || {}; + if (appId && !qrCodePrefix) { + const [passport] = await context.select('passport', { + data: { + id: 1, + }, + filter: { + enabled: true, + systemId: application.systemId!, + type: 'wechatMpForWeb', + config: { + appId, + } + }, + count: 1, + indexFrom: 0, + }, { forUpdate: true }); + if (passport) { + await context.operate('passport', { + id: await generateNewIdAsync(), + action: 'update', + data: { + enabled: false, + config: {}, + }, + filter: { + id: passport.id, + } + }, option); + count++; + } + } + } + } + return count; + } + }, { name: '删除application前,将相关的passport删除', entity: 'application', diff --git a/src/triggers/applicationPassport.ts b/src/triggers/applicationPassport.ts new file mode 100644 index 000000000..28a31ef23 --- /dev/null +++ b/src/triggers/applicationPassport.ts @@ -0,0 +1,36 @@ +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import { CreateTrigger, Trigger, } from 'oak-domain/lib/types/Trigger'; +import { EntityDict } from '../oak-app-domain/EntityDict'; +import { BRC } from '../types/RuntimeCxt'; +import { assert } from 'oak-domain/lib/utils/assert'; + +const triggers: Trigger>[] = [ + { + name: '当loginName类型的applicationPassport创建前,将其allowPwd置为ture', + entity: 'applicationPassport', + action: 'create', + when: 'before', + fn: async ({ operation }, context, option) => { + const { data } = operation; + assert(!(data instanceof Array)); + const [passport] = await context.select('passport', { + data: { + id: 1, + type: 1, + }, + filter: { + id: data?.passportId, + } + }, { + forUpdate: true, + }) + const { type } = passport || {} + if (type === 'loginName' && !data.allowPwd) { + data.allowPwd = true; + } + return 1; + } + } as CreateTrigger>, +]; + +export default triggers; diff --git a/src/triggers/index.ts b/src/triggers/index.ts index 0b9807093..55e1f4718 100644 --- a/src/triggers/index.ts +++ b/src/triggers/index.ts @@ -21,6 +21,7 @@ import oauthProviderTriggers from './oauthProvider'; import oauthUserTriggers from './oauthUser'; import oauthUserAuthTriggers from './oauthUserAuth'; import mobileTriggers from './mobile'; +import applicationPassportTriggers from './applicationPassport'; // import accountTriggers from './account'; @@ -50,4 +51,5 @@ export default [ ...oauthUserTriggers, ...oauthUserAuthTriggers, ...mobileTriggers, + ...applicationPassportTriggers, ]; diff --git a/src/triggers/oauthProvider.ts b/src/triggers/oauthProvider.ts index a44ea64be..113cea9ba 100644 --- a/src/triggers/oauthProvider.ts +++ b/src/triggers/oauthProvider.ts @@ -1,8 +1,11 @@ -import { CreateTrigger, Trigger } from 'oak-domain/lib/types'; +import { CreateTrigger, RemoveTrigger, Trigger } from 'oak-domain/lib/types'; import { BackendRuntimeContext } from '../context/BackendRuntimeContext'; import assert from 'assert'; import { BRC } from '../types/RuntimeCxt'; import { EntityDict } from '../oak-app-domain/EntityDict'; +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import { cloneDeep, pull } from 'oak-domain/lib/utils/lodash'; +import { OAuthConfig } from '../entities/Passport'; const triggers: Trigger>[] = [ { @@ -18,6 +21,193 @@ const triggers: Trigger>[] = [ return 0; } } as CreateTrigger>, + { + name: "创建provider时,新增passport", + action: "create", + when: "after", + entity: "oauthProvider", + asRoot: true, + fn: async ({ operation }, context, option) => { + const { data } = operation as EntityDict['oauthProvider']['CreateSingle']; + let count = 0; + if (data?.systemId) { + const [passport] = await context.select('passport', { + data: { + id: 1, + systemId: 1, + type: 1, + }, + filter: { + systemId: data?.systemId, + type: 'oauth', + }, + indexFrom: 0, + count: 1, + }, { forUpdate: true }) + if (!passport) { + await context.operate('passport', { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + type: 'oauth', + enabled: false, + systemId: data?.systemId, + config: { + oauthIds: [data?.id], + } + } + }, option); + count++; + } + } + return count; + } + } as CreateTrigger>, + { + name: "删除provider时,删除passport", + action: "remove", + when: "before", + entity: "oauthProvider", + asRoot: true, + fn: async ({ operation }, context, option) => { + const { filter } = operation; + const [oauthProvider] = await context.select('oauthProvider', { + data: { + id: 1, + systemId: 1, + }, + filter: filter, + }, { forUpdate: true }); + let count = 0; + if (oauthProvider) { + const { id, systemId } = oauthProvider; + const [other] = await context.select('oauthProvider', { + data: { + id: 1, + systemId: 1, + }, + filter: { + systemId, + id: { + $ne: id!, + } + }, + indexFrom: 0, + count: 1, + }, { forUpdate: true }) + if (!other) { + await context.operate('passport', { + id: await generateNewIdAsync(), + action: 'remove', + data: {}, + filter: { + systemId, + type: 'oauth' + } + }, option); + count++; + } else { + const [passport] = await context.select('passport', { + data: { + id: 1, + type: 1, + config: 1, + systemId: 1, + }, + filter: { + type: 'oauth', + systemId, + config: { + oauthIds: { + $contains: [id!], + } + } + } + }, { forUpdate: true }) + if (passport) { + const { id: passportId, config } = passport; + let newConfig = cloneDeep(config) as OAuthConfig; + pull(newConfig?.oauthIds, id); + await context.operate('passport', { + id: await generateNewIdAsync(), + action: 'update', + data: { + config: newConfig, + }, + filter: { + id: passportId, + } + }, option); + } + } + } + return count; + } + } as RemoveTrigger>, + { + name: '当provider禁用时,更新passport', + entity: 'oauthProvider', + action: 'update', + when: 'after', + check: (operation) => { + const { data } = operation as EntityDict['oauthProvider']['Update']; + return data.hasOwnProperty('ableState') && data.ableState === 'disabled'; + }, + fn: async ({ operation }, context, option) => { + const { filter } = operation; + const [oauthProvider] = await context.select('oauthProvider', { + data: { + id: 1, + systemId: 1, + ableState: 1, + }, + filter: filter, + }, { forUpdate: true }); + assert(oauthProvider, '禁用oauthProvider的filter请勿包含abledState'); + let count = 0; + const { id, systemId } = oauthProvider; + const [passport] = await context.select('passport', { + data: { + id: 1, + type: 1, + config: 1, + systemId: 1, + enabled: 1, + }, + filter: { + type: 'oauth', + systemId, + config: { + oauthIds: { + $contains: [id!], + } + } + } + }, { forUpdate: true }) + if (passport && passport.enabled) { + const { id: passportId, config } = passport; + let newConfig = cloneDeep(config) as OAuthConfig; + pull(newConfig?.oauthIds, id); + if (newConfig?.oauthIds?.length <= 0) { + //无可支持的oauthProvider,将启用了的passport关闭 + await context.operate('passport', { + id: await generateNewIdAsync(), + action: 'update', + data: { + enabled: false, + config: newConfig, + }, + filter: { + id: passport.id, + } + }, option); + count++; + } + } + return count; + } + } ]; export default triggers; diff --git a/src/triggers/system.ts b/src/triggers/system.ts index 8c9848cb1..155cf3f98 100644 --- a/src/triggers/system.ts +++ b/src/triggers/system.ts @@ -9,7 +9,7 @@ import { isEmail } from 'oak-domain/lib/utils/validator'; const { attributes: { type: passportType } } = desc; const triggers: Trigger>[] = [ { - name: '添加system时,添加短信与密码的passport', + name: '添加system时,添加手机号、账号的passport', entity: 'system', action: 'create', when: 'before', @@ -17,9 +17,9 @@ const triggers: Trigger>[] = [ const { data } = operation; assert(!(data instanceof Array)); data.passport$system = [ - //仅创建短信与密码的passport,其他type由相应的application/Email的更新来创建 + //仅创建手机号、账号、密码的passport,其他type由相应的application/Email的更新来创建 { - //短信登录默认为模拟发送 + //手机号登录默认为模拟发送 id: await generateNewIdAsync(), action: 'create', data: { @@ -32,15 +32,27 @@ const triggers: Trigger>[] = [ } }, { - //密码登录 + //账号登录 id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), - type: 'password', - enabled: false, + type: 'loginName', + config: { + }, + enabled: true, } - } + }, + // { + // //密码登录 + // id: await generateNewIdAsync(), + // action: 'create', + // data: { + // id: await generateNewIdAsync(), + // type: 'password', + // enabled: false, + // } + // } ] return 1; } diff --git a/src/types/Config.ts b/src/types/Config.ts index c070f8ba2..259834a87 100644 --- a/src/types/Config.ts +++ b/src/types/Config.ts @@ -81,7 +81,7 @@ export type LocalCosConfig = { }; // S3/Minio 的区域类型(可以根据实际需要扩展) -export type S3Zone = +export type S3Zone = | 'us-east-1' | 'us-west-1' | 'us-west-2' @@ -189,6 +189,15 @@ export type EmailConfig = { secure?: boolean; //是否ssl }; +export type PasswordConfig = { + mode?: 'all' | 'plain' | 'sha1' //密码存储模式,默认为all + min?: number; //位数最小值,默认为8 + max?: number; //位数最大值,默认为24 + verify?: boolean; //开启正则校验,默认不开启 + regexs?: string[]; + tip?: string; //登录提示语 +}; + export type QrCodeType = 'wechatMpDomainUrl' | 'wechatMpWxaCode' | 'wechatPublic' | 'wechatPublicForMp' | 'webForWechatPublic'; export type Config = { @@ -240,7 +249,8 @@ export type Config = { type?: 'password', // 采用密码作为第一安全元素 level?: 'weak' | 'medium' | 'strong'; // 强度 passwordVerifyGap?: number; // 在密码验证后的多长时间内认为是安全的,可以做敏感动作 - } + }; + Password?: PasswordConfig; }; export type AccountOrigin = 'ali' | 'tencent' | 'qiniu' | 'amap' | 'ctyun' | 'local' | 's3'; diff --git a/upgrade/5.11.0/01.sql b/upgrade/5.11.0/01.sql new file mode 100644 index 000000000..2d8b4b73f --- /dev/null +++ b/upgrade/5.11.0/01.sql @@ -0,0 +1,10 @@ +SET SESSION sql_mode = 'TRADITIONAL'; +START TRANSACTION; + +-- applicationPassport +ALTER TABLE `applicationPassport` ADD column `allowPwd` tinyint(1) DEFAULT NULL; + +-- passport +ALTER TABLE `passport` MODIFY column `type` ENUM('password', 'sms', 'email', 'wechatWeb', 'wechatMp', 'wechatPublic', 'wechatPublicForWeb', 'wechatMpForWeb', 'wechatNative', 'loginName', 'oauth') NOT NULL; + +COMMIT; \ No newline at end of file