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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,3 +23,12 @@ export declare function updateUserPassword<ED extends EntityDict>(params: {
result: string;
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 { encryptPasswordSha1 } from '../utils/password';
import { assert } from 'oak-domain/lib/utils/assert';
@ -177,20 +177,17 @@ export async function updateUserPassword(params, context, innerLogic) {
const systemId = context.getSystemId();
const closeRootMode = context.openRootMode();
try {
const [passport] = await context.select('passport', {
const [system] = await context.select('system', {
data: {
id: 1,
type: 1,
config: 1,
systemId: 1,
},
filter: {
systemId,
type: 'password',
id: systemId,
}
}, { forUpdate: true });
assert(passport);
const config = passport.config;
assert(system);
const config = system.config?.Password;
const mode = config?.mode ?? 'all';
const [user] = await context.select('user', {
data: {
@ -257,13 +254,13 @@ export async function updateUserPassword(params, context, innerLogic) {
};
}
const allowUpdate = mode === 'sha1' ? user.passwordSha1 === prevPassword : user.password === prevPassword; //sha1密文模式判断密文是否相等
let userDate = {}, changeCreateDate = {};
let userData = {}, changeCreateData = {};
if (mode === 'all') {
userDate = {
userData = {
password: newPassword,
passwordSha1: encryptPasswordSha1(newPassword),
};
changeCreateDate = {
changeCreateData = {
prevPassword,
newPassword,
prevPasswordSha1: encryptPasswordSha1(prevPassword),
@ -271,19 +268,19 @@ export async function updateUserPassword(params, context, innerLogic) {
};
}
else if (mode === 'plain') {
userDate = {
userData = {
password: newPassword,
};
changeCreateDate = {
changeCreateData = {
prevPassword,
newPassword,
};
}
else if (mode === 'sha1') {
userDate = {
userData = {
passwordSha1: newPassword,
};
changeCreateDate = {
changeCreateData = {
prevPasswordSha1: prevPassword,
newPasswordSha1: newPassword,
};
@ -292,7 +289,7 @@ export async function updateUserPassword(params, context, innerLogic) {
await context.operate('user', {
id: await generateNewIdAsync(),
action: 'update',
data: userDate,
data: userData,
filter: {
id: userId,
},
@ -306,7 +303,7 @@ export async function updateUserPassword(params, context, innerLogic) {
id: await generateNewIdAsync(),
userId,
result: 'success',
...changeCreateDate,
...changeCreateData,
},
}, {
dontCollect: true,
@ -324,7 +321,7 @@ export async function updateUserPassword(params, context, innerLogic) {
id: await generateNewIdAsync(),
userId,
result: 'fail',
...changeCreateDate,
...changeCreateData,
},
}, {
dontCollect: true,
@ -353,13 +350,13 @@ export async function updateUserPassword(params, context, innerLogic) {
dontCollect: true,
});
if (aliveCaptcha) {
let userDate = {}, changeCreateDate = {};
let userData = {}, changeCreateData = {};
if (mode === 'all') {
userDate = {
userData = {
password: newPassword,
passwordSha1: encryptPasswordSha1(newPassword),
};
changeCreateDate = {
changeCreateData = {
prevPassword: user.password,
newPassword,
prevPasswordSha1: user.passwordSha1,
@ -367,19 +364,19 @@ export async function updateUserPassword(params, context, innerLogic) {
};
}
else if (mode === 'plain') {
userDate = {
userData = {
password: newPassword,
};
changeCreateDate = {
changeCreateData = {
prevPassword: user.password,
newPassword,
};
}
else if (mode === 'sha1') {
userDate = {
userData = {
passwordSha1: newPassword,
};
changeCreateDate = {
changeCreateData = {
prevPasswordSha1: user.passwordSha1,
newPasswordSha1: newPassword,
};
@ -387,7 +384,7 @@ export async function updateUserPassword(params, context, innerLogic) {
await context.operate('user', {
id: await generateNewIdAsync(),
action: 'update',
data: userDate,
data: userData,
filter: {
id: userId,
},
@ -401,7 +398,7 @@ export async function updateUserPassword(params, context, innerLogic) {
id: await generateNewIdAsync(),
userId,
result: 'success',
...changeCreateDate,
...changeCreateData,
},
}, {
dontCollect: true,
@ -428,3 +425,83 @@ export async function updateUserPassword(params, context, innerLogic) {
throw err;
}
}
/**
* 用户账号注册
* @param params
* @param context
*/
export async function registerUserByLoginName(params, context) {
const { loginName, password } = params;
const systemId = context.getSystemId();
const closeRootMode = context.openRootMode();
try {
// 检查loginName是否重复
const [existLoginName] = await context.select('loginName', {
data: {
id: 1,
name: 1,
},
filter: {
name: loginName,
ableState: 'enabled',
},
}, { dontCollect: true, forUpdate: true });
if (existLoginName) {
closeRootMode();
throw new OakPreConditionUnsetException('账号已存在,请重新设置');
}
// 创建user并附上密码级联创建loginName
const [system] = await context.select('system', {
data: {
id: 1,
config: 1,
},
filter: {
id: systemId,
}
}, { forUpdate: true });
assert(system);
const config = system.config?.Password;
const mode = config?.mode ?? 'all';
let passwordData = {};
if (mode === 'all') {
passwordData = {
password: password,
passwordSha1: encryptPasswordSha1(password),
};
}
else if (mode === 'plain') {
passwordData = {
password: password,
};
}
else if (mode === 'sha1') {
passwordData = {
passwordSha1: password,
};
}
const userData = {
id: await generateNewIdAsync(),
loginName$user: [
{
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
name: loginName,
}
}
]
};
Object.assign(userData, passwordData);
await context.operate('user', {
id: await generateNewIdAsync(),
action: 'create',
data: userData,
}, {});
}
catch (err) {
closeRootMode();
throw err;
}
}

View File

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

View File

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

View File

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

View File

@ -20,6 +20,10 @@ type TypeRecord = Record<string, {
checked?: boolean;
disabled: boolean;
disabledTip: string;
showPwd: boolean;
allowPwd?: boolean;
pwdDisabled?: boolean;
pwdDisabledTip?: string;
}>;
type PassportOption = {
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 : ''}>
<div>{option.data.label}</div>
</Tooltip>)} style={{ width: 140 }}/>
</Tooltip>) : (<Tooltip title={typeRecords[type].disabled ? typeRecords[type].disabledTip : ''}>
<Switch disabled={typeRecords[type].disabled} checkedChildren={<CheckOutlined />} unCheckedChildren={<CloseOutlined />} checked={!!typeRecords[type].checked} onChange={(checked) => {
</Tooltip>) : (<>
<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)) {
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);
}
}}/>
</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>
});

View File

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

View File

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

View File

@ -11,6 +11,6 @@ declare namespace Download {
var onDownload: (data: ArrayBuffer | ReadableStream, filename: string) => Promise<void>;
var base64ToBlob: (base64String: string) => Blob;
var arrayBufferToBase64: (buffer: Buffer) => string;
var base64ToArrayBuffer: (base64String: string) => ArrayBuffer;
var base64ToArrayBuffer: (base64String: string) => ArrayBufferLike;
}
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 Basic from './basic/index';
import Security from './security/index';
import Password from './password/index';
export default function Render(props) {
const { entity, name, currentConfig, dirty } = props.data;
const { resetConfig, updateConfig, setValue, setValues, removeItem, cleanKey, t } = props.methods;
const { Account: account, Cos: cos, Map: map, Live: live, Sms: sms, App: app, Emails: emails, Security: security, } = currentConfig || {};
const { Account: account, Cos: cos, Map: map, Live: live, Sms: sms, App: app, Emails: emails, Security: security, Password: password, } = currentConfig || {};
return (<>
<Affix offsetTop={64}>
<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>
</div>
</>);

View File

@ -26,13 +26,43 @@ export default OakComponent({
state: '',
},
lifetimes: {
ready() {
async ready() {
const searchParams = new URLSearchParams(window.location.search);
const clientId = searchParams.get('client_id') || '';
const responseType = searchParams.get('response_type') || '';
const redirectUri = searchParams.get('redirect_uri') || '';
const scope = searchParams.get('scope') || '';
const state = searchParams.get('state') || '';
//判断是否允许oauth登录
const application = this.features.application.getApplication();
const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id });
const oauthPassport = applicationPassports?.find((ele) => ele.passport?.type === 'oauth');
const oauthIds = oauthPassport?.config?.oauthIds;
let allowOauth = false;
if (clientId) {
const { data: [oauthProvider] } = await this.features.cache.refresh('oauthProvider', {
data: {
id: 1,
clientId: 1,
systemId: 1,
},
filter: {
clientId,
systemId: application.systemId,
}
});
if (oauthProvider?.id && oauthIds?.length > 0 && oauthIds.includes(oauthProvider?.id)) {
allowOauth = true;
}
}
if (!allowOauth) {
this.setState({
hasError: true,
errorMsg: 'oauth.login',
});
this.setState({ loading: false });
return;
}
this.setState({
client_id: clientId,
response_type: responseType,

View File

@ -16,6 +16,7 @@
"missing_client_id": "缺少 client_id 参数",
"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')}>
<div className={Styles.id}>
<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) => {
update({ name: v.target.value });
}}/>
</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
</Text>) : undefined}>
<Select mode="tags" placeholder={t('typePlaceholder')} onChange={(v) => {
@ -76,13 +76,13 @@ const Upsert = (props) => {
}}/>
</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) => {
update({ authorizationEndpoint: v.target.value });
}}/>
</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) => {
update({ tokenEndpoint: v.target.value });
}}/>
@ -106,13 +106,13 @@ const Upsert = (props) => {
}}/>
</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) => {
update({ clientId: v.target.value });
}}/>
</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) => {
update({ clientSecret: v.target.value });
}}/>
@ -124,7 +124,7 @@ const Upsert = (props) => {
}} tokenSeparators={[',']} open={false}/>
</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) => {
update({ redirectUri: v.target.value });
}}/>
@ -135,7 +135,7 @@ const Upsert = (props) => {
</Form.Item>
<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>

View File

@ -1,5 +1,5 @@
import React from "react";
import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig } from "../../../entities/Passport";
import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig, NameConfig, OAuthConfig } from "../../../entities/Passport";
import { EntityDict } from "../../../oak-app-domain";
import '@wangeditor/editor/dist/css/style.css';
export default function Email(props: {
@ -8,5 +8,5 @@ export default function Email(props: {
};
t: (k: string, params?: any) => string;
changeEnabled: (enabled: boolean) => void;
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig, path: string, value: any, type?: string) => void;
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig, path: string, value: any, type?: string) => void;
}): React.JSX.Element;

View File

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

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 { 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";
export default function Password(props: {
passport: EntityDict['passport']['OpSchema'] & {
@ -7,5 +7,5 @@ export default function Password(props: {
};
t: (k: string, params?: any) => string;
changeEnabled: (enabled: boolean) => void;
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig, path: string, value: any, type?: string) => void;
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig, path: string, value: any, type?: string) => void;
}): React.JSX.Element;

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { Switch, Form, Input, Space, Tag, InputNumber, Radio, } from 'antd';
import React, { useState } from "react";
import { Switch, Form, Input, Space, Tag, InputNumber, Radio, Tooltip, } from 'antd';
import Styles from './web.module.less';
import EditorRegexs from "./editorRegexs";
export default function Password(props) {
@ -11,29 +11,26 @@ export default function Password(props) {
const [regexs, setRegexs] = useState(config?.regexs || []);
const [tip, setTip] = useState(config?.tip || '');
const [mode, setMode] = useState(config?.mode || 'all');
useEffect(() => {
const newMin = config?.min || 8;
const newMax = config?.max || 24;
const newRegexs = config?.regexs || [];
const newTip = config?.tip || '';
const newMode = config?.mode || 'all';
if (min !== newMin)
setMin(newMin);
if (max !== newMax)
setMax(newMax);
if (JSON.stringify(regexs) !== JSON.stringify(newRegexs))
setRegexs(newRegexs);
if (tip !== newTip)
setTip(newTip);
if (mode !== newMode)
setMode(newMode);
}, [config?.min, config?.max, config?.regexs, config?.tip, config?.mode, config?.verify]);
// useEffect(() => {
// const newMin = config?.min || 8;
// const newMax = config?.max || 24;
// const newRegexs = config?.regexs || [];
// const newTip = config?.tip || '';
// const newMode = config?.mode || 'all';
// if (min !== newMin) setMin(newMin);
// if (max !== newMax) setMax(newMax);
// if (JSON.stringify(regexs) !== JSON.stringify(newRegexs)) setRegexs(newRegexs);
// 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}>
<div className={Styles.title}>
<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);
}}/>
}} disabled={!enabled}/>
</Tooltip>
</div>
{enabled &&
<div>

View File

@ -1,5 +1,5 @@
import React from "react";
import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig } from "../../../entities/Passport";
import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig, NameConfig, OAuthConfig } from "../../../entities/Passport";
import { EntityDict } from "../../../oak-app-domain";
export default function Sms(props: {
passport: EntityDict['passport']['OpSchema'] & {
@ -7,5 +7,5 @@ export default function Sms(props: {
};
t: (k: string, params?: any) => string;
changeEnabled: (enabled: boolean) => void;
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig, path: string, value: any, type?: string) => void;
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig, path: string, value: any, type?: string) => void;
}): React.JSX.Element;

View File

@ -1,16 +1,21 @@
import React from 'react';
import { WebComponentProps } from 'oak-frontend-base';
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, {
passports: (EntityDict['passport']['OpSchema'] & {
appIdStr: string;
stateColor: string;
hasQrCodePrefix: boolean;
})[];
systemId: 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: () => {
id: string;
type: EntityDict['passport']['Schema']['type'];

View File

@ -1,5 +1,5 @@
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 classNames from 'classnames';
import { ExclamationCircleFilled } from '@ant-design/icons';
@ -10,20 +10,24 @@ import WechatPublicForWeb from './wechatPublicForWeb';
import WechatMpForWeb from './wechatMpForWeb';
import WechatMp from './wechatMp';
import WechatPublic from './wechatPublic';
import LoginName from './loginName';
import Oauth from './oauth';
const { confirm } = Modal;
function AppView(props) {
const { passport, t, changeEnabled, updateConfig } = props;
const { id, type, config, enabled, stateColor } = passport;
return (<div className={classNames(Styles.item, Styles.title)}>
<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);
}}/>
</Tooltip>
</div>);
}
export default function render(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 [createOpen, setCreateOpen] = useState(false);
const [newType, setNewType] = useState(undefined);
@ -93,6 +97,7 @@ export default function render(props) {
<div>* 如需启用邮箱登录请先前往配置管理邮箱设置创建系统邮箱,并完成相关配置</div>
<div>* 如需启用小程序授权登录请先前往应用管理创建小程序application,并完成基础配置</div>
<div>* 如需启用公众号授权登录请先前往应用管理创建是服务号的公众号application,并完成基础配置</div>
<div>* 如需启用OAuth授权登录请先前往OAuth管理创建OAuth供应商,并启用</div>
</div>
</Row>
{passports && passports.map((passport) => {
@ -122,7 +127,7 @@ export default function render(props) {
}, passport.id);
}} updateConfig={updateConfig}/>);
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({
enabled,
}, passport.id);
@ -141,6 +146,18 @@ export default function render(props) {
enabled,
}, 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:
return (<AppView key={passport.id} passport={passport} t={t} changeEnabled={(enabled) => {
updateItem({

View File

@ -1,12 +1,13 @@
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";
export default function wechatMpForWeb(props: {
passport: EntityDict['passport']['OpSchema'] & {
stateColor: string;
};
appIdStr: string;
hasQrCodePrefix: boolean;
t: (k: string, params?: any) => string;
changeEnabled: (enabled: boolean) => void;
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig, path: string, value: any, type?: string) => void;
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig, path: string, value: any, type?: string) => void;
}): React.JSX.Element;

View File

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

View File

@ -1,5 +1,5 @@
import React from "react";
import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig } from "../../../entities/Passport";
import { EmailConfig, MfwConfig, PfwConfig, SmsConfig, PwdConfig, NameConfig, OAuthConfig } from "../../../entities/Passport";
import { EntityDict } from "../../../oak-app-domain";
export default function wechatPublicForWeb(props: {
passport: EntityDict['passport']['OpSchema'] & {
@ -8,5 +8,5 @@ export default function wechatPublicForWeb(props: {
appIdStr: string;
t: (k: string, params?: any) => string;
changeEnabled: (enabled: boolean) => void;
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig, path: string, value: any, type?: string) => void;
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig, path: string, value: any, type?: string) => void;
}): React.JSX.Element;

View File

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

View File

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

View File

@ -2,14 +2,13 @@
<oak-icon name="mobilephone" size="28" color="#808080" />
<l-input
hide-label="{{true}}"
placeholder="{{t('placeholder.Mobile')}}"
placeholder="{{t('placeholder.Email')}}"
clear="{{true}}"
showRow="{{false}}"
l-class="my-input"
style="flex:1;"
data-attr="mobile"
maxlength="11"
value="{{mobile}}"
data-attr="email"
value="{{email}}"
bind:lininput="inputChangeMp"
bind:linclear="inputChangeMp"
/>
@ -17,13 +16,13 @@
<view class="inputItem">
<l-input
hide-label="{{true}}"
placeholder="{{t('placeholder.Captcha')}}"
placeholder="输入{{digit}}位验证码"
clear="{{true}}"
showRow="{{false}}"
l-class="my-input"
width="380"
data-attr="captcha"
maxlength="4"
maxlength="{{digit}}"
value="{{captcha}}"
bind:lininput="inputChangeMp"
bind:linclear="inputChangeMp"
@ -37,7 +36,11 @@
{{t('Login')}}
</l-button>
<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: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 File

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

View File

@ -9,12 +9,12 @@ export default function Render(props) {
const { sendCaptcha, loginByEmail, t, inputChange } = methods;
return (<Form colon={true}>
<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);
}} className={Style['loginbox-input']}/>
</Form.Item>
<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);
}} className={Style['loginbox-input']} suffix={<Button size="small" type="link" disabled={!!disabled || !validEmail || counter > 0} onClick={() => sendCaptcha()}>
{counter > 0

View File

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

View File

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

View File

@ -6,6 +6,7 @@
"l-segment": "@oak-frontend-base/miniprogram_npm/lin-ui/segment/index",
"l-segment-item": "@oak-frontend-base/miniprogram_npm/lin-ui/segment-item/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}}"
class="login-body"
allowPassword="{{allowPassword}}"
allowEmail="{{allowEmail}}"
allowWechatMp="{{allowWechatMp}}"
setLoginMode="{{setLoginModeMp}}"
/>
@ -18,15 +19,33 @@
callback="{{callback}}"
class="login-body"
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}}"
setLoginMode="{{setLoginModeMp}}"
/>
</block>
<view wx:elif="{{loginMode === 'wechatMp'}}" class="login-body">
<l-button type="default" size="long" disabled="{{loading}}" bind:lintap="loginByWechatMp" style="width:100%">
授权登录
{{t('loginMode.wechatMp')}}
</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 File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@
"usingComponents": {
"l-button": "@oak-frontend-base/miniprogram_npm/lin-ui/button/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

@ -43,3 +43,13 @@
align-items: center;
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" />
<l-input
hide-label="{{true}}"
placeholder="{{t('placeholder.Account')}}"
placeholder="{{accountPlaceholder}}"
clear="{{true}}"
showRow="{{false}}"
l-class="my-input"
@ -29,11 +29,22 @@
bind:linclear="inputChangeMp"
/>
</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%">
{{t('Login')}}
</l-button>
<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: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 File

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

View File

@ -9,8 +9,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'token', fal
validMobile: boolean;
validPassword: boolean;
allowSubmit: boolean;
allowSms: boolean;
allowEmail: boolean;
accountPlaceholder: string;
}, {
loginByAccount: () => Promise<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';
export default function Render(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;
return (<Form colon={true}>
<Form.Item name="mobile">
<Input allowClear value={account} size="large"
// 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);
}} className={Style['loginbox-input']}/>
</Form.Item>

View File

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

View File

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

View File

@ -37,7 +37,12 @@
{{t('Login')}}
</l-button>
<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: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 File

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

View File

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

View File

@ -1,8 +1,8 @@
// @ts-nocheck
// Segmented这个对象在antd里的声明是错误的
import React, { useEffect, useState } from 'react';
import { Segmented, Divider, Space } from 'antd';
import { MobileOutlined, QrcodeOutlined, DesktopOutlined, MailOutlined, ExclamationCircleOutlined, } from '@ant-design/icons';
import { Button, Segmented, Divider, Space, Tooltip, Image } from 'antd';
import { MobileOutlined, QrcodeOutlined, DesktopOutlined, MailOutlined, ExclamationCircleOutlined, LinkOutlined, } from '@ant-design/icons';
import classNames from 'classnames';
import Style from './web.module.less';
import WeChatLoginQrCode from '../../common/weChatLoginQrCode';
@ -13,8 +13,8 @@ import PasswordLogin from './password';
import EmailLogin from './email';
export default function Render(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 { t, setLoginMode } = methods;
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;
let redirectUri2 = redirectUri;
if (!(redirectUri.startsWith('https') || redirectUri.startsWith('http'))) {
const hostname = domain || window.location.hostname;
@ -36,10 +36,11 @@ export default function Render(props) {
setShowInput(false);
}
}, [loginMode]);
const InputMethods = inputOptions && inputOptions.length > 0 ? (<div className={Style['loginbox-methods']}>
<Divider plain style={{ fontSize: 13, color: '#808080', }}>{t('otherMethods')}</Divider>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 24 }}>
{inputOptions.map((ele) => {
const InputMethods = inputOptions && inputOptions.length > 0 ? (
// <div className={Style['loginbox-methods']}>
// <Divider plain style={{ fontSize: 13, color: '#808080', }}>{t('otherMethods')}</Divider>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 24 }}>
{inputOptions.map((ele) => {
let icon = <></>;
if (ele.value === 'sms') {
icon = <MobileOutlined />;
@ -53,21 +54,36 @@ export default function Render(props) {
return (<Space key={ele.value} size={4} style={{ cursor: 'pointer' }} onClick={() => {
setLoginMode(ele.value);
}}>
{icon}
<div>{ele.label}</div>
</Space>);
{icon}
<div>{ele.label}</div>
</Space>);
})}
</div>
</div>) : <></>;
const ScanMethods = scanOptions && scanOptions.length > 0 ? (<div className={Style['loginbox-methods']}>
<Divider plain style={{ fontSize: 13, color: '#808080', }}>{t('otherMethods')}</Divider>
<Space style={{ cursor: 'pointer' }} onClick={() => {
</div>
// </div>
) : <></>;
const ScanMethods = scanOptions && scanOptions.length > 0 ? (
// <div className={Style['loginbox-methods']}>
// <Divider plain style={{ fontSize: 13, color: '#808080', }}>{t('otherMethods')}</Divider>
<Space style={{ cursor: 'pointer' }} onClick={() => {
setLoginMode(scanOptions[0].value);
}}>
<QrcodeOutlined />
<div>{t('scanLogin')}</div>
</Space>
</div>) : <></>;
<QrcodeOutlined />
<div>{t('scanLogin')}</div>
</Space>
// </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']}>
<Space>
<ExclamationCircleOutlined />
@ -97,8 +113,13 @@ export default function Render(props) {
<div className={Style['loginbox-password']} style={{
display: loginMode === 'password' ? 'block' : 'none',
}}>
<PasswordLogin disabled={disabled} url={url} callback={callback} allowSms={allowSms} allowEmail={allowEmail} pwdMode={pwdMode}/>
{Tip}
<PasswordLogin disabled={disabled} url={url} callback={callback} pwdAllowMobile={pwdAllowMobile} pwdAllowEmail={pwdAllowEmail} pwdAllowLoginName={pwdAllowLoginName} pwdMode={pwdMode}/>
{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 className={Style['loginbox-mobile']} style={{
display: loginMode === 'sms' ? 'block' : 'none',
@ -112,7 +133,13 @@ export default function Render(props) {
<EmailLogin disabled={disabled} url={url} callback={callback} digit={emailDigit}/>
{Tip}
</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' &&
<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}/>
{Tip}
</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>

View File

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

View File

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

View File

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

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: {
async ready() {
const application = this.features.application.getApplication();
const { result: applicationPassports } = await this.features.cache.exec('getApplicationPassports', { applicationId: application.id });
const passwordConfig = applicationPassports.find((ele) => ele.passport.type === 'password')?.passport.config;
const system = this.features.application.getApplication().system;
const passwordConfig = system?.config.Password;
const mode = passwordConfig?.mode ?? 'all';
const pwdMin = passwordConfig?.min ?? 8;
const pwdMax = passwordConfig?.max ?? 24;

View File

@ -41,6 +41,15 @@ export default OakComponent({
const protocol = window.location.protocol === 'https:' ? 'https' : 'http';
const redirectUri = encodeURIComponent(`${protocol}://${host}${wechatUserLoginPage}?wechatLoginId=${wechatLoginId}`);
window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`;
},
async loginByWechatMp() {
const { loginUserId } = this.state;
if (!loginUserId) {
// 先小程序登录
await this.features.token.loginWechatMp();
}
await this.features.token.loginWechatMp({ wechatLoginId: this.props.oakId });
this.refresh();
}
},
});

View File

@ -9,6 +9,8 @@
flex-direction: column;
background-color: @oak-bg-color-container;
box-sizing: border-box;
align-items: center;
justify-content: center;
.safe-area-inset-bottom();
}
@ -19,19 +21,6 @@
margin-bottom: 60rpx;
}
.circle-view {
margin-top: 30rpx;
padding: 10rpx;
width: 200rpx;
height: 200rpx;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fff;
}
.title {
font-size: 32rpx;
color: @oak-text-color-primary;
@ -42,3 +31,22 @@
font-size: 28rpx;
color: @oak-text-color-secondary;
}
.circle-view {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.text {
font-size: 36rpx;
color: @oak-text-color-primary;
margin-top: 16rpx;
}
.desc {
font-size: 24rpx;
color: @oak-text-color-secondary;
margin-top: 16rpx;
}

View File

@ -1,4 +1,30 @@
<!-- index.wxml -->
<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 File

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

View File

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

View File

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

View File

@ -266,7 +266,8 @@ const i18ns = [
"missing_client_id": "缺少 client_id 参数",
"unknown": "未知错误,请稍后重试"
}
}
},
"login": "当前暂未支持该第三方应用授权登录"
}
}
},
@ -665,10 +666,15 @@ const i18ns = [
"Login": "登录",
"Send": "发送验证码",
"placeholder": {
"Captcha": "输入4位验证码",
"Captcha": "输入%{digit}位验证码",
"Email": "请输入邮箱"
},
"resendAfter": "秒后可重发"
"resendAfter": "秒后可重发",
"loginMode": {
"wechatMp": "一键登录",
"sms": "短信登录",
"password": "密码登录"
}
}
},
{
@ -692,7 +698,12 @@ const i18ns = [
"resendAfter": "秒后可重发",
"otherMethods": "其他登录方式",
"scanLogin": "扫码登录",
"tip": "未注册用户首次登录将自动注册"
"tip": "未注册用户首次登录将自动注册",
"goRegister": "去注册",
"loginMode": {
"wechatMp": "授权登录",
"other": "其他方式登录"
}
}
},
{
@ -704,10 +715,17 @@ const i18ns = [
data: {
"Login": "登录",
"placeholder": {
"Account": "请输入账号",
"Mobile": "/手机号",
"Email": "/邮箱",
"Account": "请输入",
"LoginName": "账号",
"Mobile": "手机号",
"Email": "邮箱",
"Password": "请输入密码"
},
"register": "去注册",
"loginMode": {
"wechatMp": "一键登录",
"sms": "短信登录",
"email": "邮箱登录"
}
}
},
@ -724,7 +742,12 @@ const i18ns = [
"Captcha": "输入%{digit}位短信验证码",
"Mobile": "请输入手机号"
},
"resendAfter": "秒后可重发"
"resendAfter": "秒后可重发",
"loginMode": {
"wechatMp": "一键登录",
"password": "密码登录",
"email": "邮箱登录"
}
}
},
{
@ -786,6 +809,40 @@ const i18ns = [
}
}
},
{
id: "36c643dbcc19c3258f6077a6684236ff",
namespace: "oak-general-business-c-user-register",
language: "zh-CN",
module: "oak-general-business",
position: "src/components/user/register",
data: {
"not allow register": "暂未支持自行注册,请联系管理员为您分配账号!",
"registerTitle": "账号注册",
"label": {
"loginName": "账号",
"password": "密码",
"rePwd": "密码确认"
},
"placeholder": {
"loginName": "请输入账号",
"password": "请输入密码",
"rePwd": "请再次输入密码"
},
"validator": {
"loginNameMin": "账号最短长度为%{loginNameMin}位",
"loginNameMax": "账号最大长度为%{loginNameMax}位",
"loginNameVerify": "当前账号未符合规范",
"pwdMin": "密码最短长度为%{pwdMin}位",
"pwdMax": "密码最短长度为%{pwdMax}位",
"pwdVerify": "当前密码较弱",
"pwdDiff": "两次输入的密码不一致,请检查",
"noRepwd": "请再次确认密码"
},
"register": "立即注册",
"goLogin": "已有账号?立即登录",
"success": "注册成功,请前往登录页登录"
}
},
{
id: "5bf96a3e054b8d73c76d7bb45ea90a80",
namespace: "oak-general-business-c-userEntityGrant-claim",

View File

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

View File

@ -7,6 +7,7 @@ export const entityDesc = {
application: '应用',
passport: '登录方式',
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 { Schema as System } from './System';
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
export type Type = 'password' | 'sms' | 'email' | 'wechatWeb' | 'wechatMp' | 'wechatPublic' | 'wechatPublicForWeb' | 'wechatMpForWeb' | 'wechatNative';
export type Type = 'password' | 'sms' | 'email' | 'wechatWeb' | 'wechatMp' | 'wechatPublic' | 'wechatPublicForWeb' | 'wechatMpForWeb' | 'wechatNative' | 'loginName' | 'oauth';
export type SmsConfig = {
mockSend?: boolean;
defaultOrigin?: 'ali' | 'tencent' | 'ctyun';
@ -34,10 +34,21 @@ export type PwdConfig = {
regexs?: string[];
tip?: string;
};
export type NameConfig = {
min?: number;
max?: number;
verify?: boolean;
regexs?: string[];
register?: boolean;
tip?: string;
};
export type OAuthConfig = {
oauthIds: string[];
};
export interface Schema extends EntityShape {
system: System;
type: Type;
config?: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig;
config?: SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig;
enabled: Boolean;
}
export declare const entityDesc: EntityDesc<Schema, '', '', {

View File

@ -12,14 +12,16 @@ export const entityDesc = {
v: {
type: {
email: '邮箱',
sms: '短信',
sms: '手机号',
password: '密码',
wechatMp: '小程序',
wechatPublic: '公众号',
wechatWeb: '微信网站',
wechatMpForWeb: '小程序授权网页',
wechatPublicForWeb: '公众号授权网页',
wechatNative: '微信APP授权'
wechatNative: '微信APP授权',
loginName: '账号',
oauth: 'OAuth授权'
},
},
},
@ -35,7 +37,9 @@ export const entityDesc = {
wechatMp: '#ADDCCA',
wechatMpForWeb: '#FDC454',
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?: {
wechatLoginId?: string;
}): Promise<void>;
loginWechatMp(): Promise<void>;
loginWechatMp(params?: {
wechatLoginId?: string;
}): Promise<void>;
loginWechatNative(code: string): Promise<void>;
syncUserInfoWechatMp(): Promise<void>;
logout(dontPublish?: boolean): Promise<void>;

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ export const desc = {
type: {
notNull: true,
type: "enum",
enumeration: ["password", "sms", "email", "wechatWeb", "wechatMp", "wechatPublic", "wechatPublicForWeb", "wechatMpForWeb", "wechatNative"]
enumeration: ["password", "sms", "email", "wechatWeb", "wechatMp", "wechatPublic", "wechatPublicForWeb", "wechatMpForWeb", "wechatNative", "loginName", "oauth"]
},
config: {
type: "object"

View File

@ -9,7 +9,9 @@ export const style = {
wechatMp: '#ADDCCA',
wechatMpForWeb: '#FDC454',
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 { GenericAction } from "oak-domain/lib/actions/action";
import { Boolean } from "oak-domain/lib/types/DataType";
export type Type = "password" | "sms" | "email" | "wechatWeb" | "wechatMp" | "wechatPublic" | "wechatPublicForWeb" | "wechatMpForWeb" | "wechatNative";
export type Type = "password" | "sms" | "email" | "wechatWeb" | "wechatMp" | "wechatPublic" | "wechatPublicForWeb" | "wechatMpForWeb" | "wechatNative" | "loginName" | "oauth";
export type SmsConfig = {
mockSend?: boolean;
defaultOrigin?: "ali" | "tencent" | "ctyun";
@ -35,10 +35,21 @@ export type PwdConfig = {
regexs?: string[];
tip?: string;
};
export type NameConfig = {
min?: number;
max?: number;
verify?: boolean;
regexs?: string[];
register?: boolean;
tip?: string;
};
export type OAuthConfig = {
oauthIds: string[];
};
export type OpSchema = EntityShape & {
systemId: ForeignKey<"system">;
type: Type;
config?: (SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig) | null;
config?: (SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig) | null;
enabled: Boolean;
} & {
[A in ExpressionKey]?: any;
@ -51,7 +62,7 @@ export type OpFilter = {
$$updateAt$$: Q_DateValue;
systemId: Q_StringValue;
type: Q_EnumValue<Type>;
config: JsonFilter<SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig>;
config: JsonFilter<SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig>;
enabled: Q_BooleanValue;
} & ExprOp<OpAttr | string>;
export type OpProjection = {
@ -63,7 +74,7 @@ export type OpProjection = {
$$seq$$?: number;
systemId?: number;
type?: number;
config?: number | JsonProjection<SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig>;
config?: number | JsonProjection<SmsConfig | EmailConfig | PfwConfig | MfwConfig | PwdConfig | NameConfig | OAuthConfig>;
enabled?: number;
} & Partial<ExprOp<OpAttr | string>>;
export type OpSortAttr = Partial<{

View File

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

View File

@ -30,7 +30,7 @@ const triggers = [
return !!data.config;
},
fn: async ({ operation }, context, option) => {
const { filter } = operation;
const { filter, data } = operation;
const applications = await context.select('application', {
data: {
id: 1,
@ -43,9 +43,8 @@ const triggers = [
let count = 0;
for (const application of applications) {
if (application.type === 'web') {
const { wechat } = application.config || {};
const { appId, appSecret } = wechat || {};
if (!(appId && appId !== '' && appSecret && appSecret !== '')) {
const { appId: newAppId, appSecret: newAppSecret } = data?.config?.wechat || {};
if (!newAppId || !newAppSecret) {
const [passport] = await context.select('passport', {
data: {
id: 1,
@ -100,7 +99,7 @@ const triggers = [
for (const application of applications) {
if (application.type === 'wechatPublic') {
const { appId, appSecret, isService } = application.config || {};
if (appId && appId !== '') {
if (appId) {
const [passport] = await context.select('passport', {
data: {
id: 1,
@ -116,7 +115,7 @@ const triggers = [
count: 1,
indexFrom: 0,
}, { forUpdate: true });
if (appSecret && appSecret !== '' && isService) {
if (appSecret && isService) {
if (!passport) {
await context.operate('passport', {
id: await generateNewIdAsync(),
@ -154,7 +153,7 @@ const triggers = [
}
else if (application.type === 'wechatMp') {
const { appId, appSecret, } = application.config || {};
if (appId && appId !== '') {
if (appId) {
const [passport] = await context.select('passport', {
data: {
id: 1,
@ -169,7 +168,7 @@ const triggers = [
count: 1,
indexFrom: 0,
}, { forUpdate: true });
if (appSecret && appSecret !== '') {
if (appSecret) {
if (!passport) {
await context.operate('passport', {
id: await generateNewIdAsync(),
@ -265,6 +264,66 @@ const triggers = [
return count;
}
},
{
name: 'wechatMp applicaiton清空普通链接二维码规则配置时将相应的passport禁用',
entity: 'application',
action: 'update',
when: 'after',
check: (operation) => {
const { data } = operation;
return !!data.config;
},
fn: async ({ operation }, context, option) => {
const { filter } = operation;
const applications = await context.select('application', {
data: {
id: 1,
config: 1,
type: 1,
systemId: 1,
},
filter,
}, {});
let count = 0;
for (const application of applications) {
if (application.type === 'wechatMp') {
const { qrCodePrefix, appId } = application.config || {};
if (appId && !qrCodePrefix) {
const [passport] = await context.select('passport', {
data: {
id: 1,
},
filter: {
enabled: true,
systemId: application.systemId,
type: 'wechatMpForWeb',
config: {
appId,
}
},
count: 1,
indexFrom: 0,
}, { forUpdate: true });
if (passport) {
await context.operate('passport', {
id: await generateNewIdAsync(),
action: 'update',
data: {
enabled: false,
config: {},
},
filter: {
id: passport.id,
}
}, option);
count++;
}
}
}
}
return count;
}
},
{
name: '删除application前将相关的passport删除',
entity: 'application',

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