密码登录支持邮箱

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> context: BackendRuntimeContext<ED>
) => Promise<string>; ) => Promise<string>;
loginByAccount: (
params: {
account: string;
password: string;
disableRegister?: boolean;
env: WebEnv | WechatMpEnv | NativeEnv;
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
loginByEmail: ( loginByEmail: (
params: { params: {
email: string; email: string;

View File

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

View File

@ -41,6 +41,7 @@ import { BRC } from '../types/RuntimeCxt';
import { SmsConfig } from '../entities/Passport'; import { SmsConfig } from '../entities/Passport';
import { sendEmail } from '../utils/email'; import { sendEmail } from '../utils/email';
import { EmailConfig } from '../oak-app-domain/Passport/Schema'; 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) { 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; 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>( export async function loginByEmail<ED extends EntityDict>(
params: { params: {
email: string; email: string;

View File

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

View File

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

View File

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

View File

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

View File

@ -142,6 +142,29 @@ export class Token<ED extends EntityDict> extends Feature {
this.publish(); 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) { async loginByWechatInWebEnv(wechatLoginId: string) {
const env = await this.environment.getEnv(); const env = await this.environment.getEnv();
const { result } = await this.cache.exec('loginByWechat', { const { result } = await this.cache.exec('loginByWechat', {
@ -295,7 +318,7 @@ export class Token<ED extends EntityDict> extends Feature {
type, type,
}); });
return result as string; return result as string;
}else { } else {
const { result } = await this.cache.exec('sendCaptchaByEmail', { const { result } = await this.cache.exec('sendCaptchaByEmail', {
email: content, email: content,
env: env as WebEnv, env: env as WebEnv,