merge passport
This commit is contained in:
commit
70601c5a88
|
|
@ -17,9 +17,19 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
env: WechatMpEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
loginByMobile: (params: {
|
||||
captcha?: string;
|
||||
password?: string;
|
||||
mobile: string;
|
||||
captcha: string;
|
||||
disableRegister?: boolean;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
loginByAccount: (params: {
|
||||
account: string;
|
||||
password: string;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
loginByEmail: (params: {
|
||||
email: string;
|
||||
captcha: string;
|
||||
disableRegister?: boolean;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
|
|
@ -50,11 +60,16 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
tokenValue: string;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
sendCaptcha: (params: {
|
||||
sendCaptchaByMobile: (params: {
|
||||
mobile: string;
|
||||
env: WechatMpEnv | WebEnv;
|
||||
type: 'login' | 'changePassword' | 'confirm';
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
sendCaptchaByEmail: (params: {
|
||||
email: string;
|
||||
env: WechatMpEnv | WebEnv;
|
||||
type: 'login' | 'changePassword' | 'confirm';
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
getApplication: (params: {
|
||||
type: AppType;
|
||||
domain: string;
|
||||
|
|
@ -247,5 +262,11 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
systemId: string;
|
||||
origin: EntityDict['smsTemplate']['Schema']['origin'];
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<void>;
|
||||
getApplicationPassports: (params: {
|
||||
applicationId: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<EntityDict['applicationPassport']['Schema'][]>;
|
||||
removeApplicationPassportsByPIds: (params: {
|
||||
passportIds: string[];
|
||||
}, content: BackendRuntimeContext<ED>) => Promise<void>;
|
||||
};
|
||||
export default AspectDict;
|
||||
|
|
|
|||
|
|
@ -4,64 +4,8 @@ import WechatSDK from 'oak-external-sdk/lib/WechatSDK';
|
|||
import fs from 'fs';
|
||||
import { cloneDeep } from 'oak-domain/lib/utils/lodash';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
export async function getApplication(params, context) {
|
||||
const { type, domain, data, appId } = params;
|
||||
// const [application] = await context.select(
|
||||
// 'application',
|
||||
// {
|
||||
// data: cloneDeep(applicationProjection),
|
||||
// filter: {
|
||||
// type,
|
||||
// system: {
|
||||
// domain$system: {
|
||||
// url: domain,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {}
|
||||
// );
|
||||
// //微信小程序环境下 没有就报错
|
||||
// if (type === 'wechatMp') {
|
||||
// assert(
|
||||
// application,
|
||||
// '微信小程序环境下 application必须存在小程序相关配置'
|
||||
// );
|
||||
// } else if (type === 'native') {
|
||||
// assert(application, 'APP环境下 application必须存在APP相关配置');
|
||||
// } else {
|
||||
// //web 或 wechatPublic
|
||||
// if (type === 'wechatPublic') {
|
||||
// // 如果微信公众号环境下 application不存在公众号配置,但又在公众号访问,这时可以使用web的application
|
||||
// if (!application) {
|
||||
// const [application2] = await context.select(
|
||||
// 'application',
|
||||
// {
|
||||
// data: cloneDeep(applicationProjection),
|
||||
// filter: {
|
||||
// type: 'web',
|
||||
// system: {
|
||||
// domain$system: {
|
||||
// url: domain,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {}
|
||||
// );
|
||||
// assert(
|
||||
// application2,
|
||||
// '微信公众号环境下 application不存在公众号配置,但必须存在web相关配置'
|
||||
// );
|
||||
// return application2.id as string;
|
||||
// }
|
||||
// } else {
|
||||
// assert(application, 'web环境下 application必须存在web相关配置');
|
||||
// }
|
||||
// }
|
||||
// return application.id as string;
|
||||
// 先找指定domain的应用,如果不存在再找系统下面的domain, 但无论怎么样都必须一项
|
||||
//
|
||||
async function getApplicationByDomain(context, options) {
|
||||
const { data, type, domain } = options;
|
||||
let applications = await context.select('application', {
|
||||
data,
|
||||
filter: {
|
||||
|
|
@ -76,7 +20,7 @@ export async function getApplication(params, context) {
|
|||
},
|
||||
},
|
||||
}, {});
|
||||
assert(applications.length <= 1, `指定域名的应用 只能存在一项或未指定`);
|
||||
assert(applications.length <= 1, `应用指定域名(domainId)只能存在一项或未指定`);
|
||||
if (applications.length === 0) {
|
||||
applications = await context.select('application', {
|
||||
data: data,
|
||||
|
|
@ -93,6 +37,16 @@ export async function getApplication(params, context) {
|
|||
},
|
||||
}, {});
|
||||
}
|
||||
return applications;
|
||||
}
|
||||
export async function getApplication(params, context) {
|
||||
const { type, domain, data, appId } = params;
|
||||
// 先找指定domain的应用,如果不存在再找系统下面的domain, 但无论怎么样都必须一项
|
||||
const applications = await getApplicationByDomain(context, {
|
||||
type,
|
||||
domain,
|
||||
data,
|
||||
});
|
||||
switch (type) {
|
||||
case 'wechatMp': {
|
||||
assert(applications.length === 1, `微信小程序环境下,同一个系统必须存在唯一的【${type}】应用`);
|
||||
|
|
@ -107,39 +61,13 @@ export async function getApplication(params, context) {
|
|||
case 'wechatPublic': {
|
||||
// 微信公众号环境下,未配置公众号,可以使用web的application
|
||||
if (applications.length === 0) {
|
||||
let applications2 = await context.select('application', {
|
||||
const webApplications = await getApplicationByDomain(context, {
|
||||
type: 'web',
|
||||
domain,
|
||||
data,
|
||||
filter: {
|
||||
type: 'web',
|
||||
system: {
|
||||
domain$system: {
|
||||
url: domain,
|
||||
},
|
||||
},
|
||||
domain: {
|
||||
url: domain,
|
||||
},
|
||||
},
|
||||
}, {});
|
||||
assert(applications2.length <= 1, `指定域名的应用 只能存在一项或未指定`);
|
||||
if (applications2.length === 0) {
|
||||
applications2 = await context.select('application', {
|
||||
data,
|
||||
filter: {
|
||||
type: 'web',
|
||||
system: {
|
||||
domain$system: {
|
||||
url: domain,
|
||||
},
|
||||
},
|
||||
domainId: {
|
||||
$exists: false,
|
||||
},
|
||||
},
|
||||
}, {});
|
||||
}
|
||||
assert(applications2.length === 1, '微信公众号环境下, 可以未配置公众号,但必须存在web的application');
|
||||
const application = applications2[0];
|
||||
});
|
||||
assert(webApplications.length === 1, '微信公众号环境下, 可以未配置公众号,但必须存在web的application');
|
||||
const application = webApplications[0];
|
||||
return application.id;
|
||||
}
|
||||
assert(applications.length === 1, `微信公众号环境下,同一个系统必须存在唯一的【${type}】应用 或 多个${type}应用必须配置域名`);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import { EntityDict } from "../oak-app-domain";
|
||||
import { BRC } from '../types/RuntimeCxt';
|
||||
export declare function getApplicationPassports<ED extends EntityDict>(params: {
|
||||
applicationId: string;
|
||||
}, context: BRC<ED>): Promise<Partial<ED["applicationPassport"]["Schema"]>[]>;
|
||||
export declare function removeApplicationPassportsByPIds<ED extends EntityDict>(params: {
|
||||
passportIds: string[];
|
||||
}, context: BRC<ED>): Promise<void>;
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
|
||||
export async function getApplicationPassports(params, context) {
|
||||
const { applicationId } = params;
|
||||
const closeRoot = context.openRootMode();
|
||||
const applicationPassports = await context.select('applicationPassport', {
|
||||
data: {
|
||||
id: 1,
|
||||
passportId: 1,
|
||||
passport: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
config: 1,
|
||||
},
|
||||
isDefault: 1,
|
||||
},
|
||||
filter: {
|
||||
applicationId,
|
||||
}
|
||||
}, {});
|
||||
closeRoot();
|
||||
return applicationPassports;
|
||||
}
|
||||
export async function removeApplicationPassportsByPIds(params, context) {
|
||||
const { passportIds } = params;
|
||||
const applicationPassports = await context.select('applicationPassport', {
|
||||
data: {
|
||||
id: 1,
|
||||
passportId: 1,
|
||||
},
|
||||
filter: {
|
||||
passportId: {
|
||||
$in: passportIds,
|
||||
},
|
||||
}
|
||||
}, {});
|
||||
if (applicationPassports && applicationPassports.length) {
|
||||
const ids = applicationPassports.map((ele) => ele.id);
|
||||
await context.operate('applicationPassport', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'remove',
|
||||
data: {},
|
||||
filter: {
|
||||
id: {
|
||||
$in: ids,
|
||||
}
|
||||
},
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { loginByMobile, loginWechat, loginWechatMp, syncUserInfoWechatMp, sendCaptcha, switchTo, refreshWechatPublicUserInfo, getWechatMpUserPhoneNumber, logout, loginByWechat, wakeupParasite, refreshToken } from './token';
|
||||
import { loginByAccount, loginByEmail, loginByMobile, loginWechat, loginWechatMp, syncUserInfoWechatMp, sendCaptchaByMobile, sendCaptchaByEmail, switchTo, refreshWechatPublicUserInfo, getWechatMpUserPhoneNumber, logout, loginByWechat, wakeupParasite, refreshToken } from './token';
|
||||
import { getInfoByUrl } from './extraFile';
|
||||
import { getApplication, signatureJsSDK, uploadWechatMedia, batchGetArticle, getArticle, batchGetMaterialList, getMaterial, deleteMaterial } from './application';
|
||||
import { updateConfig, updateApplicationConfig, updateStyle } from './config';
|
||||
|
|
@ -13,7 +13,10 @@ import { getCurrentMenu, getMenu, createMenu, createConditionalMenu, deleteCondi
|
|||
import { createTag, getTags, editTag, deleteTag, syncTag, oneKeySync } from './wechatPublicTag';
|
||||
import { getTagUsers, batchtagging, batchuntagging, getUserTags, getUsers, tagging, syncToLocale, syncToWechat } from './userWechatPublicTag';
|
||||
import { wechatMpJump } from './wechatMpJump';
|
||||
import { getApplicationPassports, removeApplicationPassportsByPIds } from './applicationPassport';
|
||||
declare const aspectDict: {
|
||||
loginByAccount: typeof loginByAccount;
|
||||
loginByEmail: typeof loginByEmail;
|
||||
mergeUser: typeof mergeUser;
|
||||
switchTo: typeof switchTo;
|
||||
refreshWechatPublicUserInfo: typeof refreshWechatPublicUserInfo;
|
||||
|
|
@ -23,7 +26,8 @@ declare const aspectDict: {
|
|||
wakeupParasite: typeof wakeupParasite;
|
||||
refreshToken: typeof refreshToken;
|
||||
syncUserInfoWechatMp: typeof syncUserInfoWechatMp;
|
||||
sendCaptcha: typeof sendCaptcha;
|
||||
sendCaptchaByMobile: typeof sendCaptchaByMobile;
|
||||
sendCaptchaByEmail: typeof sendCaptchaByEmail;
|
||||
getApplication: typeof getApplication;
|
||||
updateConfig: typeof updateConfig;
|
||||
updateStyle: typeof updateStyle;
|
||||
|
|
@ -69,6 +73,8 @@ declare const aspectDict: {
|
|||
syncToWechat: typeof syncToWechat;
|
||||
wechatMpJump: typeof wechatMpJump;
|
||||
syncSmsTemplate: typeof syncSmsTemplate;
|
||||
getApplicationPassports: typeof getApplicationPassports;
|
||||
removeApplicationPassportsByPIds: typeof removeApplicationPassportsByPIds;
|
||||
};
|
||||
export default aspectDict;
|
||||
export { AspectDict } from './AspectDict';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { loginByMobile, loginWechat, loginWechatMp, syncUserInfoWechatMp, sendCaptcha, switchTo, refreshWechatPublicUserInfo, getWechatMpUserPhoneNumber, logout, loginByWechat, wakeupParasite, refreshToken, } from './token';
|
||||
import { loginByAccount, loginByEmail, loginByMobile, loginWechat, loginWechatMp, syncUserInfoWechatMp, sendCaptchaByMobile, sendCaptchaByEmail, switchTo, refreshWechatPublicUserInfo, getWechatMpUserPhoneNumber, logout, loginByWechat, wakeupParasite, refreshToken, } from './token';
|
||||
import { getInfoByUrl } from './extraFile';
|
||||
import { getApplication, signatureJsSDK, uploadWechatMedia, batchGetArticle, getArticle, batchGetMaterialList, getMaterial, deleteMaterial, } from './application';
|
||||
import { updateConfig, updateApplicationConfig, updateStyle } from './config';
|
||||
|
|
@ -13,7 +13,10 @@ import { getCurrentMenu, getMenu, createMenu, createConditionalMenu, deleteCondi
|
|||
import { createTag, getTags, editTag, deleteTag, syncTag, oneKeySync, } from './wechatPublicTag';
|
||||
import { getTagUsers, batchtagging, batchuntagging, getUserTags, getUsers, tagging, syncToLocale, syncToWechat, } from './userWechatPublicTag';
|
||||
import { wechatMpJump, } from './wechatMpJump';
|
||||
import { getApplicationPassports, removeApplicationPassportsByPIds } from './applicationPassport';
|
||||
const aspectDict = {
|
||||
loginByAccount,
|
||||
loginByEmail,
|
||||
mergeUser,
|
||||
switchTo,
|
||||
refreshWechatPublicUserInfo,
|
||||
|
|
@ -23,7 +26,8 @@ const aspectDict = {
|
|||
wakeupParasite,
|
||||
refreshToken,
|
||||
syncUserInfoWechatMp,
|
||||
sendCaptcha,
|
||||
sendCaptchaByMobile,
|
||||
sendCaptchaByEmail,
|
||||
getApplication,
|
||||
updateConfig,
|
||||
updateStyle,
|
||||
|
|
@ -68,6 +72,8 @@ const aspectDict = {
|
|||
syncToLocale,
|
||||
syncToWechat,
|
||||
wechatMpJump,
|
||||
syncSmsTemplate
|
||||
syncSmsTemplate,
|
||||
getApplicationPassports,
|
||||
removeApplicationPassportsByPIds,
|
||||
};
|
||||
export default aspectDict;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,19 @@ import { EntityDict } from '../oak-app-domain';
|
|||
import { NativeEnv, WebEnv, WechatMpEnv } from 'oak-domain/lib/types/Environment';
|
||||
import { BRC } from '../types/RuntimeCxt';
|
||||
export declare function loginByMobile<ED extends EntityDict>(params: {
|
||||
captcha?: string;
|
||||
password?: string;
|
||||
mobile: string;
|
||||
captcha: string;
|
||||
disableRegister?: boolean;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BRC<ED>): Promise<string>;
|
||||
export declare function loginByAccount<ED extends EntityDict>(params: {
|
||||
account: string;
|
||||
password: string;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BRC<ED>): Promise<string>;
|
||||
export declare function loginByEmail<ED extends EntityDict>(params: {
|
||||
email: string;
|
||||
captcha: string;
|
||||
disableRegister?: boolean;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BRC<ED>): Promise<string>;
|
||||
|
|
@ -45,11 +55,16 @@ export declare function syncUserInfoWechatMp<ED extends EntityDict>({ nickname,
|
|||
iv: string;
|
||||
signature: string;
|
||||
}, context: BRC<ED>): Promise<void>;
|
||||
export declare function sendCaptcha<ED extends EntityDict>({ mobile, env, type: type2, }: {
|
||||
export declare function sendCaptchaByMobile<ED extends EntityDict>({ mobile, env, type: type2, }: {
|
||||
mobile: string;
|
||||
env: WechatMpEnv | WebEnv | NativeEnv;
|
||||
type: 'login' | 'changePassword' | 'confirm';
|
||||
}, context: BRC<ED>): Promise<string>;
|
||||
export declare function sendCaptchaByEmail<ED extends EntityDict>({ email, env, type: type2, }: {
|
||||
email: string;
|
||||
env: WechatMpEnv | WebEnv | NativeEnv;
|
||||
type: 'login' | 'changePassword' | 'confirm';
|
||||
}, context: BRC<ED>): Promise<string>;
|
||||
export declare function switchTo<ED extends EntityDict>({ userId }: {
|
||||
userId: string;
|
||||
}, context: BRC<ED>): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { tokenProjection } from '../types/Projection';
|
|||
import { sendSms } from '../utils/sms';
|
||||
import { mergeUser } from './user';
|
||||
import { cloneDeep } from 'oak-domain/lib/utils/lodash';
|
||||
import { sendEmail } from '../utils/email';
|
||||
async function makeDistinguishException(userId, context, message) {
|
||||
const [user] = await context.select('user', {
|
||||
data: {
|
||||
|
|
@ -423,88 +424,40 @@ async function loadTokenInfo(tokenValue, context) {
|
|||
}, {});
|
||||
}
|
||||
export async function loginByMobile(params, context) {
|
||||
const { mobile, captcha, password, env, disableRegister } = params;
|
||||
const { mobile, captcha, env, disableRegister } = params;
|
||||
const loginLogic = async () => {
|
||||
const systemId = context.getSystemId();
|
||||
if (captcha) {
|
||||
const result = await context.select('captcha', {
|
||||
data: {
|
||||
id: 1,
|
||||
expired: 1,
|
||||
},
|
||||
filter: {
|
||||
mobile,
|
||||
code: captcha,
|
||||
},
|
||||
sorter: [
|
||||
{
|
||||
$attr: {
|
||||
$$createAt$$: 1,
|
||||
},
|
||||
$direction: 'desc',
|
||||
const result = await context.select('captcha', {
|
||||
data: {
|
||||
id: 1,
|
||||
expired: 1,
|
||||
},
|
||||
filter: {
|
||||
origin: 'mobile',
|
||||
content: mobile,
|
||||
code: captcha,
|
||||
},
|
||||
sorter: [
|
||||
{
|
||||
$attr: {
|
||||
$$createAt$$: 1,
|
||||
},
|
||||
],
|
||||
indexFrom: 0,
|
||||
count: 1,
|
||||
}, { dontCollect: true });
|
||||
if (result.length > 0) {
|
||||
const [captchaRow] = result;
|
||||
if (captchaRow.expired) {
|
||||
throw new OakUserException('验证码已经过期');
|
||||
}
|
||||
// 到这里说明验证码已经通过
|
||||
return await setupMobile(mobile, env, context);
|
||||
}
|
||||
else {
|
||||
throw new OakUserException('验证码无效');
|
||||
$direction: 'desc',
|
||||
},
|
||||
],
|
||||
indexFrom: 0,
|
||||
count: 1,
|
||||
}, { dontCollect: true });
|
||||
if (result.length > 0) {
|
||||
const [captchaRow] = result;
|
||||
if (captchaRow.expired) {
|
||||
throw new OakUserException('验证码已经过期');
|
||||
}
|
||||
// 到这里说明验证码已经通过
|
||||
return await setupMobile(mobile, env, context);
|
||||
}
|
||||
else {
|
||||
assert(password);
|
||||
const result = await context.select('mobile', {
|
||||
data: {
|
||||
id: 1,
|
||||
userId: 1,
|
||||
ableState: 1,
|
||||
},
|
||||
filter: {
|
||||
mobile: mobile,
|
||||
user: {
|
||||
$or: [
|
||||
{
|
||||
password,
|
||||
},
|
||||
{
|
||||
passwordSha1: encryptPasswordSha1(password),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}, {
|
||||
dontCollect: true,
|
||||
});
|
||||
switch (result.length) {
|
||||
case 0: {
|
||||
throw new OakUserException('用户名与密码不匹配');
|
||||
}
|
||||
case 1: {
|
||||
const [mobileRow] = result;
|
||||
const { ableState, userId } = mobileRow;
|
||||
if (ableState === 'disabled') {
|
||||
// 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录
|
||||
const exception = await tryMakeChangeLoginWay(userId, context);
|
||||
if (exception) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
return await setupMobile(mobile, env, context);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`手机号和密码匹配出现雷同,mobile id是[${result
|
||||
.map((ele) => ele.id)
|
||||
.join(',')}], mobile是${mobile}`);
|
||||
}
|
||||
}
|
||||
throw new OakUserException('验证码无效');
|
||||
}
|
||||
};
|
||||
const closeRootMode = context.openRootMode();
|
||||
|
|
@ -529,6 +482,310 @@ export async function loginByMobile(params, context) {
|
|||
closeRootMode();
|
||||
return tokenValue;
|
||||
}
|
||||
export async function loginByAccount(params, context) {
|
||||
const { account, password, env, } = params;
|
||||
const loginLogic = async () => {
|
||||
const systemId = context.getSystemId();
|
||||
const applicationId = context.getApplicationId();
|
||||
assert(password);
|
||||
const result = await context.select('user', {
|
||||
data: {
|
||||
id: 1,
|
||||
mobile$user: {
|
||||
$entity: 'mobile',
|
||||
data: {
|
||||
id: 1,
|
||||
mobile: 1,
|
||||
ableState: 1,
|
||||
},
|
||||
},
|
||||
email$user: {
|
||||
$entity: 'email',
|
||||
data: {
|
||||
id: 1,
|
||||
email: 1,
|
||||
ableState: 1,
|
||||
}
|
||||
},
|
||||
loginName$user: {
|
||||
$entity: 'loginName',
|
||||
data: {
|
||||
id: 1,
|
||||
name: 1,
|
||||
ableState: 1,
|
||||
}
|
||||
}
|
||||
},
|
||||
filter: {
|
||||
$and: [
|
||||
{
|
||||
$or: [
|
||||
{
|
||||
mobile$user: {
|
||||
mobile: account,
|
||||
}
|
||||
},
|
||||
{
|
||||
email$user: {
|
||||
email: account,
|
||||
}
|
||||
},
|
||||
{
|
||||
loginName$user: {
|
||||
name: account,
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
$or: [
|
||||
{
|
||||
password,
|
||||
},
|
||||
{
|
||||
passwordSha1: encryptPasswordSha1(password),
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
}, {
|
||||
dontCollect: true,
|
||||
});
|
||||
switch (result.length) {
|
||||
case 0: {
|
||||
throw new OakUserException('账号与密码不匹配');
|
||||
}
|
||||
case 1: {
|
||||
const [userRow] = result;
|
||||
const { mobile$user, email$user, loginName$user, id: userId, } = userRow;
|
||||
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 allowSms = !!applicationPassports.find((ele) => ele.passport?.type === 'sms');
|
||||
const allowEmail = !!applicationPassports.find((ele) => ele.passport?.type === 'email');
|
||||
if (allowSms && mobile$user && mobile$user.length > 0 && account === mobile$user[0].mobile) {
|
||||
const ableState = mobile$user[0].ableState;
|
||||
if (ableState === 'disabled') {
|
||||
// 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录
|
||||
const exception = await tryMakeChangeLoginWay(userId, context);
|
||||
if (exception) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
return await setupMobile(account, env, context);
|
||||
}
|
||||
else if (allowEmail && email$user && email$user.length > 0 && account === email$user[0].email) {
|
||||
const ableState = email$user[0].ableState;
|
||||
if (ableState === 'disabled') {
|
||||
// 虽然密码和邮箱匹配,但邮箱已经禁用了,在可能的情况下提醒用户使用其它方法登录
|
||||
const exception = await tryMakeChangeLoginWay(userId, context);
|
||||
if (exception) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
return await setupEmail(account, env, context);
|
||||
}
|
||||
else if (loginName$user && loginName$user.length > 0 && account === loginName$user[0].name) {
|
||||
const ableState = loginName$user[0].ableState;
|
||||
if (ableState === 'disabled') {
|
||||
// 虽然密码和账号匹配,但账号已经禁用了,在可能的情况下提醒用户使用其它方法登录
|
||||
const exception = await tryMakeChangeLoginWay(userId, context);
|
||||
if (exception) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
return await setupLoginName(account, env, context);
|
||||
}
|
||||
}
|
||||
default: {
|
||||
// throw new Error('不支持的登录方式');
|
||||
throw new OakUserException('不支持的登录方式');
|
||||
}
|
||||
}
|
||||
};
|
||||
const closeRootMode = context.openRootMode();
|
||||
const tokenValue = await loginLogic();
|
||||
await loadTokenInfo(tokenValue, context);
|
||||
closeRootMode();
|
||||
return tokenValue;
|
||||
}
|
||||
export async function loginByEmail(params, context) {
|
||||
const { email, captcha, env, disableRegister } = params;
|
||||
const loginLogic = async () => {
|
||||
const systemId = context.getSystemId();
|
||||
const result = await context.select('captcha', {
|
||||
data: {
|
||||
id: 1,
|
||||
expired: 1,
|
||||
},
|
||||
filter: {
|
||||
origin: 'email',
|
||||
content: email,
|
||||
code: captcha,
|
||||
},
|
||||
sorter: [
|
||||
{
|
||||
$attr: {
|
||||
$$createAt$$: 1,
|
||||
},
|
||||
$direction: 'desc',
|
||||
},
|
||||
],
|
||||
indexFrom: 0,
|
||||
count: 1,
|
||||
}, { dontCollect: true });
|
||||
if (result.length > 0) {
|
||||
const [captchaRow] = result;
|
||||
if (captchaRow.expired) {
|
||||
throw new OakUserException('验证码已经过期');
|
||||
}
|
||||
// 到这里说明验证码已经通过
|
||||
return await setupEmail(email, env, context);
|
||||
}
|
||||
else {
|
||||
throw new OakUserException('验证码无效');
|
||||
}
|
||||
};
|
||||
const closeRootMode = context.openRootMode();
|
||||
if (disableRegister) {
|
||||
const [existEmail] = await context.select('email', {
|
||||
data: {
|
||||
id: 1,
|
||||
email: 1,
|
||||
},
|
||||
filter: {
|
||||
email: email,
|
||||
ableState: 'enabled',
|
||||
},
|
||||
}, { dontCollect: true });
|
||||
if (!existEmail) {
|
||||
closeRootMode();
|
||||
throw new OakUserException('账号不存在');
|
||||
}
|
||||
}
|
||||
const tokenValue = await loginLogic();
|
||||
await loadTokenInfo(tokenValue, context);
|
||||
closeRootMode();
|
||||
return tokenValue;
|
||||
}
|
||||
async function setupLoginName(name, env, context) {
|
||||
const result2 = await context.select('loginName', {
|
||||
data: {
|
||||
id: 1,
|
||||
name: 1,
|
||||
userId: 1,
|
||||
ableState: 1,
|
||||
user: {
|
||||
id: 1,
|
||||
userState: 1,
|
||||
refId: 1,
|
||||
ref: {
|
||||
id: 1,
|
||||
userState: 1,
|
||||
refId: 1,
|
||||
},
|
||||
wechatUser$user: {
|
||||
$entity: 'wechatUser',
|
||||
data: {
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
userSystem$user: {
|
||||
$entity: 'userSystem',
|
||||
data: {
|
||||
id: 1,
|
||||
systemId: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
name,
|
||||
ableState: 'enabled',
|
||||
},
|
||||
}, { dontCollect: true });
|
||||
assert(result2.length === 1);
|
||||
const [loginNameRow] = result2;
|
||||
const { user } = loginNameRow;
|
||||
const { userState, ref } = user;
|
||||
if (userState === 'merged') {
|
||||
return await setUpTokenAndUser(env, context, 'loginName', loginNameRow.id, undefined, ref);
|
||||
}
|
||||
return await setUpTokenAndUser(env, context, 'loginName', loginNameRow.id, undefined, user);
|
||||
}
|
||||
async function setupEmail(email, env, context) {
|
||||
const result2 = await context.select('email', {
|
||||
data: {
|
||||
id: 1,
|
||||
email: 1,
|
||||
userId: 1,
|
||||
ableState: 1,
|
||||
user: {
|
||||
id: 1,
|
||||
userState: 1,
|
||||
refId: 1,
|
||||
ref: {
|
||||
id: 1,
|
||||
userState: 1,
|
||||
refId: 1,
|
||||
},
|
||||
wechatUser$user: {
|
||||
$entity: 'wechatUser',
|
||||
data: {
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
userSystem$user: {
|
||||
$entity: 'userSystem',
|
||||
data: {
|
||||
id: 1,
|
||||
systemId: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
email,
|
||||
ableState: 'enabled',
|
||||
},
|
||||
}, { dontCollect: true });
|
||||
if (result2.length > 0) {
|
||||
// 此邮箱已经存在
|
||||
assert(result2.length === 1);
|
||||
const [emailRow] = result2;
|
||||
const { user } = emailRow;
|
||||
const { userState, ref } = user;
|
||||
if (userState === 'merged') {
|
||||
return await setUpTokenAndUser(env, context, 'email', emailRow.id, undefined, ref);
|
||||
}
|
||||
return await setUpTokenAndUser(env, context, 'email', emailRow.id, undefined, user);
|
||||
}
|
||||
else {
|
||||
//此邮箱不存在
|
||||
return await setUpTokenAndUser(env, context, 'email', undefined, {
|
||||
id: await generateNewIdAsync(),
|
||||
email,
|
||||
});
|
||||
}
|
||||
}
|
||||
async function setUserInfoFromWechat(user, userInfo, context) {
|
||||
const application = context.getApplication();
|
||||
const applicationId = context.getApplicationId();
|
||||
|
|
@ -687,18 +944,6 @@ export async function loginByWechat(params, context) {
|
|||
const { wechatLoginId, env } = params;
|
||||
const closeRootMode = context.openRootMode();
|
||||
const [wechatLoginData] = await context.select('wechatLogin', {
|
||||
data: {
|
||||
id: 1,
|
||||
userId: 1,
|
||||
type: 1,
|
||||
},
|
||||
filter: {
|
||||
id: wechatLoginId,
|
||||
},
|
||||
}, {
|
||||
dontCollect: true,
|
||||
});
|
||||
const [wechatUserLogin] = await context.select('wechatUser', {
|
||||
data: {
|
||||
id: 1,
|
||||
userId: 1,
|
||||
|
|
@ -710,14 +955,15 @@ export async function loginByWechat(params, context) {
|
|||
refId: 1,
|
||||
isRoot: 1,
|
||||
},
|
||||
type: 1,
|
||||
},
|
||||
filter: {
|
||||
userId: wechatLoginData.userId,
|
||||
id: wechatLoginId,
|
||||
},
|
||||
}, {
|
||||
dontCollect: true,
|
||||
});
|
||||
const tokenValue = await setUpTokenAndUser(env, context, 'wechatUser', wechatUserLogin.id, undefined, wechatUserLogin.user);
|
||||
const tokenValue = await setUpTokenAndUser(env, context, 'wechatLogin', wechatLoginId, undefined, wechatLoginData.user);
|
||||
await loadTokenInfo(tokenValue, context);
|
||||
closeRootMode();
|
||||
return tokenValue;
|
||||
|
|
@ -754,7 +1000,7 @@ async function loginFromWechatEnv(code, env, context, wechatLoginId) {
|
|||
wechatMp: 'mp',
|
||||
native: 'native',
|
||||
};
|
||||
const createWechatUserAndReturnTokenId = async (user) => {
|
||||
const createWechatUserAndReturnTokenId = async (user, wechatLoginId) => {
|
||||
const wechatUserCreateData = {
|
||||
id: await generateNewIdAsync(),
|
||||
unionId,
|
||||
|
|
@ -763,8 +1009,14 @@ async function loginFromWechatEnv(code, env, context, wechatLoginId) {
|
|||
applicationId: application.id,
|
||||
...wechatUserData,
|
||||
};
|
||||
const tokenValue = await setUpTokenAndUser(env, context, 'wechatUser', undefined, wechatUserCreateData, user);
|
||||
return tokenValue;
|
||||
let tokenValue;
|
||||
if (wechatLoginId) {
|
||||
tokenValue = await setUpTokenAndUser(env, context, 'wechatLogin', wechatLoginId, undefined, user);
|
||||
}
|
||||
else {
|
||||
tokenValue = await setUpTokenAndUser(env, context, 'wechatUser', undefined, wechatUserCreateData, user);
|
||||
}
|
||||
return { tokenValue, wechatUserId: wechatUserCreateData.id };
|
||||
};
|
||||
// 扫码者
|
||||
const [wechatUser] = await context.select('wechatUser', {
|
||||
|
|
@ -865,7 +1117,7 @@ async function loginFromWechatEnv(code, env, context, wechatLoginId) {
|
|||
return tokenValue;
|
||||
}
|
||||
else {
|
||||
const tokenValue = await createWechatUserAndReturnTokenId(wechatLoginData.user);
|
||||
const { tokenValue } = await createWechatUserAndReturnTokenId(wechatLoginData.user);
|
||||
await updateWechatLogin({ successed: true });
|
||||
return tokenValue;
|
||||
}
|
||||
|
|
@ -875,7 +1127,7 @@ async function loginFromWechatEnv(code, env, context, wechatLoginId) {
|
|||
// wechatUser存在直接登录
|
||||
if (wechatUser) {
|
||||
const tokenValue = await setUpTokenAndUser(env, context, 'wechatUser', wechatUser.id, undefined, wechatUser.user);
|
||||
await updateWechatLogin({ successed: true });
|
||||
await updateWechatLogin({ userId: wechatUser.userId, wechatUserId: wechatUser.id, successed: true });
|
||||
return tokenValue;
|
||||
}
|
||||
else {
|
||||
|
|
@ -890,8 +1142,8 @@ async function loginFromWechatEnv(code, env, context, wechatLoginId) {
|
|||
action: 'create',
|
||||
data: userData,
|
||||
}, {});
|
||||
const tokenValue = await createWechatUserAndReturnTokenId(userData);
|
||||
await updateWechatLogin({ userId, successed: true });
|
||||
const { tokenValue, wechatUserId } = await createWechatUserAndReturnTokenId(userData, wechatLoginId);
|
||||
await updateWechatLogin({ userId, wechatUserId, successed: true });
|
||||
return tokenValue;
|
||||
}
|
||||
}
|
||||
|
|
@ -965,7 +1217,7 @@ async function loginFromWechatEnv(code, env, context, wechatLoginId) {
|
|||
}
|
||||
}
|
||||
// 到这里都是要同时创建wechatUser和user对象了
|
||||
return await createWechatUserAndReturnTokenId();
|
||||
return (await createWechatUserAndReturnTokenId()).tokenValue;
|
||||
}
|
||||
/**
|
||||
* 公众号授权登录
|
||||
|
|
@ -976,9 +1228,24 @@ export async function loginWechat({ code, env, wechatLoginId, }, context) {
|
|||
const closeRootMode = context.openRootMode();
|
||||
const tokenValue = await loginFromWechatEnv(code, env, context, wechatLoginId);
|
||||
const [tokenInfo] = await loadTokenInfo(tokenValue, context);
|
||||
assert(tokenInfo.entity === 'wechatUser');
|
||||
assert(tokenInfo.entity === 'wechatUser' || tokenInfo.entity === 'wechatLogin');
|
||||
await context.setTokenValue(tokenValue);
|
||||
await tryRefreshWechatPublicUserInfo(tokenInfo.entityId, context);
|
||||
if (tokenInfo.entity === 'wechatUser') {
|
||||
await tryRefreshWechatPublicUserInfo(tokenInfo.entityId, context);
|
||||
}
|
||||
else if (tokenInfo.entity === 'wechatLogin') {
|
||||
const [wechatLogin] = await context.select('wechatLogin', {
|
||||
data: {
|
||||
id: 1,
|
||||
wechatUserId: 1,
|
||||
},
|
||||
filter: {
|
||||
id: tokenInfo.entityId,
|
||||
},
|
||||
}, {});
|
||||
assert(wechatLogin?.wechatUserId);
|
||||
await tryRefreshWechatPublicUserInfo(wechatLogin.wechatUserId, context);
|
||||
}
|
||||
closeRootMode();
|
||||
return tokenValue;
|
||||
}
|
||||
|
|
@ -1048,16 +1315,49 @@ export async function syncUserInfoWechatMp({ nickname, avatarUrl, encryptedData,
|
|||
// 实测发现解密出来的和userInfo完全一致……
|
||||
await setUserInfoFromWechat(user, { nickname, avatar: avatarUrl }, context);
|
||||
}
|
||||
export async function sendCaptcha({ mobile, env, type: type2, }, context) {
|
||||
export async function sendCaptchaByMobile({ mobile, env, type: type2, }, context) {
|
||||
const { type } = env;
|
||||
assert(type === 'web' || type === 'native');
|
||||
let { visitorId } = env;
|
||||
// assert(type === 'web' || type === 'native');
|
||||
let visitorId = mobile;
|
||||
if (type === 'web' || type === 'native') {
|
||||
visitorId = env.visitorId;
|
||||
}
|
||||
const application = context.getApplication();
|
||||
const { system } = application;
|
||||
const mockSend = system?.config?.Sms?.mockSend;
|
||||
const codeTemplateName = system?.config?.Sms?.defaultCodeTemplateName;
|
||||
const origin = system?.config?.Sms?.defaultOrigin;
|
||||
const duration = system?.config?.Sms?.defaultCodeDuration || 1; //多少分钟内有效;
|
||||
let mockSend = system?.config?.Sms?.mockSend;
|
||||
let codeTemplateName = system?.config?.Sms?.defaultCodeTemplateName;
|
||||
let origin = system?.config?.Sms?.defaultOrigin;
|
||||
let duration = system?.config?.Sms?.defaultCodeDuration || 1; //多少分钟内有效;
|
||||
let digit = 4; //验证码位数;
|
||||
if (type2 === 'login') {
|
||||
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: 'sms'
|
||||
},
|
||||
}
|
||||
}, {
|
||||
dontCollect: true,
|
||||
});
|
||||
assert(applicationPassport?.passport);
|
||||
const config = applicationPassport.passport.config;
|
||||
mockSend = config.mockSend;
|
||||
codeTemplateName = config.templateName;
|
||||
origin = config.defaultOrigin;
|
||||
duration = config.codeDuration || 1;
|
||||
digit = config.digit || 4;
|
||||
}
|
||||
const now = Date.now();
|
||||
const closeRootMode = context.openRootMode();
|
||||
if (process.env.NODE_ENV !== 'development' && !mockSend) {
|
||||
|
|
@ -1075,7 +1375,8 @@ export async function sendCaptcha({ mobile, env, type: type2, }, context) {
|
|||
}),
|
||||
context.count('captcha', {
|
||||
filter: {
|
||||
mobile,
|
||||
origin: 'mobile',
|
||||
content: mobile,
|
||||
$$createAt$$: {
|
||||
$gt: now - 3600 * 1000,
|
||||
},
|
||||
|
|
@ -1097,7 +1398,8 @@ export async function sendCaptcha({ mobile, env, type: type2, }, context) {
|
|||
$$createAt$$: 1,
|
||||
},
|
||||
filter: {
|
||||
mobile,
|
||||
origin: 'mobile',
|
||||
content: mobile,
|
||||
$$createAt$$: {
|
||||
$gt: now - duration * 60 * 1000,
|
||||
},
|
||||
|
|
@ -1136,11 +1438,11 @@ export async function sendCaptcha({ mobile, env, type: type2, }, context) {
|
|||
else {
|
||||
let code;
|
||||
if (process.env.NODE_ENV === 'development' || mockSend) {
|
||||
code = mobile.substring(7);
|
||||
code = mobile.substring(11 - digit);
|
||||
}
|
||||
else {
|
||||
code = Math.floor(Math.random() * 10000).toString();
|
||||
while (code.length < 4) {
|
||||
code = Math.floor(Math.random() * Math.pow(10, digit)).toString();
|
||||
while (code.length < digit) {
|
||||
code += '0';
|
||||
}
|
||||
}
|
||||
|
|
@ -1150,7 +1452,8 @@ export async function sendCaptcha({ mobile, env, type: type2, }, context) {
|
|||
action: 'create',
|
||||
data: {
|
||||
id,
|
||||
mobile,
|
||||
origin: 'mobile',
|
||||
content: mobile,
|
||||
code,
|
||||
visitorId,
|
||||
env,
|
||||
|
|
@ -1182,6 +1485,171 @@ export async function sendCaptcha({ mobile, env, type: type2, }, context) {
|
|||
}
|
||||
}
|
||||
}
|
||||
export async function sendCaptchaByEmail({ email, env, type: type2, }, context) {
|
||||
const { type } = env;
|
||||
let visitorId = email;
|
||||
if (type === 'web' || type === 'native') {
|
||||
visitorId = env.visitorId;
|
||||
}
|
||||
const application = context.getApplication();
|
||||
const { system } = application;
|
||||
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: 'email'
|
||||
},
|
||||
}
|
||||
}, {
|
||||
dontCollect: true,
|
||||
});
|
||||
assert(applicationPassport?.passport);
|
||||
const config = applicationPassport.passport.config;
|
||||
const emailConfig = system?.config.Emails?.find((ele) => ele.account === config.account);
|
||||
assert(emailConfig);
|
||||
const duration = config.codeDuration || 5;
|
||||
const digit = config.digit || 4;
|
||||
let emailOptions = {
|
||||
host: emailConfig.host,
|
||||
port: emailConfig.port,
|
||||
account: emailConfig.account,
|
||||
password: emailConfig.password,
|
||||
subject: config.subject,
|
||||
from: emailConfig.name ? `"${emailConfig.name}" <${emailConfig.account}>` : emailConfig.account,
|
||||
to: email,
|
||||
text: config.text,
|
||||
html: config.html,
|
||||
};
|
||||
const now = Date.now();
|
||||
const closeRootMode = context.openRootMode();
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
const [count1, count2] = await Promise.all([
|
||||
context.count('captcha', {
|
||||
filter: {
|
||||
visitorId,
|
||||
$$createAt$$: {
|
||||
$gt: now - 3600 * 1000,
|
||||
},
|
||||
type: type2,
|
||||
},
|
||||
}, {
|
||||
dontCollect: true,
|
||||
}),
|
||||
context.count('captcha', {
|
||||
filter: {
|
||||
origin: 'email',
|
||||
content: email,
|
||||
$$createAt$$: {
|
||||
$gt: now - 3600 * 1000,
|
||||
},
|
||||
type: type2,
|
||||
},
|
||||
}, {
|
||||
dontCollect: true,
|
||||
}),
|
||||
]);
|
||||
if (count1 > 5 || count2 > 5) {
|
||||
closeRootMode();
|
||||
throw new OakUserException('您已发送很多次短信,请休息会再发吧');
|
||||
}
|
||||
}
|
||||
const [captcha] = await context.select('captcha', {
|
||||
data: {
|
||||
id: 1,
|
||||
code: 1,
|
||||
$$createAt$$: 1,
|
||||
},
|
||||
filter: {
|
||||
origin: 'email',
|
||||
content: email,
|
||||
$$createAt$$: {
|
||||
$gt: now - duration * 60 * 1000,
|
||||
},
|
||||
expired: false,
|
||||
type: type2,
|
||||
},
|
||||
}, {
|
||||
dontCollect: true,
|
||||
});
|
||||
if (captcha) {
|
||||
const code = captcha.code;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
closeRootMode();
|
||||
return `验证码[${code}]已创建`;
|
||||
}
|
||||
else if (now - captcha.$$createAt$$ < 60000) {
|
||||
closeRootMode();
|
||||
throw new OakUserException('您的操作太迅捷啦,请稍等再点吧');
|
||||
}
|
||||
else {
|
||||
assert(config.account, '必须设置邮箱');
|
||||
// todo 再次发送
|
||||
const text = config.text?.replace('${duration}', duration.toString() + '分钟').replace('${code}', code);
|
||||
const html = config.html?.replace('${duration}', duration.toString() + '分钟').replace('${code}', code);
|
||||
emailOptions.text = text;
|
||||
emailOptions.html = html;
|
||||
const result = await sendEmail(emailOptions, context);
|
||||
closeRootMode();
|
||||
if (result.success) {
|
||||
return '验证码已发送';
|
||||
}
|
||||
return '验证码发送失败';
|
||||
}
|
||||
}
|
||||
else {
|
||||
let code;
|
||||
code = Math.floor(Math.random() * Math.random() * Math.pow(10, digit)).toString();
|
||||
while (code.length < digit) {
|
||||
code += '0';
|
||||
}
|
||||
const id = await generateNewIdAsync();
|
||||
await context.operate('captcha', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id,
|
||||
origin: 'email',
|
||||
content: email,
|
||||
code,
|
||||
visitorId,
|
||||
env,
|
||||
expired: false,
|
||||
expiresAt: now + duration * 60 * 1000,
|
||||
type: type2,
|
||||
},
|
||||
}, {
|
||||
dontCollect: true,
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
closeRootMode();
|
||||
return `验证码[${code}]已创建`;
|
||||
}
|
||||
else {
|
||||
assert(config.account, '必须设置邮箱');
|
||||
//发送邮件
|
||||
const text = config.text?.replace('${duration}', duration.toString() + '分钟').replace('${code}', code);
|
||||
const html = config.html?.replace('${duration}', duration.toString() + '分钟').replace('${code}', code);
|
||||
emailOptions.text = text;
|
||||
emailOptions.html = html;
|
||||
const result = await sendEmail(emailOptions, context);
|
||||
closeRootMode();
|
||||
if (result.success) {
|
||||
return '验证码已发送';
|
||||
}
|
||||
return '验证码发送失败';
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function switchTo({ userId }, context) {
|
||||
const reallyRoot = context.isReallyRoot();
|
||||
if (!reallyRoot) {
|
||||
|
|
|
|||
|
|
@ -275,7 +275,8 @@ export async function updateUserPassword(params, context, innerLogic) {
|
|||
id: 1,
|
||||
},
|
||||
filter: {
|
||||
mobile,
|
||||
origin: 'mobile',
|
||||
content: mobile,
|
||||
code: captcha,
|
||||
expired: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,12 +19,25 @@ export async function createWechatLogin(params, context) {
|
|||
userId,
|
||||
});
|
||||
}
|
||||
await context.operate('wechatLogin', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: createData,
|
||||
}, {
|
||||
dontCollect: true,
|
||||
});
|
||||
if (type === 'login') {
|
||||
const closeRoot = context.openRootMode();
|
||||
await context.operate('wechatLogin', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: createData,
|
||||
}, {
|
||||
dontCollect: true,
|
||||
});
|
||||
closeRoot();
|
||||
}
|
||||
else {
|
||||
await context.operate('wechatLogin', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: createData,
|
||||
}, {
|
||||
dontCollect: true,
|
||||
});
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ export async function unbindingWechat(params, context) {
|
|||
expired: 1,
|
||||
},
|
||||
filter: {
|
||||
mobile,
|
||||
origin: 'mobile',
|
||||
content: mobile,
|
||||
code: captcha,
|
||||
},
|
||||
sorter: [{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { Checker } from 'oak-domain/lib/types';
|
||||
import { EntityDict } from '../oak-app-domain';
|
||||
import { RuntimeCxt } from '../types/RuntimeCxt';
|
||||
declare const checkers: Checker<EntityDict, 'applicationPassport', RuntimeCxt<EntityDict>>[];
|
||||
export default checkers;
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
import { generateNewId } from 'oak-domain/lib/utils/uuid';
|
||||
import { pipeline } from 'oak-domain/lib/utils/executor';
|
||||
import { assert } from 'oak-domain/lib/utils/assert';
|
||||
const checkers = [
|
||||
{
|
||||
entity: 'applicationPassport',
|
||||
type: 'logical',
|
||||
action: 'create',
|
||||
checker(operation, context, option) {
|
||||
const { data } = operation;
|
||||
if (data) {
|
||||
const { id, applicationId, isDefault } = data;
|
||||
if (applicationId && isDefault) {
|
||||
return context.operate('applicationPassport', {
|
||||
id: generateNewId(),
|
||||
action: 'update',
|
||||
data: {
|
||||
isDefault: false,
|
||||
},
|
||||
filter: {
|
||||
applicationId,
|
||||
isDefault: true,
|
||||
id: {
|
||||
$ne: id,
|
||||
},
|
||||
}
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
entity: 'applicationPassport',
|
||||
type: 'logical',
|
||||
action: 'update',
|
||||
checker(operation, context, option) {
|
||||
const { data, filter } = operation;
|
||||
if (data?.isDefault) {
|
||||
return pipeline(() => context.select('applicationPassport', {
|
||||
data: {
|
||||
id: 1,
|
||||
applicationId: 1,
|
||||
},
|
||||
filter,
|
||||
}, {}), (applicationPassports) => {
|
||||
assert(applicationPassports.length === 1);
|
||||
const [applicationPassport] = applicationPassports;
|
||||
const { applicationId, id } = applicationPassport;
|
||||
return context.operate('applicationPassport', {
|
||||
id: generateNewId(),
|
||||
action: 'update',
|
||||
data: {
|
||||
isDefault: false,
|
||||
},
|
||||
filter: {
|
||||
applicationId,
|
||||
isDefault: true,
|
||||
id: {
|
||||
$ne: id,
|
||||
},
|
||||
}
|
||||
}, option);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
entity: 'applicationPassport',
|
||||
type: 'logical',
|
||||
action: 'remove',
|
||||
checker(operation, context, option) {
|
||||
const { filter } = operation;
|
||||
assert(filter);
|
||||
const remove = context.select('applicationPassport', {
|
||||
data: {
|
||||
id: 1,
|
||||
applicationId: 1,
|
||||
isDefault: 1,
|
||||
},
|
||||
filter,
|
||||
}, { forUpdate: true });
|
||||
const updateDefaultFn = (id, applicationId) => {
|
||||
return pipeline(() => context.select('applicationPassport', {
|
||||
data: {
|
||||
id: 1,
|
||||
applicationId: 1,
|
||||
isDefault: 1,
|
||||
},
|
||||
filter: {
|
||||
id: {
|
||||
$ne: id,
|
||||
},
|
||||
isDefault: false,
|
||||
applicationId,
|
||||
},
|
||||
indexFrom: 0,
|
||||
count: 1,
|
||||
}, {}), (other) => {
|
||||
if (other && other.length === 1) {
|
||||
return context.operate('applicationPassport', {
|
||||
id: generateNewId(),
|
||||
action: 'update',
|
||||
data: {
|
||||
isDefault: true,
|
||||
},
|
||||
filter: {
|
||||
id: other[0].id,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
});
|
||||
};
|
||||
if (remove instanceof Promise) {
|
||||
return remove.then((r) => {
|
||||
if (r[0]?.isDefault) {
|
||||
return updateDefaultFn(r[0].id, r[0].applicationId);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
if (remove[0]?.isDefault) {
|
||||
return updateDefaultFn(remove[0].id, remove[0].applicationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
export default checkers;
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
declare const checkers: (import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "user", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "parasite", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "address", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "application", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "token", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "mobile", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "message", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>>)[];
|
||||
declare const checkers: (import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "parasite", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "mobile", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "applicationPassport", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "address", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "application", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "token", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "user", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "message", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>>)[];
|
||||
export default checkers;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import mobileChecker from './mobile';
|
|||
import wechatPublicTagChecker from './wechatPublicTag';
|
||||
import messageChecker from './message';
|
||||
import parasite from './parasite';
|
||||
import applicationPassport from './applicationPassport';
|
||||
const checkers = [
|
||||
...mobileChecker,
|
||||
...addressCheckers,
|
||||
|
|
@ -19,5 +20,6 @@ const checkers = [
|
|||
...wechatPublicTagChecker,
|
||||
...messageChecker,
|
||||
...parasite,
|
||||
...applicationPassport,
|
||||
];
|
||||
export default checkers;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default function Render(props) {
|
|||
{
|
||||
label: <div className={Styles.tabLabel}>{t('config')}</div>,
|
||||
key: 'config',
|
||||
children: (<ConfigUpsert entity="application" entityId={id} config={config || {}} name={name} type={config?.type}/>),
|
||||
children: (<ConfigUpsert entity="application" entityId={id} config={config || {}} name={name} type={type}/>),
|
||||
},
|
||||
{
|
||||
label: <div className={Styles.tabLabel}>{t('style')}</div>,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { EntityDict } from "../../oak-app-domain";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "applicationPassport", true, {
|
||||
systemId: string;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
import { groupBy, isEqual, uniq } from "oak-domain/lib/utils/lodash";
|
||||
import { generateNewId, generateNewIdAsync } from "oak-domain/lib/utils/uuid";
|
||||
export default OakComponent({
|
||||
entity: 'applicationPassport',
|
||||
isList: true,
|
||||
projection: {
|
||||
id: 1,
|
||||
applicationId: 1,
|
||||
application: {
|
||||
id: 1,
|
||||
name: 1,
|
||||
type: 1,
|
||||
systemId: 1,
|
||||
},
|
||||
passportId: 1,
|
||||
passport: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
systemId: 1,
|
||||
enabled: 1,
|
||||
},
|
||||
isDefault: 1,
|
||||
},
|
||||
properties: {
|
||||
systemId: '',
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
filter() {
|
||||
const { systemId } = this.props;
|
||||
return {
|
||||
application: {
|
||||
systemId,
|
||||
},
|
||||
passport: {
|
||||
systemId,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
],
|
||||
formData({ data }) {
|
||||
const aps = data.filter((ele) => ele.$$deleteAt$$ !== 1);
|
||||
return {
|
||||
aps,
|
||||
};
|
||||
},
|
||||
listeners: {
|
||||
async 'aps,applications,passports'(prev, next) {
|
||||
if (!this.arraysAreEqual(prev.aps, next.aps) || !this.arraysAreEqual(prev.applications, next.applications) || !this.arraysAreEqual(prev.passports, next.passports)) {
|
||||
let apArray = [];
|
||||
const records = groupBy(next.passports, 'type');
|
||||
if (next.applications && next.applications.length > 0 && next.passports && next.passports.length > 0) {
|
||||
for (const a of next.applications) {
|
||||
let item = {
|
||||
aId: a.id,
|
||||
aName: a.name,
|
||||
typeRecords: {},
|
||||
defaultOptions: [],
|
||||
defaultValue: '',
|
||||
};
|
||||
let typeRecords = {};
|
||||
for (const key of Object.keys(records)) {
|
||||
const r = records[key];
|
||||
const render = this.getRender(key, r, a.type);
|
||||
if (render === 'select') {
|
||||
const passportOptions = r.map((ele) => {
|
||||
const { disabled, disabledTip } = this.checkDisabled(a, ele);
|
||||
return {
|
||||
label: ele.type === 'email' ? ele.config.account : ele.config.appId,
|
||||
value: ele.id,
|
||||
apId: generateNewId(),
|
||||
disabled,
|
||||
disabledTip,
|
||||
};
|
||||
});
|
||||
const d = !passportOptions.find((ele) => !ele.disabled);
|
||||
let disabledTip = '';
|
||||
if (d) {
|
||||
disabledTip = '暂不支持该登录方式';
|
||||
}
|
||||
Object.assign(typeRecords, { [key]: { render, passportOptions, chekedValue: undefined, disabled: d, disabledTip } });
|
||||
}
|
||||
else {
|
||||
const { disabled, disabledTip } = this.checkDisabled(a, r[0]);
|
||||
const apId = await generateNewIdAsync();
|
||||
Object.assign(typeRecords, { [key]: { render, pId: r[0].id, checked: false, disabled, disabledTip, apId, } });
|
||||
}
|
||||
}
|
||||
Object.assign(item, { typeRecords });
|
||||
apArray.push(item);
|
||||
}
|
||||
if (next.aps && next.aps.length > 0) {
|
||||
for (const ap of next.aps) {
|
||||
const aIdx = apArray.findIndex((ele) => ele.aId === ap.applicationId);
|
||||
if (aIdx !== -1) {
|
||||
const p = ap.passport;
|
||||
const t = p.type;
|
||||
if (apArray[aIdx].typeRecords[t].render === 'select') {
|
||||
apArray[aIdx].typeRecords[t].checkedValue = p.id;
|
||||
apArray[aIdx].typeRecords[t].apId = ap.id;
|
||||
const option = apArray[aIdx].typeRecords[t].passportOptions?.find((ele) => ele.value === p.id);
|
||||
option && Object.assign(option, { apId: ap.id });
|
||||
}
|
||||
else {
|
||||
if (apArray[aIdx].typeRecords[t].pId === p.id) {
|
||||
apArray[aIdx].typeRecords[t].checked = true;
|
||||
apArray[aIdx].typeRecords[t].apId = ap.id;
|
||||
}
|
||||
}
|
||||
apArray[aIdx].defaultOptions.push({
|
||||
label: this.t(`passport:v.type.${p.type}`),
|
||||
value: ap.id,
|
||||
});
|
||||
if (ap.isDefault) {
|
||||
apArray[aIdx].defaultValue = ap.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
apArray,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
applications: [],
|
||||
passports: [],
|
||||
apArray: [],
|
||||
types: [],
|
||||
},
|
||||
lifetimes: {
|
||||
async ready() {
|
||||
const { systemId } = this.props;
|
||||
const { data: applicationDatas } = await this.features.cache.refresh('application', {
|
||||
data: {
|
||||
id: 1,
|
||||
name: 1,
|
||||
type: 1,
|
||||
config: 1,
|
||||
systemId: 1,
|
||||
},
|
||||
filter: {
|
||||
systemId,
|
||||
}
|
||||
});
|
||||
const { data: passportDatas } = await this.features.cache.refresh('passport', {
|
||||
data: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
config: 1,
|
||||
enabled: 1,
|
||||
systemId: 1,
|
||||
},
|
||||
filter: {
|
||||
systemId,
|
||||
enabled: true,
|
||||
},
|
||||
sorter: [{
|
||||
$attr: {
|
||||
$$updateAt$$: 1,
|
||||
},
|
||||
$direction: 'desc'
|
||||
}]
|
||||
});
|
||||
const applications = applicationDatas;
|
||||
const passports = passportDatas;
|
||||
const types = uniq(passports.map((ele) => ele.type));
|
||||
this.setState({
|
||||
applications,
|
||||
passports,
|
||||
types,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
arraysAreEqual(first, second) {
|
||||
if (first?.length !== second?.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < first?.length; ++i) {
|
||||
if (!isEqual(first[i], second[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
checkDisabled(application, passport) {
|
||||
const { type: aType, config: aConfig } = application;
|
||||
const { type: pType, config: pConfig } = passport;
|
||||
switch (pType) {
|
||||
case 'sms':
|
||||
if (!pConfig.mockSend) {
|
||||
if (!pConfig.templateName || pConfig.templateName === '') {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledTip: '短信登录未配置验证码模板名称',
|
||||
};
|
||||
}
|
||||
if (!pConfig.defaultOrigin) {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledTip: '短信登录未配置默认渠道',
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'email':
|
||||
if (!pConfig.account || pConfig.account === '') {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledTip: '邮箱登录未配置账号',
|
||||
};
|
||||
}
|
||||
else if (!pConfig.subject || pConfig.subject === '') {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledTip: '邮箱登录未配置邮件主题',
|
||||
};
|
||||
}
|
||||
else if ((!pConfig.text || pConfig.text === '' || !pConfig.text?.includes('${code}')) &&
|
||||
(!pConfig.html || pConfig.html === '' || !pConfig.html?.includes('${code}'))) {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledTip: '邮箱登录未配置邮件内容模板',
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'wechatPublicForWeb':
|
||||
if (!pConfig.appId || pConfig.appId === '') {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledTip: '公众号授权登录未配置appId',
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'wechatMpForWeb':
|
||||
if (!pConfig.appId || pConfig.appId === '') {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledTip: '小程序授权登录未配置appId',
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (aType) {
|
||||
case 'web':
|
||||
if (pType === 'wechatWeb') {
|
||||
//微信网站登录 application需配置微信网站appId
|
||||
const { appId } = aConfig.wechat || {};
|
||||
if (!appId || appId === '') {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledTip: '当前application未配置微信网站appId',
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (pType === 'wechatMp' || pType === 'wechatPublic') {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledTip: '当前application不支持该登录方式',
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'wechatMp':
|
||||
if (['wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledTip: '当前application不支持该登录方式',
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'wechatPublic':
|
||||
if (['wechatMp', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledTip: '当前application不支持该登录方式',
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'native':
|
||||
if (['wechatMp', 'wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledTip: '当前application不支持该登录方式',
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {
|
||||
disabled: false,
|
||||
disabledTip: undefined,
|
||||
};
|
||||
},
|
||||
async onCheckedChange(aId, pId, checked, apId) {
|
||||
if (checked) {
|
||||
//create applicationPassport
|
||||
this.addItem({
|
||||
applicationId: aId,
|
||||
passportId: pId,
|
||||
isDefault: true,
|
||||
});
|
||||
}
|
||||
else {
|
||||
//remove id为apId的applicationPassport
|
||||
apId && this.removeItem(apId);
|
||||
}
|
||||
},
|
||||
checkLastOne(aId, pId) {
|
||||
const { apArray } = this.state;
|
||||
const idx = apArray.findIndex((ele) => ele.aId === aId);
|
||||
if (idx !== -1) {
|
||||
const records = apArray[idx].typeRecords;
|
||||
for (const key of Object.keys(records)) {
|
||||
const r = records[key];
|
||||
if ((r.checkedValue && r.checkedValue !== pId && !!r.checkedValue) || (r.pId && r.pId !== pId && !!r.checked)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
async onSelectChange(aId, addPId, removeApId) {
|
||||
if (removeApId) {
|
||||
removeApId && this.removeItem(removeApId);
|
||||
}
|
||||
if (addPId) {
|
||||
this.addItem({
|
||||
applicationId: aId,
|
||||
passportId: addPId,
|
||||
isDefault: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
getRender(pType, passports, aType) {
|
||||
let render = 'switch';
|
||||
if (passports.length > 1) {
|
||||
if (aType === 'web' || (['wechatMp', 'wechatPublic'].includes(aType) && ['email', 'sms'].includes(pType))) {
|
||||
render = 'select';
|
||||
}
|
||||
}
|
||||
return render;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"login": "登录"
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import { WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../oak-app-domain';
|
||||
type RowItem = {
|
||||
aId: string;
|
||||
aName: string;
|
||||
typeRecords: TypeRecord;
|
||||
defaultOptions: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
defaultValue: string;
|
||||
};
|
||||
type TypeRecord = Record<string, {
|
||||
render: 'switch' | 'select';
|
||||
passportOptions?: PassportOption[];
|
||||
checkedValue?: string;
|
||||
pId?: string;
|
||||
apId?: string;
|
||||
checked?: boolean;
|
||||
disabled: boolean;
|
||||
disabledTip: string;
|
||||
}>;
|
||||
type PassportOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
apId: string;
|
||||
disabled: boolean;
|
||||
disabledTip?: string;
|
||||
};
|
||||
export default function render(props: WebComponentProps<EntityDict, 'applicationPassport', true, {
|
||||
applicationPassports: EntityDict['applicationPassport']['OpSchema'][];
|
||||
systemId: string;
|
||||
applications: EntityDict['application']['Schema'][];
|
||||
passports: EntityDict['passport']['Schema'][];
|
||||
types: EntityDict['passport']['Schema']['type'][];
|
||||
apArray: RowItem[];
|
||||
}, {
|
||||
onCheckedChange: (aId: string, pId: string, checked: boolean, apId?: string) => void;
|
||||
checkLastOne: (aId: string, pId: string) => boolean;
|
||||
onSelectChange: (aId: string, addPId?: string, removeApId?: string) => void;
|
||||
}>): React.JSX.Element;
|
||||
export {};
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import React from 'react';
|
||||
import { Button, Space, Switch, Table, Tooltip, Modal, Select, } from 'antd';
|
||||
import Styles from './web.pc.module.less';
|
||||
import { CheckOutlined, CloseOutlined, ExclamationCircleFilled } from '@ant-design/icons';
|
||||
const { confirm } = Modal;
|
||||
export default function render(props) {
|
||||
const { data, methods } = props;
|
||||
const { oakFullpath, oakDirty, oakExecutable, oakExecuting, applicationPassports, systemId, applications, passports, types, apArray, } = data;
|
||||
const { clean, execute, t, onCheckedChange, updateItem, checkLastOne, onSelectChange } = methods;
|
||||
if (!(applications && applications.length > 0)) {
|
||||
return (<div>请先前往应用管理创建application</div>);
|
||||
}
|
||||
if (!(passports && passports.length > 0)) {
|
||||
return (<div>请先完成登录配置,启用登录方式</div>);
|
||||
}
|
||||
let columns = [
|
||||
{
|
||||
title: '',
|
||||
key: 'applicationName',
|
||||
dataIndex: 'aName',
|
||||
fixed: 'left',
|
||||
width: 100,
|
||||
},
|
||||
];
|
||||
const showConfirm = (aId, pId, apId) => {
|
||||
confirm({
|
||||
title: '当前application将无登录方式',
|
||||
icon: <ExclamationCircleFilled />,
|
||||
content: '关闭后,当前applicaion将无登录方式,可能影响用户登录',
|
||||
onOk() {
|
||||
onCheckedChange(aId, pId, false, apId);
|
||||
},
|
||||
onCancel() {
|
||||
},
|
||||
});
|
||||
};
|
||||
if (types && types.length > 0) {
|
||||
for (const type of types) {
|
||||
columns.push({
|
||||
title: t(`passport:v.type.${type}`),
|
||||
dataIndex: 'typeRecords',
|
||||
key: `${type} `,
|
||||
align: 'center',
|
||||
width: 120,
|
||||
render: (_, { typeRecords, aId }) => <Space direction="vertical">
|
||||
{typeRecords[type].render === 'select' ? (<Tooltip title={typeRecords[type].disabled ? typeRecords[type].disabledTip : ''}>
|
||||
<Select allowClear value={typeRecords[type].checkedValue} onChange={(value, option) => {
|
||||
onSelectChange(aId, value, typeRecords[type].apId);
|
||||
}} onClear={() => {
|
||||
if (checkLastOne(aId, typeRecords[type].pId)) {
|
||||
showConfirm(aId, typeRecords[type].pId, typeRecords[type].apId);
|
||||
}
|
||||
else {
|
||||
onSelectChange(aId, undefined, typeRecords[type].apId);
|
||||
}
|
||||
}} 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) => {
|
||||
if (!checked && checkLastOne(aId, typeRecords[type].pId)) {
|
||||
showConfirm(aId, typeRecords[type].pId, typeRecords[type].apId);
|
||||
}
|
||||
else {
|
||||
onCheckedChange(aId, typeRecords[type].pId, checked, typeRecords[type].apId);
|
||||
}
|
||||
}}/>
|
||||
</Tooltip>)}
|
||||
|
||||
</Space>
|
||||
});
|
||||
}
|
||||
columns.push({
|
||||
title: '默认登录方式',
|
||||
key: 'default',
|
||||
dataIndex: 'defaultValue',
|
||||
fixed: 'right',
|
||||
width: 140,
|
||||
render: (_, { defaultOptions, defaultValue, aId }) => <>
|
||||
<Select value={defaultValue} style={{ width: 120 }} onChange={(value) => { updateItem({ isDefault: true, }, value); }} options={defaultOptions}/>
|
||||
</>
|
||||
});
|
||||
}
|
||||
return (<>
|
||||
<div className={Styles.btns}>
|
||||
<Button disabled={!oakDirty} type="primary" danger onClick={() => clean()} style={{
|
||||
marginRight: 10,
|
||||
}}>
|
||||
重置
|
||||
</Button>
|
||||
<Button disabled={!oakDirty} type="primary" onClick={async () => {
|
||||
await execute();
|
||||
}}>
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
<Table columns={columns} dataSource={apArray} pagination={false} scroll={{ x: 1200 }}/>
|
||||
</>);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.btns {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ export default OakComponent({
|
|||
methods: {
|
||||
async sendCaptcha(mobile) {
|
||||
try {
|
||||
const result = await this.features.token.sendCaptcha(mobile, 'changePassword');
|
||||
const result = await this.features.token.sendCaptcha('mobile', mobile, 'changePassword');
|
||||
// 显示返回消息
|
||||
this.setMessage({
|
||||
type: 'success',
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { ReactComponentProps } from 'oak-frontend-base';
|
|||
import { ECode } from '../../../types/ErrorPage';
|
||||
declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends keyof ED2>(props: ReactComponentProps<ED2, T2, false, {
|
||||
code: ECode;
|
||||
title?: string | undefined;
|
||||
desc?: string | undefined;
|
||||
title?: string;
|
||||
desc?: string;
|
||||
children?: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
}>) => React.ReactElement;
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@
|
|||
}
|
||||
|
||||
&_dev {
|
||||
height: 280px;
|
||||
// height: 280px;
|
||||
border: 1px dashed var(--oak-color-primary);
|
||||
padding: 10px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&_header {
|
||||
display: flex;
|
||||
|
|
@ -36,7 +37,7 @@
|
|||
}
|
||||
|
||||
&_btn {
|
||||
margin-top: 16px;
|
||||
// margin-top: 16px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 40px;
|
||||
|
|
@ -102,16 +103,16 @@
|
|||
|
||||
&_disable {
|
||||
&_border {
|
||||
height: 202px;
|
||||
width: 202px;
|
||||
margin: 15px;
|
||||
height: 220px;
|
||||
width: 220px;
|
||||
// margin: 15px;
|
||||
background-color: #f5f7fa;
|
||||
color: rgba(0, 0, 0, .4);
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 15px;
|
||||
// padding: 15px;
|
||||
}
|
||||
|
||||
&_info {
|
||||
|
|
@ -124,7 +125,7 @@
|
|||
}
|
||||
|
||||
&_err {
|
||||
height: 280px;
|
||||
// height: 280px;
|
||||
border: 1px dashed var(--oak-color-primary);
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Row, Col, Card, Divider, Input, Form, Space, Select, Switch, message, } from 'antd';
|
||||
import { Row, Col, Card, Divider, Input, Form, Space, message, } from 'antd';
|
||||
import Styles from './web.module.less';
|
||||
export default function Web(props) {
|
||||
const { config, setValue } = props;
|
||||
|
|
@ -32,50 +32,80 @@ export default function Web(props) {
|
|||
<Input placeholder="请输入授权回调域" type="text" value={config?.wechat?.domain} onChange={(e) => setValue(`wechat.domain`, e.target.value)}/>
|
||||
</>
|
||||
</Form.Item>
|
||||
<Form.Item label="微信网站应用授权登录"
|
||||
// name="enable"
|
||||
tooltip="开启后,登录页显示微信扫码入口,微信扫码后使用微信网站应用授权登录" help="开启当前登录方式时,将同时关闭微信公众号扫码登录">
|
||||
<>
|
||||
<Switch checkedChildren="是" unCheckedChildren="否" checked={config?.wechat?.enable} onChange={(checked) => setValue(`wechat.enable`, checked)}/>
|
||||
</>
|
||||
</Form.Item>
|
||||
{/* <Form.Item
|
||||
label="微信网站应用授权登录"
|
||||
// name="enable"
|
||||
tooltip="开启后,登录页显示微信扫码入口,微信扫码后使用微信网站应用授权登录"
|
||||
help="开启当前登录方式时,将同时关闭微信公众号扫码登录"
|
||||
>
|
||||
<>
|
||||
<Switch
|
||||
checkedChildren="是"
|
||||
unCheckedChildren="否"
|
||||
checked={config?.wechat?.enable}
|
||||
onChange={(checked) =>
|
||||
setValue(`wechat.enable`, checked)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
</Form.Item> */}
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
<Col flex="auto">
|
||||
<Divider orientation="left" className={Styles.title}>
|
||||
网站-授权方式
|
||||
</Divider>
|
||||
<Form colon={true} labelAlign="left" layout="vertical" style={{ marginTop: 10 }}>
|
||||
<Form.Item label="passport">
|
||||
<>
|
||||
<Select mode="multiple" allowClear style={{ width: '100%' }} placeholder="请选择授权方式" value={config?.passport} onChange={(value) => {
|
||||
if (value.includes('wechat') && value.includes('wechatPublic')) {
|
||||
message.warning('微信网站和微信公众号中,只能选择一个');
|
||||
return;
|
||||
}
|
||||
setValue(`passport`, value);
|
||||
}} options={[
|
||||
{
|
||||
label: '邮箱',
|
||||
value: 'email',
|
||||
},
|
||||
{
|
||||
label: '手机号',
|
||||
value: 'mobile',
|
||||
},
|
||||
{
|
||||
label: '微信网站',
|
||||
value: 'wechat',
|
||||
},
|
||||
{
|
||||
label: '微信公众号',
|
||||
value: 'wechatPublic',
|
||||
},
|
||||
]}/>
|
||||
</>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Col>
|
||||
{/* <Col flex="auto">
|
||||
<Divider orientation="left" className={Styles.title}>
|
||||
网站-授权方式
|
||||
</Divider>
|
||||
<Form
|
||||
colon={true}
|
||||
labelAlign="left"
|
||||
layout="vertical"
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<Form.Item label="passport"
|
||||
//name="passport"
|
||||
>
|
||||
<>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请选择授权方式"
|
||||
value={config?.passport as Passport[]}
|
||||
onChange={(value: Passport[]) => {
|
||||
if (value.includes('wechat') && value.includes('wechatPublic')) {
|
||||
message.warning('微信网站和微信公众号中,只能选择一个')
|
||||
return;
|
||||
}
|
||||
setValue(`passport`, value);
|
||||
}}
|
||||
options={
|
||||
[
|
||||
{
|
||||
label: '邮箱',
|
||||
value: 'email',
|
||||
},
|
||||
{
|
||||
label: '手机号',
|
||||
value: 'mobile',
|
||||
},
|
||||
{
|
||||
label: '微信网站',
|
||||
value: 'wechat',
|
||||
},
|
||||
{
|
||||
label: '微信公众号',
|
||||
value: 'wechatPublic',
|
||||
},
|
||||
] as Array<{
|
||||
label: string;
|
||||
value: Passport;
|
||||
}>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Col> */}
|
||||
</Space>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Row, Col, Card, Divider, Input, Form, Space, Switch, message, Select, } from 'antd';
|
||||
import { Row, Col, Card, Divider, Input, Form, Space, Switch, Select, } from 'antd';
|
||||
import Styles from './web.module.less';
|
||||
export default function WechatPublic(props) {
|
||||
const [open, setModal] = useState(false);
|
||||
|
|
@ -39,42 +39,62 @@ export default function WechatPublic(props) {
|
|||
</Form.Item>)}
|
||||
</Form>
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<Divider orientation="left" className={Styles.title}>
|
||||
网站-授权方式
|
||||
</Divider>
|
||||
<Form colon={true} labelAlign="left" layout="vertical" style={{ marginTop: 10 }}>
|
||||
<Form.Item label="passport">
|
||||
<>
|
||||
<Select mode="multiple" allowClear style={{ width: '100%' }} placeholder="请选择授权方式" value={config?.passport} onChange={(value) => {
|
||||
if (value.includes('wechat') && value.includes('wechatPublic')) {
|
||||
// messageApi.warning('微信网站和微信公众号中,只能选择一个');
|
||||
message.warning('微信网站和微信公众号中,只能选择一个');
|
||||
return;
|
||||
}
|
||||
setValue(`passport`, value);
|
||||
}} options={[
|
||||
{
|
||||
label: '邮箱',
|
||||
value: 'email',
|
||||
},
|
||||
{
|
||||
label: '手机号',
|
||||
value: 'mobile',
|
||||
},
|
||||
{
|
||||
label: '微信网站',
|
||||
value: 'wechat',
|
||||
},
|
||||
{
|
||||
label: '微信公众号',
|
||||
value: 'wechatPublic',
|
||||
},
|
||||
]}/>
|
||||
</>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Col>
|
||||
{/* <Col flex="auto">
|
||||
<Divider orientation="left" className={Styles.title}>
|
||||
网站-授权方式
|
||||
</Divider>
|
||||
<Form
|
||||
colon={true}
|
||||
labelAlign="left"
|
||||
layout="vertical"
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<Form.Item label="passport"
|
||||
//name="passport"
|
||||
>
|
||||
<>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请选择授权方式"
|
||||
value={config?.passport as Passport[]}
|
||||
onChange={(value: Passport[]) => {
|
||||
if (value.includes('wechat') && value.includes('wechatPublic')) {
|
||||
// messageApi.warning('微信网站和微信公众号中,只能选择一个');
|
||||
message.warning('微信网站和微信公众号中,只能选择一个')
|
||||
return;
|
||||
}
|
||||
setValue(`passport`, value);
|
||||
}}
|
||||
options={
|
||||
[
|
||||
{
|
||||
label: '邮箱',
|
||||
value: 'email',
|
||||
},
|
||||
{
|
||||
label: '手机号',
|
||||
value: 'mobile',
|
||||
},
|
||||
{
|
||||
label: '微信网站',
|
||||
value: 'wechat',
|
||||
},
|
||||
{
|
||||
label: '微信公众号',
|
||||
value: 'wechatPublic',
|
||||
},
|
||||
] as Array<{
|
||||
label: string;
|
||||
value: Passport;
|
||||
}>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Col> */}
|
||||
<Col flex="auto">
|
||||
<Divider orientation="left" className={Styles.title}>
|
||||
微信公众号-跳转小程序-小程序配置
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Style } from '../../../../types/Style';
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../../oak-app-domain").EntityDict, keyof import("../../../../oak-app-domain").EntityDict, false, {
|
||||
style: Style;
|
||||
entity: "application" | "platform" | "system";
|
||||
entity: "application" | "system" | "platform";
|
||||
entityId: string;
|
||||
name: string;
|
||||
}>) => React.ReactElement;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import { Config } from '../../../../types/Config';
|
||||
export default function Email(props: {
|
||||
emails: Required<Config>['Emails'];
|
||||
setValue: (path: string, value: any) => void;
|
||||
removeItem: (path: string, index: number) => void;
|
||||
cleanKey: (path: string, key: string) => void;
|
||||
}): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import React from 'react';
|
||||
import { Tabs, Col, Divider, Input, Form, Space, Modal, } from 'antd';
|
||||
import Styles from './web.module.less';
|
||||
const { confirm } = Modal;
|
||||
export default function Email(props) {
|
||||
const { emails, setValue, removeItem, cleanKey, } = props;
|
||||
return (<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
||||
{/* <Row>
|
||||
<Card className={Styles.tips}>
|
||||
每种均可配置一个,相应的服务所使用的帐号请准确对应
|
||||
</Card>
|
||||
</Row> */}
|
||||
<Col flex="auto">
|
||||
<Divider orientation="left" className={Styles.title}>
|
||||
邮箱配置
|
||||
</Divider>
|
||||
<Tabs tabPosition={'top'} size={'middle'} type="editable-card" hideAdd={!(emails.length > 0)} onEdit={(targetKey, action) => {
|
||||
if (action === 'add') {
|
||||
setValue(`${emails.length}`, {});
|
||||
}
|
||||
else {
|
||||
removeItem('', parseInt(targetKey, 10));
|
||||
}
|
||||
}} items={emails.length > 0
|
||||
? emails.map((ele, idx) => ({
|
||||
key: `${idx}`,
|
||||
label: ele.account ? ele.account : `邮箱${idx + 1}`,
|
||||
children: (<Form colon={false} labelAlign="left" layout="vertical" style={{ marginTop: 10 }}>
|
||||
<Form.Item label="主机名">
|
||||
<>
|
||||
<Input placeholder="请输入主机名(例:smtp.163.com)" type="text" value={ele.host} onChange={(e) => {
|
||||
setValue(`${idx}.host`, e.target.value);
|
||||
}}/>
|
||||
</>
|
||||
</Form.Item>
|
||||
<Form.Item label="端口">
|
||||
<Input placeholder="请输入端口号(例:465)" value={ele.port} onChange={(e) => {
|
||||
setValue(`${idx}.port`, Number(e.target.value));
|
||||
}}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="账号">
|
||||
<Input placeholder="请输入邮箱账号(例:xxxx@163.com)" type="text" value={ele.account} onChange={(e) => {
|
||||
setValue(`${idx}.account`, e.target.value);
|
||||
}}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="授权码">
|
||||
<Input.Password type="password" value={ele.password} onChange={(e) => {
|
||||
setValue(`${idx}.password`, e.target.value);
|
||||
}}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="发件人名称" tooltip="选填,若未填写则显示邮箱账号">
|
||||
<Input placeholder="请输入发件人名称" type="text" value={ele.name} onChange={(e) => {
|
||||
setValue(`${idx}.name`, e.target.value);
|
||||
}}/>
|
||||
</Form.Item>
|
||||
</Form>),
|
||||
}))
|
||||
: [
|
||||
{
|
||||
label: '新建帐号',
|
||||
key: '0',
|
||||
children: (<Form colon={true} labelAlign="left" layout="vertical" style={{ marginTop: 10 }}>
|
||||
|
||||
<Form.Item label="主机名">
|
||||
<>
|
||||
<Input placeholder="请输入主机名(例:smtp.163.com)" type="text" value="" onChange={(e) => {
|
||||
setValue(`0.host`, e.target.value);
|
||||
}}/>
|
||||
</>
|
||||
</Form.Item>
|
||||
<Form.Item label="端口">
|
||||
<Input placeholder="请输入端口号(例:465)" value="" onChange={(e) => {
|
||||
setValue(`0.port`, Number(e.target.value));
|
||||
}}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="账号">
|
||||
<Input placeholder="请输入邮箱账号(例:xxxx@163.com)" type="text" value="" onChange={(e) => {
|
||||
setValue(`0.account`, e.target.value);
|
||||
}}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="授权码">
|
||||
<Input.Password type="password" value="" onChange={(e) => {
|
||||
setValue(`0.password`, e.target.value);
|
||||
}}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="发件人名称" tooltip="选填,若未填写则显示邮箱账号">
|
||||
<Input placeholder="请输入发件人名称" type="text" value="" onChange={(e) => {
|
||||
setValue(`0.name`, e.target.value);
|
||||
}}/>
|
||||
</Form.Item>
|
||||
</Form>),
|
||||
},
|
||||
]}></Tabs>
|
||||
</Col>
|
||||
</Space>);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Config } from '../../../types/Config';
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, keyof import("../../../oak-app-domain").EntityDict, false, {
|
||||
config: Config;
|
||||
entity: "platform" | "system";
|
||||
entity: "system" | "platform";
|
||||
name: string;
|
||||
entityId: string;
|
||||
}>) => React.ReactElement;
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ import Cos from './cos/index';
|
|||
import Map from './map/index';
|
||||
import Live from './live/index';
|
||||
import Sms from './sms/index';
|
||||
import Email from './email/index';
|
||||
import Basic from './basic/index';
|
||||
export default function Render(props) {
|
||||
const { entity, name, currentConfig, dirty } = props.data;
|
||||
const { resetConfig, updateConfig, setValue, removeItem, cleanKey, t } = props.methods;
|
||||
const { Account: account, Cos: cos, Map: map, Live: live, Sms: sms, App: app } = currentConfig || {};
|
||||
const { Account: account, Cos: cos, Map: map, Live: live, Sms: sms, App: app, Emails: emails, } = currentConfig || {};
|
||||
return (<>
|
||||
<Affix offsetTop={64}>
|
||||
<Alert message={<div>
|
||||
|
|
@ -63,6 +64,11 @@ export default function Render(props) {
|
|||
label: '短信设置',
|
||||
children: (<Sms sms={sms || {}} setValue={(path, value) => setValue(`Sms.${path}`, value)} removeItem={(path, index) => removeItem(`Sms.${path}`, index)} cleanKey={(path, key) => cleanKey(`Sms.${path}`, key)}/>),
|
||||
},
|
||||
{
|
||||
key: '邮箱设置',
|
||||
label: '邮箱设置',
|
||||
children: (<Email emails={emails || []} setValue={(path, value) => setValue(`Emails.${path}`, value)} removeItem={(path, index) => removeItem(`Emails`, index)} cleanKey={(path, key) => cleanKey(`Emails.${path}`, key)}/>),
|
||||
},
|
||||
{
|
||||
key: '基础设置',
|
||||
label: '基础设置',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="react" />
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
|
||||
import { ReactComponentProps } from 'oak-frontend-base/lib/types/Page';
|
||||
|
|
@ -8,29 +7,12 @@ type AfterCommit = (() => void) | undefined;
|
|||
type BeforeCommit = (() => boolean | undefined | Promise<boolean | undefined>) | undefined;
|
||||
declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends keyof ED2>(props: ReactComponentProps<ED2, T2, true, {
|
||||
entity: keyof ED2;
|
||||
action?: string | undefined;
|
||||
action?: string;
|
||||
size?: ButtonProps['size'] | AmButtonProps['size'];
|
||||
block?: boolean | undefined;
|
||||
block?: boolean;
|
||||
type?: ButtonProps['type'] | AmButtonProps['type'];
|
||||
executeText?: string | undefined;
|
||||
buttonProps?: (ButtonProps & {
|
||||
color?: "default" | "success" | "warning" | "primary" | "danger" | undefined;
|
||||
fill?: "none" | "solid" | "outline" | undefined;
|
||||
size?: "small" | "middle" | "large" | "mini" | undefined;
|
||||
block?: boolean | undefined;
|
||||
loading?: boolean | "auto" | undefined;
|
||||
loadingText?: string | undefined;
|
||||
loadingIcon?: import("react").ReactNode;
|
||||
disabled?: boolean | undefined;
|
||||
onClick?: ((event: import("react").MouseEvent<HTMLButtonElement, MouseEvent>) => unknown) | undefined;
|
||||
type?: "button" | "submit" | "reset" | undefined;
|
||||
shape?: "default" | "rounded" | "rectangular" | undefined;
|
||||
children?: import("react").ReactNode;
|
||||
} & Pick<import("react").ClassAttributes<HTMLButtonElement> & import("react").ButtonHTMLAttributes<HTMLButtonElement>, "id" | "onMouseDown" | "onMouseUp" | "onTouchStart" | "onTouchEnd"> & {
|
||||
className?: string | undefined;
|
||||
style?: (import("react").CSSProperties & Partial<Record<"--text-color" | "--background-color" | "--border-radius" | "--border-width" | "--border-style" | "--border-color", string>>) | undefined;
|
||||
tabIndex?: number | undefined;
|
||||
} & import("react").AriaAttributes) | undefined;
|
||||
executeText?: string;
|
||||
buttonProps?: ButtonProps & AmButtonProps;
|
||||
afterCommit?: AfterCommit;
|
||||
beforeCommit?: BeforeCommit;
|
||||
}>) => React.ReactElement;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export default OakComponent({
|
|||
async sendCaptcha() {
|
||||
const { mobile } = this.state;
|
||||
try {
|
||||
const result = await this.features.token.sendCaptcha(mobile, 'login');
|
||||
const result = await this.features.token.sendCaptcha('mobile', mobile, 'login');
|
||||
// 显示返回消息
|
||||
this.setMessage({
|
||||
type: 'success',
|
||||
|
|
@ -85,9 +85,9 @@ export default OakComponent({
|
|||
},
|
||||
async loginByMobile() {
|
||||
const { eventLoggedIn, callback } = this.props;
|
||||
const { mobile, password, captcha } = this.state;
|
||||
const { mobile, captcha } = this.state;
|
||||
try {
|
||||
await this.features.token.loginByMobile(mobile, password, captcha);
|
||||
await this.features.token.loginByMobile(mobile, captcha);
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
import { EmailConfig, MfwConfig, PfwConfig, SmsConfig } from "../../../entities/Passport";
|
||||
import { EntityDict } from "../../../oak-app-domain";
|
||||
import '@wangeditor/editor/dist/css/style.css';
|
||||
export default function Email(props: {
|
||||
passport: EntityDict['passport']['OpSchema'] & {
|
||||
stateColor: string;
|
||||
};
|
||||
t: (k: string, params?: any) => string;
|
||||
changeEnabled: (enabled: boolean) => void;
|
||||
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig, path: string, value: any) => void;
|
||||
}): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Space, Switch, Alert, Typography, Form, Input, Radio, Tag } from 'antd';
|
||||
import Styles from './web.module.less';
|
||||
import '@wangeditor/editor/dist/css/style.css'; // 引入 css
|
||||
import { Editor, Toolbar } from "@wangeditor/editor-for-react";
|
||||
const { TextArea } = Input;
|
||||
const { Text } = Typography;
|
||||
export default function Email(props) {
|
||||
const { passport, t, changeEnabled, updateConfig } = props;
|
||||
const { id, type, enabled, stateColor } = passport;
|
||||
const config = passport.config || {};
|
||||
const [subject, setSubject] = useState(config?.subject || '');
|
||||
const [eContentType, setEContentType] = useState('text');
|
||||
const [text, setText] = useState(config?.text || '');
|
||||
const [html, setHtml] = useState(config?.html || '');
|
||||
const [emailCodeDuration, setEmailCodeDuration] = useState(config?.codeDuration || '');
|
||||
const [emailDigit, setEmailDigit] = useState(config?.digit || '');
|
||||
// editor 实例
|
||||
const [editor, setEditor] = useState(null); // TS 语法
|
||||
// 工具栏配置
|
||||
const toolbarConfig = {}; // TS 语法
|
||||
// 编辑器配置
|
||||
const editorConfig = {
|
||||
autoFocus: false,
|
||||
placeholder: '请输入内容...',
|
||||
};
|
||||
// 及时销毁 editor
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (editor == null)
|
||||
return;
|
||||
editor.destroy();
|
||||
setEditor(null);
|
||||
};
|
||||
}, [editor]);
|
||||
useEffect(() => {
|
||||
setSubject(config?.subject || '');
|
||||
setText(config?.text || '');
|
||||
setHtml(config?.html || '');
|
||||
setEmailCodeDuration(config?.codeDuration || '');
|
||||
setEmailDigit(config?.digit || '');
|
||||
if (config?.html) {
|
||||
setEContentType('html');
|
||||
}
|
||||
else {
|
||||
setEContentType('text');
|
||||
}
|
||||
}, [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>
|
||||
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} style={{ maxWidth: 900, marginTop: 16 }}>
|
||||
<Form.Item label="账号">
|
||||
<Input type="text" value={config.account} disabled={true}/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{enabled &&
|
||||
<div>
|
||||
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} style={{ maxWidth: 900, marginTop: 16 }}>
|
||||
<Form.Item label="邮件主题">
|
||||
<Input placeholder="请输入邮件主题" type="text" value={subject} onChange={(e) => {
|
||||
setSubject(e.target.value);
|
||||
}} onBlur={() => {
|
||||
if (subject !== config?.subject) {
|
||||
updateConfig(id, config, 'subject', subject);
|
||||
}
|
||||
}}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="邮件内容模板">
|
||||
<>
|
||||
<Space size={8} style={{ marginBottom: 8 }}>
|
||||
<Radio.Group onChange={(e) => setEContentType(e.target.value)} value={eContentType}>
|
||||
<Radio.Button value="text">纯文本</Radio.Button>
|
||||
<Radio.Button value="html">HTML</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Alert message={<div>
|
||||
<span>请使用</span>
|
||||
<Text mark> ${'{code}'}</Text>
|
||||
<span>作为验证码占位符,</span>
|
||||
<Text mark> ${'{duration}'}</Text>
|
||||
<span>作为验证码有效时间占位符(包含单位分钟)。</span>
|
||||
</div>} type="info"/>
|
||||
</Space>
|
||||
{eContentType === 'text' ? (<TextArea rows={6} value={text} onChange={(e) => {
|
||||
setText(e.target.value);
|
||||
}} onBlur={() => {
|
||||
if (text !== config?.text) {
|
||||
updateConfig(id, config, 'text', text);
|
||||
}
|
||||
}}/>) : (<div style={{ border: '1px solid #ccc' }}>
|
||||
<Toolbar editor={editor} defaultConfig={toolbarConfig} mode="default" style={{ borderBottom: '1px solid #ccc' }}/>
|
||||
<Editor defaultConfig={editorConfig} value={html} onCreated={setEditor} onChange={editor => {
|
||||
setHtml(editor.getHtml());
|
||||
updateConfig(id, config, 'html', editor.getHtml());
|
||||
}} mode="default" style={{ height: '260px', overflowY: 'hidden' }}/>
|
||||
</div>)}
|
||||
</>
|
||||
</Form.Item>
|
||||
<Form.Item label="验证码有效时间" tooltip="邮箱验证码发送有效时间,不填为5分钟">
|
||||
<Input placeholder="请输入验证码有效时间" type="number" value={emailCodeDuration} min={0} onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
if (val) {
|
||||
setEmailCodeDuration(Number(val));
|
||||
}
|
||||
else {
|
||||
setEmailCodeDuration('');
|
||||
}
|
||||
}} onBlur={() => {
|
||||
if (Number(emailCodeDuration) > 0) {
|
||||
updateConfig(id, config, 'codeDuration', emailCodeDuration);
|
||||
}
|
||||
else {
|
||||
updateConfig(id, config, 'codeDuration', undefined);
|
||||
}
|
||||
}} suffix="分钟"/>
|
||||
</Form.Item>
|
||||
<Form.Item label="验证码位数" tooltip="邮箱验证码位数,可设置4~8位">
|
||||
<Input placeholder="请输入验证码有效位数" type="number" value={emailDigit} min={4} max={8} onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
if (val) {
|
||||
setEmailDigit(Number(val));
|
||||
}
|
||||
else {
|
||||
setEmailDigit('');
|
||||
}
|
||||
}} onBlur={() => {
|
||||
if (Number(emailDigit) > 0) {
|
||||
updateConfig(id, config, 'digit', emailDigit);
|
||||
}
|
||||
else {
|
||||
updateConfig(id, config, 'digit', undefined);
|
||||
}
|
||||
}}/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>}
|
||||
</div>);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { EntityDict } from "../../oak-app-domain";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "passport", true, {
|
||||
systemId: string;
|
||||
systemName: string;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
import { cloneDeep, isEqual, set, } from "oak-domain/lib/utils/lodash";
|
||||
export default OakComponent({
|
||||
entity: 'passport',
|
||||
isList: true,
|
||||
projection: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
config: 1,
|
||||
systemId: 1,
|
||||
enabled: 1,
|
||||
},
|
||||
properties: {
|
||||
systemId: '',
|
||||
systemName: '',
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
filter() {
|
||||
const { systemId } = this.props;
|
||||
return {
|
||||
systemId,
|
||||
};
|
||||
}
|
||||
}
|
||||
],
|
||||
// sorters: [
|
||||
// {
|
||||
// sorter: {
|
||||
// $attr: {
|
||||
// enabled: 1,
|
||||
// },
|
||||
// $direction: 'desc',
|
||||
// }
|
||||
// }
|
||||
// ],
|
||||
formData({ data }) {
|
||||
const passports = data.map((ele) => {
|
||||
const stateColor = ele.type ? this.features.style.getColor('passport', 'type', ele.type) : '#00BFFF';
|
||||
let appIdStr;
|
||||
if (ele.type === 'wechatMpForWeb') {
|
||||
appIdStr = this.getAppIdStr('wechatMp', ele.config?.appId);
|
||||
}
|
||||
else if (ele.type === 'wechatPublicForWeb') {
|
||||
appIdStr = this.getAppIdStr('wechatPublic', ele.config?.appId);
|
||||
}
|
||||
return {
|
||||
...ele,
|
||||
appIdStr,
|
||||
stateColor,
|
||||
};
|
||||
});
|
||||
return {
|
||||
passports,
|
||||
};
|
||||
},
|
||||
data: {},
|
||||
lifetimes: {},
|
||||
methods: {
|
||||
updateConfig(id, config, path, value) {
|
||||
const newConfig = cloneDeep(config);
|
||||
set(newConfig, path, value);
|
||||
if (path === 'mockSend' && !value) {
|
||||
if (!newConfig.templateName || newConfig.templateName === '') {
|
||||
this.setMessage({
|
||||
type: 'warning',
|
||||
content: '短信登录未配置模板名称,将无法正常使用短信登录'
|
||||
});
|
||||
}
|
||||
else if (!newConfig.defaultOrigin) {
|
||||
this.setMessage({
|
||||
type: 'warning',
|
||||
content: '短信登录未选择默认渠道,将无法正常使用短信登录'
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (path === 'appId' && (!value || value === '')) {
|
||||
this.setMessage({
|
||||
type: 'warning',
|
||||
content: '未填写appId,该登录方式将无法正常使用'
|
||||
});
|
||||
}
|
||||
this.updateItem({
|
||||
config: newConfig,
|
||||
}, id);
|
||||
},
|
||||
checkConfrim() {
|
||||
const { passports } = this.state;
|
||||
let warnings = [];
|
||||
for (const passport of passports) {
|
||||
const { type, config, enabled, id } = passport;
|
||||
if (enabled) {
|
||||
//检查启用的passport对应的config是否设置
|
||||
switch (type) {
|
||||
case 'sms':
|
||||
if (!config.mockSend) {
|
||||
if (!config.templateName || config.templateName === '') {
|
||||
warnings.push({
|
||||
id,
|
||||
type,
|
||||
tip: '短信登录未配置验证码模板名称',
|
||||
});
|
||||
}
|
||||
if (!config.defaultOrigin) {
|
||||
const smsWarning = warnings.find((ele) => ele.id === id);
|
||||
if (smsWarning) {
|
||||
Object.assign(smsWarning, { tip: '短信登录未选择默认渠道且未配置验证码模板名称' });
|
||||
}
|
||||
else {
|
||||
warnings.push({
|
||||
id,
|
||||
type,
|
||||
tip: '短信登录未选择默认渠道',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'email':
|
||||
if (!config.account || config.account === '') {
|
||||
warnings.push({
|
||||
id,
|
||||
type,
|
||||
tip: '邮箱登录未指定邮箱账号',
|
||||
});
|
||||
}
|
||||
else if (!config.subject || config.subject === '') {
|
||||
const emailWarning = warnings.find((ele) => ele.id === id);
|
||||
if (emailWarning) {
|
||||
Object.assign(emailWarning, { tip: emailWarning.tip + '、邮件主题' });
|
||||
}
|
||||
else {
|
||||
warnings.push({
|
||||
id,
|
||||
type,
|
||||
tip: '邮箱登录未配置邮件主题',
|
||||
});
|
||||
}
|
||||
}
|
||||
else if ((!config.text || config.text === '' || !config.text?.includes('${code}')) &&
|
||||
(!config.html || config.html === '' || !config.html?.includes('${code}'))) {
|
||||
const emailWarning = warnings.find((ele) => ele.id === id);
|
||||
if (emailWarning) {
|
||||
Object.assign(emailWarning, { tip: emailWarning.tip + '、邮件内容模板' });
|
||||
}
|
||||
else {
|
||||
warnings.push({
|
||||
id,
|
||||
type,
|
||||
tip: '邮箱登录未配置邮件内容模板',
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'wechatPublicForWeb':
|
||||
if (!config.appId || config.appId === '') {
|
||||
warnings.push({
|
||||
id,
|
||||
type,
|
||||
tip: '公众号授权登录未选择appId',
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'wechatMpForWeb':
|
||||
if (!config.appId || config.appId === '') {
|
||||
warnings.push({
|
||||
id,
|
||||
type,
|
||||
tip: '小程序授权登录未选择appId',
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return warnings;
|
||||
},
|
||||
async myConfirm(ids) {
|
||||
//存在不完整的配置更新,保存更新时将相应的applicationPassport移除
|
||||
await this.features.cache.exec('removeApplicationPassportsByPIds', { passportIds: ids });
|
||||
await this.execute();
|
||||
},
|
||||
arraysAreEqual(first, second) {
|
||||
if (first?.length !== second?.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < first?.length; ++i) {
|
||||
if (!isEqual(first[i], second[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getAppIdStr(type, appId) {
|
||||
const systemId = this.features.application.getApplication().systemId;
|
||||
const [application] = this.features.cache.get('application', {
|
||||
data: {
|
||||
id: 1,
|
||||
name: 1,
|
||||
},
|
||||
filter: {
|
||||
systemId,
|
||||
config: {
|
||||
appId,
|
||||
},
|
||||
type,
|
||||
}
|
||||
});
|
||||
return application?.name ? appId + ' (applicationName:' + application.name + ')' : appId;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"login": "登录"
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import React from "react";
|
||||
import { EmailConfig, MfwConfig, PfwConfig, SmsConfig } from "../../../entities/Passport";
|
||||
import { EntityDict } from "../../../oak-app-domain";
|
||||
export default function Sms(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, path: string, value: any) => void;
|
||||
}): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Switch, Form, Input, Select, Tag } from 'antd';
|
||||
import Styles from './web.module.less';
|
||||
export default function Sms(props) {
|
||||
const { passport, t, changeEnabled, updateConfig } = props;
|
||||
const { id, type, enabled, stateColor } = passport;
|
||||
const config = passport.config || {};
|
||||
const [templateName, setTemplateName] = useState(config?.templateName || '');
|
||||
const [smsCodeDuration, setSmsCodeDuration] = useState(config?.codeDuration || '');
|
||||
const [smsDigit, setSmsDigit] = useState(config?.digit || '');
|
||||
useEffect(() => {
|
||||
setTemplateName(config?.templateName || '');
|
||||
setSmsCodeDuration(config?.codeDuration || '');
|
||||
setSmsDigit(config?.digit || '');
|
||||
}, [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="开启模拟发送短信,发短信不会调用api">
|
||||
<Switch checkedChildren="是" unCheckedChildren="否" checked={config?.mockSend} onChange={(checked) => {
|
||||
updateConfig(id, config, 'mockSend', checked);
|
||||
}}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="默认渠道" tooltip="发送短信渠道,如阿里云、腾讯云、天翼云">
|
||||
<>
|
||||
<Select placeholder="请选择渠道" value={config?.defaultOrigin} style={{ width: 120 }} onChange={(value) => {
|
||||
updateConfig(id, config, 'defaultOrigin', value);
|
||||
}} options={[
|
||||
{ value: 'ali', label: '阿里云' },
|
||||
{ value: 'tencent', label: '腾讯云' },
|
||||
{ value: 'ctyun', label: '天翼云' },
|
||||
]}/>
|
||||
</>
|
||||
</Form.Item>
|
||||
<Form.Item label="验证码模版名" tooltip="短信验证码模版名">
|
||||
<Input placeholder="请输入验证码模版名" type="text" value={templateName} onChange={(e) => {
|
||||
setTemplateName(e.target.value);
|
||||
}} onBlur={() => {
|
||||
if (templateName !== config?.templateName) {
|
||||
updateConfig(id, config, 'templateName', templateName);
|
||||
}
|
||||
}}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="验证码有效时间" tooltip="短信验证码发送有效时间,不填为1分钟">
|
||||
<Input placeholder="请输入验证码有效时间" type="number" value={smsCodeDuration} min={0} onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
if (val) {
|
||||
setSmsCodeDuration(Number(val));
|
||||
}
|
||||
else {
|
||||
setSmsCodeDuration('');
|
||||
}
|
||||
}} onBlur={() => {
|
||||
if (Number(smsCodeDuration) > 0) {
|
||||
updateConfig(id, config, 'codeDuration', smsCodeDuration);
|
||||
}
|
||||
else {
|
||||
updateConfig(id, config, 'codeDuration', undefined);
|
||||
}
|
||||
}} suffix="分钟"/>
|
||||
</Form.Item>
|
||||
<Form.Item label="验证码位数" tooltip="短信验证码位数,可设置4~8位">
|
||||
<Input placeholder="请输入验证码有效位数" type="number" value={smsDigit} min={4} max={8} onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
if (val) {
|
||||
setSmsDigit(Number(val));
|
||||
}
|
||||
else {
|
||||
setSmsDigit('');
|
||||
}
|
||||
}} onBlur={() => {
|
||||
if (Number(smsDigit) > 0) {
|
||||
updateConfig(id, config, 'digit', smsDigit);
|
||||
}
|
||||
else {
|
||||
updateConfig(id, config, 'digit', undefined);
|
||||
}
|
||||
}}/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>}
|
||||
</div>);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import { WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../oak-app-domain';
|
||||
import { SmsConfig, EmailConfig, PfwConfig, MfwConfig } from '../../entities/Passport';
|
||||
export default function render(props: WebComponentProps<EntityDict, 'passport', true, {
|
||||
passports: (EntityDict['passport']['OpSchema'] & {
|
||||
appIdStr: string;
|
||||
stateColor: string;
|
||||
})[];
|
||||
systemId: string;
|
||||
systemName: string;
|
||||
}, {
|
||||
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig, path: string, value: any) => void;
|
||||
checkConfrim: () => {
|
||||
id: string;
|
||||
type: EntityDict['passport']['Schema']['type'];
|
||||
tip: string;
|
||||
}[];
|
||||
myConfirm: (ids: string[]) => void;
|
||||
}>): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button, Space, Switch, Affix, Alert, Typography, Modal, Divider, Tag, Row } from 'antd';
|
||||
import Styles from './web.pc.module.less';
|
||||
import classNames from 'classnames';
|
||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||
import Sms from './sms';
|
||||
import Email from './email';
|
||||
import WechatPublicForWeb from './wechatPublicForWeb';
|
||||
import WechatMpForWeb from './wechatMpForWeb';
|
||||
import WechatMp from './wechatMp';
|
||||
import WechatPublic from './wechatPublic';
|
||||
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) => {
|
||||
changeEnabled(checked);
|
||||
}}/>
|
||||
</div>);
|
||||
}
|
||||
export default function render(props) {
|
||||
const { data, methods } = props;
|
||||
const { oakFullpath, oakExecutable, oakExecuting, oakDirty, oakLoading, systemId, passports, systemName, } = data;
|
||||
const { clean, execute, t, updateItem, updateConfig, checkConfrim, myConfirm } = methods;
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [newType, setNewType] = useState(undefined);
|
||||
const showConfirm = (warnings) => {
|
||||
confirm({
|
||||
title: '确定保存当前更新吗?',
|
||||
icon: <ExclamationCircleFilled />,
|
||||
width: 540,
|
||||
content: <Space direction='vertical'>
|
||||
<div>当前登录方式配置存在以下问题可能影响登录:</div>
|
||||
{warnings.map((ele) => {
|
||||
return (<div key={ele.id}>
|
||||
<div style={{ width: 200 }}>
|
||||
<Divider orientation="left">{t(`passport:v.type.${ele.type}`)}{t('login')}</Divider>
|
||||
</div>
|
||||
<div style={{ fontSize: 13 }}>{ele.tip}</div>
|
||||
</div>);
|
||||
})}
|
||||
</Space>,
|
||||
async onOk() {
|
||||
const ids = warnings.map((ele) => ele.id);
|
||||
await myConfirm(ids);
|
||||
},
|
||||
onCancel() {
|
||||
},
|
||||
});
|
||||
};
|
||||
return (<>
|
||||
<Affix offsetTop={64}>
|
||||
<Alert message={<div>
|
||||
<text>
|
||||
您正在更新
|
||||
<Typography.Text keyboard>
|
||||
system
|
||||
</Typography.Text>
|
||||
对象
|
||||
<Typography.Text keyboard>
|
||||
{systemName}
|
||||
</Typography.Text>
|
||||
的登录配置,请谨慎操作
|
||||
</text>
|
||||
</div>} type="info" showIcon action={<Space size={12}>
|
||||
{/* <Button
|
||||
disabled={oakLoading || oakExecuting}
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
创建
|
||||
</Button> */}
|
||||
<Button disabled={!oakDirty} type="primary" danger onClick={() => clean()}>
|
||||
重置
|
||||
</Button>
|
||||
<Button disabled={!oakDirty} type="primary" onClick={async () => {
|
||||
const warnings = checkConfrim();
|
||||
if (warnings && warnings.length > 0) {
|
||||
showConfirm(warnings);
|
||||
}
|
||||
else {
|
||||
await execute();
|
||||
}
|
||||
}}>
|
||||
确定
|
||||
</Button>
|
||||
</Space>}/>
|
||||
</Affix>
|
||||
<Row>
|
||||
<div className={Styles.tips}>
|
||||
<div>* 如需启用邮箱登录,请先前往配置管理邮箱设置,创建系统邮箱,并完成相关配置</div>
|
||||
<div>* 如需启用小程序授权登录,请先前往应用管理,创建小程序application,并完成基础配置</div>
|
||||
<div>* 如需启用公众号授权登录,请先前往应用管理,创建是服务号的公众号application,并完成基础配置</div>
|
||||
</div>
|
||||
</Row>
|
||||
{passports && passports.map((passport) => {
|
||||
switch (passport.type) {
|
||||
case 'sms':
|
||||
return (<Sms key={passport.id} passport={passport} t={t} changeEnabled={(enabled) => {
|
||||
updateItem({
|
||||
enabled,
|
||||
}, passport.id);
|
||||
}} updateConfig={updateConfig}/>);
|
||||
case 'email':
|
||||
return (<Email key={passport.id} passport={passport} t={t} changeEnabled={(enabled) => {
|
||||
updateItem({
|
||||
enabled,
|
||||
}, passport.id);
|
||||
}} updateConfig={updateConfig}/>);
|
||||
case 'wechatPublicForWeb':
|
||||
return (<WechatPublicForWeb key={passport.id} passport={passport} appIdStr={passport?.appIdStr} t={t} changeEnabled={(enabled) => {
|
||||
updateItem({
|
||||
enabled,
|
||||
}, passport.id);
|
||||
}} updateConfig={updateConfig}/>);
|
||||
case 'wechatMpForWeb':
|
||||
return (<WechatMpForWeb key={passport.id} passport={passport} appIdStr={passport?.appIdStr} t={t} changeEnabled={(enabled) => {
|
||||
updateItem({
|
||||
enabled,
|
||||
}, passport.id);
|
||||
}} updateConfig={updateConfig}/>);
|
||||
case 'wechatMp':
|
||||
return (<WechatMp key={passport.id} passport={passport} t={t} changeEnabled={(enabled) => {
|
||||
updateItem({
|
||||
enabled,
|
||||
}, passport.id);
|
||||
}}/>);
|
||||
case 'wechatPublic':
|
||||
return (<WechatPublic key={passport.id} passport={passport}
|
||||
// publicAppIds={publicAppIds}
|
||||
t={t} changeEnabled={(enabled) => {
|
||||
updateItem({
|
||||
enabled,
|
||||
}, passport.id);
|
||||
}}/>);
|
||||
default:
|
||||
return (<AppView key={passport.id} passport={passport} t={t} changeEnabled={(enabled) => {
|
||||
updateItem({
|
||||
enabled,
|
||||
}, passport.id);
|
||||
}} updateConfig={updateConfig}/>);
|
||||
}
|
||||
})}
|
||||
|
||||
{/* <Modal
|
||||
title="新建短信登录配置"
|
||||
open={createOpen}
|
||||
onOk={() => {
|
||||
methods.addItem({
|
||||
type: newType,
|
||||
systemId,
|
||||
enabled: false,
|
||||
})
|
||||
setCreateOpen(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setNewType(undefined);
|
||||
setCreateOpen(false);
|
||||
}}
|
||||
destroyOnClose
|
||||
>
|
||||
</Modal> */}
|
||||
</>);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
.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;
|
||||
}
|
||||
|
||||
.tips {
|
||||
border: 1px solid var(--oak-border-color);
|
||||
border-radius: 12px;
|
||||
color: var(--oak-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
padding: 10px;
|
||||
margin: 8px 0px;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import React from "react";
|
||||
import { EntityDict } from "../../../oak-app-domain";
|
||||
export default function wechatMp(props: {
|
||||
passport: EntityDict['passport']['OpSchema'] & {
|
||||
stateColor: string;
|
||||
};
|
||||
t: (k: string, params?: any) => string;
|
||||
changeEnabled: (enabled: boolean) => void;
|
||||
}): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import React from "react";
|
||||
import { Switch, Tag } from 'antd';
|
||||
import Styles from './web.module.less';
|
||||
export default function wechatMp(props) {
|
||||
const { passport, t, changeEnabled, } = props;
|
||||
const { id, type, enabled, stateColor } = passport;
|
||||
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
|
||||
// disabled={!(mpAppIds && mpAppIds.length > 0)}
|
||||
checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} onChange={(checked) => {
|
||||
changeEnabled(checked);
|
||||
}}/>
|
||||
{/* </Tooltip> */}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
import { EmailConfig, MfwConfig, PfwConfig, SmsConfig } from "../../../entities/Passport";
|
||||
import { EntityDict } from "../../../oak-app-domain";
|
||||
export default function wechatMpForWeb(props: {
|
||||
passport: EntityDict['passport']['OpSchema'] & {
|
||||
stateColor: string;
|
||||
};
|
||||
appIdStr: string;
|
||||
t: (k: string, params?: any) => string;
|
||||
changeEnabled: (enabled: boolean) => void;
|
||||
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig, path: string, value: any) => void;
|
||||
}): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import React from "react";
|
||||
import { Switch, Form, Tag, Input } from 'antd';
|
||||
import Styles from './web.module.less';
|
||||
export default function wechatMpForWeb(props) {
|
||||
const { passport, appIdStr, 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
|
||||
// disabled={!(mpAppIds && mpAppIds.length > 0)}
|
||||
checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} onChange={(checked) => {
|
||||
changeEnabled(checked);
|
||||
}}/>
|
||||
{/* </Tooltip> */}
|
||||
</div>
|
||||
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} style={{ maxWidth: 900, marginTop: 16 }}>
|
||||
<Form.Item label="appId">
|
||||
<Input value={appIdStr} disabled={true}/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
</div>);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import React from "react";
|
||||
import { EntityDict } from "../../../oak-app-domain";
|
||||
export default function wechatPublic(props: {
|
||||
passport: EntityDict['passport']['OpSchema'] & {
|
||||
stateColor: string;
|
||||
};
|
||||
t: (k: string, params?: any) => string;
|
||||
changeEnabled: (enabled: boolean) => void;
|
||||
}): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import React from "react";
|
||||
import { Switch, Tag } from 'antd';
|
||||
import Styles from './web.module.less';
|
||||
export default function wechatPublic(props) {
|
||||
const { passport, t, changeEnabled, } = props;
|
||||
const { id, type, enabled, stateColor } = passport;
|
||||
return (<div className={Styles.item}>
|
||||
<div className={Styles.title}>
|
||||
<Tag color={stateColor}>{t(`passport:v.type.${type}`)}</Tag>
|
||||
{/* <Tooltip title={(publicAppIds && publicAppIds.length > 0) ? '' : '如需启用公众号登录,请先前往应用管理,创建是服务号的公众号application,并完成基础配置'}> */}
|
||||
<Switch
|
||||
// disabled={!(publicAppIds && publicAppIds.length > 0)}
|
||||
checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} onChange={(checked) => {
|
||||
changeEnabled(checked);
|
||||
}}/>
|
||||
{/* </Tooltip> */}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
import { EmailConfig, MfwConfig, PfwConfig, SmsConfig } from "../../../entities/Passport";
|
||||
import { EntityDict } from "../../../oak-app-domain";
|
||||
export default function wechatPublicForWeb(props: {
|
||||
passport: EntityDict['passport']['OpSchema'] & {
|
||||
stateColor: string;
|
||||
};
|
||||
appIdStr: string;
|
||||
t: (k: string, params?: any) => string;
|
||||
changeEnabled: (enabled: boolean) => void;
|
||||
updateConfig: (id: string, config: SmsConfig | EmailConfig | PfwConfig | MfwConfig, path: string, value: any) => void;
|
||||
}): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import React from "react";
|
||||
import { Switch, Form, Tag, Input } from 'antd';
|
||||
import Styles from './web.module.less';
|
||||
export default function wechatPublicForWeb(props) {
|
||||
const { passport, appIdStr, 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={(publicAppIds && publicAppIds.length > 0) ? '' : '如需启用公众号授权登录,请先前往应用管理,创建是服务号的公众号application,并完成基础配置'}> */}
|
||||
<Switch
|
||||
// disabled={!(publicAppIds && publicAppIds.length > 0)}
|
||||
checkedChildren="开启" unCheckedChildren="关闭" checked={enabled} onChange={(checked) => {
|
||||
changeEnabled(checked);
|
||||
}}/>
|
||||
{/* </Tooltip> */}
|
||||
</div>
|
||||
|
||||
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} style={{ maxWidth: 900, marginTop: 16 }}>
|
||||
<Form.Item label="appId">
|
||||
<Input value={appIdStr} disabled={true}/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
</div>);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -4,5 +4,6 @@
|
|||
"config": "配置管理",
|
||||
"style": "样式管理",
|
||||
"application-list": "应用管理",
|
||||
"smsTemplate-list": "短信模板管理"
|
||||
"smsTemplate-list": "短信模板管理",
|
||||
"login": "登录管理"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ import StyleUpsert from '../../config/style/platform';
|
|||
import DomainList from '../../domain/list';
|
||||
import SmsTemplateList from '../../messageTypeSmsTemplate/tab';
|
||||
import ApplicationList from '../application';
|
||||
import Passport from '../passport';
|
||||
import Styles from './web.pc.module.less';
|
||||
export default function Render(props) {
|
||||
const { id, config, oakFullpath, name, style } = props.data;
|
||||
const { t, } = props.methods;
|
||||
if (id && oakFullpath) {
|
||||
return (<div className={Styles.container}>
|
||||
<Tabs tabPosition="left" items={[
|
||||
<Tabs tabPosition="left" style={{ minHeight: '50vh' }} items={[
|
||||
{
|
||||
label: (<div className={Styles.tabLabel}>
|
||||
{t('detail')}
|
||||
|
|
@ -55,6 +56,14 @@ export default function Render(props) {
|
|||
key: 'smsTemplate-list',
|
||||
children: (<SmsTemplateList oakPath={`$system-messageTypeSmsTemplateList-${id}`} oakAutoUnmount={true} systemId={id}/>),
|
||||
},
|
||||
{
|
||||
label: (<div className={Styles.tabLabel}>
|
||||
{t('login')}
|
||||
</div>),
|
||||
key: 'passport-list',
|
||||
destroyInactiveTabPane: true,
|
||||
children: (<Passport oakPath={`$system-passport`} systemId={id} systemName={name}/>),
|
||||
},
|
||||
]}/>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, keyof import("../../../oak-app-domain").EntityDict, false, {
|
||||
systemId: string;
|
||||
systemName: string;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
export default OakComponent({
|
||||
isList: false,
|
||||
formData({ data }) {
|
||||
return {};
|
||||
},
|
||||
properties: {
|
||||
systemId: '',
|
||||
systemName: '',
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
import { WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
export default function Render(props: WebComponentProps<EntityDict, 'passport', false, {
|
||||
systemId: string;
|
||||
systemName: string;
|
||||
}>): React.JSX.Element | null;
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
import { Tabs } from 'antd';
|
||||
import PassportList from '../../passport';
|
||||
import ApplicationPassport from '../../applicationPassport';
|
||||
export default function Render(props) {
|
||||
const { systemId, oakFullpath, systemName, } = props.data;
|
||||
const { t, } = props.methods;
|
||||
const items = [
|
||||
{
|
||||
key: 'passport',
|
||||
label: '配置',
|
||||
// destroyInactiveTabPane: true,
|
||||
children: <PassportList oakPath={`${oakFullpath}/config`} systemId={systemId} systemName={systemName}/>
|
||||
},
|
||||
{
|
||||
key: 'application',
|
||||
label: '应用',
|
||||
destroyInactiveTabPane: true,
|
||||
children: <ApplicationPassport oakPath={`${oakFullpath}/applicationPassport`} systemId={systemId}/>
|
||||
},
|
||||
];
|
||||
if (oakFullpath) {
|
||||
return (<>
|
||||
<Tabs items={items}/>
|
||||
</>);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -312,7 +312,7 @@ export default OakComponent({
|
|||
async sendCaptcha() {
|
||||
const { mobile } = this.state;
|
||||
try {
|
||||
const result = await this.features.token.sendCaptcha(mobile, 'login');
|
||||
const result = await this.features.token.sendCaptcha('mobile', mobile, 'login');
|
||||
// 显示返回消息
|
||||
this.setMessage({
|
||||
type: 'success',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { EntityDict } from "../../../../oak-app-domain";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, keyof EntityDict, false, {
|
||||
disabled: string;
|
||||
url: string;
|
||||
callback: (() => void) | undefined;
|
||||
setLoginMode: (value: string) => void;
|
||||
digit: number;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import { LOCAL_STORAGE_KEYS } from '../../../../config/constants';
|
||||
import { isCaptcha, isEmail } from "oak-domain/lib/utils/validator";
|
||||
const SEND_KEY = LOCAL_STORAGE_KEYS.captchaSendAt;
|
||||
const SEND_CAPTCHA_LATENCY = process.env.NODE_ENV === 'development' ? 10 : 60;
|
||||
export default OakComponent({
|
||||
isList: false,
|
||||
projection: {
|
||||
id: 1,
|
||||
email: 1,
|
||||
userId: 1,
|
||||
},
|
||||
data: {
|
||||
counter: 0,
|
||||
loading: false,
|
||||
lastSendAt: undefined,
|
||||
email: '',
|
||||
captcha: '',
|
||||
validEmail: false,
|
||||
validCaptcha: false,
|
||||
allowSubmit: false,
|
||||
},
|
||||
properties: {
|
||||
disabled: '',
|
||||
url: '', // 登录系统之后要返回的页面
|
||||
callback: undefined, // 登录成功回调,排除微信登录方式
|
||||
setLoginMode: (value) => undefined,
|
||||
digit: 4, //验证码位数
|
||||
},
|
||||
formData({ features, props }) {
|
||||
const { lastSendAt } = this.state;
|
||||
let counter = 0;
|
||||
if (typeof lastSendAt === 'number') {
|
||||
const now = Date.now();
|
||||
counter = Math.max(SEND_CAPTCHA_LATENCY - Math.ceil((now - lastSendAt) / 1000), 0);
|
||||
if (counter > 0) {
|
||||
this.counterHandler = setTimeout(() => this.reRender(), 1000);
|
||||
}
|
||||
else if (this.counterHandler) {
|
||||
clearTimeout(this.counterHandler);
|
||||
this.counterHandler = undefined;
|
||||
}
|
||||
}
|
||||
return {
|
||||
counter,
|
||||
};
|
||||
},
|
||||
lifetimes: {},
|
||||
listeners: {
|
||||
'validEmail,validCaptcha'(prev, next) {
|
||||
const { allowSubmit } = this.state;
|
||||
if (allowSubmit) {
|
||||
if (!(next.validEmail && next.validCaptcha)) {
|
||||
this.setState({
|
||||
allowSubmit: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (next.validEmail && next.validCaptcha) {
|
||||
this.setState({
|
||||
allowSubmit: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async sendCaptcha() {
|
||||
const { email } = this.state;
|
||||
try {
|
||||
const result = await this.features.token.sendCaptcha('email', email, 'login');
|
||||
// 显示返回消息
|
||||
this.setMessage({
|
||||
type: 'success',
|
||||
content: result,
|
||||
});
|
||||
const lastSendAt = Date.now();
|
||||
await this.save(SEND_KEY, lastSendAt);
|
||||
this.setState({
|
||||
lastSendAt,
|
||||
}, () => this.reRender());
|
||||
}
|
||||
catch (err) {
|
||||
this.setMessage({
|
||||
type: 'error',
|
||||
content: err.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
async loginByEmail() {
|
||||
const { url, callback } = this.props;
|
||||
const { email, captcha } = this.state;
|
||||
try {
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
await this.features.token.loginByEmail(email, captcha);
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (callback) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (url) {
|
||||
this.redirectTo({
|
||||
url,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
this.setMessage({
|
||||
type: 'error',
|
||||
content: err.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
inputChange(type, value) {
|
||||
const { digit } = this.props;
|
||||
switch (type) {
|
||||
case 'email':
|
||||
const validEmail = !!isEmail(value);
|
||||
this.setState({
|
||||
email: value,
|
||||
validEmail,
|
||||
});
|
||||
break;
|
||||
case 'captcha':
|
||||
const validCaptcha = !!isCaptcha(value, digit);
|
||||
this.setState({
|
||||
captcha: value,
|
||||
validCaptcha
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
inputChangeMp(event) {
|
||||
const { detail, target: { dataset }, } = event;
|
||||
const { attr } = dataset;
|
||||
const { value } = detail;
|
||||
this.inputChange(attr, value);
|
||||
},
|
||||
changeLoginMp(e) {
|
||||
const { setLoginMode } = this.props;
|
||||
const { value } = e.currentTarget.dataset;
|
||||
setLoginMode && setLoginMode(value);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"navigationBarTitleText": "登录",
|
||||
"enablePullDownRefresh": false,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/** index.wxss **/
|
||||
@import "../../../../config/styles/mp/index.less";
|
||||
@import "../../../../config/styles/mp/mixins.less";
|
||||
|
||||
.page-body {
|
||||
height: 100vh;
|
||||
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();
|
||||
}
|
||||
|
||||
.inputItem {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 10rpx;
|
||||
border: 1rpx solid @oak-text-color-placeholder;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 28rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.my-input {
|
||||
padding-right: 0rpx !important;
|
||||
padding-left: 0rpx !important;
|
||||
height: 56rpx !important;
|
||||
line-height: 56rpx !important;
|
||||
}
|
||||
|
||||
.captcha {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.methods {
|
||||
width: 100%;
|
||||
padding: 0rpx 8rpx;
|
||||
font-size: 28rpx;
|
||||
margin-top: 28rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<view class="inputItem">
|
||||
<oak-icon name="mobilephone" size="28" color="#808080" />
|
||||
<l-input
|
||||
hide-label="{{true}}"
|
||||
placeholder="{{t('placeholder.Mobile')}}"
|
||||
clear="{{true}}"
|
||||
showRow="{{false}}"
|
||||
l-class="my-input"
|
||||
style="flex:1;"
|
||||
data-attr="mobile"
|
||||
maxlength="11"
|
||||
value="{{mobile}}"
|
||||
bind:lininput="inputChangeMp"
|
||||
bind:linclear="inputChangeMp"
|
||||
/>
|
||||
</view>
|
||||
<view class="inputItem">
|
||||
<l-input
|
||||
hide-label="{{true}}"
|
||||
placeholder="{{t('placeholder.Captcha')}}"
|
||||
clear="{{true}}"
|
||||
showRow="{{false}}"
|
||||
l-class="my-input"
|
||||
width="380"
|
||||
data-attr="captcha"
|
||||
maxlength="4"
|
||||
value="{{captcha}}"
|
||||
bind:lininput="inputChangeMp"
|
||||
bind:linclear="inputChangeMp"
|
||||
>
|
||||
<l-button slot="right" plain="{{true}}" bg-color="#fff" height="56" disabled="{{!!disabled || !validMobile || counter > 0}}" l-class="captcha" catch:lintap="sendCaptcha">
|
||||
{{counter > 0 ? counter + t('resendAfter'): t('Send')}}
|
||||
</l-button>
|
||||
</input>
|
||||
</view>
|
||||
<l-button size="long" disabled="{{!!disabled || !allowSubmit || loading}}" catch:lintap="loginByCaptcha" 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:else></view>
|
||||
<view wx:if="{{allowPassword}}" style="color:#835D01" bindtap="changeLoginMp" data-value="password">密码登录</view>
|
||||
</view>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Login": "登录",
|
||||
"Send": "发送验证码",
|
||||
"placeholder": {
|
||||
"Captcha": "输入4位验证码",
|
||||
"email": "请输入邮箱"
|
||||
},
|
||||
"resendAfter": "秒后可重发"
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
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, {
|
||||
counter: number;
|
||||
loading: boolean;
|
||||
disabled?: string;
|
||||
email: string;
|
||||
captcha: string;
|
||||
validEmail: boolean;
|
||||
validCaptcha: boolean;
|
||||
allowSubmit: boolean;
|
||||
digit: number;
|
||||
}, {
|
||||
sendCaptcha: () => Promise<void>;
|
||||
loginByEmail: () => Promise<void>;
|
||||
inputChange: (type: 'mobile' | 'captcha', value: string) => void;
|
||||
}>): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Form, Input, Button } from 'antd';
|
||||
import { MailOutlined, } from '@ant-design/icons';
|
||||
import Style from './web.module.less';
|
||||
export default function Render(props) {
|
||||
const { data, methods } = props;
|
||||
const { counter, loading, disabled, email, captcha, validEmail, validCaptcha, allowSubmit, digit } = data;
|
||||
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) => {
|
||||
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) => {
|
||||
inputChange('captcha', e.target.value);
|
||||
}} className={Style['loginbox-input']} suffix={<Button size="small" type="link" disabled={!!disabled || !validEmail || counter > 0} onClick={() => sendCaptcha()}>
|
||||
{counter > 0
|
||||
? counter + t('resendAfter')
|
||||
: t('Send')}
|
||||
</Button>}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button block size="large" type="primary" disabled={disabled || !allowSubmit || loading} loading={loading} onClick={() => loginByEmail()}>
|
||||
{t('Login')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>);
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
.loginbox {
|
||||
&-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%);
|
||||
}
|
||||
|
||||
&-hd {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
&-bd {
|
||||
height: 310px;
|
||||
}
|
||||
|
||||
&-only {
|
||||
padding-top: 32px !important;
|
||||
}
|
||||
|
||||
&-mobile {
|
||||
position: relative;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
&-password {
|
||||
position: relative;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
&-qrcode {
|
||||
padding: 0 32px;
|
||||
font-size: 14px;
|
||||
|
||||
&__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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, keyof import("../../../oak-app-domain").EntityDict, false, {
|
||||
import { EntityDict } from "../../../oak-app-domain";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, keyof EntityDict, false, {
|
||||
onlyCaptcha: boolean;
|
||||
onlyPassword: boolean;
|
||||
disabled: string;
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
import { LOCAL_STORAGE_KEYS } from '../../../config/constants';
|
||||
const SEND_KEY = LOCAL_STORAGE_KEYS.captchaSendAt;
|
||||
const LOGIN_MODE = LOCAL_STORAGE_KEYS.loginMode;
|
||||
const SEND_CAPTCHA_LATENCY = process.env.NODE_ENV === 'development' ? 10 : 60;
|
||||
export default OakComponent({
|
||||
isList: false,
|
||||
projection: {
|
||||
id: 1,
|
||||
mobile: 1,
|
||||
userId: 1,
|
||||
},
|
||||
data: {
|
||||
appId: '',
|
||||
mobile: '',
|
||||
password: '',
|
||||
captcha: '',
|
||||
counter: 0,
|
||||
loginAgreed: false,
|
||||
loginMode: 2,
|
||||
loginMode: 'sms',
|
||||
loading: false,
|
||||
lastSendAt: undefined,
|
||||
isSupportWechat: false,
|
||||
isSupportWechatPublic: false,
|
||||
isSupportWechatGrant: false,
|
||||
domain: undefined,
|
||||
passportTypes: [],
|
||||
inputOptions: [],
|
||||
scanOptions: [],
|
||||
allowSms: false,
|
||||
allowEmail: false,
|
||||
allowPassword: false,
|
||||
allowWechatMp: false,
|
||||
setLoginModeMp(value) { this.setLoginMode(value); },
|
||||
smsDigit: 4, //短信验证码位数
|
||||
emailDigit: 4, //邮箱验证码位数
|
||||
},
|
||||
properties: {
|
||||
onlyCaptcha: false,
|
||||
|
|
@ -33,134 +29,146 @@ export default OakComponent({
|
|||
callback: undefined, // 登录成功回调,排除微信登录方式
|
||||
},
|
||||
formData({ features, props }) {
|
||||
const { lastSendAt } = this.state;
|
||||
let counter = 0;
|
||||
if (typeof lastSendAt === 'number') {
|
||||
const now = Date.now();
|
||||
counter = Math.max(SEND_CAPTCHA_LATENCY - Math.ceil((now - lastSendAt) / 1000), 0);
|
||||
if (counter > 0) {
|
||||
this.counterHandler = setTimeout(() => this.reRender(), 1000);
|
||||
}
|
||||
else if (this.counterHandler) {
|
||||
clearTimeout(this.counterHandler);
|
||||
this.counterHandler = undefined;
|
||||
}
|
||||
}
|
||||
return {
|
||||
counter,
|
||||
};
|
||||
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,
|
||||
// })
|
||||
// }
|
||||
},
|
||||
lifetimes: {
|
||||
async ready() {
|
||||
const application = this.features.application.getApplication();
|
||||
let loginMode = (await this.load(LOGIN_MODE)) || 2;
|
||||
const lastSendAt = await this.load(SEND_KEY);
|
||||
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 { onlyCaptcha, onlyPassword } = this.props;
|
||||
let loginMode = (await this.load(LOGIN_MODE)) || defaultPassport?.passport?.type || 'sms';
|
||||
let inputOptions = [], scanOptions = [];
|
||||
if (onlyPassword) {
|
||||
loginMode = 'password';
|
||||
inputOptions = [{
|
||||
label: this.t('passport:v.type.password') + this.t('Login'),
|
||||
value: 'password',
|
||||
}];
|
||||
}
|
||||
else if (onlyCaptcha) {
|
||||
loginMode = 'sms';
|
||||
inputOptions = [{
|
||||
label: this.t('passport:v.type.sms') + this.t('Login'),
|
||||
value: 'sms',
|
||||
}];
|
||||
}
|
||||
else {
|
||||
passportTypes.forEach((ele) => {
|
||||
if (ele === 'sms' || ele === 'email' || ele === 'password') {
|
||||
inputOptions.push({
|
||||
label: this.t(`passport:v.type.${ele}`) + this.t('Login'),
|
||||
value: ele
|
||||
});
|
||||
}
|
||||
else if (ele === 'wechatWeb' || ele === 'wechatMpForWeb' || ele === 'wechatPublicForWeb') {
|
||||
scanOptions.push({
|
||||
label: this.t(`passport:v.type.${ele}`) + this.t('Login'),
|
||||
value: ele
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!passportTypes.includes(loginMode)) {
|
||||
loginMode = defaultPassport.passport.type;
|
||||
}
|
||||
const appType = application?.type;
|
||||
const config = application?.config;
|
||||
let appId;
|
||||
let domain; //网站扫码授权回调域
|
||||
let isSupportWechat = false; // 微信扫码网站登录
|
||||
let isSupportWechatPublic = false; // 微信扫码公众号登录
|
||||
let isSupportWechatGrant = false; // 微信公众号授权登录
|
||||
if (appType === 'wechatPublic') {
|
||||
const config2 = config;
|
||||
const isService = config2?.isService; //是否服务号 服务号才能授权登录
|
||||
appId = config2?.appId;
|
||||
isSupportWechatGrant = !!(isService && appId);
|
||||
isSupportWechat = !!config2?.passport?.includes('wechat');
|
||||
isSupportWechatPublic =
|
||||
!!config2?.passport?.includes('wechatPublic'); //是否开启
|
||||
isSupportWechatGrant = !!(isService && appId && passportTypes.includes('wechatPublic'));
|
||||
}
|
||||
else if (appType === 'web') {
|
||||
const config2 = config;
|
||||
appId = config2?.wechat?.appId;
|
||||
domain = config2?.wechat?.domain;
|
||||
isSupportWechat = !!config2?.passport?.includes('wechat');
|
||||
isSupportWechatPublic =
|
||||
!!config2?.passport?.includes('wechatPublic'); //是否开启
|
||||
}
|
||||
if (isSupportWechatGrant) {
|
||||
loginMode = 1;
|
||||
}
|
||||
else if (this.props.onlyPassword) {
|
||||
loginMode = 1;
|
||||
}
|
||||
else if (this.props.onlyCaptcha) {
|
||||
loginMode = 2;
|
||||
}
|
||||
else {
|
||||
const isSupportScan = isSupportWechat || isSupportWechatPublic;
|
||||
loginMode = loginMode === 3 && !isSupportScan ? 2 : loginMode;
|
||||
}
|
||||
const allowSms = passportTypes.includes('sms') && !onlyPassword;
|
||||
const allowEmail = passportTypes.includes('email') && !onlyCaptcha && !onlyPassword;
|
||||
const allowPassword = passportTypes.includes('password') && !onlyCaptcha;
|
||||
const allowWechatMp = passportTypes.includes('wechatMp') && !onlyCaptcha && !onlyPassword;
|
||||
this.setState({
|
||||
loginMode,
|
||||
appId,
|
||||
lastSendAt,
|
||||
isSupportWechat,
|
||||
isSupportWechatPublic,
|
||||
isSupportWechatGrant,
|
||||
domain,
|
||||
passportTypes,
|
||||
inputOptions,
|
||||
scanOptions,
|
||||
allowSms,
|
||||
allowEmail,
|
||||
allowPassword,
|
||||
allowWechatMp,
|
||||
smsDigit,
|
||||
emailDigit,
|
||||
}, () => this.reRender());
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async sendCaptcha(mobile) {
|
||||
try {
|
||||
const result = await this.features.token.sendCaptcha(mobile, 'login');
|
||||
// 显示返回消息
|
||||
this.setMessage({
|
||||
type: 'success',
|
||||
content: result,
|
||||
});
|
||||
const lastSendAt = Date.now();
|
||||
await this.save(SEND_KEY, lastSendAt);
|
||||
this.setState({
|
||||
lastSendAt,
|
||||
}, () => this.reRender());
|
||||
}
|
||||
catch (err) {
|
||||
this.setMessage({
|
||||
type: 'error',
|
||||
content: err.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
async loginByMobile(mobile, password, captcha) {
|
||||
const { url, callback } = this.props;
|
||||
try {
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
await this.features.token.loginByMobile(mobile, password, captcha);
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (callback) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (url) {
|
||||
this.redirectTo({
|
||||
url,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
this.setMessage({
|
||||
type: 'error',
|
||||
content: err.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
setLoginMode(value) {
|
||||
this.features.localStorage.save(LOGIN_MODE, value);
|
||||
this.setState({
|
||||
loginMode: value,
|
||||
});
|
||||
},
|
||||
changeLoginMp() {
|
||||
const { allowSms, allowPassword } = this.state;
|
||||
let loginMode = 'wechatMp';
|
||||
if (allowSms) {
|
||||
loginMode = 'sms';
|
||||
}
|
||||
else if (allowPassword) {
|
||||
loginMode = 'password';
|
||||
}
|
||||
this.setLoginMode(loginMode);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
"navigationBarTitleText": "登录",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"l-button": "@oak-frontend-base/miniprogram_npm/lin-ui/button/index"
|
||||
"l-button": "@oak-frontend-base/miniprogram_npm/lin-ui/button/index",
|
||||
"l-segment": "@oak-frontend-base/miniprogram_npm/lin-ui/segment/index",
|
||||
"l-segment-item": "@oak-frontend-base/miniprogram_npm/lin-ui/segment-item/index",
|
||||
"password": "../login/password/index",
|
||||
"sms": "../login/sms/index"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
|
||||
/** index.wxss **/
|
||||
@import "../../../config/styles/mp/index.less";
|
||||
@import "../../../config/styles/mp/mixins.less";
|
||||
|
||||
.page-body {
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
|
@ -15,3 +14,15 @@
|
|||
.safe-area-inset-bottom();
|
||||
}
|
||||
|
||||
.login-box {
|
||||
padding: 36rpx;
|
||||
min-width: 78vw;
|
||||
}
|
||||
|
||||
.login-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
|
@ -1,5 +1,32 @@
|
|||
<view class="page-body">
|
||||
<l-button type="default" shape="semicircle" disabled="{{loading}}" width="320" bind:lintap="loginByWechatMp">
|
||||
授权登录
|
||||
</l-button>
|
||||
<view class="login-box">
|
||||
<block wx:if="{{loginMode === 'sms'}}">
|
||||
<sms
|
||||
disabled="{{disabled}}"
|
||||
url="{{url}}"
|
||||
callback="{{callback}}"
|
||||
class="login-body"
|
||||
allowPassword="{{allowPassword}}"
|
||||
allowWechatMp="{{allowWechatMp}}"
|
||||
setLoginMode="{{setLoginModeMp}}"
|
||||
/>
|
||||
</block>
|
||||
<block wx:elif="{{loginMode ==='password'}}">
|
||||
<password
|
||||
disabled="{{disabled}}"
|
||||
url="{{url}}"
|
||||
callback="{{callback}}"
|
||||
class="login-body"
|
||||
allowSms="{{allowSms}}"
|
||||
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%">
|
||||
授权登录
|
||||
</l-button>
|
||||
<view wx:if="{{allowSms || allowPassword}}" style="font-size:28rpx; margin-top:28rpx; color:#8F976A" bind:tap="changeLoginMp">其他方式登录</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -10,5 +10,8 @@
|
|||
"Mobile": "请输入手机号",
|
||||
"Password": "请输入密码"
|
||||
},
|
||||
"resendAfter": "秒后可重发"
|
||||
"resendAfter": "秒后可重发",
|
||||
"otherMethods": "其他登录方式",
|
||||
"scanLogin": "扫码登录",
|
||||
"tip": "未注册用户首次登录将自动注册"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { EntityDict } from "../../../../oak-app-domain";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, keyof EntityDict, false, {
|
||||
disabled: string;
|
||||
redirectUri: string;
|
||||
url: string;
|
||||
callback: (() => void) | undefined;
|
||||
allowSms: boolean;
|
||||
allowEmail: boolean;
|
||||
allowWechatMp: boolean;
|
||||
setLoginMode: (value: string) => void;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import { isPassword } from "oak-domain/lib/utils/validator";
|
||||
export default OakComponent({
|
||||
isList: false,
|
||||
formData({ features, props }) {
|
||||
return {};
|
||||
},
|
||||
data: {
|
||||
counter: 0,
|
||||
loading: false,
|
||||
domain: undefined,
|
||||
account: '',
|
||||
password: '',
|
||||
validMobile: false,
|
||||
vaildEmail: false,
|
||||
vaildAccount: false,
|
||||
validPassword: false,
|
||||
allowSubmit: false,
|
||||
},
|
||||
properties: {
|
||||
disabled: '',
|
||||
redirectUri: '', // 微信登录后的redirectUri,要指向wechatUser/login去处理
|
||||
url: '', // 登录系统之后要返回的页面
|
||||
callback: undefined, // 登录成功回调,排除微信登录方式
|
||||
allowSms: false,
|
||||
allowEmail: false,
|
||||
allowWechatMp: false, //小程序切换授权登录
|
||||
setLoginMode: (value) => undefined,
|
||||
},
|
||||
lifetimes: {},
|
||||
listeners: {
|
||||
'vaildAccount,validPassword'(prev, next) {
|
||||
const { allowSubmit } = this.state;
|
||||
if (allowSubmit) {
|
||||
if (!(next.vaildAccount && next.validPassword)) {
|
||||
this.setState({
|
||||
allowSubmit: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (next.vaildAccount && next.validPassword) {
|
||||
this.setState({
|
||||
allowSubmit: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loginByAccount() {
|
||||
const { url, callback } = this.props;
|
||||
const { account, password } = this.state;
|
||||
try {
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
await this.features.token.loginByAccount(account, password);
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (callback) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (url) {
|
||||
this.redirectTo({
|
||||
url,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
this.setMessage({
|
||||
type: 'error',
|
||||
content: err.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
inputChange(type, value) {
|
||||
const { allowSms, allowEmail } = this.props;
|
||||
switch (type) {
|
||||
case 'account':
|
||||
// const validMobile = allowSms && !!isMobile(value);
|
||||
// const vaildEmail = allowEmail && !!isEmail(value);
|
||||
const vaildAccount = !!(value && value.trim() && value.trim() !== '');
|
||||
this.setState({
|
||||
account: value,
|
||||
// validMobile,
|
||||
// vaildEmail,
|
||||
vaildAccount,
|
||||
});
|
||||
break;
|
||||
case 'password':
|
||||
const validPassword = !!isPassword(value);
|
||||
this.setState({
|
||||
password: value,
|
||||
validPassword
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
inputChangeMp(event) {
|
||||
const { detail, target: { dataset }, } = event;
|
||||
const { attr } = dataset;
|
||||
const { value } = detail;
|
||||
this.inputChange(attr, value);
|
||||
},
|
||||
changeLoginMp(e) {
|
||||
const { setLoginMode } = this.props;
|
||||
const { value } = e.currentTarget.dataset;
|
||||
setLoginMode && setLoginMode(value);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"navigationBarTitleText": "登录",
|
||||
"enablePullDownRefresh": false,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/** index.wxss **/
|
||||
@import "../../../../config/styles/mp/index.less";
|
||||
@import "../../../../config/styles/mp/mixins.less";
|
||||
|
||||
// .page-body {
|
||||
// height: 100vh;
|
||||
// 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();
|
||||
// }
|
||||
|
||||
.inputItem {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 10rpx;
|
||||
border: 1rpx solid @oak-text-color-placeholder;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 28rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.my-input {
|
||||
padding-right: 0rpx !important;
|
||||
padding-left: 0rpx !important;
|
||||
height: 56rpx !important;
|
||||
line-height: 56rpx !important;
|
||||
}
|
||||
|
||||
.methods {
|
||||
width: 100%;
|
||||
padding: 0rpx 8rpx;
|
||||
font-size: 28rpx;
|
||||
margin-top: 28rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<view class="inputItem">
|
||||
<oak-icon name="mine" size="28" color="#808080" />
|
||||
<l-input
|
||||
hide-label="{{true}}"
|
||||
placeholder="{{t('placeholder.Account')}}"
|
||||
clear="{{true}}"
|
||||
showRow="{{false}}"
|
||||
l-class="my-input"
|
||||
style="flex:1;"
|
||||
data-attr="account"
|
||||
value="{{account}}"
|
||||
bind:lininput="inputChangeMp"
|
||||
bind:linclear="inputChangeMp"
|
||||
/>
|
||||
</view>
|
||||
<view class="inputItem">
|
||||
<oak-icon name="lock" size="28" color="#808080" />
|
||||
<l-input
|
||||
hide-label="{{true}}"
|
||||
placeholder="{{t('placeholder.Password')}}"
|
||||
clear="{{true}}"
|
||||
showRow="{{false}}"
|
||||
l-class="my-input"
|
||||
style="flex:1;"
|
||||
type="password"
|
||||
data-attr="password"
|
||||
value="{{password}}"
|
||||
bind:lininput="inputChangeMp"
|
||||
bind:linclear="inputChangeMp"
|
||||
/>
|
||||
</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:else></view>
|
||||
<view wx:if="{{allowSms}}" style="color:#835D01" bindtap="changeLoginMp" data-value="sms">短信登录</view>
|
||||
</view>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Login": "登录",
|
||||
"placeholder": {
|
||||
"Account": "请输入账号",
|
||||
"Mobile": "/手机号",
|
||||
"Email": "/邮箱",
|
||||
"Password": "请输入密码"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
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, {
|
||||
loading: boolean;
|
||||
disabled?: string;
|
||||
account: string;
|
||||
password: string;
|
||||
validMobile: boolean;
|
||||
validPassword: boolean;
|
||||
allowSubmit: boolean;
|
||||
allowSms: boolean;
|
||||
allowEmail: boolean;
|
||||
}, {
|
||||
loginByAccount: () => Promise<void>;
|
||||
inputChange: (type: 'account' | 'password', value: string) => void;
|
||||
}>): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// @ts-nocheck
|
||||
// Segmented这个对象在antd里的声明是错误的
|
||||
import React from 'react';
|
||||
import { Form, Input, Button } from 'antd';
|
||||
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 { loginByAccount, t, inputChange } = methods;
|
||||
return (<Form colon={true}>
|
||||
<Form.Item name="mobile">
|
||||
<Input allowClear value={account} size="large"
|
||||
// maxLength={11}
|
||||
prefix={<UserOutlined />} placeholder={allowSms ? t('placeholder.Account') + t('placeholder.Mobile') : (allowEmail ? t('placeholder.Account') + t('placeholder.Email') : t('placeholder.Account'))} onChange={(e) => {
|
||||
inputChange('account', e.target.value);
|
||||
}} className={Style['loginbox-input']}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="password">
|
||||
<Input.Password allowClear size="large" value={password} prefix={<LockOutlined />} placeholder={t('placeholder.Password')} onChange={(e) => {
|
||||
inputChange('password', e.target.value);
|
||||
}} className={Style['loginbox-input']}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<>
|
||||
<Button block size="large" type="primary" disabled={!!disabled || !allowSubmit || loading} loading={loading} onClick={() => loginByAccount()}>
|
||||
{t('Login')}
|
||||
</Button>
|
||||
</>
|
||||
</Form.Item>
|
||||
</Form>);
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
.loginbox {
|
||||
&-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%);
|
||||
}
|
||||
|
||||
&-hd {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
&-bd {
|
||||
height: 310px;
|
||||
}
|
||||
|
||||
&-only {
|
||||
padding-top: 32px !important;
|
||||
}
|
||||
|
||||
&-mobile {
|
||||
position: relative;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
&-password {
|
||||
position: relative;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
&-qrcode {
|
||||
padding: 0 32px;
|
||||
font-size: 14px;
|
||||
|
||||
&__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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { EntityDict } from "../../../../oak-app-domain";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, keyof EntityDict, false, {
|
||||
disabled: string;
|
||||
url: string;
|
||||
callback: (() => void) | undefined;
|
||||
allowPassword: boolean;
|
||||
allowWechatMp: boolean;
|
||||
setLoginMode: (value: string) => void;
|
||||
digit: number;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import { LOCAL_STORAGE_KEYS } from '../../../../config/constants';
|
||||
import { isCaptcha, isMobile } from "oak-domain/lib/utils/validator";
|
||||
const SEND_KEY = LOCAL_STORAGE_KEYS.captchaSendAt;
|
||||
const SEND_CAPTCHA_LATENCY = process.env.NODE_ENV === 'development' ? 10 : 60;
|
||||
export default OakComponent({
|
||||
isList: false,
|
||||
projection: {
|
||||
id: 1,
|
||||
mobile: 1,
|
||||
userId: 1,
|
||||
},
|
||||
data: {
|
||||
counter: 0,
|
||||
loading: false,
|
||||
lastSendAt: undefined,
|
||||
mobile: '',
|
||||
captcha: '',
|
||||
validMobile: false,
|
||||
validCaptcha: false,
|
||||
allowSubmit: false,
|
||||
},
|
||||
properties: {
|
||||
disabled: '',
|
||||
url: '', // 登录系统之后要返回的页面
|
||||
callback: undefined, // 登录成功回调,排除微信登录方式
|
||||
allowPassword: false, //小程序切换密码登录
|
||||
allowWechatMp: false, //小程序切换授权登录
|
||||
setLoginMode: (value) => undefined,
|
||||
digit: 4 //验证码位数,
|
||||
},
|
||||
formData({ features, props }) {
|
||||
const { lastSendAt } = this.state;
|
||||
let counter = 0;
|
||||
if (typeof lastSendAt === 'number') {
|
||||
const now = Date.now();
|
||||
counter = Math.max(SEND_CAPTCHA_LATENCY - Math.ceil((now - lastSendAt) / 1000), 0);
|
||||
if (counter > 0) {
|
||||
this.counterHandler = setTimeout(() => this.reRender(), 1000);
|
||||
}
|
||||
else if (this.counterHandler) {
|
||||
clearTimeout(this.counterHandler);
|
||||
this.counterHandler = undefined;
|
||||
}
|
||||
}
|
||||
return {
|
||||
counter,
|
||||
};
|
||||
},
|
||||
lifetimes: {},
|
||||
listeners: {
|
||||
'validMobile,validCaptcha'(prev, next) {
|
||||
const { allowSubmit } = this.state;
|
||||
if (allowSubmit) {
|
||||
if (!(next.validMobile && next.validCaptcha)) {
|
||||
this.setState({
|
||||
allowSubmit: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (next.validMobile && next.validCaptcha) {
|
||||
this.setState({
|
||||
allowSubmit: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async sendCaptcha() {
|
||||
const { mobile } = this.state;
|
||||
try {
|
||||
const result = await this.features.token.sendCaptcha('mobile', mobile, 'login');
|
||||
// 显示返回消息
|
||||
this.setMessage({
|
||||
type: 'success',
|
||||
content: result,
|
||||
});
|
||||
const lastSendAt = Date.now();
|
||||
await this.save(SEND_KEY, lastSendAt);
|
||||
this.setState({
|
||||
lastSendAt,
|
||||
}, () => this.reRender());
|
||||
}
|
||||
catch (err) {
|
||||
this.setMessage({
|
||||
type: 'error',
|
||||
content: err.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
async loginByCaptcha() {
|
||||
const { url, callback } = this.props;
|
||||
const { mobile, captcha } = this.state;
|
||||
try {
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
await this.features.token.loginByMobile(mobile, captcha);
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (callback) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (url) {
|
||||
this.redirectTo({
|
||||
url,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
this.setMessage({
|
||||
type: 'error',
|
||||
content: err.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
inputChange(type, value) {
|
||||
const { digit } = this.props;
|
||||
switch (type) {
|
||||
case 'mobile':
|
||||
const validMobile = !!isMobile(value);
|
||||
this.setState({
|
||||
mobile: value,
|
||||
validMobile,
|
||||
});
|
||||
break;
|
||||
case 'captcha':
|
||||
const validCaptcha = !!isCaptcha(value, digit);
|
||||
this.setState({
|
||||
captcha: value,
|
||||
validCaptcha
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
inputChangeMp(event) {
|
||||
const { detail, target: { dataset }, } = event;
|
||||
const { attr } = dataset;
|
||||
const { value } = detail;
|
||||
this.inputChange(attr, value);
|
||||
},
|
||||
changeLoginMp(e) {
|
||||
const { setLoginMode } = this.props;
|
||||
const { value } = e.currentTarget.dataset;
|
||||
setLoginMode && setLoginMode(value);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"navigationBarTitleText": "登录",
|
||||
"enablePullDownRefresh": false,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/** index.wxss **/
|
||||
@import "../../../../config/styles/mp/index.less";
|
||||
@import "../../../../config/styles/mp/mixins.less";
|
||||
|
||||
.page-body {
|
||||
height: 100vh;
|
||||
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();
|
||||
}
|
||||
|
||||
.inputItem {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 10rpx;
|
||||
border: 1rpx solid @oak-text-color-placeholder;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 28rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.my-input {
|
||||
padding-right: 0rpx !important;
|
||||
padding-left: 0rpx !important;
|
||||
height: 56rpx !important;
|
||||
line-height: 56rpx !important;
|
||||
}
|
||||
|
||||
.captcha {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.methods {
|
||||
width: 100%;
|
||||
padding: 0rpx 8rpx;
|
||||
font-size: 28rpx;
|
||||
margin-top: 28rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<view class="inputItem">
|
||||
<oak-icon name="mobilephone" size="28" color="#808080" />
|
||||
<l-input
|
||||
hide-label="{{true}}"
|
||||
placeholder="{{t('placeholder.Mobile')}}"
|
||||
clear="{{true}}"
|
||||
showRow="{{false}}"
|
||||
l-class="my-input"
|
||||
style="flex:1;"
|
||||
data-attr="mobile"
|
||||
maxlength="11"
|
||||
value="{{mobile}}"
|
||||
bind:lininput="inputChangeMp"
|
||||
bind:linclear="inputChangeMp"
|
||||
/>
|
||||
</view>
|
||||
<view class="inputItem">
|
||||
<l-input
|
||||
hide-label="{{true}}"
|
||||
placeholder="{{t('placeholder.Captcha')}}"
|
||||
clear="{{true}}"
|
||||
showRow="{{false}}"
|
||||
l-class="my-input"
|
||||
width="380"
|
||||
data-attr="captcha"
|
||||
maxlength="4"
|
||||
value="{{captcha}}"
|
||||
bind:lininput="inputChangeMp"
|
||||
bind:linclear="inputChangeMp"
|
||||
>
|
||||
<l-button slot="right" plain="{{true}}" bg-color="#fff" height="56" disabled="{{!!disabled || !validMobile || counter > 0}}" l-class="captcha" catch:lintap="sendCaptcha">
|
||||
{{counter > 0 ? counter + t('resendAfter'): t('Send')}}
|
||||
</l-button>
|
||||
</input>
|
||||
</view>
|
||||
<l-button size="long" disabled="{{!!disabled || !allowSubmit || loading}}" catch:lintap="loginByCaptcha" 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:else></view>
|
||||
<view wx:if="{{allowPassword}}" style="color:#835D01" bindtap="changeLoginMp" data-value="password">密码登录</view>
|
||||
</view>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Login": "登录",
|
||||
"Send": "发送验证码",
|
||||
"placeholder": {
|
||||
"Captcha": "输入%{digit}位短信验证码",
|
||||
"Mobile": "请输入手机号"
|
||||
},
|
||||
"resendAfter": "秒后可重发"
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
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, {
|
||||
counter: number;
|
||||
loading: boolean;
|
||||
disabled?: string;
|
||||
mobile: string;
|
||||
captcha: string;
|
||||
validMobile: boolean;
|
||||
validCaptcha: boolean;
|
||||
allowSubmit: boolean;
|
||||
digit: number;
|
||||
}, {
|
||||
sendCaptcha: () => Promise<void>;
|
||||
loginByCaptcha: () => Promise<void>;
|
||||
inputChange: (type: 'mobile' | 'captcha', value: string) => void;
|
||||
}>): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Form, Input, Button } from 'antd';
|
||||
import { MobileOutlined, } from '@ant-design/icons';
|
||||
import Style from './web.module.less';
|
||||
export default function Render(props) {
|
||||
const { data, methods } = props;
|
||||
const { counter, loading, disabled, mobile, captcha, validMobile, validCaptcha, allowSubmit, digit } = data;
|
||||
const { sendCaptcha, loginByCaptcha, t, inputChange } = methods;
|
||||
return (<Form colon={true}>
|
||||
<Form.Item name="mobile">
|
||||
<Input allowClear value={mobile} type="tel" size="large" maxLength={11} prefix={<MobileOutlined />} placeholder={t('placeholder.Mobile')} onChange={(e) => {
|
||||
inputChange('mobile', 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', { digit })} onChange={(e) => {
|
||||
inputChange('captcha', e.target.value);
|
||||
}} className={Style['loginbox-input']} suffix={<Button size="small" type="link" disabled={!!disabled || !validMobile || counter > 0} onClick={() => sendCaptcha()}>
|
||||
{counter > 0
|
||||
? counter + t('resendAfter')
|
||||
: t('Send')}
|
||||
</Button>}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button block size="large" type="primary" disabled={disabled || !allowSubmit || loading} loading={loading} onClick={() => loginByCaptcha()}>
|
||||
{t('Login')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>);
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
.loginbox {
|
||||
&-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%);
|
||||
}
|
||||
|
||||
&-hd {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
&-bd {
|
||||
height: 310px;
|
||||
}
|
||||
|
||||
&-only {
|
||||
padding-top: 32px !important;
|
||||
}
|
||||
|
||||
&-mobile {
|
||||
position: relative;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
&-password {
|
||||
position: relative;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
&-qrcode {
|
||||
padding: 0 32px;
|
||||
font-size: 14px;
|
||||
|
||||
&__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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,29 @@
|
|||
import React from 'react';
|
||||
import { WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
type Option = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
export default function Render(props: WebComponentProps<EntityDict, 'token', false, {
|
||||
counter: number;
|
||||
loginMode?: number;
|
||||
appId: string;
|
||||
onlyCaptcha?: boolean;
|
||||
onlyPassword?: boolean;
|
||||
loading: boolean;
|
||||
width: string;
|
||||
isSupportWechat: boolean;
|
||||
isSupportWechatPublic: boolean;
|
||||
isSupportWechatGrant: boolean;
|
||||
domain?: string;
|
||||
disabled?: string;
|
||||
redirectUri: string;
|
||||
url: string;
|
||||
passportTypes: EntityDict['passport']['Schema']['type'][];
|
||||
callback: (() => void) | undefined;
|
||||
inputOptions: Option[];
|
||||
scanOptions: Option[];
|
||||
smsDigit: number;
|
||||
emailDigit: number;
|
||||
allowSms: boolean;
|
||||
allowEmail: boolean;
|
||||
}, {
|
||||
sendCaptcha: (mobile: string) => Promise<void>;
|
||||
loginByMobile: (mobile: string, password?: string, captcha?: string) => Promise<void>;
|
||||
setLoginMode: (value: number) => void;
|
||||
}>): React.JSX.Element;
|
||||
export {};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue