密码登录支持邮箱

This commit is contained in:
lxy 2024-08-21 14:36:50 +08:00
parent 48f1d59a7d
commit b63361782d
8 changed files with 257 additions and 24 deletions

View File

@ -30,6 +30,15 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
loginByAccount: (
params: {
account: string;
password: string;
disableRegister?: boolean;
env: WebEnv | WechatMpEnv | NativeEnv;
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
loginByEmail: (
params: {
email: string;

View File

@ -1,4 +1,5 @@
import {
loginByAccount,
loginByEmail,
loginByMobile,
loginWechat,
@ -65,6 +66,7 @@ import {
import { getApplicationPassports, removeApplicationPassportsByPIds } from './applicationPassport';
const aspectDict = {
loginByAccount,
loginByEmail,
mergeUser,
switchTo,

View File

@ -41,6 +41,7 @@ import { BRC } from '../types/RuntimeCxt';
import { SmsConfig } from '../entities/Passport';
import { sendEmail } from '../utils/email';
import { EmailConfig } from '../oak-app-domain/Passport/Schema';
import { isEmail, isMobile } from 'oak-domain/lib/utils/validator';
async function makeDistinguishException<ED extends EntityDict>(userId: string, context: BRC<ED>, message?: string) {
@ -717,6 +718,189 @@ export async function loginByMobile<ED extends EntityDict>(
return tokenValue;
}
export async function loginByAccount<ED extends EntityDict>(
params: {
account: string;
password: string;
disableRegister?: boolean;
env: WebEnv | WechatMpEnv | NativeEnv;
},
context: BRC<ED>
): Promise<string> {
const { account, password, env, disableRegister } = params;
const loginLogic = async () => {
const systemId = context.getSystemId();
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,
}
}
},
filter: {
$and: [
{
$or: [
{
mobile$user: {
mobile: account,
}
},
{
email$user: {
email: 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, id: userId, } = userRow;
if (mobile$user && mobile$user.length > 0) {
const ableState = mobile$user[0].ableState;
if (ableState === 'disabled') {
// 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录
const exception = await tryMakeChangeLoginWay<ED>(
userId as string,
context
);
if (exception) {
throw exception;
}
}
return await setupMobile<ED>(account, env, context);
} else if (email$user && email$user.length > 0) {
const ableState = email$user[0].ableState;
if (ableState === 'disabled') {
// 虽然密码和邮箱匹配,但邮箱已经禁用了,在可能的情况下提醒用户使用其它方法登录
const exception = await tryMakeChangeLoginWay<ED>(
userId as string,
context
);
if (exception) {
throw exception;
}
}
return await setupEmail<ED>(account, env, context);
}
return ''
}
default: {
throw new Error(
// `手机号和密码匹配出现雷同mobile id是[${result
// .map((ele) => ele.id)
// .join(',')}], mobile是${mobile}`
);
}
}
};
const closeRootMode = context.openRootMode();
if (disableRegister) {
if (isMobile(account)) {
const [existMobile] = await context.select(
'mobile',
{
data: {
id: 1,
mobile: 1,
},
filter: {
mobile: account!,
ableState: 'enabled',
},
},
{ dontCollect: true }
);
if (!existMobile) {
closeRootMode();
throw new OakUserException('账号不存在');
}
} else if (isEmail(account)) {
const [existEmail] = await context.select(
'email',
{
data: {
id: 1,
email: 1,
},
filter: {
email: account!,
ableState: 'enabled',
},
},
{ dontCollect: true }
);
if (!existEmail) {
closeRootMode();
throw new OakUserException('账号不存在');
}
} else {
// const [existMobile] = await context.select(
// 'mobile',
// {
// data: {
// id: 1,
// mobile: 1,
// },
// filter: {
// mobile: mobile!,
// ableState: 'enabled',
// },
// },
// { dontCollect: true }
// );
// if (!existMobile) {
// closeRootMode();
// throw new OakUserException('账号不存在');
// }
}
}
const tokenValue = await loginLogic();
await loadTokenInfo<ED>(tokenValue, context);
closeRootMode();
return tokenValue;
}
export async function loginByEmail<ED extends EntityDict>(
params: {
email: string;

View File

@ -22,6 +22,7 @@ export default OakComponent({
inputOptions: [] as Option[],
scanOptions: [] as Option[],
allowSms: false,
allowEmail: false,
allowPassword: false,
allowWechatMp: false,
setLoginModeMp(value: string) { this.setLoginMode(value) },
@ -131,7 +132,7 @@ export default OakComponent({
let appId;
let domain; //网站扫码授权回调域
let isSupportWechatGrant = false; // 微信公众号授权登录
let allowSms = false, allowPassword = false, allowWechatMp = false; //小程序登录显示
if (appType === 'wechatPublic') {
const config2 = config as WechatPublicConfig;
const isService = config2?.isService; //是否服务号 服务号才能授权登录
@ -141,12 +142,14 @@ export default OakComponent({
const config2 = config as WebConfig;
appId = config2?.wechat?.appId;
domain = config2?.wechat?.domain;
} else if (appType === 'wechatMp') {
allowSms = passportTypes.includes('sms') && !onlyPassword;
allowPassword = passportTypes.includes('password') && !onlyCaptcha;
allowWechatMp = passportTypes.includes('wechatMp') && !onlyCaptcha && !onlyPassword;
}
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,
@ -157,6 +160,7 @@ export default OakComponent({
inputOptions,
scanOptions,
allowSms,
allowEmail,
allowPassword,
allowWechatMp,
smsDigit,

View File

@ -2,7 +2,7 @@ import { WebConfig, WechatPublicConfig, AppType } from "../../../../entities/App
import { LOCAL_STORAGE_KEYS } from '../../../../config/constants';
import { EntityDict } from "../../../../oak-app-domain";
import { isMobile, isPassword } from "oak-domain/lib/utils/validator";
import { isEmail, isMobile, isPassword } from "oak-domain/lib/utils/validator";
export default OakComponent({
isList: false,
@ -14,9 +14,10 @@ export default OakComponent({
counter: 0,
loading: false,
domain: undefined as string | undefined,
mobile: '',
account: '',
password: '',
validMobile: false,
vaildEmail: false,
validPassword: false,
allowSubmit: false,
},
@ -25,23 +26,24 @@ export default OakComponent({
redirectUri: '', // 微信登录后的redirectUri要指向wechatUser/login去处理
url: '', // 登录系统之后要返回的页面
callback: undefined as (() => void) | undefined, // 登录成功回调,排除微信登录方式
allowSms: false, //小程序切换短信登录
allowSms: false,
allowEmail: false,
allowWechatMp: false, //小程序切换授权登录
setLoginMode: (value: string) => undefined as void,
},
lifetimes: {
},
listeners: {
'validMobile,validPassword'(prev, next) {
'validMobile,vaildEmail,validPassword'(prev, next) {
const { allowSubmit } = this.state;
if (allowSubmit) {
if (!(next.validMobile && next.validPassword)) {
if (!((next.validMobile || next.vaildEmail) && next.validPassword)) {
this.setState({
allowSubmit: false,
})
}
} else {
if (next.validMobile && next.validPassword) {
if ((next.validMobile || next.vaildEmail) && next.validPassword) {
this.setState({
allowSubmit: true,
})
@ -52,13 +54,13 @@ export default OakComponent({
methods: {
async loginByPassword() {
const { url, callback } = this.props;
const { mobile, password } = this.state;
const { account, password } = this.state;
try {
this.setState({
loading: true,
});
await this.features.token.loginByMobile(
mobile,
await this.features.token.loginByAccount(
account,
password,
);
this.setState({
@ -84,13 +86,16 @@ export default OakComponent({
});
}
},
inputChange(type: 'mobile' | 'password', value: string) {
inputChange(type: 'account' | 'password', value: string) {
const { allowSms, allowEmail } = this.props;
switch (type) {
case 'mobile':
const validMobile = !!isMobile(value);
case 'account':
const validMobile = allowSms && !!isMobile(value);
const vaildEmail = allowEmail && !!isEmail(value);
this.setState({
mobile: value,
account: value,
validMobile,
vaildEmail,
})
break;
case 'password':

View File

@ -9,7 +9,7 @@ import {
import { Form, Input, Button, Checkbox, Typography, Segmented } from 'antd';
import {
LockOutlined,
MobileOutlined,
UserOutlined,
} from '@ant-design/icons';
import { WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../oak-app-domain';
@ -36,7 +36,7 @@ export default function Render(
},
{
loginByPassword: () => Promise<void>;
inputChange: (type: 'mobile' | 'password', value: string) => void;
inputChange: (type: 'account' | 'password', value: string) => void;
}
>
) {
@ -52,11 +52,11 @@ export default function Render(
value={mobile}
type="tel"
size="large"
maxLength={11}
prefix={<MobileOutlined />}
// maxLength={11}
prefix={<UserOutlined />}
placeholder={t('placeholder.Mobile')}
onChange={(e) => {
inputChange('mobile', e.target.value);
inputChange('account', e.target.value);
}}
className={Style['loginbox-input']}
/>

View File

@ -53,6 +53,8 @@ export default function Render(
scanOptions: Option[];
smsDigit: number;
emailDigit: number;
allowSms: boolean;
allowEmail: boolean;
},
{
setLoginMode: (value: number) => void;
@ -76,6 +78,8 @@ export default function Render(
scanOptions,
smsDigit,
emailDigit,
allowSms,
allowEmail,
} = data;
const { t, setLoginMode } = methods;
@ -208,6 +212,8 @@ export default function Render(
disabled={disabled}
url={url}
callback={callback}
allowSms={allowSms}
allowEmail={allowEmail}
/>
{Tip}
</div>

View File

@ -142,6 +142,29 @@ export class Token<ED extends EntityDict> extends Feature {
this.publish();
}
async loginByAccount(
account: string,
password: string,
disableRegister?: boolean
) {
const env = await this.environment.getEnv();
const { result } = await this.cache.exec(
'loginByAccount',
{
account,
password,
disableRegister,
env,
},
undefined,
true
);
this.tokenValue = result;
// await this.storage.save(LOCAL_STORAGE_KEYS.token, result);
this.publish();
}
async loginByWechatInWebEnv(wechatLoginId: string) {
const env = await this.environment.getEnv();
const { result } = await this.cache.exec('loginByWechat', {