merge passport

This commit is contained in:
Xu Chang 2024-08-30 14:32:59 +08:00
commit 70601c5a88
408 changed files with 16106 additions and 1950 deletions

View File

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

View File

@ -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}应用必须配置域名`);

8
es/aspects/applicationPassport.d.ts vendored Normal file
View File

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

View File

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

10
es/aspects/index.d.ts vendored
View File

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

View File

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

21
es/aspects/token.d.ts vendored
View File

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

View File

@ -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) {

View File

@ -275,7 +275,8 @@ export async function updateUserPassword(params, context, innerLogic) {
id: 1,
},
filter: {
mobile,
origin: 'mobile',
content: mobile,
code: captcha,
expired: false,
},

View File

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

View File

@ -36,7 +36,8 @@ export async function unbindingWechat(params, context) {
expired: 1,
},
filter: {
mobile,
origin: 'mobile',
content: mobile,
code: captcha,
},
sorter: [{

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
{
"login": "登录"
}

View File

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

View File

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

View File

@ -0,0 +1,7 @@
.btns {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 16px;
margin-bottom: 12px;
}

View File

@ -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',

View File

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

View File

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

View File

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

View File

@ -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}>
微信公众号-跳转小程序-小程序配置

View File

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

View File

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

View File

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

View File

@ -0,0 +1,16 @@
.label {
color: var(--oak-text-color-primary);
font-size: 28px;
line-height: 36px;
}
.tips {
color: var(--oak-text-color-placeholder);
font-size: 12px;
}
.title {
margin-bottom: 0px;
margin-top:36px;
}

View File

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

View File

@ -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: '基础设置',

View File

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

View File

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

12
es/components/passport/email/index.d.ts vendored Normal file
View File

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

View File

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

View File

@ -0,0 +1,12 @@
.item {
padding: 10px 16px;
border-radius: 12px;
border: 1px solid var(--oak-border-color);
margin: 10px 0px;
}
.title {
display: flex;
align-items: center;
justify-content: space-between;
}

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

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

View File

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

View File

@ -0,0 +1,3 @@
{
"login": "登录"
}

11
es/components/passport/sms/index.d.ts vendored Normal file
View File

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

View File

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

View File

@ -0,0 +1,12 @@
.item {
padding: 10px 16px;
border-radius: 12px;
border: 1px solid var(--oak-border-color);
margin: 10px 0px;
}
.title {
display: flex;
align-items: center;
justify-content: space-between;
}

20
es/components/passport/web.pc.d.ts vendored Normal file
View File

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

View File

@ -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> */}
</>);
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
.item {
padding: 10px 16px;
border-radius: 12px;
border: 1px solid var(--oak-border-color);
margin: 10px 0px;
}
.title {
display: flex;
align-items: center;
justify-content: space-between;
}

View File

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

View File

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

View File

@ -0,0 +1,12 @@
.item {
padding: 10px 16px;
border-radius: 12px;
border: 1px solid var(--oak-border-color);
margin: 10px 0px;
}
.title {
display: flex;
align-items: center;
justify-content: space-between;
}

View File

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

View File

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

View File

@ -0,0 +1,12 @@
.item {
padding: 10px 16px;
border-radius: 12px;
border: 1px solid var(--oak-border-color);
margin: 10px 0px;
}
.title {
display: flex;
align-items: center;
justify-content: space-between;
}

View File

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

View File

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

View File

@ -0,0 +1,12 @@
.item {
padding: 10px 16px;
border-radius: 12px;
border: 1px solid var(--oak-border-color);
margin: 10px 0px;
}
.title {
display: flex;
align-items: center;
justify-content: space-between;
}

View File

@ -4,5 +4,6 @@
"config": "配置管理",
"style": "样式管理",
"application-list": "应用管理",
"smsTemplate-list": "短信模板管理"
"smsTemplate-list": "短信模板管理",
"login": "登录管理"
}

View File

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

View File

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

View File

@ -0,0 +1,10 @@
export default OakComponent({
isList: false,
formData({ data }) {
return {};
},
properties: {
systemId: '',
systemName: '',
}
});

View File

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

View File

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

View File

@ -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',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
{
"Login": "登录",
"Send": "发送验证码",
"placeholder": {
"Captcha": "输入4位验证码",
"email": "请输入邮箱"
},
"resendAfter": "秒后可重发"
}

18
es/components/user/login/email/web.d.ts vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,5 +10,8 @@
"Mobile": "请输入手机号",
"Password": "请输入密码"
},
"resendAfter": "秒后可重发"
"resendAfter": "秒后可重发",
"otherMethods": "其他登录方式",
"scanLogin": "扫码登录",
"tip": "未注册用户首次登录将自动注册"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
{
"Login": "登录",
"placeholder": {
"Account": "请输入账号",
"Mobile": "/手机号",
"Email": "/邮箱",
"Password": "请输入密码"
}
}

View File

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

View File

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

View File

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

11
es/components/user/login/sms/index.d.ts vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
{
"Login": "登录",
"Send": "发送验证码",
"placeholder": {
"Captcha": "输入%{digit}位短信验证码",
"Mobile": "请输入手机号"
},
"resendAfter": "秒后可重发"
}

18
es/components/user/login/sms/web.d.ts vendored Normal file
View File

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

View File

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

View File

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

View File

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