merge passport2

This commit is contained in:
lxy 2025-12-31 16:21:35 +08:00
commit 570f6e8df5
213 changed files with 5925 additions and 1092 deletions

View File

@ -134,11 +134,13 @@ export type AspectDict<ED extends EntityDict> = {
* *
* @param code code * @param code code
* @param env * @param env
* @param wechatLoginId ID
* @returns token * @returns token
*/ */
loginWechatMp: ({ code, env, }: { loginWechatMp: ({ code, env, wechatLoginId, }: {
code: string; code: string;
env: WechatMpEnv; env: WechatMpEnv;
wechatLoginId?: string;
}, context: BackendRuntimeContext<ED>) => Promise<string>; }, context: BackendRuntimeContext<ED>) => Promise<string>;
/** /**
* APP * APP
@ -299,6 +301,7 @@ export type AspectDict<ED extends EntityDict> = {
type: EntityDict['wechatLogin']['Schema']['type']; type: EntityDict['wechatLogin']['Schema']['type'];
interval: number; interval: number;
router: EntityDict['wechatLogin']['Schema']['router']; router: EntityDict['wechatLogin']['Schema']['router'];
qrCodeType?: EntityDict['wechatLogin']['Schema']['qrCodeType'];
}, context: BackendRuntimeContext<ED>) => Promise<string>; }, context: BackendRuntimeContext<ED>) => Promise<string>;
/** /**
* *
@ -758,5 +761,16 @@ export type AspectDict<ED extends EntityDict> = {
headers?: Record<string, string | string[]>; headers?: Record<string, string | string[]>;
formdata?: Record<string, any>; formdata?: Record<string, any>;
}>; }>;
/**
*
* @param loginName
* @param password
* @param context
* @returns
*/
registerUserByLoginName: (params: {
loginName: string;
password: string;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
}; };
export default AspectDict; export default AspectDict;

View File

@ -12,9 +12,15 @@ export async function getApplicationPassports(params, context) {
config: 1, config: 1,
}, },
isDefault: 1, isDefault: 1,
allowPwd: 1,
}, },
filter: { filter: {
applicationId, applicationId,
passport: {
type: {
$ne: 'password',
}
}
} }
}, {}); }, {});
closeRoot(); closeRoot();

View File

@ -4,7 +4,7 @@ import { getApplication, signatureJsSDK, uploadWechatMedia, batchGetArticle, get
import { updateConfig, updateApplicationConfig, updateStyle } from './config'; import { updateConfig, updateApplicationConfig, updateStyle } from './config';
import { syncWechatTemplate, getMessageType } from './template'; import { syncWechatTemplate, getMessageType } from './template';
import { syncSmsTemplate } from './sms'; import { syncSmsTemplate } from './sms';
import { mergeUser, getChangePasswordChannels, updateUserPassword } from './user'; import { mergeUser, getChangePasswordChannels, updateUserPassword, registerUserByLoginName } from './user';
import { createWechatLogin } from './wechatLogin'; import { createWechatLogin } from './wechatLogin';
import { unbindingWechat } from './wechatUser'; import { unbindingWechat } from './wechatUser';
import { getMpUnlimitWxaCode } from './wechatQrCode'; import { getMpUnlimitWxaCode } from './wechatQrCode';
@ -88,6 +88,7 @@ declare const aspectDict: {
setUserAvatarFromWechat: typeof setUserAvatarFromWechat; setUserAvatarFromWechat: typeof setUserAvatarFromWechat;
mergeChunkedUpload: typeof mergeChunkedUpload; mergeChunkedUpload: typeof mergeChunkedUpload;
presignFile: typeof presignFile; presignFile: typeof presignFile;
registerUserByLoginName: typeof registerUserByLoginName;
}; };
export default aspectDict; export default aspectDict;
export { AspectDict } from './AspectDict'; export { AspectDict } from './AspectDict';

View File

@ -4,7 +4,7 @@ import { getApplication, signatureJsSDK, uploadWechatMedia, batchGetArticle, get
import { updateConfig, updateApplicationConfig, updateStyle } from './config'; import { updateConfig, updateApplicationConfig, updateStyle } from './config';
import { syncWechatTemplate, getMessageType } from './template'; import { syncWechatTemplate, getMessageType } from './template';
import { syncSmsTemplate } from './sms'; import { syncSmsTemplate } from './sms';
import { mergeUser, getChangePasswordChannels, updateUserPassword } from './user'; import { mergeUser, getChangePasswordChannels, updateUserPassword, registerUserByLoginName } from './user';
import { createWechatLogin } from './wechatLogin'; import { createWechatLogin } from './wechatLogin';
import { unbindingWechat } from './wechatUser'; import { unbindingWechat } from './wechatUser';
import { getMpUnlimitWxaCode } from './wechatQrCode'; import { getMpUnlimitWxaCode } from './wechatQrCode';
@ -90,5 +90,6 @@ const aspectDict = {
// extraFile新增 // extraFile新增
mergeChunkedUpload, mergeChunkedUpload,
presignFile, presignFile,
registerUserByLoginName,
}; };
export default aspectDict; export default aspectDict;

View File

@ -16,6 +16,7 @@ export async function loginByOauth(params, context) {
// 验证 state 并获取 OAuth 配置 // 验证 state 并获取 OAuth 配置
const [state] = await context.select("oauthState", { const [state] = await context.select("oauthState", {
data: { data: {
providerId: 1,
provider: { provider: {
type: 1, type: 1,
clientId: 1, clientId: 1,
@ -32,6 +33,31 @@ export async function loginByOauth(params, context) {
state: stateCode, 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?.oauthIds && applicationPassport?.passport?.config)?.oauthIds.includes(state.providerId);
if (!allowOauth) {
throw new OakUserException('error::user.loginWayDisabled');
}
assert(state, '无效的 state 参数'); assert(state, '无效的 state 参数');
assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用'); assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
// 如果已经使用 // 如果已经使用

View File

@ -77,9 +77,10 @@ export declare function loginWechat<ED extends EntityDict>({ code, env, wechatLo
* @param context * @param context
* @returns * @returns
*/ */
export declare function loginWechatMp<ED extends EntityDict>({ code, env, }: { export declare function loginWechatMp<ED extends EntityDict>({ code, env, wechatLoginId, }: {
code: string; code: string;
env: WechatMpEnv; env: WechatMpEnv;
wechatLoginId?: string;
}, context: BRC<ED>): Promise<string>; }, context: BRC<ED>): Promise<string>;
/** /**
* wx.getUserProfile拿到的用户信息 * wx.getUserProfile拿到的用户信息

View File

@ -536,23 +536,9 @@ export async function loginByMobile(params, context) {
} }
export async function verifyPassword(params, context) { export async function verifyPassword(params, context) {
const { password } = params; const { password } = params;
const systemId = context.getSystemId(); const { system } = context.getApplication();
const [pwdPassport] = await context.select('passport', { const pwdConfig = system?.config.Password;
data: { const pwdMode = pwdConfig?.mode ?? 'all';
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';
let pwdFilter = {}; let pwdFilter = {};
if (pwdMode === 'all') { if (pwdMode === 'all') {
pwdFilter = { pwdFilter = {
@ -607,8 +593,35 @@ export async function loginByAccount(params, context) {
assert(password); assert(password);
assert(account); assert(account);
const accountType = isEmail(account) ? 'email' : (isMobile(account) ? 'mobile' : 'loginName'); 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') { if (accountType === 'email') {
const { config, emailConfig } = await getAndCheckPassportByEmail(context, account); const { config, emailConfig } = await getAndCheckPassportByEmail(context, account);
if (!allowEmail) {
throw new OakUserException('暂不支持邮箱登录');
}
const existEmail = await context.select('email', { const existEmail = await context.select('email', {
data: { data: {
id: 1, id: 1,
@ -655,49 +668,23 @@ export async function loginByAccount(params, context) {
throw new OakUserException('error::user.passwordUnmath'); throw new OakUserException('error::user.passwordUnmath');
} }
case 1: { 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 [userRow] = result;
const { email$user, id: userId, } = userRow; const { email$user, id: userId, } = userRow;
needUpdatePassword = !(userRow.password || userRow.passwordSha1); needUpdatePassword = !(userRow.password || userRow.passwordSha1);
if (allowEmail) { const email = email$user?.find(ele => ele.email.toLowerCase() === account.toLowerCase());
const email = email$user?.find(ele => ele.email.toLowerCase() === account.toLowerCase()); if (email) {
if (email) { const ableState = email.ableState;
const ableState = email.ableState; if (ableState === 'disabled') {
if (ableState === 'disabled') { // 虽然密码和邮箱匹配,但邮箱已经禁用了,在可能的情况下提醒用户使用其它方法登录
// 虽然密码和邮箱匹配,但邮箱已经禁用了,在可能的情况下提醒用户使用其它方法登录 const exception = await tryMakeChangeLoginWay(userId, context);
const exception = await tryMakeChangeLoginWay(userId, context); if (exception) {
if (exception) { throw exception;
throw exception;
}
} }
return await setupEmail(account, env, context);
}
else {
throw new OakUserException('error::user.emailUnexists');
} }
return await setupEmail(account, env, context);
} }
else { else {
throw new OakUserException('error::user.loginWayDisabled'); throw new OakUserException('error::user.emailUnexists');
} }
} }
default: { default: {
@ -706,6 +693,9 @@ export async function loginByAccount(params, context) {
} }
} }
else if (accountType === 'mobile') { else if (accountType === 'mobile') {
if (!allowSms) {
throw new OakUserException('暂不支持手机号登录');
}
const existMobile = await context.select('mobile', { const existMobile = await context.select('mobile', {
data: { data: {
id: 1, id: 1,
@ -752,53 +742,27 @@ export async function loginByAccount(params, context) {
throw new OakUserException('手机号与密码不匹配'); throw new OakUserException('手机号与密码不匹配');
} }
case 1: { 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 [userRow] = result;
const { mobile$user, id: userId, } = userRow; const { mobile$user, id: userId, } = userRow;
needUpdatePassword = !(userRow.password || userRow.passwordSha1); needUpdatePassword = !(userRow.password || userRow.passwordSha1);
const allowSms = !!applicationPassports.find((ele) => ele.passport?.type === 'sms'); const mobile = mobile$user?.find(ele => ele.mobile === account);
if (allowSms) { if (mobile) {
const mobile = mobile$user?.find(ele => ele.mobile === account); const ableState = mobile.ableState;
if (mobile) { if (ableState === 'disabled') {
const ableState = mobile.ableState; // 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录
if (ableState === 'disabled') { const exception = await tryMakeChangeLoginWay(userId, context);
// 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录 if (exception) {
const exception = await tryMakeChangeLoginWay(userId, context); throw exception;
if (exception) {
throw exception;
}
} }
return await setupMobile(account, env, context);
}
else {
throw new OakUserException('手机号未注册');
} }
return await setupMobile(account, env, context);
} }
else { else {
throw new OakUserException('暂不支持手机号登录'); throw new OakUserException('手机号未注册');
} }
} }
default: { default: {
throw new OakUserException('不支持的登录方式'); throw new OakUserException('error::user.loginWayDisabled');
} }
} }
} }
@ -869,35 +833,15 @@ export async function loginByAccount(params, context) {
} }
} }
default: { default: {
throw new OakUserException('不支持的登录方式'); throw new OakUserException('error::user.loginWayDisabled');
} }
} }
} }
}; };
const closeRootMode = context.openRootMode(); const closeRootMode = context.openRootMode();
const application = context.getApplication(); const { system } = context.getApplication();
const [applicationPassport] = await context.select('applicationPassport', { const pwdConfig = system?.config.Password;
data: { const pwdMode = pwdConfig?.mode ?? 'all';
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';
let pwdFilter = {}, updateData = {}; let pwdFilter = {}, updateData = {};
if (pwdMode === 'all') { if (pwdMode === 'all') {
pwdFilter = { pwdFilter = {
@ -1981,9 +1925,9 @@ export async function loginWechat({ code, env, wechatLoginId, }, context) {
* @param context * @param context
* @returns * @returns
*/ */
export async function loginWechatMp({ code, env, }, context) { export async function loginWechatMp({ code, env, wechatLoginId, }, context) {
const closeRootMode = context.openRootMode(); const closeRootMode = context.openRootMode();
const tokenValue = await loginFromWechatEnv(code, env, context); const tokenValue = await loginFromWechatEnv(code, env, context, wechatLoginId);
await loadTokenInfo(tokenValue, context); await loadTokenInfo(tokenValue, context);
closeRootMode(); closeRootMode();
return tokenValue; return tokenValue;

View File

@ -23,3 +23,12 @@ export declare function updateUserPassword<ED extends EntityDict>(params: {
result: string; result: string;
times?: undefined; times?: undefined;
}>; }>;
/**
*
* @param params
* @param context
*/
export declare function registerUserByLoginName<ED extends EntityDict>(params: {
loginName: string;
password: string;
}, context: BRC<ED>): Promise<void>;

View File

@ -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 { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
import { encryptPasswordSha1 } from '../utils/password'; import { encryptPasswordSha1 } from '../utils/password';
import { assert } from 'oak-domain/lib/utils/assert'; import { assert } from 'oak-domain/lib/utils/assert';
@ -177,20 +177,17 @@ export async function updateUserPassword(params, context, innerLogic) {
const systemId = context.getSystemId(); const systemId = context.getSystemId();
const closeRootMode = context.openRootMode(); const closeRootMode = context.openRootMode();
try { try {
const [passport] = await context.select('passport', { const [system] = await context.select('system', {
data: { data: {
id: 1, id: 1,
type: 1,
config: 1, config: 1,
systemId: 1,
}, },
filter: { filter: {
systemId, id: systemId,
type: 'password',
} }
}, { forUpdate: true }); }, { forUpdate: true });
assert(passport); assert(system);
const config = passport.config; const config = system.config?.Password;
const mode = config?.mode ?? 'all'; const mode = config?.mode ?? 'all';
const [user] = await context.select('user', { const [user] = await context.select('user', {
data: { data: {
@ -257,13 +254,13 @@ export async function updateUserPassword(params, context, innerLogic) {
}; };
} }
const allowUpdate = mode === 'sha1' ? user.passwordSha1 === prevPassword : user.password === prevPassword; //sha1密文模式判断密文是否相等 const allowUpdate = mode === 'sha1' ? user.passwordSha1 === prevPassword : user.password === prevPassword; //sha1密文模式判断密文是否相等
let userDate = {}, changeCreateDate = {}; let userData = {}, changeCreateData = {};
if (mode === 'all') { if (mode === 'all') {
userDate = { userData = {
password: newPassword, password: newPassword,
passwordSha1: encryptPasswordSha1(newPassword), passwordSha1: encryptPasswordSha1(newPassword),
}; };
changeCreateDate = { changeCreateData = {
prevPassword, prevPassword,
newPassword, newPassword,
prevPasswordSha1: encryptPasswordSha1(prevPassword), prevPasswordSha1: encryptPasswordSha1(prevPassword),
@ -271,19 +268,19 @@ export async function updateUserPassword(params, context, innerLogic) {
}; };
} }
else if (mode === 'plain') { else if (mode === 'plain') {
userDate = { userData = {
password: newPassword, password: newPassword,
}; };
changeCreateDate = { changeCreateData = {
prevPassword, prevPassword,
newPassword, newPassword,
}; };
} }
else if (mode === 'sha1') { else if (mode === 'sha1') {
userDate = { userData = {
passwordSha1: newPassword, passwordSha1: newPassword,
}; };
changeCreateDate = { changeCreateData = {
prevPasswordSha1: prevPassword, prevPasswordSha1: prevPassword,
newPasswordSha1: newPassword, newPasswordSha1: newPassword,
}; };
@ -292,7 +289,7 @@ export async function updateUserPassword(params, context, innerLogic) {
await context.operate('user', { await context.operate('user', {
id: await generateNewIdAsync(), id: await generateNewIdAsync(),
action: 'update', action: 'update',
data: userDate, data: userData,
filter: { filter: {
id: userId, id: userId,
}, },
@ -306,7 +303,7 @@ export async function updateUserPassword(params, context, innerLogic) {
id: await generateNewIdAsync(), id: await generateNewIdAsync(),
userId, userId,
result: 'success', result: 'success',
...changeCreateDate, ...changeCreateData,
}, },
}, { }, {
dontCollect: true, dontCollect: true,
@ -324,7 +321,7 @@ export async function updateUserPassword(params, context, innerLogic) {
id: await generateNewIdAsync(), id: await generateNewIdAsync(),
userId, userId,
result: 'fail', result: 'fail',
...changeCreateDate, ...changeCreateData,
}, },
}, { }, {
dontCollect: true, dontCollect: true,
@ -353,13 +350,13 @@ export async function updateUserPassword(params, context, innerLogic) {
dontCollect: true, dontCollect: true,
}); });
if (aliveCaptcha) { if (aliveCaptcha) {
let userDate = {}, changeCreateDate = {}; let userData = {}, changeCreateData = {};
if (mode === 'all') { if (mode === 'all') {
userDate = { userData = {
password: newPassword, password: newPassword,
passwordSha1: encryptPasswordSha1(newPassword), passwordSha1: encryptPasswordSha1(newPassword),
}; };
changeCreateDate = { changeCreateData = {
prevPassword: user.password, prevPassword: user.password,
newPassword, newPassword,
prevPasswordSha1: user.passwordSha1, prevPasswordSha1: user.passwordSha1,
@ -367,19 +364,19 @@ export async function updateUserPassword(params, context, innerLogic) {
}; };
} }
else if (mode === 'plain') { else if (mode === 'plain') {
userDate = { userData = {
password: newPassword, password: newPassword,
}; };
changeCreateDate = { changeCreateData = {
prevPassword: user.password, prevPassword: user.password,
newPassword, newPassword,
}; };
} }
else if (mode === 'sha1') { else if (mode === 'sha1') {
userDate = { userData = {
passwordSha1: newPassword, passwordSha1: newPassword,
}; };
changeCreateDate = { changeCreateData = {
prevPasswordSha1: user.passwordSha1, prevPasswordSha1: user.passwordSha1,
newPasswordSha1: newPassword, newPasswordSha1: newPassword,
}; };
@ -387,7 +384,7 @@ export async function updateUserPassword(params, context, innerLogic) {
await context.operate('user', { await context.operate('user', {
id: await generateNewIdAsync(), id: await generateNewIdAsync(),
action: 'update', action: 'update',
data: userDate, data: userData,
filter: { filter: {
id: userId, id: userId,
}, },
@ -401,7 +398,7 @@ export async function updateUserPassword(params, context, innerLogic) {
id: await generateNewIdAsync(), id: await generateNewIdAsync(),
userId, userId,
result: 'success', result: 'success',
...changeCreateDate, ...changeCreateData,
}, },
}, { }, {
dontCollect: true, dontCollect: true,
@ -428,3 +425,83 @@ export async function updateUserPassword(params, context, innerLogic) {
throw err; 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;
}
}

View File

@ -4,4 +4,5 @@ export declare function createWechatLogin<ED extends EntityDict>(params: {
type: EntityDict['wechatLogin']['Schema']['type']; type: EntityDict['wechatLogin']['Schema']['type'];
interval: number; interval: number;
router: EntityDict['wechatLogin']['Schema']['router']; router: EntityDict['wechatLogin']['Schema']['router'];
qrCodeType?: EntityDict['wechatLogin']['Schema']['qrCodeType'];
}, context: BRC<ED>): Promise<string>; }, context: BRC<ED>): Promise<string>;

View File

@ -1,6 +1,6 @@
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
export async function createWechatLogin(params, context) { export async function createWechatLogin(params, context) {
const { type, interval, router } = params; const { type, interval, qrCodeType = "wechatPublic", router } = params;
let userId; let userId;
if (type === 'bind') { if (type === 'bind') {
userId = context.getCurrentUserId(); userId = context.getCurrentUserId();
@ -26,7 +26,7 @@ export async function createWechatLogin(params, context) {
type, type,
expiresAt: Date.now() + interval, expiresAt: Date.now() + interval,
expired: false, expired: false,
qrCodeType: 'wechatPublic', qrCodeType,
successed: false, successed: false,
router: _router, router: _router,
}; };

View File

@ -20,6 +20,7 @@ export default OakComponent({
enabled: 1, enabled: 1,
}, },
isDefault: 1, isDefault: 1,
allowPwd: 1,
}, },
properties: { properties: {
systemId: '', systemId: '',
@ -34,6 +35,9 @@ export default OakComponent({
}, },
passport: { passport: {
systemId, systemId,
type: {
$ne: 'password'
}
}, },
}; };
} }
@ -83,8 +87,9 @@ export default OakComponent({
} }
else { else {
const { disabled, disabledTip } = this.checkDisabled(a, r[0]); const { disabled, disabledTip } = this.checkDisabled(a, r[0]);
const { showPwd, pwdDisabled, pwdDisabledTip } = this.checkPwd(r[0]);
const apId = await generateNewIdAsync(); 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 }); Object.assign(item, { typeRecords });
@ -106,8 +111,15 @@ export default OakComponent({
if (apArray[aIdx].typeRecords[t].pId === p.id) { if (apArray[aIdx].typeRecords[t].pId === p.id) {
apArray[aIdx].typeRecords[t].checked = true; apArray[aIdx].typeRecords[t].checked = true;
apArray[aIdx].typeRecords[t].apId = ap.id; 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({ apArray[aIdx].defaultOptions.push({
label: this.t(`passport:v.type.${p.type}`), label: this.t(`passport:v.type.${p.type}`),
value: ap.id, value: ap.id,
@ -157,6 +169,9 @@ export default OakComponent({
filter: { filter: {
systemId, systemId,
enabled: true, enabled: true,
type: {
$ne: 'password',
}
}, },
sorter: [{ sorter: [{
$attr: { $attr: {
@ -193,36 +208,36 @@ export default OakComponent({
switch (pType) { switch (pType) {
case 'sms': case 'sms':
if (!pConfig.mockSend) { if (!pConfig.mockSend) {
if (!pConfig.templateName || pConfig.templateName === '') { if (!pConfig.templateName) {
return { return {
disabled: true, disabled: true,
disabledTip: '短信登录未配置验证码模板名称', disabledTip: '手机号登录未配置验证码模板名称',
}; };
} }
if (!pConfig.defaultOrigin) { if (!pConfig.defaultOrigin) {
return { return {
disabled: true, disabled: true,
disabledTip: '短信登录未配置默认渠道', disabledTip: '手机号登录未配置默认渠道',
}; };
} }
} }
break; break;
case 'email': case 'email':
if (!pConfig.mockSend) { if (!pConfig.mockSend) {
if (!pConfig.account || pConfig.account === '') { if (!pConfig.account) {
return { return {
disabled: true, disabled: true,
disabledTip: '邮箱登录未配置账号', disabledTip: '邮箱登录未配置账号',
}; };
} }
else if (!pConfig.subject || pConfig.subject === '') { else if (!pConfig.subject) {
return { return {
disabled: true, disabled: true,
disabledTip: '邮箱登录未配置邮件主题', disabledTip: '邮箱登录未配置邮件主题',
}; };
} }
else if ((!pConfig.text || pConfig.text === '' || !pConfig.text?.includes('${code}')) && else if ((!pConfig.text || !pConfig.text?.includes('${code}')) &&
(!pConfig.html || pConfig.html === '' || !pConfig.html?.includes('${code}'))) { (!pConfig.html || !pConfig.html?.includes('${code}'))) {
return { return {
disabled: true, disabled: true,
disabledTip: '邮箱登录未配置邮件内容模板', disabledTip: '邮箱登录未配置邮件内容模板',
@ -231,7 +246,7 @@ export default OakComponent({
} }
break; break;
case 'wechatPublicForWeb': case 'wechatPublicForWeb':
if (!pConfig.appId || pConfig.appId === '') { if (!pConfig.appId) {
return { return {
disabled: true, disabled: true,
disabledTip: '公众号授权登录未配置appId', disabledTip: '公众号授权登录未配置appId',
@ -239,13 +254,27 @@ export default OakComponent({
} }
break; break;
case 'wechatMpForWeb': case 'wechatMpForWeb':
if (!pConfig.appId || pConfig.appId === '') { if (!pConfig.appId) {
return { return {
disabled: true, disabled: true,
disabledTip: '小程序授权登录未配置appId', disabledTip: '小程序授权登录未配置appId',
}; };
} }
break; 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: default:
break; break;
} }
@ -269,7 +298,7 @@ export default OakComponent({
} }
break; break;
case 'wechatMp': case 'wechatMp':
if (['wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) { if (['wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb', 'oauth'].includes(pType)) {
return { return {
disabled: true, disabled: true,
disabledTip: '当前application不支持该登录方式', disabledTip: '当前application不支持该登录方式',
@ -277,7 +306,7 @@ export default OakComponent({
} }
break; break;
case 'wechatPublic': case 'wechatPublic':
if (['wechatMp', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) { if (['wechatMp', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb', 'oauth'].includes(pType)) {
return { return {
disabled: true, disabled: true,
disabledTip: '当前application不支持该登录方式', disabledTip: '当前application不支持该登录方式',
@ -285,7 +314,7 @@ export default OakComponent({
} }
break; break;
case 'native': case 'native':
if (['wechatMp', 'wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) { if (['wechatMp', 'wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb', 'oauth'].includes(pType)) {
return { return {
disabled: true, disabled: true,
disabledTip: '当前application不支持该登录方式', disabledTip: '当前application不支持该登录方式',
@ -301,13 +330,25 @@ export default OakComponent({
}; };
}, },
async onCheckedChange(aId, pId, checked, apId) { async onCheckedChange(aId, pId, checked, apId) {
const { passports } = this.state;
const passportType = passports?.find((ele) => ele.id === pId)?.type;
if (checked) { if (checked) {
//create applicationPassport //create applicationPassport
this.addItem({ if (passportType === 'loginName') {
applicationId: aId, this.addItem({
passportId: pId, applicationId: aId,
isDefault: true, passportId: pId,
}); isDefault: true,
allowPwd: true,
});
}
else {
this.addItem({
applicationId: aId,
passportId: pId,
isDefault: true,
});
}
} }
else { else {
//remove id为apId的applicationPassport //remove id为apId的applicationPassport
@ -349,6 +390,20 @@ export default OakComponent({
} }
} }
return render; 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,
};
},
} }
}); });

View File

@ -20,6 +20,10 @@ type TypeRecord = Record<string, {
checked?: boolean; checked?: boolean;
disabled: boolean; disabled: boolean;
disabledTip: string; disabledTip: string;
showPwd: boolean;
allowPwd?: boolean;
pwdDisabled?: boolean;
pwdDisabledTip?: string;
}>; }>;
type PassportOption = { type PassportOption = {
label: string; label: string;

View File

@ -56,8 +56,11 @@ export default function render(props) {
}} disabled={typeRecords[type].disabled} options={typeRecords[type].passportOptions} optionRender={(option) => (<Tooltip title={(option.data.disabled) ? option.data.disabledTip : ''}> }} disabled={typeRecords[type].disabled} options={typeRecords[type].passportOptions} optionRender={(option) => (<Tooltip title={(option.data.disabled) ? option.data.disabledTip : ''}>
<div>{option.data.label}</div> <div>{option.data.label}</div>
</Tooltip>)} style={{ width: 140 }}/> </Tooltip>)} style={{ width: 140 }}/>
</Tooltip>) : (<Tooltip title={typeRecords[type].disabled ? typeRecords[type].disabledTip : ''}> </Tooltip>) : (<>
<Switch disabled={typeRecords[type].disabled} checkedChildren={<CheckOutlined />} unCheckedChildren={<CloseOutlined />} checked={!!typeRecords[type].checked} onChange={(checked) => { <Tooltip title={typeRecords[type].disabled ? typeRecords[type].disabledTip : ''}>
<Space>
<div>启用:</div>
<Switch disabled={typeRecords[type].disabled} checkedChildren={<CheckOutlined />} unCheckedChildren={<CloseOutlined />} checked={!!typeRecords[type].checked} onChange={(checked) => {
if (!checked && checkLastOne(aId, typeRecords[type].pId)) { if (!checked && checkLastOne(aId, typeRecords[type].pId)) {
showConfirm(aId, typeRecords[type].pId, typeRecords[type].apId); 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); onCheckedChange(aId, typeRecords[type].pId, checked, typeRecords[type].apId);
} }
}}/> }}/>
</Tooltip>)} </Space>
</Tooltip>
{typeRecords[type].showPwd &&
<Space>
<div>允许密码登录:</div>
<Tooltip title={typeRecords[type].pwdDisabled ? typeRecords[type].pwdDisabledTip : ''}>
<Switch size="small" disabled={typeRecords[type].pwdDisabled} checkedChildren={<CheckOutlined />} unCheckedChildren={<CloseOutlined />} checked={!!typeRecords[type].allowPwd} onChange={(checked) => {
methods.updateItem({
allowPwd: checked
}, typeRecords[type].apId);
}}/>
</Tooltip>
</Space>}
</>)}
</Space> </Space>
}); });

View File

@ -54,9 +54,8 @@ export default OakComponent({
lifetimes: { lifetimes: {
async ready() { async ready() {
const lastSendAt = await this.load(SEND_KEY); const lastSendAt = await this.load(SEND_KEY);
const application = this.features.application.getApplication(); const system = this.features.application.getApplication().system;
const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); const passwordConfig = system?.config.Password;
const passwordConfig = applicationPassports.find((ele) => ele.passport.type === 'password')?.passport.config;
const mode = passwordConfig?.mode ?? 'all'; const mode = passwordConfig?.mode ?? 'all';
const pwdMin = passwordConfig?.min ?? 8; const pwdMin = passwordConfig?.min ?? 8;
const pwdMax = passwordConfig?.max ?? 24; const pwdMax = passwordConfig?.max ?? 24;

View File

@ -23,9 +23,8 @@ export default OakComponent({
}, },
lifetimes: { lifetimes: {
async ready() { async ready() {
const application = this.features.application.getApplication(); const system = this.features.application.getApplication().system;
const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); const passwordConfig = system?.config.Password;
const passwordConfig = applicationPassports.find((ele) => ele.passport.type === 'password')?.passport.config;
const mode = passwordConfig?.mode ?? 'all'; const mode = passwordConfig?.mode ?? 'all';
const pwdMin = passwordConfig?.min ?? 8; const pwdMin = passwordConfig?.min ?? 8;
const pwdMax = passwordConfig?.max ?? 24; const pwdMax = passwordConfig?.max ?? 24;

View File

@ -11,6 +11,6 @@ declare namespace Download {
var onDownload: (data: ArrayBuffer | ReadableStream, filename: string) => Promise<void>; var onDownload: (data: ArrayBuffer | ReadableStream, filename: string) => Promise<void>;
var base64ToBlob: (base64String: string) => Blob; var base64ToBlob: (base64String: string) => Blob;
var arrayBufferToBase64: (buffer: Buffer) => string; var arrayBufferToBase64: (buffer: Buffer) => string;
var base64ToArrayBuffer: (base64String: string) => ArrayBuffer; var base64ToArrayBuffer: (base64String: string) => ArrayBufferLike;
} }
export default Download; export default Download;

View File

@ -0,0 +1,7 @@
import React from 'react';
import { Config } from '../../../../types/Config';
export default function Password(props: {
password: Required<Config>['Password'];
setValue: (path: string, value: any) => void;
setValues: (value: Record<string, any>) => void;
}): React.JSX.Element;

View File

@ -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 (<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
<Col flex="auto">
<Divider orientation="left" className={Styles.title}>
密码设置
</Divider>
<Form labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} style={{ maxWidth: 600 }}>
<Form.Item label="密码存储模式" tooltip="密码存储模式">
<Radio.Group onChange={({ target }) => {
const { value } = target;
setValue('mode', value);
}} value={mode}>
<Radio value="all">明文与SHA1加密</Radio>
<Radio value="plain">仅明文</Radio>
<Radio value="sha1">仅SHA1加密</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="密码位数范围" tooltip="密码位数范围">
<Space>
<InputNumber min={1} max={max} value={min} onChange={(value) => {
setValue('min', value);
}}/>
<div>~</div>
<InputNumber min={min} value={max} onChange={(value) => {
setValue('max', value);
}}/>
</Space>
</Form.Item>
<Form.Item label="开启正则校验" tooltip="开启后将使用下方设置的正则表达式对密码进行校验">
<Switch checkedChildren="开启" unCheckedChildren="关闭" checked={!!verify} onChange={(checked) => {
setValue('verigy', checked);
}}/>
</Form.Item>
<Form.Item label="正则" tooltip="可同时设置多组正则,系统将按照【与】逻辑进行校验,每个正则请以^开头以$结尾">
<>
{!!verify ? (<>
<EditorRegexs regexs={regexs || []} updateRegexs={(regexs) => {
setValue('regexs', regexs);
}}/>
</>) : (<div></div>)}
</>
</Form.Item>
<Form.Item label="密码提示语" tooltip="此提示语将显示在用户设置密码的输入框附近,用于清晰告知用户密码的格式要求">
<Input placeholder="请输入密码提示语" type="text" value={tip} onChange={(e) => {
setNewTip(e.target.value);
}} onBlur={() => {
if (newTip && newTip !== tip) {
setValue('tip', newTip);
}
}}/>
</Form.Item>
</Form>
</Col>
</Space>);
}

View File

@ -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;
}

View File

@ -9,10 +9,11 @@ import Sms from './sms/index';
import Email from './email/index'; import Email from './email/index';
import Basic from './basic/index'; import Basic from './basic/index';
import Security from './security/index'; import Security from './security/index';
import Password from './password/index';
export default function Render(props) { export default function Render(props) {
const { entity, name, currentConfig, dirty } = props.data; const { entity, name, currentConfig, dirty } = props.data;
const { resetConfig, updateConfig, setValue, setValues, removeItem, cleanKey, t } = props.methods; 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 (<> return (<>
<Affix offsetTop={64}> <Affix offsetTop={64}>
<Alert message={<div> <Alert message={<div>
@ -84,6 +85,15 @@ export default function Render(props) {
}); });
}}/>), }}/>),
}, },
{
key: '密码设置',
label: '密码设置',
children: (<Password password={password || {}} setValue={(path, value) => setValue(`Password.${path}`, value)} setValues={(value) => {
setValues({
Password: value
});
}}/>),
},
]}></Tabs> ]}></Tabs>
</div> </div>
</>); </>);

View File

@ -26,13 +26,43 @@ export default OakComponent({
state: '', state: '',
}, },
lifetimes: { lifetimes: {
ready() { async ready() {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const clientId = searchParams.get('client_id') || ''; const clientId = searchParams.get('client_id') || '';
const responseType = searchParams.get('response_type') || ''; const responseType = searchParams.get('response_type') || '';
const redirectUri = searchParams.get('redirect_uri') || ''; const redirectUri = searchParams.get('redirect_uri') || '';
const scope = searchParams.get('scope') || ''; const scope = searchParams.get('scope') || '';
const state = searchParams.get('state') || ''; 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({ this.setState({
client_id: clientId, client_id: clientId,
response_type: responseType, response_type: responseType,

View File

@ -16,6 +16,7 @@
"missing_client_id": "缺少 client_id 参数", "missing_client_id": "缺少 client_id 参数",
"unknown": "未知错误,请稍后重试" "unknown": "未知错误,请稍后重试"
} }
} },
"login": "当前暂未支持该第三方应用授权登录"
} }
} }

View File

@ -50,13 +50,13 @@ const Upsert = (props) => {
return (<Modal open={open && !hideModal} destroyOnClose={true} width={600} onCancel={handleCancel} onOk={handleOk} title={t('oauthProvider')} okText={isCreation ? t('create') : t('update')} cancelText={t('cancel')}> return (<Modal open={open && !hideModal} destroyOnClose={true} width={600} onCancel={handleCancel} onOk={handleOk} title={t('oauthProvider')} okText={isCreation ? t('create') : t('update')} cancelText={t('cancel')}>
<div className={Styles.id}> <div className={Styles.id}>
<Form form={form} layout="vertical" autoComplete="off"> <Form form={form} layout="vertical" autoComplete="off">
<Form.Item label={t('name')} name="name" rules={[{ required: true, message: t('nameRequired') }]}> <Form.Item label={t('name')} name="name" required={true} rules={[{ required: true, message: t('nameRequired') }]}>
<Input placeholder={t('namePlaceholder')} onChange={(v) => { <Input placeholder={t('namePlaceholder')} onChange={(v) => {
update({ name: v.target.value }); update({ name: v.target.value });
}}/> }}/>
</Form.Item> </Form.Item>
<Form.Item label={t('type')} name="type" rules={[{ required: true, message: t('typeRequired') }]} extra={item.type && item.type !== 'oak' && item.type !== 'gitea' ? (<Text type="warning"> <Form.Item label={t('type')} name="type" required={true} rules={[{ required: true, message: t('typeRequired') }]} extra={item.type && item.type !== 'oak' && item.type !== 'gitea' ? (<Text type="warning">
{item.type}不是预设类型请自行注入 handler {item.type}不是预设类型请自行注入 handler
</Text>) : undefined}> </Text>) : undefined}>
<Select mode="tags" placeholder={t('typePlaceholder')} onChange={(v) => { <Select mode="tags" placeholder={t('typePlaceholder')} onChange={(v) => {
@ -76,13 +76,13 @@ const Upsert = (props) => {
}}/> }}/>
</Form.Item> </Form.Item>
<Form.Item label={t('authorizationEndpoint')} name="authorizationEndpoint" rules={[{ required: true, message: t('authorizationEndpointRequired') }]}> <Form.Item label={t('authorizationEndpoint')} name="authorizationEndpoint" required={true} rules={[{ required: true, message: t('authorizationEndpointRequired') }]}>
<Input placeholder={t('authorizationEndpointPlaceholder')} onChange={(v) => { <Input placeholder={t('authorizationEndpointPlaceholder')} onChange={(v) => {
update({ authorizationEndpoint: v.target.value }); update({ authorizationEndpoint: v.target.value });
}}/> }}/>
</Form.Item> </Form.Item>
<Form.Item label={t('tokenEndpoint')} name="tokenEndpoint" rules={[{ required: true, message: t('tokenEndpointRequired') }]}> <Form.Item label={t('tokenEndpoint')} name="tokenEndpoint" required={true} rules={[{ required: true, message: t('tokenEndpointRequired') }]}>
<Input placeholder={t('tokenEndpointPlaceholder')} onChange={(v) => { <Input placeholder={t('tokenEndpointPlaceholder')} onChange={(v) => {
update({ tokenEndpoint: v.target.value }); update({ tokenEndpoint: v.target.value });
}}/> }}/>
@ -106,13 +106,13 @@ const Upsert = (props) => {
}}/> }}/>
</Form.Item> </Form.Item>
<Form.Item label={t('clientId')} name="clientId" rules={[{ required: true, message: t('clientIdRequired') }]}> <Form.Item label={t('clientId')} name="clientId" required={true} rules={[{ required: true, message: t('clientIdRequired') }]}>
<Input placeholder={t('clientIdPlaceholder')} onChange={(v) => { <Input placeholder={t('clientIdPlaceholder')} onChange={(v) => {
update({ clientId: v.target.value }); update({ clientId: v.target.value });
}}/> }}/>
</Form.Item> </Form.Item>
<Form.Item label={t('clientSecret')} name="clientSecret" rules={[{ required: true, message: t('clientSecretRequired') }]}> <Form.Item label={t('clientSecret')} name="clientSecret" required={true} rules={[{ required: true, message: t('clientSecretRequired') }]}>
<Input.Password placeholder={t('clientSecretPlaceholder')} onChange={(v) => { <Input.Password placeholder={t('clientSecretPlaceholder')} onChange={(v) => {
update({ clientSecret: v.target.value }); update({ clientSecret: v.target.value });
}}/> }}/>
@ -124,7 +124,7 @@ const Upsert = (props) => {
}} tokenSeparators={[',']} open={false}/> }} tokenSeparators={[',']} open={false}/>
</Form.Item> </Form.Item>
<Form.Item label={t('redirectUri')} name="redirectUri" rules={[{ required: true, message: t('redirectUriRequired') }]}> <Form.Item label={t('redirectUri')} name="redirectUri" required={true} rules={[{ required: true, message: t('redirectUriRequired') }]}>
<Input placeholder={t('redirectUriPlaceholder')} onChange={(v) => { <Input placeholder={t('redirectUriPlaceholder')} onChange={(v) => {
update({ redirectUri: v.target.value }); update({ redirectUri: v.target.value });
}}/> }}/>
@ -135,7 +135,7 @@ const Upsert = (props) => {
</Form.Item> </Form.Item>
<Form.Item label={t('ableState')} name="ableState" valuePropName="checked"> <Form.Item label={t('ableState')} name="ableState" valuePropName="checked">
<Switch onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })}/> <Switch checked={item.ableState === 'enabled'} onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })}/>
</Form.Item> </Form.Item>
</Form> </Form>

View File

@ -1,5 +1,5 @@
import React from "react"; 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 { EntityDict } from "../../../oak-app-domain";
import '@wangeditor/editor/dist/css/style.css'; import '@wangeditor/editor/dist/css/style.css';
export default function Email(props: { export default function Email(props: {
@ -8,5 +8,5 @@ export default function Email(props: {
}; };
t: (k: string, params?: any) => string; t: (k: string, params?: any) => string;
changeEnabled: (enabled: boolean) => void; 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; }): React.JSX.Element;

View File

@ -36,9 +36,10 @@ export default OakComponent({
formData({ data }) { formData({ data }) {
const passports = data.map((ele) => { const passports = data.map((ele) => {
const stateColor = ele.type ? this.features.style.getColor('passport', 'type', ele.type) : '#00BFFF'; const stateColor = ele.type ? this.features.style.getColor('passport', 'type', ele.type) : '#00BFFF';
let appIdStr; let appIdStr, hasQrCodePrefix = false;
if (ele.type === 'wechatMpForWeb') { if (ele.type === 'wechatMpForWeb') {
appIdStr = this.getAppIdStr('wechatMp', ele.config?.appId); appIdStr = this.getAppIdStr('wechatMp', ele.config?.appId);
hasQrCodePrefix = this.checkMpQrCodePrefix(ele.config?.appId);
} }
else if (ele.type === 'wechatPublicForWeb') { else if (ele.type === 'wechatPublicForWeb') {
appIdStr = this.getAppIdStr('wechatPublic', ele.config?.appId); appIdStr = this.getAppIdStr('wechatPublic', ele.config?.appId);
@ -47,14 +48,44 @@ export default OakComponent({
...ele, ...ele,
appIdStr, appIdStr,
stateColor, stateColor,
hasQrCodePrefix,
}; };
}); });
return { return {
passports, passports,
}; };
}, },
data: {}, data: {
lifetimes: {}, 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: { methods: {
updateConfig(id, config, path, value, type) { updateConfig(id, config, path, value, type) {
const newConfig = cloneDeep(config); const newConfig = cloneDeep(config);
@ -64,13 +95,13 @@ export default OakComponent({
if (!newConfig.templateName || newConfig.templateName === '') { if (!newConfig.templateName || newConfig.templateName === '') {
this.setMessage({ this.setMessage({
type: 'warning', type: 'warning',
content: '短信登录未配置模板名称,将无法正常使用短信登录' content: '手机号登录未配置模板名称,将无法正常使用手机号登录'
}); });
} }
else if (!newConfig.defaultOrigin) { else if (!newConfig.defaultOrigin) {
this.setMessage({ this.setMessage({
type: 'warning', type: 'warning',
content: '短信登录未选择默认渠道,将无法正常使用短信登录' content: '手机号登录未选择默认渠道,将无法正常使用手机号登录'
}); });
} }
} }
@ -78,20 +109,20 @@ export default OakComponent({
if (!newConfig.account || newConfig.account === '') { if (!newConfig.account || newConfig.account === '') {
this.setMessage({ this.setMessage({
type: 'warning', type: 'warning',
content: '邮箱登录未指定邮箱账号,将无法正常使用短信登录' content: '邮箱登录未指定邮箱账号,将无法正常使用邮箱登录'
}); });
} }
else if (!newConfig.subject || newConfig.subject === '') { else if (!newConfig.subject || newConfig.subject === '') {
this.setMessage({ this.setMessage({
type: 'warning', type: 'warning',
content: '邮箱登录未配置邮件主题,将无法正常使用短信登录' content: '邮箱登录未配置邮件主题,将无法正常使用邮箱登录'
}); });
} }
else if ((!newConfig.text || newConfig.text === '' || !newConfig.text?.includes('${code}')) && else if ((!newConfig.text || newConfig.text === '' || !newConfig.text?.includes('${code}')) &&
(!newConfig.html || newConfig.html === '' || !newConfig.html?.includes('${code}'))) { (!newConfig.html || newConfig.html === '' || !newConfig.html?.includes('${code}'))) {
this.setMessage({ this.setMessage({
type: 'warning', type: 'warning',
content: '邮箱登录未配置邮件内容模板,将无法正常使用短信登录' content: '邮箱登录未配置邮件内容模板,将无法正常使用邮箱登录'
}); });
} }
} }
@ -102,6 +133,12 @@ export default OakComponent({
content: '未填写appId该登录方式将无法正常使用' content: '未填写appId该登录方式将无法正常使用'
}); });
} }
else if (type === 'oauth ' && path === 'oauthId' && !(value && value.length > 0)) {
this.setMessage({
type: 'warning',
content: '未选择oauth提供商将无法正常使用OAuth授权登录'
});
}
this.updateItem({ this.updateItem({
config: newConfig, config: newConfig,
}, id); }, id);
@ -110,7 +147,7 @@ export default OakComponent({
const { passports } = this.state; const { passports } = this.state;
let warnings = []; let warnings = [];
for (const passport of passports) { for (const passport of passports) {
const { type, config, enabled, id } = passport; const { type, config = {}, enabled, id } = passport;
if (enabled) { if (enabled) {
//检查启用的passport对应的config是否设置 //检查启用的passport对应的config是否设置
switch (type) { switch (type) {
@ -120,7 +157,7 @@ export default OakComponent({
warnings.push({ warnings.push({
id, id,
type, type,
tip: '短信登录未配置验证码模板名称', tip: '手机号登录未配置验证码模板名称',
}); });
} }
if (!config.defaultOrigin) { if (!config.defaultOrigin) {
@ -132,7 +169,7 @@ export default OakComponent({
warnings.push({ warnings.push({
id, id,
type, type,
tip: '短信登录未选择默认渠道', tip: '手机号登录未选择默认渠道',
}); });
} }
} }
@ -140,14 +177,14 @@ export default OakComponent({
break; break;
case 'email': case 'email':
if (!config.mockSend) { if (!config.mockSend) {
if (!config.account || config.account === '') { if (!config.account) {
warnings.push({ warnings.push({
id, id,
type, type,
tip: '邮箱登录未指定邮箱账号', tip: '邮箱登录未指定邮箱账号',
}); });
} }
else if (!config.subject || config.subject === '') { else if (!config.subject) {
const emailWarning = warnings.find((ele) => ele.id === id); const emailWarning = warnings.find((ele) => ele.id === id);
if (emailWarning) { if (emailWarning) {
Object.assign(emailWarning, { tip: emailWarning.tip + '、邮件主题' }); Object.assign(emailWarning, { tip: emailWarning.tip + '、邮件主题' });
@ -160,8 +197,8 @@ export default OakComponent({
}); });
} }
} }
else if ((!config.text || config.text === '' || !config.text?.includes('${code}')) && else if ((!config.text || !config.text?.includes('${code}')) &&
(!config.html || config.html === '' || !config.html?.includes('${code}'))) { (!config.html || !config.html?.includes('${code}'))) {
const emailWarning = warnings.find((ele) => ele.id === id); const emailWarning = warnings.find((ele) => ele.id === id);
if (emailWarning) { if (emailWarning) {
Object.assign(emailWarning, { tip: emailWarning.tip + '、邮件内容模板' }); Object.assign(emailWarning, { tip: emailWarning.tip + '、邮件内容模板' });
@ -177,7 +214,7 @@ export default OakComponent({
} }
break; break;
case 'wechatPublicForWeb': case 'wechatPublicForWeb':
if (!config.appId || config.appId === '') { if (!config.appId) {
warnings.push({ warnings.push({
id, id,
type, type,
@ -186,7 +223,7 @@ export default OakComponent({
} }
break; break;
case 'wechatMpForWeb': case 'wechatMpForWeb':
if (!config.appId || config.appId === '') { if (!config.appId) {
warnings.push({ warnings.push({
id, id,
type, type,
@ -194,6 +231,15 @@ export default OakComponent({
}); });
} }
break; break;
case 'oauth':
if (!(config.oauthIds && config.oauthIds.length > 0)) {
warnings.push({
id,
type,
tip: 'OAuth授权登录未选择oauth供应商',
});
}
break;
default: default:
break; break;
} }
@ -233,6 +279,24 @@ export default OakComponent({
} }
}); });
return application?.name ? appId + ' applicationName' + application.name + '' : appId; 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;
} }
}, },
}); });

View File

@ -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;

View File

@ -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 (<div className={Styles.item}>
<div className={Styles.title}>
<Tag color={stateColor}>{t(`passport:v.type.${type}`)}</Tag>
<Switch checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} onChange={(checked) => {
changeEnabled(checked);
}}/>
</div>
{enabled &&
<div>
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} style={{ maxWidth: 900, marginTop: 16 }}>
<Form.Item label="账号位数范围" tooltip="账号位数范围">
<Space>
<InputNumber min={1} max={max} value={min} onChange={(value) => {
updateConfig(id, config, 'min', value, 'loginName');
}}/>
<div>~</div>
<InputNumber min={min} value={max} onChange={(value) => {
updateConfig(id, config, 'max', value, 'loginName');
}}/>
</Space>
</Form.Item>
<Form.Item label="开启正则校验" tooltip="开启后将使用下方设置的正则表达式对账号进行校验">
<Switch checkedChildren="开启" unCheckedChildren="关闭" checked={!!config?.verify} onChange={(checked) => {
updateConfig(id, config, 'verify', checked, 'loginName');
}}/>
</Form.Item>
<Form.Item label="正则" tooltip="可同时设置多组正则,系统将按照【与】逻辑进行校验,每个正则请以^开头以$结尾">
<>
{!!config?.verify ? (<>
<EditorRegexs regexs={regexs} updateRegexs={(regexs) => {
updateConfig(id, config, 'regexs', regexs, 'loginName');
}}/>
</>) : (<div></div>)}
</>
</Form.Item>
<Form.Item label="是否允许注册账号" tooltip="开启后用户可自行注册账号">
<Switch checkedChildren="开启" unCheckedChildren="关闭" checked={!!config?.register} onChange={(checked) => {
updateConfig(id, config, 'register', checked, 'loginName');
}}/>
</Form.Item>
<Form.Item label="账号提示语" tooltip="此提示语将显示在用户设置账号的输入框附近,用于清晰告知用户账号的格式要求">
<Input placeholder="请输入账号提示语" type="text" value={tip} onChange={(e) => {
setTip(e.target.value);
}} onBlur={() => {
if (tip && tip !== config?.tip) {
updateConfig(id, config, 'tip', tip, 'loginName');
}
}}/>
</Form.Item>
</Form>
</div>}
</div>);
}

View File

@ -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;
}

15
es/components/passport/oauth/index.d.ts vendored Normal file
View File

@ -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;

View File

@ -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 (<div className={Styles.item}>
<div className={Styles.title}>
<Tag color={stateColor}>{t(`passport:v.type.${type}`)}</Tag>
<Tooltip title={(oauthOptions && oauthOptions?.length > 0) ? '' : '请先启用oauth供应商'}>
<Switch checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} onChange={(checked) => {
changeEnabled(checked);
}} disabled={!(oauthOptions && oauthOptions?.length > 0)}/>
</Tooltip>
</div>
{enabled &&
<div>
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} style={{ maxWidth: 900, marginTop: 16 }}>
<Form.Item label='oauth供应商'>
<Select mode="multiple" style={{ width: '100%' }} placeholder="请选择oauth供应商" value={oauthIds} onChange={(value) => {
updateConfig(id, config, 'oauthIds', value, 'oauth');
}} options={oauthOptions}/>
</Form.Item>
</Form>
</div>}
</div>);
}

View File

@ -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;
}

View File

@ -1,5 +1,5 @@
import React from "react"; 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 { EntityDict } from "../../../oak-app-domain";
export default function Password(props: { export default function Password(props: {
passport: EntityDict['passport']['OpSchema'] & { passport: EntityDict['passport']['OpSchema'] & {
@ -7,5 +7,5 @@ export default function Password(props: {
}; };
t: (k: string, params?: any) => string; t: (k: string, params?: any) => string;
changeEnabled: (enabled: boolean) => void; 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; }): React.JSX.Element;

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useState } from "react";
import { Switch, Form, Input, Space, Tag, InputNumber, Radio, } from 'antd'; import { Switch, Form, Input, Space, Tag, InputNumber, Radio, Tooltip, } from 'antd';
import Styles from './web.module.less'; import Styles from './web.module.less';
import EditorRegexs from "./editorRegexs"; import EditorRegexs from "./editorRegexs";
export default function Password(props) { export default function Password(props) {
@ -11,29 +11,26 @@ export default function Password(props) {
const [regexs, setRegexs] = useState(config?.regexs || []); const [regexs, setRegexs] = useState(config?.regexs || []);
const [tip, setTip] = useState(config?.tip || ''); const [tip, setTip] = useState(config?.tip || '');
const [mode, setMode] = useState(config?.mode || 'all'); const [mode, setMode] = useState(config?.mode || 'all');
useEffect(() => { // useEffect(() => {
const newMin = config?.min || 8; // const newMin = config?.min || 8;
const newMax = config?.max || 24; // const newMax = config?.max || 24;
const newRegexs = config?.regexs || []; // const newRegexs = config?.regexs || [];
const newTip = config?.tip || ''; // const newTip = config?.tip || '';
const newMode = config?.mode || 'all'; // const newMode = config?.mode || 'all';
if (min !== newMin) // if (min !== newMin) setMin(newMin);
setMin(newMin); // if (max !== newMax) setMax(newMax);
if (max !== newMax) // if (JSON.stringify(regexs) !== JSON.stringify(newRegexs)) setRegexs(newRegexs);
setMax(newMax); // if (tip !== newTip) setTip(newTip);
if (JSON.stringify(regexs) !== JSON.stringify(newRegexs)) // if (mode !== newMode) setMode(newMode);
setRegexs(newRegexs); // }, [config?.min, config?.max, config?.regexs, config?.tip, config?.mode, config?.verify]);
if (tip !== newTip)
setTip(newTip);
if (mode !== newMode)
setMode(newMode);
}, [config?.min, config?.max, config?.regexs, config?.tip, config?.mode, config?.verify]);
return (<div className={Styles.item}> return (<div className={Styles.item}>
<div className={Styles.title}> <div className={Styles.title}>
<Tag color={stateColor}>{t(`passport:v.type.${type}`)}</Tag> <Tag color={stateColor}>{t(`passport:v.type.${type}`)}</Tag>
<Switch checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} onChange={(checked) => { <Tooltip title="密码登录方式已调整,请关闭该登录方式,对于密码的相关配置请前往系统配置管理中修改密码设置。" placement="topLeft">
<Switch checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} onChange={(checked) => {
changeEnabled(checked); changeEnabled(checked);
}}/> }} disabled={!enabled}/>
</Tooltip>
</div> </div>
{enabled && {enabled &&
<div> <div>

View File

@ -1,5 +1,5 @@
import React from "react"; 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 { EntityDict } from "../../../oak-app-domain";
export default function Sms(props: { export default function Sms(props: {
passport: EntityDict['passport']['OpSchema'] & { passport: EntityDict['passport']['OpSchema'] & {
@ -7,5 +7,5 @@ export default function Sms(props: {
}; };
t: (k: string, params?: any) => string; t: (k: string, params?: any) => string;
changeEnabled: (enabled: boolean) => void; 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; }): React.JSX.Element;

View File

@ -1,16 +1,21 @@
import React from 'react'; import React from 'react';
import { WebComponentProps } from 'oak-frontend-base'; import { WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../oak-app-domain'; import { EntityDict } from '../../oak-app-domain';
import { SmsConfig, EmailConfig, PfwConfig, MfwConfig, PwdConfig } from '../../entities/Passport'; import { SmsConfig, EmailConfig, PfwConfig, MfwConfig, PwdConfig, NameConfig, OAuthConfig } from '../../entities/Passport';
export default function render(props: WebComponentProps<EntityDict, 'passport', true, { export default function render(props: WebComponentProps<EntityDict, 'passport', true, {
passports: (EntityDict['passport']['OpSchema'] & { passports: (EntityDict['passport']['OpSchema'] & {
appIdStr: string; appIdStr: string;
stateColor: string; stateColor: string;
hasQrCodePrefix: boolean;
})[]; })[];
systemId: string; systemId: string;
systemName: string; systemName: string;
oauthOptions: {
label: string;
value: string;
}[];
}, { }, {
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;
checkConfrim: () => { checkConfrim: () => {
id: string; id: string;
type: EntityDict['passport']['Schema']['type']; type: EntityDict['passport']['Schema']['type'];

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Button, Space, Switch, Affix, Alert, Typography, Modal, Divider, Tag, Row } from 'antd'; import { Button, Space, Switch, Affix, Alert, Typography, Modal, Divider, Tag, Row, Tooltip, } from 'antd';
import Styles from './web.pc.module.less'; import Styles from './web.pc.module.less';
import classNames from 'classnames'; import classNames from 'classnames';
import { ExclamationCircleFilled } from '@ant-design/icons'; import { ExclamationCircleFilled } from '@ant-design/icons';
@ -10,20 +10,24 @@ import WechatPublicForWeb from './wechatPublicForWeb';
import WechatMpForWeb from './wechatMpForWeb'; import WechatMpForWeb from './wechatMpForWeb';
import WechatMp from './wechatMp'; import WechatMp from './wechatMp';
import WechatPublic from './wechatPublic'; import WechatPublic from './wechatPublic';
import LoginName from './loginName';
import Oauth from './oauth';
const { confirm } = Modal; const { confirm } = Modal;
function AppView(props) { function AppView(props) {
const { passport, t, changeEnabled, updateConfig } = props; const { passport, t, changeEnabled, updateConfig } = props;
const { id, type, config, enabled, stateColor } = passport; const { id, type, config, enabled, stateColor } = passport;
return (<div className={classNames(Styles.item, Styles.title)}> return (<div className={classNames(Styles.item, Styles.title)}>
<Tag color={stateColor}>{t(`passport:v.type.${type}`)}</Tag> <Tag color={stateColor}>{t(`passport:v.type.${type}`)}</Tag>
<Switch checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} onChange={(checked) => { <Tooltip title="暂未支持该登录方式">
<Switch checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} disabled={!enabled} onChange={(checked) => {
changeEnabled(checked); changeEnabled(checked);
}}/> }}/>
</Tooltip>
</div>); </div>);
} }
export default function render(props) { export default function render(props) {
const { data, methods } = props; const { data, methods } = props;
const { oakFullpath, oakExecuting, oakDirty, oakLoading, systemId, passports, systemName, } = data; const { oakFullpath, oakExecuting, oakDirty, oakLoading, systemId, passports, systemName, oauthOptions } = data;
const { clean, execute, t, updateItem, updateConfig, checkConfrim, myConfirm } = methods; const { clean, execute, t, updateItem, updateConfig, checkConfrim, myConfirm } = methods;
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [newType, setNewType] = useState(undefined); const [newType, setNewType] = useState(undefined);
@ -93,6 +97,7 @@ export default function render(props) {
<div>* 如需启用邮箱登录请先前往配置管理邮箱设置创建系统邮箱,并完成相关配置</div> <div>* 如需启用邮箱登录请先前往配置管理邮箱设置创建系统邮箱,并完成相关配置</div>
<div>* 如需启用小程序授权登录请先前往应用管理创建小程序application,并完成基础配置</div> <div>* 如需启用小程序授权登录请先前往应用管理创建小程序application,并完成基础配置</div>
<div>* 如需启用公众号授权登录请先前往应用管理创建是服务号的公众号application,并完成基础配置</div> <div>* 如需启用公众号授权登录请先前往应用管理创建是服务号的公众号application,并完成基础配置</div>
<div>* 如需启用OAuth授权登录请先前往OAuth管理创建OAuth供应商,并启用</div>
</div> </div>
</Row> </Row>
{passports && passports.map((passport) => { {passports && passports.map((passport) => {
@ -122,7 +127,7 @@ export default function render(props) {
}, passport.id); }, passport.id);
}} updateConfig={updateConfig}/>); }} updateConfig={updateConfig}/>);
case 'wechatMpForWeb': case 'wechatMpForWeb':
return (<WechatMpForWeb key={passport.id} passport={passport} appIdStr={passport?.appIdStr} t={t} changeEnabled={(enabled) => { return (<WechatMpForWeb key={passport.id} passport={passport} appIdStr={passport?.appIdStr} hasQrCodePrefix={passport?.hasQrCodePrefix} t={t} changeEnabled={(enabled) => {
updateItem({ updateItem({
enabled, enabled,
}, passport.id); }, passport.id);
@ -141,6 +146,18 @@ export default function render(props) {
enabled, enabled,
}, passport.id); }, passport.id);
}}/>); }}/>);
case 'loginName':
return (<LoginName key={passport.id} passport={passport} t={t} changeEnabled={(enabled) => {
updateItem({
enabled,
}, passport.id);
}} updateConfig={updateConfig}/>);
case 'oauth':
return (<Oauth key={passport.id} passport={passport} t={t} changeEnabled={(enabled) => {
updateItem({
enabled,
}, passport.id);
}} updateConfig={updateConfig} oauthOptions={oauthOptions}/>);
default: default:
return (<AppView key={passport.id} passport={passport} t={t} changeEnabled={(enabled) => { return (<AppView key={passport.id} passport={passport} t={t} changeEnabled={(enabled) => {
updateItem({ updateItem({

View File

@ -1,12 +1,13 @@
import React from "react"; 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 { EntityDict } from "../../../oak-app-domain";
export default function wechatMpForWeb(props: { export default function wechatMpForWeb(props: {
passport: EntityDict['passport']['OpSchema'] & { passport: EntityDict['passport']['OpSchema'] & {
stateColor: string; stateColor: string;
}; };
appIdStr: string; appIdStr: string;
hasQrCodePrefix: boolean;
t: (k: string, params?: any) => string; t: (k: string, params?: any) => string;
changeEnabled: (enabled: boolean) => void; 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; }): React.JSX.Element;

View File

@ -1,20 +1,20 @@
import React from "react"; import React from "react";
import { Switch, Form, Tag, Input } from 'antd'; import { Switch, Form, Tooltip, Tag, Input } from 'antd';
import Styles from './web.module.less'; import Styles from './web.module.less';
export default function wechatMpForWeb(props) { export default function wechatMpForWeb(props) {
const { passport, appIdStr, t, changeEnabled, updateConfig } = props; const { passport, appIdStr, hasQrCodePrefix, t, changeEnabled, updateConfig } = props;
const { id, type, enabled, stateColor } = passport; const { id, type, enabled, stateColor } = passport;
const config = passport.config || {}; const config = passport.config || {};
return (<div className={Styles.item}> return (<div className={Styles.item}>
<div className={Styles.title}> <div className={Styles.title}>
<Tag color={stateColor}>{t(`passport:v.type.${type}`)}</Tag> <Tag color={stateColor}>{t(`passport:v.type.${type}`)}</Tag>
{/* <Tooltip title={(mpAppIds && mpAppIds.length > 0) ? '' : '如需启用小程序授权登录请先前往应用管理创建小程序application,并完成基础配置'}> */} <Tooltip title={hasQrCodePrefix ? '' : '请先配置小程序普通链接二维码规则'}>
<Switch <Switch
// disabled={!(mpAppIds && mpAppIds.length > 0)} // disabled={!(mpAppIds && mpAppIds.length > 0)}
checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} onChange={(checked) => { checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} onChange={(checked) => {
changeEnabled(checked); changeEnabled(checked);
}}/> }} disabled={!hasQrCodePrefix}/>
{/* </Tooltip> */} </Tooltip>
</div> </div>
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} style={{ maxWidth: 900, marginTop: 16 }}> <Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} style={{ maxWidth: 900, marginTop: 16 }}>
<Form.Item label="appId"> <Form.Item label="appId">

View File

@ -1,5 +1,5 @@
import React from "react"; 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 { EntityDict } from "../../../oak-app-domain";
export default function wechatPublicForWeb(props: { export default function wechatPublicForWeb(props: {
passport: EntityDict['passport']['OpSchema'] & { passport: EntityDict['passport']['OpSchema'] & {
@ -8,5 +8,5 @@ export default function wechatPublicForWeb(props: {
appIdStr: string; appIdStr: string;
t: (k: string, params?: any) => string; t: (k: string, params?: any) => string;
changeEnabled: (enabled: boolean) => void; 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; }): React.JSX.Element;

View File

@ -5,5 +5,8 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
callback: (() => void) | undefined; callback: (() => void) | undefined;
setLoginMode: (value: string) => void; setLoginMode: (value: string) => void;
digit: number; digit: number;
allowSms: boolean;
allowPassword: boolean;
allowWechatMp: boolean;
}>) => React.ReactElement; }>) => React.ReactElement;
export default _default; export default _default;

View File

@ -25,6 +25,9 @@ export default OakComponent({
callback: undefined, // 登录成功回调,排除微信登录方式 callback: undefined, // 登录成功回调,排除微信登录方式
setLoginMode: (value) => undefined, setLoginMode: (value) => undefined,
digit: 4, //验证码位数 digit: 4, //验证码位数
allowSms: false, //小程序切换手机号验证码登录
allowPassword: false, //小程序切换密码登录
allowWechatMp: false, //小程序切换授权登录
}, },
formData({ features, props }) { formData({ features, props }) {
const { lastSendAt } = this.state; const { lastSendAt } = this.state;

View File

@ -2,14 +2,13 @@
<oak-icon name="mobilephone" size="28" color="#808080" /> <oak-icon name="mobilephone" size="28" color="#808080" />
<l-input <l-input
hide-label="{{true}}" hide-label="{{true}}"
placeholder="{{t('placeholder.Mobile')}}" placeholder="{{t('placeholder.Email')}}"
clear="{{true}}" clear="{{true}}"
showRow="{{false}}" showRow="{{false}}"
l-class="my-input" l-class="my-input"
style="flex:1;" style="flex:1;"
data-attr="mobile" data-attr="email"
maxlength="11" value="{{email}}"
value="{{mobile}}"
bind:lininput="inputChangeMp" bind:lininput="inputChangeMp"
bind:linclear="inputChangeMp" bind:linclear="inputChangeMp"
/> />
@ -17,13 +16,13 @@
<view class="inputItem"> <view class="inputItem">
<l-input <l-input
hide-label="{{true}}" hide-label="{{true}}"
placeholder="{{t('placeholder.Captcha')}}" placeholder="输入{{digit}}位验证码"
clear="{{true}}" clear="{{true}}"
showRow="{{false}}" showRow="{{false}}"
l-class="my-input" l-class="my-input"
width="380" width="380"
data-attr="captcha" data-attr="captcha"
maxlength="4" maxlength="{{digit}}"
value="{{captcha}}" value="{{captcha}}"
bind:lininput="inputChangeMp" bind:lininput="inputChangeMp"
bind:linclear="inputChangeMp" bind:linclear="inputChangeMp"
@ -37,7 +36,11 @@
{{t('Login')}} {{t('Login')}}
</l-button> </l-button>
<view class="methods"> <view class="methods">
<view wx:if="{{allowWechatMp}}" style="color:#8F976A" bindtap="changeLoginMp" data-value="wechatMp">一键登录</view> <view wx:if="{{allowWechatMp}}" style="color:#8F976A" bindtap="changeLoginMp" data-value="wechatMp">{{t('loginMode.wechatMp')}}</view>
<view wx:else></view> <view wx:else></view>
<view wx:if="{{allowPassword}}" style="color:#835D01" bindtap="changeLoginMp" data-value="password">密码登录</view> <view style="color:#835D01; display:flex; align-items:center; justify-content:flex-end; gap:8rpx">
<view wx:if="{{allowSms}}" bindtap="changeLoginMp" data-value="sms">{{t('loginMode.sms')}}</view>
<view wx:if="{{allowSms && allowPassword}}">/</view>
<view wx:if="{{allowPassword}}" bindtap="changeLoginMp" data-value="password">{{t('loginMode.password')}}</view>
</view>
</view> </view>

View File

@ -2,8 +2,13 @@
"Login": "登录", "Login": "登录",
"Send": "发送验证码", "Send": "发送验证码",
"placeholder": { "placeholder": {
"Captcha": "输入4位验证码", "Captcha": "输入%{digit}位验证码",
"Email": "请输入邮箱" "Email": "请输入邮箱"
}, },
"resendAfter": "秒后可重发" "resendAfter": "秒后可重发",
"loginMode": {
"wechatMp": "一键登录",
"sms": "短信登录",
"password": "密码登录"
}
} }

View File

@ -9,12 +9,12 @@ export default function Render(props) {
const { sendCaptcha, loginByEmail, t, inputChange } = methods; const { sendCaptcha, loginByEmail, t, inputChange } = methods;
return (<Form colon={true}> return (<Form colon={true}>
<Form.Item name="email"> <Form.Item name="email">
<Input allowClear value={email} type="email" size="large" prefix={<MailOutlined />} placeholder={t('placeholder.Email')} onChange={(e) => { <Input allowClear value={email} type="email" size="large" prefix={<MailOutlined />} placeholder={t('placeholder.Email', { digit })} onChange={(e) => {
inputChange('email', e.target.value); inputChange('email', e.target.value);
}} className={Style['loginbox-input']}/> }} className={Style['loginbox-input']}/>
</Form.Item> </Form.Item>
<Form.Item name="captcha"> <Form.Item name="captcha">
<Input allowClear value={captcha} size="large" maxLength={digit} placeholder={t('placeholder.Captcha')} onChange={(e) => { <Input allowClear value={captcha} size="large" maxLength={digit} placeholder={t('placeholder.Captcha', { digit })} onChange={(e) => {
inputChange('captcha', e.target.value); inputChange('captcha', e.target.value);
}} className={Style['loginbox-input']} suffix={<Button size="small" type="link" disabled={!!disabled || !validEmail || counter > 0} onClick={() => sendCaptcha()}> }} className={Style['loginbox-input']} suffix={<Button size="small" type="link" disabled={!!disabled || !validEmail || counter > 0} onClick={() => sendCaptcha()}>
{counter > 0 {counter > 0

View File

@ -6,5 +6,8 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
redirectUri: string; redirectUri: string;
url: string; url: string;
callback: (() => void) | undefined; callback: (() => void) | undefined;
goRegister: (() => void) | undefined;
isRegisterBack: boolean;
goOauthLogin: ((oauthProviderId: string) => void) | undefined;
}>) => React.ReactElement; }>) => React.ReactElement;
export default _default; export default _default;

View File

@ -12,79 +12,105 @@ export default OakComponent({
passportTypes: [], passportTypes: [],
inputOptions: [], inputOptions: [],
scanOptions: [], scanOptions: [],
allowSms: false, oauthOptions: [],
allowEmail: false, pwdAllowMobile: false, //密码登录允许使用手机号
pwdAllowEmail: false, //密码登录允许使用邮箱
pwdAllowLoginName: false, //密码登录允许使用账号
allowPassword: false, allowPassword: false,
allowSms: false, //小程序使用
allowEmail: false, //小程序使用
allowWechatMp: false, allowWechatMp: false,
setLoginModeMp(value) { this.setLoginMode(value); }, setLoginModeMp(value) { this.setLoginMode(value); },
smsDigit: 4, //短信验证码位数 smsDigit: 4, //短信验证码位数
emailDigit: 4, //邮箱验证码位数 emailDigit: 4, //邮箱验证码位数
pwdMode: 'all', //密码明文密文存储模式 pwdMode: 'all', //密码明文密文存储模式
allowRegister: false, //开启账号登录且允许注册
}, },
properties: { properties: {
onlyCaptcha: false, onlyCaptcha: false, //仅支持手机号验证码登录
onlyPassword: false, onlyPassword: false,
disabled: '', disabled: '',
redirectUri: '', // 微信登录后的redirectUri要指向wechatUser/login去处理 redirectUri: '', // 微信登录后的redirectUri要指向wechatUser/login去处理
url: '', // 登录系统之后要返回的页面 url: '', // 登录系统之后要返回的页面
callback: undefined, // 登录成功回调,排除微信登录方式 callback: undefined, // 登录成功回调,排除微信登录方式
goRegister: undefined, //跳转注册
isRegisterBack: false, //从注册页跳回登录时将优先选中账号登录方式
goOauthLogin: undefined //跳转指定第三方授权
}, },
formData({ features, props }) { formData({ features, props }) {
return {}; return {};
}, },
listeners: { listeners: {
// 'onlyPassword,onlyCaptcha'(prev, next) { // 'onlyPassword,onlyCaptcha'(prev, next) {
// let loginMode = this.state.loginMode, inputOptions = this.state.inputOptions, scanOptions = this.state.scanOptions; // let loginMode = this.state.loginMode, inputOptions = this.state.inputOptions, scanOptions = this.state.scanOptions;
// if (next.onlyPassword) { // if (next.onlyPassword) {
// loginMode = 'password'; // loginMode = 'password';
// inputOptions = [{ // inputOptions = [{
// label: this.t('passport:v.type.password'), // label: this.t('passport:v.type.password'),
// value: 'password', // value: 'password',
// }]; // }];
// } else if (next.onlyCaptcha) { // } else if (next.onlyCaptcha) {
// loginMode = 'sms'; // loginMode = 'sms';
// inputOptions = [{ // inputOptions = [{
// label: this.t('passport:v.type.sms'), // label: this.t('passport:v.type.sms'),
// value: 'sms', // value: 'sms',
// }]; // }];
// } else { // } else {
// const { passportTypes } = this.state; // const { passportTypes } = this.state;
// if (passportTypes && passportTypes.length > 0) { // if (passportTypes && passportTypes.length > 0) {
// passportTypes.forEach((ele: EntityDict['passport']['Schema']['type']) => { // passportTypes.forEach((ele: EntityDict['passport']['Schema']['type']) => {
// if (ele === 'sms' || ele === 'email' || ele === 'password') { // if (ele === 'sms' || ele === 'email' || ele === 'password') {
// inputOptions.push({ // inputOptions.push({
// label: this.t(`passport:v.type.${ele}`), // label: this.t(`passport:v.type.${ele}`),
// value: ele // value: ele
// }) // })
// } else if (ele === 'wechatMpForWeb' || ele === 'wechatPublicForWeb') { // } else if (ele === 'wechatMpForWeb' || ele === 'wechatPublicForWeb') {
// scanOptions.push({ // scanOptions.push({
// label: this.t(`passport:v.type.${ele}`), // label: this.t(`passport:v.type.${ele}`),
// value: ele // value: ele
// }) // })
// } // }
// }); // });
// } // }
// } // }
// this.setState({ // this.setState({
// loginMode, // loginMode,
// inputOptions, // inputOptions,
// scanOptions, // scanOptions,
// }) // })
// } // }
isRegisterBack(prev, next) {
if (prev.isRegisterBack !== next.isRegisterBack && next.isRegisterBack) {
const { passportTypes } = this.state;
const { onlyCaptcha } = this.props;
if (passportTypes.includes('loginName') && !onlyCaptcha) {
this.setState({
loginMode: 'password'
});
}
}
}
}, },
lifetimes: { lifetimes: {
async ready() { async ready() {
const { isRegisterBack } = this.props;
const application = this.features.application.getApplication(); const application = this.features.application.getApplication();
const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id });
const defaultPassport = applicationPassports.find((ele) => ele.isDefault); const defaultPassport = applicationPassports.find((ele) => ele.isDefault);
const passportTypes = applicationPassports.map((ele) => ele.passport.type); const passportTypes = applicationPassports.map((ele) => ele.passport.type);
const smsDigit = applicationPassports.find((ele) => ele.passport.type === 'sms')?.passport?.config?.digit || 4; const smsDigit = applicationPassports.find((ele) => ele.passport.type === 'sms')?.passport?.config?.digit || 4;
const emailDigit = applicationPassports.find((ele) => ele.passport.type === 'email')?.passport?.config?.digit || 4; const emailDigit = applicationPassports.find((ele) => ele.passport.type === 'email')?.passport?.config?.digit || 4;
const pwdMode = applicationPassports.find((ele) => ele.passport.type === 'password')?.passport?.config?.mode || 'all'; const pwdConfig = application?.system?.config?.Password;
const pwdMode = pwdConfig?.mode || 'all';
const { onlyCaptcha, onlyPassword } = this.props; const { onlyCaptcha, onlyPassword } = this.props;
const smsAP = applicationPassports.find((ele) => ele.passport.type === 'sms');
const loginNameAP = applicationPassports.find((ele) => ele.passport.type === 'loginName');
const emailAP = applicationPassports.find((ele) => ele.passport.type === 'email');
const showPassword = (smsAP && smsAP?.allowPwd) || (loginNameAP && loginNameAP?.allowPwd) || (emailAP && emailAP?.allowPwd); //(手机号、账号、邮箱登录中)存在至少一种开启密码登录的登录方式且非仅手机验证码登录
let loginMode = (await this.load(LOGIN_MODE)) || defaultPassport?.passport?.type || 'sms'; let loginMode = (await this.load(LOGIN_MODE)) || defaultPassport?.passport?.type || 'sms';
let inputOptions = [], scanOptions = []; let inputOptions = [], scanOptions = [];
if (onlyPassword) { let oauthOptions = [];
if (onlyPassword && showPassword) {
loginMode = 'password'; loginMode = 'password';
inputOptions = [{ inputOptions = [{
label: this.t('passport:v.type.password') + this.t('Login'), label: this.t('passport:v.type.password') + this.t('Login'),
@ -99,8 +125,14 @@ export default OakComponent({
}]; }];
} }
else { else {
if (showPassword) {
inputOptions.push({
label: this.t(`passport:v.type.password`) + this.t('Login'),
value: 'password'
});
}
passportTypes.forEach((ele) => { passportTypes.forEach((ele) => {
if (ele === 'sms' || ele === 'email' || ele === 'password') { if (ele === 'sms' || ele === 'email') {
inputOptions.push({ inputOptions.push({
label: this.t(`passport:v.type.${ele}`) + this.t('Login'), label: this.t(`passport:v.type.${ele}`) + this.t('Login'),
value: ele value: ele
@ -113,8 +145,36 @@ export default OakComponent({
}); });
} }
}); });
const oauthAp = applicationPassports.find((ele) => ele.passport.type === 'oauth');
const { oauthIds } = oauthAp?.passport?.config || {};
if (oauthIds && oauthIds.length > 0) {
const { data: oauthProviders } = await this.features.cache.refresh('oauthProvider', {
data: {
id: 1,
name: 1,
logo: 1,
},
filter: {
id: {
$in: oauthIds,
}
}
});
if (oauthProviders && oauthProviders?.length > 0) {
oauthOptions = oauthProviders?.map((ele) => {
return {
name: ele.name,
value: ele.id,
logo: ele.logo ?? undefined,
};
});
}
}
} }
if (!passportTypes.includes(loginMode)) { if (isRegisterBack && !onlyCaptcha) {
loginMode = 'password';
}
if ((loginMode !== 'password' && !passportTypes.includes(loginMode)) || (loginMode === 'password' && !showPassword)) {
loginMode = defaultPassport.passport.type; loginMode = defaultPassport.passport.type;
} }
const appType = application?.type; const appType = application?.type;
@ -133,10 +193,14 @@ export default OakComponent({
appId = config2?.wechat?.appId; appId = config2?.wechat?.appId;
domain = config2?.wechat?.domain; domain = config2?.wechat?.domain;
} }
const allowSms = passportTypes.includes('sms') && !onlyPassword; const pwdAllowMobile = smsAP && smsAP?.allowPwd;
const allowEmail = passportTypes.includes('email') && !onlyCaptcha && !onlyPassword; const pwdAllowEmail = emailAP && emailAP?.allowPwd;
const allowPassword = passportTypes.includes('password') && !onlyCaptcha; const pwdAllowLoginName = loginNameAP && loginNameAP?.allowPwd;
const allowWechatMp = passportTypes.includes('wechatMp') && !onlyCaptcha && !onlyPassword; const allowWechatMp = passportTypes.includes('wechatMp') && !onlyCaptcha && !onlyPassword;
const allowPassword = !onlyCaptcha && showPassword;
const allowSms = passportTypes.includes('sms') && !onlyPassword;
const allowEmail = passportTypes.includes('email') && !onlyPassword && !onlyCaptcha;
const allowRegister = loginNameAP && loginNameAP?.passport?.config?.register;
this.setState({ this.setState({
loginMode, loginMode,
appId, appId,
@ -145,10 +209,15 @@ export default OakComponent({
passportTypes, passportTypes,
inputOptions, inputOptions,
scanOptions, scanOptions,
oauthOptions,
pwdAllowMobile,
pwdAllowEmail,
pwdAllowLoginName,
allowPassword,
allowSms, allowSms,
allowEmail, allowEmail,
allowPassword,
allowWechatMp, allowWechatMp,
allowRegister,
smsDigit, smsDigit,
emailDigit, emailDigit,
pwdMode, pwdMode,
@ -163,7 +232,7 @@ export default OakComponent({
}); });
}, },
changeLoginMp() { changeLoginMp() {
const { allowSms, allowPassword } = this.state; const { allowSms, allowPassword, allowEmail } = this.state;
let loginMode = 'wechatMp'; let loginMode = 'wechatMp';
if (allowSms) { if (allowSms) {
loginMode = 'sms'; loginMode = 'sms';
@ -171,6 +240,9 @@ export default OakComponent({
else if (allowPassword) { else if (allowPassword) {
loginMode = 'password'; loginMode = 'password';
} }
else if (allowEmail) {
loginMode = 'email';
}
this.setLoginMode(loginMode); this.setLoginMode(loginMode);
}, },
async loginByWechatMp() { async loginByWechatMp() {

View File

@ -6,6 +6,7 @@
"l-segment": "@oak-frontend-base/miniprogram_npm/lin-ui/segment/index", "l-segment": "@oak-frontend-base/miniprogram_npm/lin-ui/segment/index",
"l-segment-item": "@oak-frontend-base/miniprogram_npm/lin-ui/segment-item/index", "l-segment-item": "@oak-frontend-base/miniprogram_npm/lin-ui/segment-item/index",
"password": "../login/password/index", "password": "../login/password/index",
"sms": "../login/sms/index" "sms": "../login/sms/index",
"email": "../login/email/index"
} }
} }

View File

@ -7,6 +7,7 @@
callback="{{callback}}" callback="{{callback}}"
class="login-body" class="login-body"
allowPassword="{{allowPassword}}" allowPassword="{{allowPassword}}"
allowEmail="{{allowEmail}}"
allowWechatMp="{{allowWechatMp}}" allowWechatMp="{{allowWechatMp}}"
setLoginMode="{{setLoginModeMp}}" setLoginMode="{{setLoginModeMp}}"
/> />
@ -18,15 +19,33 @@
callback="{{callback}}" callback="{{callback}}"
class="login-body" class="login-body"
allowSms="{{allowSms}}" allowSms="{{allowSms}}"
allowEmail="{{allowEmail}}"
allowWechatMp="{{allowWechatMp}}"
pwdAllowMobile="{{pwdAllowMobile}}"
pwdAllowEmail="{{pwdAllowEmail}}"
pwdAllowLoginName="{{pwdAllowLoginName}}"
setLoginMode="{{setLoginModeMp}}"
allowRegister="{{allowRegister}}"
goRegister="{{goRegister}}"
/>
</block>
<block wx:elif="{{loginMode ==='email'}}">
<email
disabled="{{disabled}}"
url="{{url}}"
callback="{{callback}}"
class="login-body"
allowSms="{{allowSms}}"
allowPassword="{{allowPassword}}"
allowWechatMp="{{allowWechatMp}}" allowWechatMp="{{allowWechatMp}}"
setLoginMode="{{setLoginModeMp}}" setLoginMode="{{setLoginModeMp}}"
/> />
</block> </block>
<view wx:elif="{{loginMode === 'wechatMp'}}" class="login-body"> <view wx:elif="{{loginMode === 'wechatMp'}}" class="login-body">
<l-button type="default" size="long" disabled="{{loading}}" bind:lintap="loginByWechatMp" style="width:100%"> <l-button type="default" size="long" disabled="{{loading}}" bind:lintap="loginByWechatMp" style="width:100%">
授权登录 {{t('loginMode.wechatMp')}}
</l-button> </l-button>
<view wx:if="{{allowSms || allowPassword}}" style="font-size:28rpx; margin-top:28rpx; color:#8F976A" bind:tap="changeLoginMp">其他方式登录</view> <view wx:if="{{allowSms || allowPassword || allowEmail}}" style="font-size:28rpx; margin-top:28rpx; color:#8F976A" bind:tap="changeLoginMp">{{t('loginMode.other')}}</view>
</view> </view>
</view> </view>
</view> </view>

View File

@ -13,5 +13,10 @@
"resendAfter": "秒后可重发", "resendAfter": "秒后可重发",
"otherMethods": "其他登录方式", "otherMethods": "其他登录方式",
"scanLogin": "扫码登录", "scanLogin": "扫码登录",
"tip": "未注册用户首次登录将自动注册" "tip": "未注册用户首次登录将自动注册",
"goRegister": "去注册",
"loginMode": {
"wechatMp": "授权登录",
"other": "其他方式登录"
}
} }

View File

@ -4,10 +4,15 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
redirectUri: string; redirectUri: string;
url: string; url: string;
callback: (() => void) | undefined; callback: (() => void) | undefined;
pwdAllowMobile: boolean;
pwdAllowEmail: boolean;
pwdAllowLoginName: boolean;
allowSms: boolean; allowSms: boolean;
allowEmail: boolean; allowEmail: boolean;
allowWechatMp: boolean; allowWechatMp: boolean;
setLoginMode: (value: string) => void; setLoginMode: (value: string) => void;
pwdMode: string; pwdMode: string;
allowRegister: boolean;
goRegister: () => void;
}>) => React.ReactElement; }>) => React.ReactElement;
export default _default; export default _default;

View File

@ -3,7 +3,21 @@ import { encryptPasswordSha1 } from "../../../../utils/password";
export default OakComponent({ export default OakComponent({
isList: false, isList: false,
formData({ features, props }) { formData({ features, props }) {
return {}; const { pwdAllowMobile, pwdAllowEmail, pwdAllowLoginName } = this.props;
let tips = [];
if (pwdAllowLoginName) {
tips.push(this.t('placeholder.LoginName'));
}
if (pwdAllowMobile) {
tips.push(this.t('placeholder.Mobile'));
}
if (pwdAllowEmail) {
tips.push(this.t('placeholder.Email'));
}
const accountPlaceholder = tips.length > 0 ? this.t('placeholder.Account') + tips.join('/') : this.t('placeholder.Account');
return {
accountPlaceholder,
};
}, },
data: { data: {
counter: 0, counter: 0,
@ -22,11 +36,16 @@ export default OakComponent({
redirectUri: '', // 微信登录后的redirectUri要指向wechatUser/login去处理 redirectUri: '', // 微信登录后的redirectUri要指向wechatUser/login去处理
url: '', // 登录系统之后要返回的页面 url: '', // 登录系统之后要返回的页面
callback: undefined, // 登录成功回调,排除微信登录方式 callback: undefined, // 登录成功回调,排除微信登录方式
allowSms: false, pwdAllowMobile: false,
allowEmail: false, pwdAllowEmail: false,
pwdAllowLoginName: false,
allowSms: false, //小程序切换手机号验证码登录
allowEmail: false, //小程序切换邮箱登录
allowWechatMp: false, //小程序切换授权登录 allowWechatMp: false, //小程序切换授权登录
setLoginMode: (value) => undefined, setLoginMode: (value) => undefined,
pwdMode: 'all', //密码明文密文存储模式 pwdMode: 'all', //密码明文密文存储模式
allowRegister: false,
goRegister: () => undefined,
}, },
lifetimes: {}, lifetimes: {},
listeners: { listeners: {
@ -86,10 +105,10 @@ export default OakComponent({
} }
}, },
inputChange(type, value) { inputChange(type, value) {
const { allowSms, allowEmail } = this.props; const { allowMobile, allowEmail } = this.props;
switch (type) { switch (type) {
case 'account': case 'account':
// const validMobile = allowSms && !!isMobile(value); // const validMobile = allowMobile && !!isMobile(value);
// const vaildEmail = allowEmail && !!isEmail(value); // const vaildEmail = allowEmail && !!isEmail(value);
const vaildAccount = !!(value && value.trim() && value.trim() !== ''); const vaildAccount = !!(value && value.trim() && value.trim() !== '');
this.setState({ this.setState({
@ -120,6 +139,10 @@ export default OakComponent({
const { setLoginMode } = this.props; const { setLoginMode } = this.props;
const { value } = e.currentTarget.dataset; const { value } = e.currentTarget.dataset;
setLoginMode && setLoginMode(value); setLoginMode && setLoginMode(value);
},
goRegisterMp() {
const { goRegister } = this.props;
goRegister && goRegister();
} }
}, },
}); });

View File

@ -4,6 +4,7 @@
"usingComponents": { "usingComponents": {
"l-button": "@oak-frontend-base/miniprogram_npm/lin-ui/button/index", "l-button": "@oak-frontend-base/miniprogram_npm/lin-ui/button/index",
"l-input": "@oak-frontend-base/miniprogram_npm/lin-ui/input/index", "l-input": "@oak-frontend-base/miniprogram_npm/lin-ui/input/index",
"oak-icon": "@oak-frontend-base/components/icon/index" "oak-icon": "@oak-frontend-base/components/icon/index",
"l-icon": "@oak-frontend-base/miniprogram_npm/lin-ui/icon/index"
} }
} }

View File

@ -42,4 +42,14 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
}
.registerBox {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 24rpx;
color: @oak-color-primary;
font-size: 28rpx;
} }

View File

@ -2,7 +2,7 @@
<oak-icon name="mine" size="28" color="#808080" /> <oak-icon name="mine" size="28" color="#808080" />
<l-input <l-input
hide-label="{{true}}" hide-label="{{true}}"
placeholder="{{t('placeholder.Account')}}" placeholder="{{accountPlaceholder}}"
clear="{{true}}" clear="{{true}}"
showRow="{{false}}" showRow="{{false}}"
l-class="my-input" l-class="my-input"
@ -29,11 +29,22 @@
bind:linclear="inputChangeMp" bind:linclear="inputChangeMp"
/> />
</view> </view>
<view wx:if="{{allowRegister}}" class="registerBox">
<l-button catch:lintap="goRegisterMp" special="{{true}}">
<view>{{t('register')}}</view>
<l-icon name="right" size="24" style="transform:translateY(8rpx); margin-left:8rpx"/>
</l-button>
</view>
<l-button size="long" disabled="{{!!disabled || !allowSubmit || loading}}" catch:lintap="loginByAccount" height="{{80}}" style="width:100%"> <l-button size="long" disabled="{{!!disabled || !allowSubmit || loading}}" catch:lintap="loginByAccount" height="{{80}}" style="width:100%">
{{t('Login')}} {{t('Login')}}
</l-button> </l-button>
<view class="methods"> <view class="methods">
<view wx:if="{{allowWechatMp}}" style="color:#8F976A" bindtap="changeLoginMp" data-value="wechatMp">一键登录</view> <view wx:if="{{allowWechatMp}}" style="color:#8F976A" bindtap="changeLoginMp" data-value="wechatMp">{{t('loginMode.wechatMp')}}</view>
<view wx:else></view> <view wx:else></view>
<view wx:if="{{allowSms}}" style="color:#835D01" bindtap="changeLoginMp" data-value="sms">短信登录</view> <!-- <view wx:if="{{allowSms}}" style="color:#835D01" bindtap="changeLoginMp" data-value="sms">短信登录</view> -->
<view style="color:#835D01; display:flex; align-items:center; justify-content:flex-end; gap:8rpx">
<view wx:if="{{allowSms}}" bindtap="changeLoginMp" data-value="sms">{{t('loginMode.sms')}}</view>
<view wx:if="{{allowSms && allowEmail}}">/</view>
<view wx:if="{{allowEmail}}"bindtap="changeLoginMp" data-value="email">{{t('loginMode.email')}}</view>
</view>
</view> </view>

View File

@ -1,9 +1,16 @@
{ {
"Login": "登录", "Login": "登录",
"placeholder": { "placeholder": {
"Account": "请输入账号", "Account": "请输入",
"Mobile": "/手机号", "LoginName": "账号",
"Email": "/邮箱", "Mobile": "手机号",
"Email": "邮箱",
"Password": "请输入密码" "Password": "请输入密码"
},
"register": "去注册",
"loginMode": {
"wechatMp": "一键登录",
"sms": "短信登录",
"email": "邮箱登录"
} }
} }

View File

@ -9,8 +9,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'token', fal
validMobile: boolean; validMobile: boolean;
validPassword: boolean; validPassword: boolean;
allowSubmit: boolean; allowSubmit: boolean;
allowSms: boolean; accountPlaceholder: string;
allowEmail: boolean;
}, { }, {
loginByAccount: () => Promise<void>; loginByAccount: () => Promise<void>;
inputChange: (type: 'account' | 'password', value: string) => void; inputChange: (type: 'account' | 'password', value: string) => void;

View File

@ -6,13 +6,13 @@ import { LockOutlined, UserOutlined, } from '@ant-design/icons';
import Style from './web.module.less'; import Style from './web.module.less';
export default function Render(props) { export default function Render(props) {
const { data, methods } = props; const { data, methods } = props;
const { loading, disabled, account, password, validMobile, validPassword, allowSubmit, allowSms, allowEmail, } = data; const { loading, disabled, account, password, validMobile, validPassword, allowSubmit, accountPlaceholder } = data;
const { loginByAccount, t, inputChange } = methods; const { loginByAccount, t, inputChange } = methods;
return (<Form colon={true}> return (<Form colon={true}>
<Form.Item name="mobile"> <Form.Item name="mobile">
<Input allowClear value={account} size="large" <Input allowClear value={account} size="large"
// maxLength={11} // maxLength={11}
prefix={<UserOutlined />} placeholder={t('placeholder.Account') + (allowSms ? t('placeholder.Mobile') : '') + (allowEmail ? t('placeholder.Email') : '')} onChange={(e) => { prefix={<UserOutlined />} placeholder={accountPlaceholder} onChange={(e) => {
inputChange('account', e.target.value); inputChange('account', e.target.value);
}} className={Style['loginbox-input']}/> }} className={Style['loginbox-input']}/>
</Form.Item> </Form.Item>

View File

@ -4,6 +4,7 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
url: string; url: string;
callback: (() => void) | undefined; callback: (() => void) | undefined;
allowPassword: boolean; allowPassword: boolean;
allowEmail: boolean;
allowWechatMp: boolean; allowWechatMp: boolean;
setLoginMode: (value: string) => void; setLoginMode: (value: string) => void;
digit: number; digit: number;

View File

@ -24,6 +24,7 @@ export default OakComponent({
url: '', // 登录系统之后要返回的页面 url: '', // 登录系统之后要返回的页面
callback: undefined, // 登录成功回调,排除微信登录方式 callback: undefined, // 登录成功回调,排除微信登录方式
allowPassword: false, //小程序切换密码登录 allowPassword: false, //小程序切换密码登录
allowEmail: false, //小程序切换邮箱登录
allowWechatMp: false, //小程序切换授权登录 allowWechatMp: false, //小程序切换授权登录
setLoginMode: (value) => undefined, setLoginMode: (value) => undefined,
digit: 4 //验证码位数, digit: 4 //验证码位数,

View File

@ -37,7 +37,12 @@
{{t('Login')}} {{t('Login')}}
</l-button> </l-button>
<view class="methods"> <view class="methods">
<view wx:if="{{allowWechatMp}}" style="color:#8F976A" bindtap="changeLoginMp" data-value="wechatMp">一键登录</view> <view wx:if="{{allowWechatMp}}" style="color:#8F976A" bindtap="changeLoginMp" data-value="wechatMp">{{t('loginMode.wechatMp')}}</view>
<view wx:else></view> <view wx:else></view>
<view wx:if="{{allowPassword}}" style="color:#835D01" bindtap="changeLoginMp" data-value="password">密码登录</view> <!-- <view wx:if="{{allowPassword}}" style="color:#835D01" bindtap="changeLoginMp" data-value="password">密码登录</view> -->
<view style="color:#835D01; display:flex; align-items:center; justify-content:flex-end; gap:8rpx">
<view wx:if="{{allowPassword}}" bindtap="changeLoginMp" data-value="password">{{t('loginMode.password')}}</view>
<view wx:if="{{allowPassword && allowEmail}}">/</view>
<view wx:if="{{allowEmail}}" bindtap="changeLoginMp" data-value="email">{{t('loginMode.email')}}</view>
</view>
</view> </view>

View File

@ -5,5 +5,10 @@
"Captcha": "输入%{digit}位短信验证码", "Captcha": "输入%{digit}位短信验证码",
"Mobile": "请输入手机号" "Mobile": "请输入手机号"
}, },
"resendAfter": "秒后可重发" "resendAfter": "秒后可重发",
"loginMode": {
"wechatMp": "一键登录",
"password": "密码登录",
"email": "邮箱登录"
}
} }

View File

@ -21,9 +21,18 @@ export default function Render(props: WebComponentProps<EntityDict, 'token', fal
scanOptions: Option[]; scanOptions: Option[];
smsDigit: number; smsDigit: number;
emailDigit: number; emailDigit: number;
allowSms: boolean; pwdAllowMobile: boolean;
allowEmail: boolean; pwdAllowEmail: boolean;
pwdAllowLoginName: boolean;
pwdMode: 'all' | 'plain' | 'sha1'; pwdMode: 'all' | 'plain' | 'sha1';
allowRegister: boolean;
oauthOptions: {
name: string;
value: string;
logo?: string;
}[];
goRegister: () => void;
goOauthLogin: (oauthProviderId: string) => void;
}, { }, {
setLoginMode: (value: number) => void; setLoginMode: (value: number) => void;
}>): React.JSX.Element; }>): React.JSX.Element;

View File

@ -1,8 +1,8 @@
// @ts-nocheck // @ts-nocheck
// Segmented这个对象在antd里的声明是错误的 // Segmented这个对象在antd里的声明是错误的
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Segmented, Divider, Space } from 'antd'; import { Button, Segmented, Divider, Space, Tooltip, Image } from 'antd';
import { MobileOutlined, QrcodeOutlined, DesktopOutlined, MailOutlined, ExclamationCircleOutlined, } from '@ant-design/icons'; import { MobileOutlined, QrcodeOutlined, DesktopOutlined, MailOutlined, ExclamationCircleOutlined, LinkOutlined, } from '@ant-design/icons';
import classNames from 'classnames'; import classNames from 'classnames';
import Style from './web.module.less'; import Style from './web.module.less';
import WeChatLoginQrCode from '../../common/weChatLoginQrCode'; import WeChatLoginQrCode from '../../common/weChatLoginQrCode';
@ -13,8 +13,8 @@ import PasswordLogin from './password';
import EmailLogin from './email'; import EmailLogin from './email';
export default function Render(props) { export default function Render(props) {
const { data, methods } = props; const { data, methods } = props;
const { width, loading, loginMode, appId, domain, isSupportWechatGrant, disabled, redirectUri, url, passportTypes, callback, inputOptions, scanOptions, smsDigit, emailDigit, allowSms, allowEmail, pwdMode, } = data; const { width, loading, loginMode, appId, domain, isSupportWechatGrant, disabled, redirectUri, url, passportTypes, callback, inputOptions, scanOptions, smsDigit, emailDigit, pwdAllowMobile, pwdAllowEmail, pwdAllowLoginName, pwdMode, allowRegister, oauthOptions, goRegister, goOauthLogin, } = data;
const { t, setLoginMode } = methods; const { t, setLoginMode, } = methods;
let redirectUri2 = redirectUri; let redirectUri2 = redirectUri;
if (!(redirectUri.startsWith('https') || redirectUri.startsWith('http'))) { if (!(redirectUri.startsWith('https') || redirectUri.startsWith('http'))) {
const hostname = domain || window.location.hostname; const hostname = domain || window.location.hostname;
@ -36,10 +36,11 @@ export default function Render(props) {
setShowInput(false); setShowInput(false);
} }
}, [loginMode]); }, [loginMode]);
const InputMethods = inputOptions && inputOptions.length > 0 ? (<div className={Style['loginbox-methods']}> const InputMethods = inputOptions && inputOptions.length > 0 ? (
<Divider plain style={{ fontSize: 13, color: '#808080', }}>{t('otherMethods')}</Divider> // <div className={Style['loginbox-methods']}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 24 }}> // <Divider plain style={{ fontSize: 13, color: '#808080', }}>{t('otherMethods')}</Divider>
{inputOptions.map((ele) => { <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 24 }}>
{inputOptions.map((ele) => {
let icon = <></>; let icon = <></>;
if (ele.value === 'sms') { if (ele.value === 'sms') {
icon = <MobileOutlined />; icon = <MobileOutlined />;
@ -53,21 +54,36 @@ export default function Render(props) {
return (<Space key={ele.value} size={4} style={{ cursor: 'pointer' }} onClick={() => { return (<Space key={ele.value} size={4} style={{ cursor: 'pointer' }} onClick={() => {
setLoginMode(ele.value); setLoginMode(ele.value);
}}> }}>
{icon} {icon}
<div>{ele.label}</div> <div>{ele.label}</div>
</Space>); </Space>);
})} })}
</div> </div>
</div>) : <></>; // </div>
const ScanMethods = scanOptions && scanOptions.length > 0 ? (<div className={Style['loginbox-methods']}> ) : <></>;
<Divider plain style={{ fontSize: 13, color: '#808080', }}>{t('otherMethods')}</Divider> const ScanMethods = scanOptions && scanOptions.length > 0 ? (
<Space style={{ cursor: 'pointer' }} onClick={() => { // <div className={Style['loginbox-methods']}>
// <Divider plain style={{ fontSize: 13, color: '#808080', }}>{t('otherMethods')}</Divider>
<Space style={{ cursor: 'pointer' }} onClick={() => {
setLoginMode(scanOptions[0].value); setLoginMode(scanOptions[0].value);
}}> }}>
<QrcodeOutlined /> <QrcodeOutlined />
<div>{t('scanLogin')}</div> <div>{t('scanLogin')}</div>
</Space> </Space>
</div>) : <></>; // </div>
) : <></>;
const OauthMethods = oauthOptions && oauthOptions.length > 0 ? (
// <div className={Style['loginbox-methods']}>
// <Divider plain style={{ fontSize: 13, color: '#808080', }}>{t('otherMethods')}</Divider>
<Space style={{ cursor: 'pointer' }}>
{oauthOptions?.map((ele) => (<Tooltip title={ele.name}>
<Image preview={false} width={20} height={20} src={ele.logo} onClick={() => {
goOauthLogin(ele.value);
}} placeholder={<LinkOutlined />}/>
</Tooltip>))}
</Space>
// </div>
) : <></>;
const Tip = <div className={Style['loginbox-tip']}> const Tip = <div className={Style['loginbox-tip']}>
<Space> <Space>
<ExclamationCircleOutlined /> <ExclamationCircleOutlined />
@ -97,8 +113,13 @@ export default function Render(props) {
<div className={Style['loginbox-password']} style={{ <div className={Style['loginbox-password']} style={{
display: loginMode === 'password' ? 'block' : 'none', display: loginMode === 'password' ? 'block' : 'none',
}}> }}>
<PasswordLogin disabled={disabled} url={url} callback={callback} allowSms={allowSms} allowEmail={allowEmail} pwdMode={pwdMode}/> <PasswordLogin disabled={disabled} url={url} callback={callback} pwdAllowMobile={pwdAllowMobile} pwdAllowEmail={pwdAllowEmail} pwdAllowLoginName={pwdAllowLoginName} pwdMode={pwdMode}/>
{Tip} {allowRegister && (<div className={Style['loginbox-register']}>
{/* <Button type='link' iconPosition='end' icon={<RightOutlined />}>去注册</Button> */}
<Button type='link' onClick={() => {
goRegister && goRegister();
}}>{`${t('goRegister')} >`}</Button>
</div>)}
</div> </div>
<div className={Style['loginbox-mobile']} style={{ <div className={Style['loginbox-mobile']} style={{
display: loginMode === 'sms' ? 'block' : 'none', display: loginMode === 'sms' ? 'block' : 'none',
@ -112,7 +133,13 @@ export default function Render(props) {
<EmailLogin disabled={disabled} url={url} callback={callback} digit={emailDigit}/> <EmailLogin disabled={disabled} url={url} callback={callback} digit={emailDigit}/>
{Tip} {Tip}
</div> </div>
{ScanMethods} {(scanOptions?.length > 0 || oauthOptions?.length > 0) ? (<div className={Style['loginbox-methods']}>
<Divider plain style={{ fontSize: 13, color: '#808080', }}>{t('otherMethods')}</Divider>
<Space split={<Divider type="vertical"/>}>
{ScanMethods}
{OauthMethods}
</Space>
</div>) : (<></>)}
</>) : (<> </>) : (<>
{loginMode === 'wechatWeb' && {loginMode === 'wechatWeb' &&
<div className={Style['loginbox-qrcode']}> <div className={Style['loginbox-qrcode']}>
@ -123,7 +150,17 @@ export default function Render(props) {
<WechatLoginQrCodeForPublic type="login" oakPath="$login-wechatLogin/qrCode" oakAutoUnmount={true} url={state} size={180}/> <WechatLoginQrCodeForPublic type="login" oakPath="$login-wechatLogin/qrCode" oakAutoUnmount={true} url={state} size={180}/>
{Tip} {Tip}
</div>} </div>}
{InputMethods} {loginMode === 'wechatMpForWeb' && <div className={Style['loginbox-qrcode']}>
<WechatLoginQrCodeForPublic type="login" oakPath="$login-wechatLogin/qrCode" oakAutoUnmount={true} url={state} size={180} qrCodeType={'wechatMpDomainUrl'}/>
{Tip}
</div>}
{(inputOptions?.length > 0 || oauthOptions?.length > 0) ? (<div className={Style['loginbox-methods']}>
<Divider plain style={{ fontSize: 13, color: '#808080', }}>{t('otherMethods')}</Divider>
<Space split={<Divider type="vertical"/>}>
{InputMethods}
{OauthMethods}
</Space>
</div>) : (<></>)}
</>)} </>)}
</>)} </>)}
</div> </div>

View File

@ -41,25 +41,27 @@
position: relative; position: relative;
padding: 32px; padding: 32px;
height: 220px; height: 220px;
padding-bottom: 0px;
} }
&-password { &-password {
position: relative; position: relative;
padding: 32px; padding: 32px;
height: 220px; height: 220px;
padding-bottom: 0px;
} }
&-email { &-email {
position: relative; position: relative;
padding: 32px; padding: 32px;
height: 220px; height: 220px;
padding-bottom: 0px;
} }
&-qrcode { &-qrcode {
padding: 0 32px; padding: 16px 32px;
font-size: 14px; font-size: 14px;
height: 268px; min-height: 268px;
padding-top: 16px;
&__sociallogin { &__sociallogin {
text-align: center; text-align: center;
@ -126,4 +128,11 @@
font-size: 13px; font-size: 13px;
color: #808080; color: #808080;
} }
&-register {
display: flex;
align-items: center;
justify-content: flex-end;
}
} }

View File

@ -34,9 +34,8 @@ export default OakComponent({
}, },
lifetimes: { lifetimes: {
async ready() { async ready() {
const application = this.features.application.getApplication(); const system = this.features.application.getApplication().system;
const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); const passwordConfig = system?.config.Password;
const passwordConfig = applicationPassports.find((ele) => ele.passport.type === 'password')?.passport.config;
const mode = passwordConfig?.mode ?? 'all'; const mode = passwordConfig?.mode ?? 'all';
const pwdMin = passwordConfig?.min ?? 8; const pwdMin = passwordConfig?.min ?? 8;
const pwdMax = passwordConfig?.max ?? 24; const pwdMax = passwordConfig?.max ?? 24;

View File

@ -11,9 +11,8 @@ export default OakComponent({
lifetimes: { lifetimes: {
async ready() { async ready() {
this.features.token.getToken(); this.features.token.getToken();
const application = this.features.application.getApplication(); const system = this.features.application.getApplication().system;
const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); const passwordConfig = system?.config.Password;
const passwordConfig = applicationPassports.find((ele) => ele.passport.type === 'password')?.passport.config;
const mode = passwordConfig?.mode ?? 'all'; const mode = passwordConfig?.mode ?? 'all';
this.setState({ this.setState({
mode, mode,

View File

@ -0,0 +1,6 @@
import { EntityDict } from "../../../oak-app-domain";
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, keyof EntityDict, false, {
goLogin: (() => void) | undefined;
goBack: (() => void) | undefined;
}>) => React.ReactElement;
export default _default;

View File

@ -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();
}
},
});

View File

@ -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"
}
}

View File

@ -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;
}

View File

@ -0,0 +1,104 @@
<view class="page-body">
<view class="register-box">
<view class="regitser-title">{{t('registerTitle')}}</view>
<view class="register-body">
<l-form>
<l-form-item
label="{{t('label.loginName')}}"
label-placement="column"
l-form-item-class="formItem"
label-slot="{{true}}"
>
<view slot="label" style="margin:16rpx 0rpx">
<view class="label">
<view style="color:red">*</view>
<view>{{t('label.loginName')}}</view>
</view>
<view class="help">({{loginNameTip}})</view>
</view>
<l-input
value="{{loginName}}"
data-attr="loginName"
rules="{{loginNameRulesMp}}"
bind:lininput="setValueMp"
show-row="{{false}}"
tip-type="text"
hide-label="{{true}}"
show-row="{{false}}"
l-class="my-input"
l-input-class="input"
style="flex:1;"
/>
</l-form-item>
<l-form-item
label="{{t('label.password')}}"
label-placement="column"
l-form-item-class="formItem"
label-slot="{{true}}"
>
<view slot="label" style="margin:16rpx 0rpx">
<view class="label">
<view style="color:red">*</view>
<view>{{t('label.password')}}</view>
</view>
<view class="help">({{pwdTip}})</view>
</view>
<l-input
value="{{password}}"
type="password"
data-attr="password"
rules="{{passwordRulesMp}}"
bind:lininput="setValueMp"
show-row="{{false}}"
tip-type="text"
hide-label="{{true}}"
show-row="{{false}}"
l-class="my-input"
l-input-class="input"
style="flex:1;"
/>
</l-form-item>
<l-form-item
label="{{t('label.rePwd')}}"
label-placement="column"
l-form-item-class="formItem"
label-slot="{{true}}"
>
<view slot="label" style="margin:16rpx 0rpx">
<view class="label">
<view style="color:red">*</view>
<view>{{t('label.rePwd')}}</view>
</view>
</view>
<l-input
value="{{confirm}}"
type="password"
data-attr="confirm"
rules="{{confirmRulesMp}}"
bind:lininput="setValueMp"
show-row="{{false}}"
tip-type="text"
hide-label="{{true}}"
show-row="{{false}}"
l-class="my-input"
l-input-class="input"
style="flex:1;"
/>
</l-form-item>
<l-button
disabled="{{!(loginName && password && confirm) || loginNameHasErr || passwordHasErr || confirmHasErr}}"
catch:lintap="onConfirmMp"
size="long"
l-class="my-btn"
>
{{t('register')}}
</l-button>
</l-form>
</view>
<view class="login">
<l-button catch:lintap="goLoginMp" special="{{true}}">
<view>{{t('goLogin')}}</view>
</l-button>
</view>
</view>
</view>

View File

@ -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": "注册成功,请前往登录页登录"
}

21
es/components/user/register/web.d.ts vendored Normal file
View File

@ -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<EntityDict, 'token', false, {
width: string;
allowRegister: boolean;
loginNameMin: number;
loginNameMax: number;
loginNameNeedVerify: boolean;
loginNameRegexs: string[];
loginNameTip: string;
pwdMin: number;
pwdMax: number;
pwdNeedVerify: boolean;
pwdRegexs: string[];
pwdTip: string;
goLogin: () => void;
goBack: () => void;
}, {
onConfirm: (loginName: string, password: string) => Promise<void>;
}>): React.JSX.Element;

View File

@ -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 (<div className={classNames(Style['registerbox-wrap'], {
[Style['registerbox-wrap__mobile']]: width === 'xs',
})}>
<div style={{ minHeight: 200, boxSizing: 'border-box', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{t('not allow register')}
</div>
</div>);
}
return (<div className={classNames(Style['registerbox-wrap'], {
[Style['registerbox-wrap__mobile']]: width === 'xs',
})}>
{!!goBack && <LeftOutlined style={{ position: 'absolute', top: 40, left: 32, color: '#555555' }} onClick={() => goBack()}/>}
<div className={Style['registerbox-hd']}>
<div>{t('registerTitle')}</div>
</div>
<div className={Style['registerbox-bd']}>
<Form style={{ maxWidth: 400 }} layout="vertical">
<Form.Item label={t('label.loginName')} name="loginName" tooltip={loginNameTip} help={loginNameHelp} hasFeedback validateStatus={loginNameStatus} rules={[
{
required: true,
message: t('placeholder.loginName'),
validator: (_, value) => {
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');
}
},
]}>
<Input autoFocus onChange={({ currentTarget }) => setLoginName(currentTarget.value)} placeholder={t('placeholder.loginName')} value={loginName}/>
</Form.Item>
<Form.Item label={t('label.password')} name="password" help={validateHelp} tooltip={pwdTip} rules={[
{
required: true,
message: t('placeholder.password'),
validator: (_, value) => {
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}>
<Input.Password value={password} onChange={(e) => {
const strValue = e.target.value;
setPassword(strValue);
}} iconRender={(visible) => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)} placeholder={t('placeholder.password')}/>
</Form.Item>
<Form.Item label={t('label.rePwd')} name="passwordConfirm" rules={[
{
required: true,
validator: (_, value) => {
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>
<Input.Password value={password2} onChange={(e) => {
const strValue = e.target.value;
setPassword2(strValue);
}} iconRender={(visible) => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)} placeholder={t('placeholder.rePwd')}/>
</Form.Item>
<Button block size="large" type="primary" onClick={async () => {
await onConfirm(loginName, password);
}} disabled={!(loginName && loginNameStatus !== 'error' && password && validateStatus !== 'error') || oakExecuting || oakLoading}>
{t('register')}
</Button>
</Form>
{!!goLogin && (<div className={Style['registerbox-login']}>
<Button type="link" onClick={() => {
goLogin && goLogin();
}}>
{t('goLogin')}
</Button>
</div>)}
</div>
</div>);
}

View File

@ -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;
}
}

View File

@ -84,9 +84,8 @@ export default OakComponent({
}, },
lifetimes: { lifetimes: {
async ready() { async ready() {
const application = this.features.application.getApplication(); const system = this.features.application.getApplication().system;
const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id }); const passwordConfig = system?.config.Password;
const passwordConfig = applicationPassports.find((ele) => ele.passport.type === 'password')?.passport.config;
const mode = passwordConfig?.mode ?? 'all'; const mode = passwordConfig?.mode ?? 'all';
const pwdMin = passwordConfig?.min ?? 8; const pwdMin = passwordConfig?.min ?? 8;
const pwdMax = passwordConfig?.max ?? 24; const pwdMax = passwordConfig?.max ?? 24;

View File

@ -41,6 +41,15 @@ export default OakComponent({
const protocol = window.location.protocol === 'https:' ? 'https' : 'http'; const protocol = window.location.protocol === 'https:' ? 'https' : 'http';
const redirectUri = encodeURIComponent(`${protocol}://${host}${wechatUserLoginPage}?wechatLoginId=${wechatLoginId}`); 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`; 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();
} }
}, },
}); });

View File

@ -9,6 +9,8 @@
flex-direction: column; flex-direction: column;
background-color: @oak-bg-color-container; background-color: @oak-bg-color-container;
box-sizing: border-box; box-sizing: border-box;
align-items: center;
justify-content: center;
.safe-area-inset-bottom(); .safe-area-inset-bottom();
} }
@ -19,19 +21,6 @@
margin-bottom: 60rpx; 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 { .title {
font-size: 32rpx; font-size: 32rpx;
color: @oak-text-color-primary; color: @oak-text-color-primary;
@ -41,4 +30,23 @@
margin-top: 16rpx; margin-top: 16rpx;
font-size: 28rpx; font-size: 28rpx;
color: @oak-text-color-secondary; 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;
} }

View File

@ -1,4 +1,30 @@
<!-- index.wxml --> <!-- index.wxml -->
<view class="page-body"> <view class="page-body">
绑定小程序尚未实现 <block wx:if="{{expired}}">
<view class="circle-view">
<l-icon name="warning" size="120" />
<text class="text">二维码已过期,请重新扫码</text>
<text class="desc">抱歉,该码已过期</text>
</view>
</block>
<block wx:if="{{type==='login'}}">
<view wx:if="{{successed}}" class="circle-view">
<l-icon name="success" size="120" />
<text class="text">登录成功</text>
</view>
<block wx:else>
<l-button
type="default"
size="long"
disabled="{{oakExecuting || oakLoading}}"
bind:lintap="loginByWechatMp"
style="width:100%"
>
一键登录
</l-button>
</block>
</block>
<view wx:else>
绑定小程序尚未实现
</view>
</view> </view>

View File

@ -5,5 +5,6 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
size: undefined; size: undefined;
disableBack: boolean; disableBack: boolean;
wechatLoginConfirmPage: string; wechatLoginConfirmPage: string;
qrCodeType: EntityDict["wechatLogin"]["Schema"]["qrCodeType"];
}>) => React.ReactElement; }>) => React.ReactElement;
export default _default; export default _default;

View File

@ -30,17 +30,19 @@ export default OakComponent({
url: '', // 扫码登录/绑定成功跳转的页面 url: '', // 扫码登录/绑定成功跳转的页面
size: undefined, size: undefined,
disableBack: false, // 扫码登录/绑定成功后 是否禁用返回 disableBack: false, // 扫码登录/绑定成功后 是否禁用返回
wechatLoginConfirmPage: '/wechatLogin/confirm' wechatLoginConfirmPage: '/wechatLogin/confirm',
qrCodeType: 'wechatPublic',
}, },
methods: { methods: {
async createWechatLogin() { async createWechatLogin() {
const { type = 'bind', wechatLoginConfirmPage } = this.props; const { type = 'bind', wechatLoginConfirmPage, qrCodeType } = this.props;
const { result: wechatLoginId } = await this.features.cache.exec('createWechatLogin', { const { result: wechatLoginId } = await this.features.cache.exec('createWechatLogin', {
type, type,
interval: Interval, interval: Interval,
router: { router: {
pathname: wechatLoginConfirmPage pathname: wechatLoginConfirmPage
} },
qrCodeType,
}); });
this.setState({ this.setState({
wechatLoginId, wechatLoginId,

View File

@ -6,10 +6,10 @@ export default OakComponent({
}, },
lifetimes: { lifetimes: {
attached() { attached() {
if (process.env.OAK_PLATFORM === 'web') { // if (process.env.OAK_PLATFORM === 'web') {
//处理微信授权登录 //处理微信授权登录
this.login(); this.login();
} // }
}, },
}, },
methods: { methods: {

View File

@ -266,7 +266,8 @@ const i18ns = [
"missing_client_id": "缺少 client_id 参数", "missing_client_id": "缺少 client_id 参数",
"unknown": "未知错误,请稍后重试" "unknown": "未知错误,请稍后重试"
} }
} },
"login": "当前暂未支持该第三方应用授权登录"
} }
} }
}, },
@ -665,10 +666,15 @@ const i18ns = [
"Login": "登录", "Login": "登录",
"Send": "发送验证码", "Send": "发送验证码",
"placeholder": { "placeholder": {
"Captcha": "输入4位验证码", "Captcha": "输入%{digit}位验证码",
"Email": "请输入邮箱" "Email": "请输入邮箱"
}, },
"resendAfter": "秒后可重发" "resendAfter": "秒后可重发",
"loginMode": {
"wechatMp": "一键登录",
"sms": "短信登录",
"password": "密码登录"
}
} }
}, },
{ {
@ -692,7 +698,12 @@ const i18ns = [
"resendAfter": "秒后可重发", "resendAfter": "秒后可重发",
"otherMethods": "其他登录方式", "otherMethods": "其他登录方式",
"scanLogin": "扫码登录", "scanLogin": "扫码登录",
"tip": "未注册用户首次登录将自动注册" "tip": "未注册用户首次登录将自动注册",
"goRegister": "去注册",
"loginMode": {
"wechatMp": "授权登录",
"other": "其他方式登录"
}
} }
}, },
{ {
@ -704,10 +715,17 @@ const i18ns = [
data: { data: {
"Login": "登录", "Login": "登录",
"placeholder": { "placeholder": {
"Account": "请输入账号", "Account": "请输入",
"Mobile": "/手机号", "LoginName": "账号",
"Email": "/邮箱", "Mobile": "手机号",
"Email": "邮箱",
"Password": "请输入密码" "Password": "请输入密码"
},
"register": "去注册",
"loginMode": {
"wechatMp": "一键登录",
"sms": "短信登录",
"email": "邮箱登录"
} }
} }
}, },
@ -724,7 +742,12 @@ const i18ns = [
"Captcha": "输入%{digit}位短信验证码", "Captcha": "输入%{digit}位短信验证码",
"Mobile": "请输入手机号" "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", id: "5bf96a3e054b8d73c76d7bb45ea90a80",
namespace: "oak-general-business-c-userEntityGrant-claim", namespace: "oak-general-business-c-userEntityGrant-claim",

View File

@ -7,5 +7,6 @@ export interface Schema extends EntityShape {
application: Application; application: Application;
passport: Passport; passport: Passport;
isDefault: Boolean; isDefault: Boolean;
allowPwd?: Boolean;
} }
export declare const entityDesc: EntityDesc<Schema>; export declare const entityDesc: EntityDesc<Schema>;

View File

@ -7,6 +7,7 @@ export const entityDesc = {
application: '应用', application: '应用',
passport: '登录方式', passport: '登录方式',
isDefault: '是否默认', isDefault: '是否默认',
allowPwd: '是否支持密码登录',
}, },
}, },
}, },

View File

@ -2,7 +2,7 @@ import { Boolean } from 'oak-domain/lib/types/DataType';
import { EntityShape } from 'oak-domain/lib/types/Entity'; import { EntityShape } from 'oak-domain/lib/types/Entity';
import { Schema as System } from './System'; import { Schema as System } from './System';
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc'; 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 = { export type SmsConfig = {
mockSend?: boolean; mockSend?: boolean;
defaultOrigin?: 'ali' | 'tencent' | 'ctyun'; defaultOrigin?: 'ali' | 'tencent' | 'ctyun';
@ -34,10 +34,21 @@ export type PwdConfig = {
regexs?: string[]; regexs?: string[];
tip?: 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 { export interface Schema extends EntityShape {
system: System; system: System;
type: Type; type: Type;
config?: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig; config?: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig;
enabled: Boolean; enabled: Boolean;
} }
export declare const entityDesc: EntityDesc<Schema, '', '', { export declare const entityDesc: EntityDesc<Schema, '', '', {

View File

@ -12,14 +12,16 @@ export const entityDesc = {
v: { v: {
type: { type: {
email: '邮箱', email: '邮箱',
sms: '短信', sms: '手机号',
password: '密码', password: '密码',
wechatMp: '小程序', wechatMp: '小程序',
wechatPublic: '公众号', wechatPublic: '公众号',
wechatWeb: '微信网站', wechatWeb: '微信网站',
wechatMpForWeb: '小程序授权网页', wechatMpForWeb: '小程序授权网页',
wechatPublicForWeb: '公众号授权网页', wechatPublicForWeb: '公众号授权网页',
wechatNative: '微信APP授权' wechatNative: '微信APP授权',
loginName: '账号',
oauth: 'OAuth授权'
}, },
}, },
}, },
@ -35,7 +37,9 @@ export const entityDesc = {
wechatMp: '#ADDCCA', wechatMp: '#ADDCCA',
wechatMpForWeb: '#FDC454', wechatMpForWeb: '#FDC454',
wechatPublicForWeb: '#C0A27C', wechatPublicForWeb: '#C0A27C',
wechatNative: '#C0A27C' wechatNative: '#C0A27C',
loginName: '#456B3C',
oauth: '#3C4655',
} }
} }
} }

View File

@ -27,7 +27,9 @@ export declare class Token<ED extends EntityDict> extends Feature {
loginWechat(code: string, params?: { loginWechat(code: string, params?: {
wechatLoginId?: string; wechatLoginId?: string;
}): Promise<void>; }): Promise<void>;
loginWechatMp(): Promise<void>; loginWechatMp(params?: {
wechatLoginId?: string;
}): Promise<void>;
loginWechatNative(code: string): Promise<void>; loginWechatNative(code: string): Promise<void>;
syncUserInfoWechatMp(): Promise<void>; syncUserInfoWechatMp(): Promise<void>;
logout(dontPublish?: boolean): Promise<void>; logout(dontPublish?: boolean): Promise<void>;

View File

@ -194,12 +194,13 @@ export class Token extends Feature {
this.publish(); this.publish();
this.checkNeedSetPassword(); this.checkNeedSetPassword();
} }
async loginWechatMp() { async loginWechatMp(params) {
const { code } = await wx.login(); const { code } = await wx.login();
const env = await this.environment.getEnv(); const env = await this.environment.getEnv();
const { result } = await this.cache.exec('loginWechatMp', { const { result } = await this.cache.exec('loginWechatMp', {
code, code,
env: env, env: env,
wechatLoginId: params?.wechatLoginId,
}); });
this.tokenValue = result; this.tokenValue = result;
await this.storage.save(LOCAL_STORAGE_KEYS.token, result); await this.storage.save(LOCAL_STORAGE_KEYS.token, result);

View File

@ -14,6 +14,9 @@ export const desc = {
isDefault: { isDefault: {
notNull: true, notNull: true,
type: "boolean" type: "boolean"
},
allowPwd: {
type: "boolean"
} }
}, },
actionType: "crud", actionType: "crud",

View File

@ -7,6 +7,7 @@ export type OpSchema = EntityShape & {
applicationId: ForeignKey<"application">; applicationId: ForeignKey<"application">;
passportId: ForeignKey<"passport">; passportId: ForeignKey<"passport">;
isDefault: Boolean; isDefault: Boolean;
allowPwd?: Boolean | null;
} & { } & {
[A in ExpressionKey]?: any; [A in ExpressionKey]?: any;
}; };
@ -19,6 +20,7 @@ export type OpFilter = {
applicationId: Q_StringValue; applicationId: Q_StringValue;
passportId: Q_StringValue; passportId: Q_StringValue;
isDefault: Q_BooleanValue; isDefault: Q_BooleanValue;
allowPwd: Q_BooleanValue;
} & ExprOp<OpAttr | string>; } & ExprOp<OpAttr | string>;
export type OpProjection = { export type OpProjection = {
"#id"?: NodeId; "#id"?: NodeId;
@ -30,6 +32,7 @@ export type OpProjection = {
applicationId?: number; applicationId?: number;
passportId?: number; passportId?: number;
isDefault?: number; isDefault?: number;
allowPwd?: number;
} & Partial<ExprOp<OpAttr | string>>; } & Partial<ExprOp<OpAttr | string>>;
export type OpSortAttr = Partial<{ export type OpSortAttr = Partial<{
id: number; id: number;
@ -39,6 +42,7 @@ export type OpSortAttr = Partial<{
applicationId: number; applicationId: number;
passportId: number; passportId: number;
isDefault: number; isDefault: number;
allowPwd: number;
[k: string]: any; [k: string]: any;
} | ExprOp<OpAttr | string>>; } | ExprOp<OpAttr | string>>;
export type OpAction = OakMakeAction<GenericAction | string>; export type OpAction = OakMakeAction<GenericAction | string>;

View File

@ -3,6 +3,7 @@
"attr": { "attr": {
"application": "应用", "application": "应用",
"passport": "登录方式", "passport": "登录方式",
"isDefault": "是否默认" "isDefault": "是否默认",
"allowPwd": "是否支持密码登录"
} }
} }

View File

@ -9,7 +9,7 @@ export const desc = {
type: { type: {
notNull: true, notNull: true,
type: "enum", 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: { config: {
type: "object" type: "object"

View File

@ -9,7 +9,9 @@ export const style = {
wechatMp: '#ADDCCA', wechatMp: '#ADDCCA',
wechatMpForWeb: '#FDC454', wechatMpForWeb: '#FDC454',
wechatPublicForWeb: '#C0A27C', wechatPublicForWeb: '#C0A27C',
wechatNative: '#C0A27C' wechatNative: '#C0A27C',
loginName: '#456B3C',
oauth: '#3C4655',
} }
} }
}; };

View File

@ -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 { MakeAction as OakMakeAction, EntityShape } from "oak-domain/lib/types/Entity";
import { GenericAction } from "oak-domain/lib/actions/action"; import { GenericAction } from "oak-domain/lib/actions/action";
import { Boolean } from "oak-domain/lib/types/DataType"; 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 = { export type SmsConfig = {
mockSend?: boolean; mockSend?: boolean;
defaultOrigin?: "ali" | "tencent" | "ctyun"; defaultOrigin?: "ali" | "tencent" | "ctyun";
@ -35,10 +35,21 @@ export type PwdConfig = {
regexs?: string[]; regexs?: string[];
tip?: 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 & { export type OpSchema = EntityShape & {
systemId: ForeignKey<"system">; systemId: ForeignKey<"system">;
type: Type; type: Type;
config?: (SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig) | null; config?: (SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig) | null;
enabled: Boolean; enabled: Boolean;
} & { } & {
[A in ExpressionKey]?: any; [A in ExpressionKey]?: any;
@ -51,7 +62,7 @@ export type OpFilter = {
$$updateAt$$: Q_DateValue; $$updateAt$$: Q_DateValue;
systemId: Q_StringValue; systemId: Q_StringValue;
type: Q_EnumValue<Type>; type: Q_EnumValue<Type>;
config: JsonFilter<SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig>; config: JsonFilter<SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig>;
enabled: Q_BooleanValue; enabled: Q_BooleanValue;
} & ExprOp<OpAttr | string>; } & ExprOp<OpAttr | string>;
export type OpProjection = { export type OpProjection = {
@ -63,7 +74,7 @@ export type OpProjection = {
$$seq$$?: number; $$seq$$?: number;
systemId?: number; systemId?: number;
type?: number; type?: number;
config?: number | JsonProjection<SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig>; config?: number | JsonProjection<SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig>;
enabled?: number; enabled?: number;
} & Partial<ExprOp<OpAttr | string>>; } & Partial<ExprOp<OpAttr | string>>;
export type OpSortAttr = Partial<{ export type OpSortAttr = Partial<{

View File

@ -9,14 +9,16 @@
"v": { "v": {
"type": { "type": {
"email": "邮箱", "email": "邮箱",
"sms": "短信", "sms": "手机号",
"password": "密码", "password": "密码",
"wechatMp": "小程序", "wechatMp": "小程序",
"wechatPublic": "公众号", "wechatPublic": "公众号",
"wechatWeb": "微信网站", "wechatWeb": "微信网站",
"wechatMpForWeb": "小程序授权网页", "wechatMpForWeb": "小程序授权网页",
"wechatPublicForWeb": "公众号授权网页", "wechatPublicForWeb": "公众号授权网页",
"wechatNative": "微信APP授权" "wechatNative": "微信APP授权",
"loginName": "账号",
"oauth": "OAuth授权"
} }
} }
} }

View File

@ -30,7 +30,7 @@ const triggers = [
return !!data.config; return !!data.config;
}, },
fn: async ({ operation }, context, option) => { fn: async ({ operation }, context, option) => {
const { filter } = operation; const { filter, data } = operation;
const applications = await context.select('application', { const applications = await context.select('application', {
data: { data: {
id: 1, id: 1,
@ -43,9 +43,8 @@ const triggers = [
let count = 0; let count = 0;
for (const application of applications) { for (const application of applications) {
if (application.type === 'web') { if (application.type === 'web') {
const { wechat } = application.config || {}; const { appId: newAppId, appSecret: newAppSecret } = data?.config?.wechat || {};
const { appId, appSecret } = wechat || {}; if (!newAppId || !newAppSecret) {
if (!(appId && appId !== '' && appSecret && appSecret !== '')) {
const [passport] = await context.select('passport', { const [passport] = await context.select('passport', {
data: { data: {
id: 1, id: 1,
@ -100,7 +99,7 @@ const triggers = [
for (const application of applications) { for (const application of applications) {
if (application.type === 'wechatPublic') { if (application.type === 'wechatPublic') {
const { appId, appSecret, isService } = application.config || {}; const { appId, appSecret, isService } = application.config || {};
if (appId && appId !== '') { if (appId) {
const [passport] = await context.select('passport', { const [passport] = await context.select('passport', {
data: { data: {
id: 1, id: 1,
@ -116,7 +115,7 @@ const triggers = [
count: 1, count: 1,
indexFrom: 0, indexFrom: 0,
}, { forUpdate: true }); }, { forUpdate: true });
if (appSecret && appSecret !== '' && isService) { if (appSecret && isService) {
if (!passport) { if (!passport) {
await context.operate('passport', { await context.operate('passport', {
id: await generateNewIdAsync(), id: await generateNewIdAsync(),
@ -154,7 +153,7 @@ const triggers = [
} }
else if (application.type === 'wechatMp') { else if (application.type === 'wechatMp') {
const { appId, appSecret, } = application.config || {}; const { appId, appSecret, } = application.config || {};
if (appId && appId !== '') { if (appId) {
const [passport] = await context.select('passport', { const [passport] = await context.select('passport', {
data: { data: {
id: 1, id: 1,
@ -169,7 +168,7 @@ const triggers = [
count: 1, count: 1,
indexFrom: 0, indexFrom: 0,
}, { forUpdate: true }); }, { forUpdate: true });
if (appSecret && appSecret !== '') { if (appSecret) {
if (!passport) { if (!passport) {
await context.operate('passport', { await context.operate('passport', {
id: await generateNewIdAsync(), id: await generateNewIdAsync(),
@ -265,6 +264,66 @@ const triggers = [
return count; 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删除', name: '删除application前将相关的passport删除',
entity: 'application', entity: 'application',

5
es/triggers/applicationPassport.d.ts vendored Normal file
View File

@ -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<EntityDict, 'applicationPassport', BRC<EntityDict>>[];
export default triggers;

View File

@ -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;

Some files were not shown because too many files have changed in this diff Show More