邮箱支持配置后缀,当设置后缀,会检查输入的邮箱是否符合要求

This commit is contained in:
wkj 2025-05-06 16:25:33 +08:00
parent dae20932a9
commit 606b9e2023
79 changed files with 1217 additions and 267 deletions

View File

@ -103,7 +103,7 @@ export async function createSession(params, context) {
origin: 'wechat',
type: 'image',
tag1: 'image',
objectId: await generateNewIdAsync(),
objectId: await generateNewIdAsync(), // 这个域用来标识唯一性
sort: 1000,
uploadState: 'success',
extra1: data.MediaId,
@ -128,7 +128,7 @@ export async function createSession(params, context) {
origin: 'wechat',
type: 'video',
tag1: 'video',
objectId: await generateNewIdAsync(),
objectId: await generateNewIdAsync(), // 这个域用来标识唯一性
sort: 1000,
uploadState: 'success',
extra1: data.MediaId,
@ -150,7 +150,7 @@ export async function createSession(params, context) {
origin: 'wechat',
type: 'audio',
tag1: 'audio',
objectId: await generateNewIdAsync(),
objectId: await generateNewIdAsync(), // 这个域用来标识唯一性
sort: 1000,
uploadState: 'success',
extra1: data.MediaId,

View File

@ -476,6 +476,28 @@ export async function loginByMobile(params, context) {
}
};
const closeRootMode = context.openRootMode();
const application = context.getApplication();
const [applicationPassport] = await context.select('applicationPassport', {
data: {
id: 1,
passportId: 1,
passport: {
id: 1,
config: 1,
type: 1,
},
applicationId: 1,
},
filter: {
applicationId: application?.id,
passport: {
type: 'sms'
},
}
}, {
dontCollect: true,
});
assert(applicationPassport?.passport);
if (disableRegister) {
const [existMobile] = await context.select('mobile', {
data: {
@ -538,6 +560,49 @@ export async function loginByAccount(params, context) {
assert(account);
const accountType = isEmail(account) ? 'email' : (isMobile(account) ? 'mobile' : 'loginName');
if (accountType === 'email') {
// 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 as EmailConfig;
// const emailConfig = system?.config.Emails?.find((ele) => ele.account === config.account);
// assert(emailConfig);
// const emailSuffix = config.emailSuffix;
// // 检查邮箱后缀是否满足配置
// if (emailSuffix?.length! > 0) {
// let isValid = false;
// for (const suffix of emailSuffix!) {
// if (account.endsWith(suffix)) {
// isValid = true;
// break;
// }
// }
// if (!isValid) {
// throw new OakUserException('error::user.emailSuffixIsInvalid');
// }
// }
const existEmail = await context.select('email', {
data: {
id: 1,
@ -822,6 +887,28 @@ export async function loginByAccount(params, context) {
}
};
const closeRootMode = context.openRootMode();
const application = context.getApplication();
const [applicationPassport] = await context.select('applicationPassport', {
data: {
id: 1,
passportId: 1,
passport: {
id: 1,
config: 1,
type: 1,
},
applicationId: 1,
},
filter: {
applicationId: application?.id,
passport: {
type: 'password'
},
}
}, {
dontCollect: true,
});
assert(applicationPassport?.passport);
const tokenValue = await loginLogic();
const [tokenInfo] = await loadTokenInfo(tokenValue, context);
const { userId } = tokenInfo;
@ -889,6 +976,46 @@ export async function loginByEmail(params, context) {
}
};
const closeRootMode = context.openRootMode();
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 emailSuffix = config.emailSuffix;
// 检查邮箱后缀是否满足配置
if (emailSuffix?.length > 0) {
let isValid = false;
for (const suffix of emailSuffix) {
if (email.endsWith(suffix)) {
isValid = true;
break;
}
}
if (!isValid) {
throw new OakUserException('邮箱后缀不符合要求');
}
}
if (disableRegister) {
const [existEmail] = await context.select('email', {
data: {
@ -942,7 +1069,7 @@ export async function bindByMobile(params, context) {
throw new OakUserException('验证码已经过期');
}
// 到这里说明验证码已经通过
//检查当前user是否已绑定mobile
// 检查当前user是否已绑定mobile
const [boundMobile] = await context.select('mobile', {
data: {
id: 1,
@ -1102,6 +1229,46 @@ export async function bindByEmail(params, context) {
}
};
const closeRootMode = context.openRootMode();
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 emailSuffix = config.emailSuffix;
// 检查邮箱后缀是否满足配置
if (emailSuffix?.length > 0) {
let isValid = false;
for (const suffix of emailSuffix) {
if (email.endsWith(suffix)) {
isValid = true;
break;
}
}
if (!isValid) {
throw new OakUserException('邮箱后缀不符合要求');
}
}
const [otherUserEmail] = await context.select('email', {
data: {
id: 1,
@ -2026,13 +2193,28 @@ export async function sendCaptchaByEmail({ email, env, type: captchaType, }, con
const duration = config.codeDuration || 5;
const digit = config.digit || 4;
const mockSend = config.mockSend;
const emailSuffix = config.emailSuffix;
// 检查邮箱后缀是否满足配置
if (emailSuffix?.length > 0) {
let isValid = false;
for (const suffix of emailSuffix) {
if (email.endsWith(suffix)) {
isValid = true;
break;
}
}
if (!isValid) {
throw new OakUserException('邮箱后缀不符合要求');
}
}
let emailOptions = {
host: emailConfig.host,
port: emailConfig.port,
account: emailConfig.account,
password: emailConfig.password,
subject: config.subject,
// host: emailConfig.host,
// port: emailConfig.port,
// secure: emailConfig.secure,
// account: emailConfig.account,
// password: emailConfig.password,
from: emailConfig.name ? `"${emailConfig.name}" <${emailConfig.account}>` : emailConfig.account,
subject: config.subject,
to: email,
text: config.text,
html: config.html,
@ -2341,8 +2523,8 @@ export async function refreshToken(params, context) {
// 只有server模式去刷新token
// 'development' | 'production' | 'staging'
const intervals = {
development: 7200 * 1000,
staging: 600 * 1000,
development: 7200 * 1000, // 2小时
staging: 600 * 1000, // 十分钟
production: 600 * 1000, // 十分钟
};
let applicationId = token.applicationId;

View File

@ -161,7 +161,7 @@ export async function createWechatQrCode(options, context) {
permanent,
url,
expired: false,
expiresAt: Date.now() + 2592000 * 1000,
expiresAt: Date.now() + 2592000 * 1000, // wecharQrCode里的过期时间都放到最大由上层关联对象来主动过期by Xc, 20230131)
props,
};
// 直接创建

View File

@ -23,9 +23,9 @@ export default OakComponent({
tocFixed: true,
tocPosition: 'none',
highlightBgColor: 'none',
headerTop: 0,
headerTop: 0, //页面中吸顶部分高度
className: '',
scrollId: '',
scrollId: '', // 滚动条所在容器id不传默认body
tocWidth: undefined,
tocHeight: undefined,
},

View File

@ -28,9 +28,9 @@ export default OakComponent({
tocFixed: true,
tocPosition: 'none',
highlightBgColor: 'none',
headerTop: 0,
headerTop: 0, //页面中吸顶部分高度
className: '',
scrollId: '',
scrollId: '', // 滚动条所在容器id不传默认body
tocWidth: undefined,
tocHeight: undefined,
},

View File

@ -4,7 +4,7 @@ export default OakComponent({
properties: {
articleMenuId: '',
onChildEditArticleChange: (data) => undefined,
show: 'edit',
show: 'edit', // edit为编辑doc为查看preview为预览
getBreadcrumbItemsByParent: (breadcrumbItems) => undefined,
breadcrumbItems: [],
drawerOpen: false,

View File

@ -25,11 +25,11 @@ export default OakComponent({
properties: {
articleMenuId: '',
changeIsEdit: () => undefined,
tocPosition: 'none',
highlightBgColor: 'none',
onArticlePreview: (content, title) => undefined,
origin: 'qiniu',
scrollId: '',
tocPosition: 'none', //目录显示位置none为不显示目录
highlightBgColor: 'none', //点击目录时标题高亮背景色none为不显示高亮背景色
onArticlePreview: (content, title) => undefined, //预览文章
origin: 'qiniu', // 默认为七牛云
scrollId: '', // 滚动条所在容器id不传默认页面编辑器容器id
height: 600,
},
listeners: {

View File

@ -31,7 +31,7 @@ function customCheckImageFn(src, alt, url) {
export default function Render(props) {
const { methods, data } = props;
const { t, setEditor, check, uploadFile, update, setHtml, gotoPreview, clearContentTip, } = methods;
const { oakFullpath, id, content, editor, origin = 'qiniu', tocPosition = 'none', highlightBgColor = 'none', scrollId, tocWidth, tocHeight, height = 600 } = data;
const { oakId, oakFullpath, id, content, editor, origin = 'qiniu', tocPosition = 'none', highlightBgColor = 'none', scrollId, tocWidth, tocHeight, height = 600 } = data;
const [articleId, setArticleId] = useState('');
const [toc, setToc] = useState([]);
const [showToc, setShowToc] = useState(false);
@ -78,7 +78,7 @@ export default function Render(props) {
const filename = name.substring(0, name.lastIndexOf("."));
const extraFile = {
entity: "article",
entityId: articleId,
entityId: oakId || articleId,
origin: origin,
type: "image",
tag1: "source",
@ -109,7 +109,7 @@ export default function Render(props) {
const filename = name.substring(0, name.lastIndexOf("."));
const extraFile = {
entity: "article",
entityId: articleId,
entityId: oakId || articleId,
origin: origin,
type: "video",
tag1: "source",

View File

@ -47,7 +47,7 @@ export default OakComponent({
onRemove: () => undefined,
onUpdateName: async (name) => undefined,
onChildEditArticleChange: (data) => undefined,
show: 'edit',
show: 'edit', // edit为编辑doc为查看preview为预览
getBreadcrumbItemsByParent: (breadcrumbItems) => undefined,
breadItems: [],
drawerOpen: false,

View File

@ -6,7 +6,7 @@ export default OakComponent({
entityId: '',
parentId: '',
onGrandChildEditArticleChange: (data) => undefined,
show: 'edit',
show: 'edit', // edit为编辑doc为查看preview为预览
articleMenuId: '',
articleId: '',
getBreadcrumbItems: (breadcrumbItems) => undefined,

View File

@ -14,18 +14,18 @@ export default OakComponent({
properties: {
entity: '',
entityId: '',
show: 'edit',
articleMenuId: '',
articleId: '',
tocPosition: 'none',
highlightBgColor: 'none',
onMenuView: () => undefined,
onMenuViewById: (articleMenuId) => undefined,
onArticleView: (articleId) => undefined,
onArticlePreview: (content, title) => undefined,
onArticleEdit: (articleId) => undefined,
show: 'edit', // edit为编辑doc为查看preview为预览
articleMenuId: '', // 菜单id
articleId: '', //文章id
tocPosition: 'none', //文章目录显示位置none为不显示目录
highlightBgColor: 'none', //点击文章目录时标题高亮背景色none为不显示高亮背景色
onMenuView: () => undefined, //查看全部菜单
onMenuViewById: (articleMenuId) => undefined, //查看指定id菜单
onArticleView: (articleId) => undefined, //查看文章
onArticlePreview: (content, title) => undefined, //预览文章
onArticleEdit: (articleId) => undefined, //编辑文章
setCopyArticleUrl: (articleId) => '',
origin: 'qiniu',
origin: 'qiniu', // cos origin默认七牛云
scrollId: '', // 滚动条所在容器id不传默认页面编辑器容器id
},
});

View File

@ -37,7 +37,7 @@ export default OakComponent({
code: '',
title: '',
desc: '',
icon: '',
icon: '', //web独有
imagePath: '', //小程序独有
},
lifetimes: {

View File

@ -12,7 +12,7 @@ export default OakComponent({
color: '#666',
selectedColor: '',
border: false,
selectedIconPath: '',
selectedIconPath: '', //一般在list设置
iconPath: '',
},
lifetimes: {

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Tabs, Col, Divider, Input, Form, Space, Modal, DatePicker, } from 'antd';
import { Tabs, Col, Divider, Input, Form, Space, Modal, Switch, DatePicker, } from 'antd';
import Styles from './web.module.less';
import dayjs from 'dayjs';
const { confirm } = Modal;
@ -41,6 +41,11 @@ export default function Email(props) {
setValue(`${idx}.port`, Number(e.target.value));
}}/>
</Form.Item>
<Form.Item label="启用SSL加密" tooltip="当连接使用的是SSL端口请启用SSL加密功能以保障数据传输安全">
<>
<Switch checkedChildren="是" unCheckedChildren="否" checked={ele?.secure} onChange={(checked) => setValue(`${idx}.secure`, checked)}/>
</>
</Form.Item>
<Form.Item label="账号">
<Input placeholder="请输入邮箱账号(例xxxx@163.com)" type="text" value={ele.account} onChange={(e) => {
setValue(`${idx}.account`, e.target.value);

View File

@ -47,19 +47,19 @@ export default OakComponent({
bucket: '',
autoUpload: false,
maxNumber: 20,
extension: [],
selectCount: 1,
sourceType: ['album', 'camera'],
mediaType: ['image'],
mode: 'aspectFit',
size: 3,
showUploadList: true,
showUploadProgress: false,
accept: 'image/*',
disablePreview: false,
disableDelete: false,
disableAdd: false,
disableDownload: false,
extension: [], //小程序独有 chooseMessageFile
selectCount: 1, // 每次打开图片时,可选中的数量 小程序独有
sourceType: ['album', 'camera'], // 小程序独有 chooseMedia
mediaType: ['image'], // 小程序独有 chooseMedia
mode: 'aspectFit', // 图片显示模式
size: 3, // 每行可显示的个数 小程序独有
showUploadList: true, //web独有
showUploadProgress: false, // web独有
accept: 'image/*', // web独有
disablePreview: false, // 图片是否可预览
disableDelete: false, // 图片是否可删除
disableAdd: false, // 上传按钮隐藏
disableDownload: false, // 下载按钮隐藏
type: 'image',
origin: 'qiniu',
tag1: '',
@ -67,40 +67,40 @@ export default OakComponent({
entity: '',
entityId: '',
theme: 'image',
enableCrop: false,
enableCompross: false,
enableCrop: false, //启用裁剪
enableCompross: false, //启用压缩
//图片裁剪
cropQuality: 1,
showRest: false,
showGrid: false,
fillColor: 'white',
rotationSlider: false,
aspectSlider: false,
zoomSlider: true,
resetText: '重置',
aspect: 1 / 1,
minZoom: 1,
maxZoom: 3,
cropShape: 'rect',
cropperProps: {},
modalTitle: '编辑图片',
modalWidth: '40vw',
modalOk: '确定',
modalCancel: '取消',
cropQuality: 1, //图片裁剪质量范围0 ~ 1
showRest: false, //显示重置按钮,重置缩放及旋转
showGrid: false, //显示裁切区域网格(九宫格)
fillColor: 'white', //裁切图像填充色
rotationSlider: false, //图片旋转控制
aspectSlider: false, //裁切比率控制
zoomSlider: true, //图片缩放控制
resetText: '重置', //重置按钮文字
aspect: 1 / 1, //裁切区域宽高比width / height
minZoom: 1, //最小缩放倍数
maxZoom: 3, //最大缩放倍数
cropShape: 'rect', //裁切区域形状,'rect' 或 'round'
cropperProps: {}, //recat-easy-crop的props
modalTitle: '编辑图片', //弹窗标题
modalWidth: '40vw', //弹窗宽度
modalOk: '确定', //确定按钮文字
modalCancel: '取消', //取消按钮的文字
//图片压缩
strict: true,
checkOrientation: true,
retainExif: false,
maxWidth: Infinity,
maxHeight: Infinity,
minWidth: 0,
minHeight: 0,
compressWidth: undefined,
compressHeight: undefined,
resize: 'none',
compressQuality: 0.8,
mimeType: 'auto',
convertTypes: ['image/png'],
strict: true, //当压缩后的图片尺寸大于原图尺寸时输出原图
checkOrientation: true, //读取图像的Exif方向值并自动旋转或翻转图像仅限 JPEG 图像)
retainExif: false, //压缩后保留图片的Exif信息
maxWidth: Infinity, //输出图片的最大宽度值需大于0
maxHeight: Infinity, //输出图片的最大高度值需大于0
minWidth: 0, //输出图片的最小宽度值需大于0且不应大于maxWidth
minHeight: 0, //输出图片的最小高度。值需大于0且不应大于maxHeight
compressWidth: undefined, //输出图像的宽度。如果未指定则将使用原始图像的宽度若设置了height则宽度将根据自然纵横比自动计算。
compressHeight: undefined, //输出图像的高度。如果未指定则将使用原始图像的高度若设置了width则高度将根据自然纵横比自动计算。
resize: 'none', //仅在同时指定了width和height时生效
compressQuality: 0.8, //输出图像的质量。范围0 ~ 1
mimeType: 'auto', //输出图片的 MIME 类型。默认情况下,将使用源图片文件的原始 MIME 类型。
convertTypes: ['image/png'], //文件类型包含在其中且文件大小超过该convertSize值的文件将被转换为 JPEG。
convertSize: Infinity, //文件类型包含在convertTypes中且文件大小超过此值的文件将转换为 JPEGInfinity表示禁用该功能
},
features: ['extraFile'],

View File

@ -13,7 +13,7 @@ export default function render(props) {
};
return (<>
{enableCrop ? (<ImgCrop showReset={showRest} showGrid={showGrid} fillColor={fillColor} rotationSlider={rotationSlider} aspectSlider={aspectSlider} zoomSlider={zoomSlider} resetText={resetText} aspect={aspect} minZoom={minZoom} maxZoom={maxZoom} cropShape={cropShape} quality={cropQuality} cropperProps={{
restrictPosition: false,
restrictPosition: false, //允许移动图片位置
...cropperProps,
}} modalTitle={modalTitle} modalWidth={modalWidth} modalOk={modalOk} modalCancel={modalCancel}>
<ExtrafileUpload oakPath={oakFullpath} bucket={bucket} autoUpload={autoUpload} maxNumber={maxNumber} mode={mode} showUploadList={showUploadList} showUploadProgress={showUploadProgress} accept={accept} disablePreview={disablePreview} disableDelete={disableDelete} disableAdd={disableAdd} disableDownload={disableDownload} disabled={disabled} type={type} origin={origin} tag1={tag1} tag2={tag2} entity={entity} entityId={entityId} theme={theme} children={children} beforeUpload={async (file) => {

View File

@ -55,9 +55,9 @@ export default OakComponent({
data: {
isModalOpen: false,
isModalOpen1: false,
renderImgs: [],
renderImgs: [], // 读取的原文图片在modal使用
methodsType: '',
bridgeUrl: '',
bridgeUrl: '', // 通过桥接方式获得的url
selectedId: -1,
},
properties: {

View File

@ -67,10 +67,10 @@ export default OakComponent({
},
],
properties: {
mode: 'aspectFit',
size: 3,
disablePreview: false,
disableDownload: false,
mode: 'aspectFit', // 图片显示模式
size: 3, // 每行可显示的个数 小程序独有
disablePreview: false, // 图片是否可预览
disableDownload: false, // 下载按钮隐藏
tag1: '',
tag2: '',
entity: '',

View File

@ -50,19 +50,19 @@ export default OakComponent({
bucket: '',
autoUpload: false,
maxNumber: 20,
extension: [],
selectCount: 1,
sourceType: ['album', 'camera'],
mediaType: ['image'],
mode: 'aspectFit',
size: 3,
showUploadList: true,
showUploadProgress: false,
accept: 'image/*',
disablePreview: false,
disableDelete: false,
disableAdd: false,
disableDownload: false,
extension: [], //小程序独有 chooseMessageFile
selectCount: 1, // 每次打开图片时,可选中的数量 小程序独有
sourceType: ['album', 'camera'], // 小程序独有 chooseMedia
mediaType: ['image'], // 小程序独有 chooseMedia
mode: 'aspectFit', // 图片显示模式
size: 3, // 每行可显示的个数 小程序独有
showUploadList: true, //web独有
showUploadProgress: false, // web独有
accept: 'image/*', // web独有
disablePreview: false, // 图片是否可预览
disableDelete: false, // 图片是否可删除
disableAdd: false, // 上传按钮隐藏
disableDownload: false, // 下载按钮隐藏
type: 'image',
origin: 'qiniu',
tag1: '',
@ -147,7 +147,7 @@ export default OakComponent({
type,
tag1,
tag2,
objectId: generateNewId(),
objectId: generateNewId(), // 这个域用来标识唯一性
entity,
filename,
size,

View File

@ -1,10 +1,60 @@
import React, { useEffect, useState } from "react";
import { Switch, Alert, Typography, Form, Input, Radio, Tag } from 'antd';
import React, { useEffect, useState, useRef } from "react";
import { Switch, Alert, Typography, Form, Input, Radio, Tag, Tooltip, Flex } from 'antd';
import Styles from './web.module.less';
import '@wangeditor/editor/dist/css/style.css'; // 引入 css
import { Editor, Toolbar } from "@wangeditor/editor-for-react";
import { PlusOutlined, CloseOutlined } from '@ant-design/icons';
const { TextArea } = Input;
const { Text } = Typography;
function RenderEmailSuffix(props) {
const { emailSuffix, onChange, t } = props;
const [inputVisible, setInputVisible] = useState(false);
const [inputValue, setInputValue] = useState('');
const inputRef = useRef(null);
const tagInputStyle = {
width: 100,
height: 22,
marginInlineEnd: 8,
verticalAlign: 'top',
};
const tagPlusStyle = {
height: 22,
borderStyle: 'dashed',
};
const handleInputChange = (e) => {
setInputValue(e.target.value);
};
const handleInputConfirm = () => {
if (inputValue && !emailSuffix?.includes(inputValue)) {
onChange([...emailSuffix || [], inputValue]);
}
setInputVisible(false);
setInputValue('');
};
const handleClose = (removedTag) => {
const emailSuffix2 = emailSuffix.filter((tag) => tag !== removedTag);
onChange(emailSuffix2);
};
const showInput = () => {
setInputVisible(true);
};
return (<Flex gap="4px 0" wrap="wrap">
{(emailSuffix || []).map((tag, index) => {
const isLongTag = tag.length > 20;
const tagElem = (<Tag closeIcon={<CloseOutlined />} key={tag} onClose={() => handleClose(tag)}>
<span>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</span>
</Tag>);
return isLongTag ? (<Tooltip title={tag} key={tag}>
{tagElem}
</Tooltip>) : (tagElem);
})}
{inputVisible ? (<Input ref={inputRef} type="text" size="small" style={tagInputStyle} value={inputValue} onChange={handleInputChange} onBlur={handleInputConfirm} onPressEnter={handleInputConfirm}/>) : (<Tag style={tagPlusStyle} icon={<PlusOutlined />} onClick={showInput}>
{t('common::action.add')}
</Tag>)}
</Flex>);
}
export default function Email(props) {
const { passport, t, changeEnabled, updateConfig } = props;
const { id, type, enabled, stateColor } = passport;
@ -16,6 +66,7 @@ export default function Email(props) {
const [html, setHtml] = useState(config?.html || '');
const [emailCodeDuration, setEmailCodeDuration] = useState(config?.codeDuration || '');
const [emailDigit, setEmailDigit] = useState(config?.digit || '');
const [emailSuffix, setEmailSuffix] = useState(config?.emailSuffix || []);
// editor 实例
const [editor, setEditor] = useState(null); // TS 语法
// 工具栏配置
@ -40,6 +91,7 @@ export default function Email(props) {
setHtml(config?.html || '');
setEmailCodeDuration(config?.codeDuration || '');
setEmailDigit(config?.digit || '');
setEmailSuffix(config?.emailSuffix || []);
if (config?.html) {
setEContentType('html');
}
@ -150,6 +202,13 @@ export default function Email(props) {
else {
updateConfig(id, config, 'digit', undefined, 'email');
}
}}/>
</Form.Item>
<Form.Item label="邮箱后缀" tooltip="允许的邮箱后缀(如: @qq.com不填则不校验">
<RenderEmailSuffix t={t} emailSuffix={emailSuffix} onChange={(v) => {
if (v !== config?.emailSuffix) {
updateConfig(id, config, 'emailSuffix', v, 'email');
}
}}/>
</Form.Item>
</Form>

View File

@ -7,6 +7,40 @@ export default OakComponent({
config: 1,
description: 1,
style: 1,
// system$platform: {
// $entity: 'system',
// data: {
// id: 1,
// name: 1,
// config: 1,
// description: 1,
// super: 1,
// folder: 1,
// platformId: 1,
// style: 1,
// domain$system: {
// $entity: 'domain',
// data: {
// id: 1,
// systemId: 1,
// url: 1,
// },
// },
// application$system: {
// $entity: 'application',
// data: {
// id: 1,
// name: 1,
// config: 1,
// description: 1,
// type: 1,
// systemId: 1,
// domainId: 1,
// style: 1,
// }
// }
// }
// }
},
formData({ data }) {
return data || {};

View File

@ -29,7 +29,7 @@ export default function render(props) {
{
label: <div className={Styles.tabLabel}>{t('system-list')}</div>,
key: 'system',
children: (<PlatformSystem oakPath={`${oakFullpath}.system$platform`} platformId={id}/>),
children: (<PlatformSystem oakPath={`${oakFullpath}-PlatformSystem`} platformId={id}/>),
},
]}/>
</div>);

View File

@ -1,9 +1,7 @@
.container {
padding: 8px;
height: 100%;
.tabLabel {
// writing-mode: vertical-rl;
letter-spacing: .2rem;
}
// .tabLabel {
// letter-spacing: .2rem;
// }
}

View File

@ -32,6 +32,13 @@ export default OakComponent({
}
}
},
filters: [{
filter() {
return {
platformId: this.props.platformId,
};
},
}],
properties: {
platformId: '',
},

View File

@ -32,7 +32,9 @@ export default function render(props) {
</Button>}>
{t('confirmToRemove')}
</Modal>
<Tabs type="editable-card" onEdit={(key, action) => {
<Tabs type="editable-card"
// destroyInactiveTabPane={true}
onEdit={(key, action) => {
if (action === 'add') {
const id = addItem({ platformId, config: { App: {} } });
setCreateId(id);

View File

@ -20,7 +20,7 @@ export default OakComponent({
properties: {
sessionId: '',
isEntity: false,
entityDisplay: (data) => [],
entityDisplay: (data) => [], // user端指示如何显示entity对象名称
entityProjection: null, // user端指示需要取哪些entity的属性来显示entityDisplay
},
methods: {

View File

@ -140,12 +140,12 @@ export default OakComponent({
unSub: undefined,
},
properties: {
entity: '',
entityFilter: null,
entity: '', // entity端指示相应的entity
entityFilter: null, // entity端指示相应的entity查询条件
entityFilterSubStr: '',
entityDisplay: (data) => [],
entityProjection: null,
sessionId: '',
entityDisplay: (data) => [], // user端指示如何显示entity对象名称
entityProjection: null, // user端指示需要取哪些entity的属性来显示entityDisplay
sessionId: '', // 指示需要打开的默认session
dialog: false,
onItemClick: null,
},

View File

@ -102,7 +102,7 @@ export default OakComponent({
dialog: false,
entity: '',
entityId: '',
entityDisplay: (data) => [],
entityDisplay: (data) => [], // user端指示如何显示entity对象名称
entityProjection: null, // user端指示需要取哪些entity的属性来显示entityDisplay
},
filters: [

View File

@ -18,18 +18,6 @@ export default OakComponent({
url: 1,
},
},
// application$system: {
// $entity: 'application',
// data: {
// id: 1,
// name: 1,
// config: 1,
// description: 1,
// type: 1,
// systemId: 1,
// domainId: 1,
// }
// }
},
formData({ data }) {
return data || {};

View File

@ -62,7 +62,7 @@ export default function Render(props) {
</div>),
key: 'passport-list',
destroyInactiveTabPane: true,
children: (<Passport oakPath={`$system-passport`} systemId={id} systemName={name}/>),
children: (<Passport oakPath={`$system-passport-${id}`} systemId={id} systemName={name}/>),
},
]}/>
</div>);

View File

@ -21,8 +21,8 @@ export default OakComponent({
},
properties: {
disabled: '',
url: '',
callback: undefined,
url: '', // 登录系统之后要返回的页面
callback: undefined, // 登录成功回调,排除微信登录方式
setLoginMode: (value) => undefined,
digit: 4, //验证码位数
},

View File

@ -17,15 +17,15 @@ export default OakComponent({
allowPassword: false,
allowWechatMp: false,
setLoginModeMp(value) { this.setLoginMode(value); },
smsDigit: 4,
smsDigit: 4, //短信验证码位数
emailDigit: 4, //邮箱验证码位数
},
properties: {
onlyCaptcha: false,
onlyPassword: false,
disabled: '',
redirectUri: '',
url: '',
redirectUri: '', // 微信登录后的redirectUri要指向wechatUser/login去处理
url: '', // 登录系统之后要返回的页面
callback: undefined, // 登录成功回调,排除微信登录方式
},
formData({ features, props }) {

View File

@ -18,12 +18,12 @@ export default OakComponent({
},
properties: {
disabled: '',
redirectUri: '',
url: '',
callback: undefined,
redirectUri: '', // 微信登录后的redirectUri要指向wechatUser/login去处理
url: '', // 登录系统之后要返回的页面
callback: undefined, // 登录成功回调,排除微信登录方式
allowSms: false,
allowEmail: false,
allowWechatMp: false,
allowWechatMp: false, //小程序切换授权登录
setLoginMode: (value) => undefined,
},
lifetimes: {},

View File

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

View File

@ -17,7 +17,7 @@ export default OakComponent({
id: 1,
entity: 1,
entityId: 1,
type: 1,
type: 1, //类型
ticket: 1,
url: 1,
buffer: 1,

View File

@ -64,7 +64,7 @@ export default OakComponent({
id: 1,
entity: 1,
entityId: 1,
type: 1,
type: 1, //类型
ticket: 1,
url: 1,
buffer: 1,

View File

@ -5,7 +5,7 @@ export default OakComponent({
id: 1,
entity: 1,
entityId: 1,
type: 1,
type: 1, //类型
ticket: 1,
url: 1,
expired: 1,

View File

@ -4,7 +4,7 @@ export default OakComponent({
id: 1,
entity: 1,
entityId: 1,
type: 1,
type: 1, //类型
ticket: 1,
url: 1,
buffer: 1,

View File

@ -626,7 +626,8 @@ const i18ns = [
"loginWayDisabled": "暂不允许该登录方式",
"hasToSetPassword": "需要设置密码",
"hasToVerifyPassword": "需要验证密码",
"cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息"
"cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息",
"emailSuffixIsInvalid": "邮箱后缀不合法"
},
"distinguishUser": "需要鉴别用户身份",
"mobileUnset": "需要先登记手机号",

View File

@ -3,10 +3,10 @@ export const entityDesc = {
zh_CN: {
name: '直播流',
attr: {
title: '名称',
title: '名称', // 用户定义直播间名称,
streamTitle: '直播流名称',
liveonly: '活跃状态',
hub: '直播空间名称',
hub: '直播空间名称', // 所属直播空间名称
entity: '所属实体',
entityId: '所属实体id',
rtmpPushUrl: '推流地址',

View File

@ -18,6 +18,7 @@ export type EmailConfig = {
html?: string;
codeDuration?: number;
digit?: number;
emailSuffix?: string[];
};
export type PfwConfig = {
appId: string;

View File

@ -11,7 +11,8 @@
"loginWayDisabled": "暂不允许该登录方式",
"hasToSetPassword": "需要设置密码",
"hasToVerifyPassword": "需要验证密码",
"cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息"
"cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息",
"emailSuffixIsInvalid": "邮箱后缀不合法"
},
"distinguishUser": "需要鉴别用户身份",
"mobileUnset": "需要先登记手机号",

View File

@ -19,6 +19,7 @@ export type EmailConfig = {
html?: string;
codeDuration?: number;
digit?: number;
emailSuffix?: string[];
};
export type PfwConfig = {
appId: string;

View File

@ -1,2 +1,2 @@
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
export default _default;

View File

@ -61,7 +61,7 @@ async function sendNotification(notification, context) {
await instance.sendSubscribedMessage({
templateId: templateId,
data: data,
openId: data1.openId,
openId: data1.openId, // 在notification创建时就赋值了
page,
state: StateDict[process.env.NODE_ENV],
});

18
es/types/Config.d.ts vendored
View File

@ -123,6 +123,15 @@ export type TencentSmsConfig = {
defaultSignName: string;
endpoint: string;
};
export type EmailConfig = {
host: string;
port: number;
account: string;
password: string;
passwordExpiredAt?: number;
name?: string;
secure?: boolean;
};
export type QrCodeType = 'wechatMpDomainUrl' | 'wechatMpWxaCode' | 'wechatPublic' | 'wechatPublicForMp' | 'webForWechatPublic';
export type Config = {
Account?: {
@ -165,14 +174,7 @@ export type Config = {
needUploadIDCardPhoto?: boolean;
needManualVerification?: boolean;
};
Emails?: {
host: string;
port: number;
account: string;
password: string;
passwordExpiredAt?: number;
name?: string;
}[];
Emails?: EmailConfig[];
Security?: {
type?: 'password';
level?: 'weak' | 'medium' | 'strong';

11
es/types/Email.d.ts vendored
View File

@ -26,13 +26,14 @@ interface Attachment extends AttachmentLike {
raw?: string | Buffer | AttachmentLike | undefined;
}
export type EmailOptions = {
host: string;
port: number;
account: string;
password: string;
subject: string;
host?: string;
port?: number;
secure?: boolean;
account?: string;
password?: string;
from: string;
to: string;
subject: string;
text?: string;
html?: string;
attachments?: Attachment[];

View File

@ -1,8 +1,13 @@
import { EntityDict } from '../../oak-app-domain';
import { BRC } from '../../types/RuntimeCxt';
import Email, { EmailOptions } from '../../types/Email';
import { BackendRuntimeContext } from '../../context/BackendRuntimeContext';
import { EmailConfig } from '../../types/Config';
export default class Nodemailer implements Email<EntityDict> {
name: string;
getConfig(context: BackendRuntimeContext<EntityDict>, systemId?: string): Promise<{
config: EmailConfig;
}>;
sendEmail(options: EmailOptions, context: BRC<EntityDict>): Promise<{
success: boolean;
error?: undefined;

View File

@ -1,16 +1,50 @@
//https://www.nodemailer.com/
import nodemailer from 'nodemailer';
import { assert } from 'oak-domain/lib/utils/assert';
import { get } from 'oak-domain/lib/utils/lodash';
export default class Nodemailer {
name = 'nodemailer';
async getConfig(context, systemId) {
let system;
if (systemId) {
[system] = await context.select('system', {
data: {
id: 1,
config: 1,
},
filter: {
id: systemId,
}
}, {
dontCollect: true,
});
}
else {
system = context.getApplication().system;
}
const { config: systemConfig } = system;
const emailConfig = get(systemConfig, 'Emails.0', {});
const { host, port, password, account } = emailConfig;
assert(host, 'host未配置');
assert(port, 'port未配置');
assert(account, 'account未配置');
assert(password, 'password未配置');
return {
config: emailConfig,
};
}
async sendEmail(options, context) {
const { host, port, account, password, subject, from, text, html, to, attachments } = options;
const { subject, from, text, html, to, attachments } = options;
const { config, } = await this.getConfig(context);
const transport = Object.assign(config, options);
const _from = from || (config.name ? `"${config.name}" <${config.account}>` : config.account);
const transporter = nodemailer.createTransport({
host,
port,
secure: port === 465,
host: transport.host,
port: transport.port,
secure: transport.secure,
auth: {
user: account,
pass: password,
user: transport.account,
pass: transport.password,
},
});
async function verifyTransporter() {
@ -31,7 +65,7 @@ export default class Nodemailer {
console.log('Server is ready to take our messages');
}
let mailOptions = {
from,
from: _from,
to,
subject,
text,

View File

@ -106,7 +106,7 @@ async function createSession(params, context) {
origin: 'wechat',
type: 'image',
tag1: 'image',
objectId: await (0, uuid_1.generateNewIdAsync)(),
objectId: await (0, uuid_1.generateNewIdAsync)(), // 这个域用来标识唯一性
sort: 1000,
uploadState: 'success',
extra1: data.MediaId,
@ -131,7 +131,7 @@ async function createSession(params, context) {
origin: 'wechat',
type: 'video',
tag1: 'video',
objectId: await (0, uuid_1.generateNewIdAsync)(),
objectId: await (0, uuid_1.generateNewIdAsync)(), // 这个域用来标识唯一性
sort: 1000,
uploadState: 'success',
extra1: data.MediaId,
@ -153,7 +153,7 @@ async function createSession(params, context) {
origin: 'wechat',
type: 'audio',
tag1: 'audio',
objectId: await (0, uuid_1.generateNewIdAsync)(),
objectId: await (0, uuid_1.generateNewIdAsync)(), // 这个域用来标识唯一性
sort: 1000,
uploadState: 'success',
extra1: data.MediaId,

View File

@ -480,6 +480,28 @@ async function loginByMobile(params, context) {
}
};
const closeRootMode = context.openRootMode();
const application = context.getApplication();
const [applicationPassport] = await context.select('applicationPassport', {
data: {
id: 1,
passportId: 1,
passport: {
id: 1,
config: 1,
type: 1,
},
applicationId: 1,
},
filter: {
applicationId: application?.id,
passport: {
type: 'sms'
},
}
}, {
dontCollect: true,
});
(0, assert_1.assert)(applicationPassport?.passport);
if (disableRegister) {
const [existMobile] = await context.select('mobile', {
data: {
@ -544,6 +566,49 @@ async function loginByAccount(params, context) {
(0, assert_1.assert)(account);
const accountType = (0, validator_1.isEmail)(account) ? 'email' : ((0, validator_1.isMobile)(account) ? 'mobile' : 'loginName');
if (accountType === 'email') {
// 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 as EmailConfig;
// const emailConfig = system?.config.Emails?.find((ele) => ele.account === config.account);
// assert(emailConfig);
// const emailSuffix = config.emailSuffix;
// // 检查邮箱后缀是否满足配置
// if (emailSuffix?.length! > 0) {
// let isValid = false;
// for (const suffix of emailSuffix!) {
// if (account.endsWith(suffix)) {
// isValid = true;
// break;
// }
// }
// if (!isValid) {
// throw new OakUserException('error::user.emailSuffixIsInvalid');
// }
// }
const existEmail = await context.select('email', {
data: {
id: 1,
@ -828,6 +893,28 @@ async function loginByAccount(params, context) {
}
};
const closeRootMode = context.openRootMode();
const application = context.getApplication();
const [applicationPassport] = await context.select('applicationPassport', {
data: {
id: 1,
passportId: 1,
passport: {
id: 1,
config: 1,
type: 1,
},
applicationId: 1,
},
filter: {
applicationId: application?.id,
passport: {
type: 'password'
},
}
}, {
dontCollect: true,
});
(0, assert_1.assert)(applicationPassport?.passport);
const tokenValue = await loginLogic();
const [tokenInfo] = await loadTokenInfo(tokenValue, context);
const { userId } = tokenInfo;
@ -896,6 +983,46 @@ async function loginByEmail(params, context) {
}
};
const closeRootMode = context.openRootMode();
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,
});
(0, assert_1.assert)(applicationPassport?.passport);
const config = applicationPassport.passport.config;
const emailConfig = system?.config.Emails?.find((ele) => ele.account === config.account);
(0, assert_1.assert)(emailConfig);
const emailSuffix = config.emailSuffix;
// 检查邮箱后缀是否满足配置
if (emailSuffix?.length > 0) {
let isValid = false;
for (const suffix of emailSuffix) {
if (email.endsWith(suffix)) {
isValid = true;
break;
}
}
if (!isValid) {
throw new types_1.OakUserException('邮箱后缀不符合要求');
}
}
if (disableRegister) {
const [existEmail] = await context.select('email', {
data: {
@ -950,7 +1077,7 @@ async function bindByMobile(params, context) {
throw new types_1.OakUserException('验证码已经过期');
}
// 到这里说明验证码已经通过
//检查当前user是否已绑定mobile
// 检查当前user是否已绑定mobile
const [boundMobile] = await context.select('mobile', {
data: {
id: 1,
@ -1111,6 +1238,46 @@ async function bindByEmail(params, context) {
}
};
const closeRootMode = context.openRootMode();
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,
});
(0, assert_1.assert)(applicationPassport?.passport);
const config = applicationPassport.passport.config;
const emailConfig = system?.config.Emails?.find((ele) => ele.account === config.account);
(0, assert_1.assert)(emailConfig);
const emailSuffix = config.emailSuffix;
// 检查邮箱后缀是否满足配置
if (emailSuffix?.length > 0) {
let isValid = false;
for (const suffix of emailSuffix) {
if (email.endsWith(suffix)) {
isValid = true;
break;
}
}
if (!isValid) {
throw new types_1.OakUserException('邮箱后缀不符合要求');
}
}
const [otherUserEmail] = await context.select('email', {
data: {
id: 1,
@ -2043,13 +2210,28 @@ async function sendCaptchaByEmail({ email, env, type: captchaType, }, context) {
const duration = config.codeDuration || 5;
const digit = config.digit || 4;
const mockSend = config.mockSend;
const emailSuffix = config.emailSuffix;
// 检查邮箱后缀是否满足配置
if (emailSuffix?.length > 0) {
let isValid = false;
for (const suffix of emailSuffix) {
if (email.endsWith(suffix)) {
isValid = true;
break;
}
}
if (!isValid) {
throw new types_1.OakUserException('邮箱后缀不符合要求');
}
}
let emailOptions = {
host: emailConfig.host,
port: emailConfig.port,
account: emailConfig.account,
password: emailConfig.password,
subject: config.subject,
// host: emailConfig.host,
// port: emailConfig.port,
// secure: emailConfig.secure,
// account: emailConfig.account,
// password: emailConfig.password,
from: emailConfig.name ? `"${emailConfig.name}" <${emailConfig.account}>` : emailConfig.account,
subject: config.subject,
to: email,
text: config.text,
html: config.html,
@ -2363,8 +2545,8 @@ async function refreshToken(params, context) {
// 只有server模式去刷新token
// 'development' | 'production' | 'staging'
const intervals = {
development: 7200 * 1000,
staging: 600 * 1000,
development: 7200 * 1000, // 2小时
staging: 600 * 1000, // 十分钟
production: 600 * 1000, // 十分钟
};
let applicationId = token.applicationId;

View File

@ -165,7 +165,7 @@ async function createWechatQrCode(options, context) {
permanent,
url,
expired: false,
expiresAt: Date.now() + 2592000 * 1000,
expiresAt: Date.now() + 2592000 * 1000, // wecharQrCode里的过期时间都放到最大由上层关联对象来主动过期by Xc, 20230131)
props,
};
// 直接创建

View File

@ -628,7 +628,8 @@ const i18ns = [
"loginWayDisabled": "暂不允许该登录方式",
"hasToSetPassword": "需要设置密码",
"hasToVerifyPassword": "需要验证密码",
"cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息"
"cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息",
"emailSuffixIsInvalid": "邮箱后缀不合法"
},
"distinguishUser": "需要鉴别用户身份",
"mobileUnset": "需要先登记手机号",

View File

@ -6,10 +6,10 @@ exports.entityDesc = {
zh_CN: {
name: '直播流',
attr: {
title: '名称',
title: '名称', // 用户定义直播间名称,
streamTitle: '直播流名称',
liveonly: '活跃状态',
hub: '直播空间名称',
hub: '直播空间名称', // 所属直播空间名称
entity: '所属实体',
entityId: '所属实体id',
rtmpPushUrl: '推流地址',

View File

@ -18,6 +18,7 @@ export type EmailConfig = {
html?: string;
codeDuration?: number;
digit?: number;
emailSuffix?: string[];
};
export type PfwConfig = {
appId: string;

View File

@ -11,7 +11,8 @@
"loginWayDisabled": "暂不允许该登录方式",
"hasToSetPassword": "需要设置密码",
"hasToVerifyPassword": "需要验证密码",
"cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息"
"cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息",
"emailSuffixIsInvalid": "邮箱后缀不合法"
},
"distinguishUser": "需要鉴别用户身份",
"mobileUnset": "需要先登记手机号",

View File

@ -19,6 +19,7 @@ export type EmailConfig = {
html?: string;
codeDuration?: number;
digit?: number;
emailSuffix?: string[];
};
export type PfwConfig = {
appId: string;

View File

@ -1,2 +1,2 @@
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
export default _default;

View File

@ -64,7 +64,7 @@ async function sendNotification(notification, context) {
await instance.sendSubscribedMessage({
templateId: templateId,
data: data,
openId: data1.openId,
openId: data1.openId, // 在notification创建时就赋值了
page,
state: StateDict[process.env.NODE_ENV],
});

18
lib/types/Config.d.ts vendored
View File

@ -123,6 +123,15 @@ export type TencentSmsConfig = {
defaultSignName: string;
endpoint: string;
};
export type EmailConfig = {
host: string;
port: number;
account: string;
password: string;
passwordExpiredAt?: number;
name?: string;
secure?: boolean;
};
export type QrCodeType = 'wechatMpDomainUrl' | 'wechatMpWxaCode' | 'wechatPublic' | 'wechatPublicForMp' | 'webForWechatPublic';
export type Config = {
Account?: {
@ -165,14 +174,7 @@ export type Config = {
needUploadIDCardPhoto?: boolean;
needManualVerification?: boolean;
};
Emails?: {
host: string;
port: number;
account: string;
password: string;
passwordExpiredAt?: number;
name?: string;
}[];
Emails?: EmailConfig[];
Security?: {
type?: 'password';
level?: 'weak' | 'medium' | 'strong';

11
lib/types/Email.d.ts vendored
View File

@ -26,13 +26,14 @@ interface Attachment extends AttachmentLike {
raw?: string | Buffer | AttachmentLike | undefined;
}
export type EmailOptions = {
host: string;
port: number;
account: string;
password: string;
subject: string;
host?: string;
port?: number;
secure?: boolean;
account?: string;
password?: string;
from: string;
to: string;
subject: string;
text?: string;
html?: string;
attachments?: Attachment[];

View File

@ -1,8 +1,13 @@
import { EntityDict } from '../../oak-app-domain';
import { BRC } from '../../types/RuntimeCxt';
import Email, { EmailOptions } from '../../types/Email';
import { BackendRuntimeContext } from '../../context/BackendRuntimeContext';
import { EmailConfig } from '../../types/Config';
export default class Nodemailer implements Email<EntityDict> {
name: string;
getConfig(context: BackendRuntimeContext<EntityDict>, systemId?: string): Promise<{
config: EmailConfig;
}>;
sendEmail(options: EmailOptions, context: BRC<EntityDict>): Promise<{
success: boolean;
error?: undefined;

View File

@ -3,17 +3,51 @@ Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
//https://www.nodemailer.com/
const nodemailer_1 = tslib_1.__importDefault(require("nodemailer"));
const assert_1 = require("oak-domain/lib/utils/assert");
const lodash_1 = require("oak-domain/lib/utils/lodash");
class Nodemailer {
name = 'nodemailer';
async getConfig(context, systemId) {
let system;
if (systemId) {
[system] = await context.select('system', {
data: {
id: 1,
config: 1,
},
filter: {
id: systemId,
}
}, {
dontCollect: true,
});
}
else {
system = context.getApplication().system;
}
const { config: systemConfig } = system;
const emailConfig = (0, lodash_1.get)(systemConfig, 'Emails.0', {});
const { host, port, password, account } = emailConfig;
(0, assert_1.assert)(host, 'host未配置');
(0, assert_1.assert)(port, 'port未配置');
(0, assert_1.assert)(account, 'account未配置');
(0, assert_1.assert)(password, 'password未配置');
return {
config: emailConfig,
};
}
async sendEmail(options, context) {
const { host, port, account, password, subject, from, text, html, to, attachments } = options;
const { subject, from, text, html, to, attachments } = options;
const { config, } = await this.getConfig(context);
const transport = Object.assign(config, options);
const _from = from || (config.name ? `"${config.name}" <${config.account}>` : config.account);
const transporter = nodemailer_1.default.createTransport({
host,
port,
secure: port === 465,
host: transport.host,
port: transport.port,
secure: transport.secure,
auth: {
user: account,
pass: password,
user: transport.account,
pass: transport.password,
},
});
async function verifyTransporter() {
@ -34,7 +68,7 @@ class Nodemailer {
console.log('Server is ready to take our messages');
}
let mailOptions = {
from,
from: _from,
to,
subject,
text,

View File

@ -650,6 +650,32 @@ export async function loginByMobile<ED extends EntityDict>(
};
const closeRootMode = context.openRootMode();
const application = context.getApplication();
const [applicationPassport] = await context.select('applicationPassport',
{
data: {
id: 1,
passportId: 1,
passport: {
id: 1,
config: 1,
type: 1,
},
applicationId: 1,
},
filter: {
applicationId: application?.id!,
passport: {
type: 'sms'
},
}
},
{
dontCollect: true,
}
);
assert(applicationPassport?.passport);
if (disableRegister) {
const [existMobile] = await context.select(
'mobile',
@ -736,6 +762,50 @@ export async function loginByAccount<ED extends EntityDict>(
assert(account);
const accountType = isEmail(account) ? 'email' : (isMobile(account) ? 'mobile' : 'loginName');
if (accountType === 'email') {
// 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 as EmailConfig;
// const emailConfig = system?.config.Emails?.find((ele) => ele.account === config.account);
// assert(emailConfig);
// const emailSuffix = config.emailSuffix;
// // 检查邮箱后缀是否满足配置
// if (emailSuffix?.length! > 0) {
// let isValid = false;
// for (const suffix of emailSuffix!) {
// if (account.endsWith(suffix)) {
// isValid = true;
// break;
// }
// }
// if (!isValid) {
// throw new OakUserException('error::user.emailSuffixIsInvalid');
// }
// }
const existEmail = await context.select(
'email',
{
@ -1057,6 +1127,31 @@ export async function loginByAccount<ED extends EntityDict>(
}
};
const closeRootMode = context.openRootMode();
const application = context.getApplication();
const [applicationPassport] = await context.select('applicationPassport',
{
data: {
id: 1,
passportId: 1,
passport: {
id: 1,
config: 1,
type: 1,
},
applicationId: 1,
},
filter: {
applicationId: application?.id!,
passport: {
type: 'password'
},
}
},
{
dontCollect: true,
}
);
assert(applicationPassport?.passport);
const tokenValue = await loginLogic();
const [ tokenInfo ] = await loadTokenInfo<ED>(tokenValue, context);
@ -1145,6 +1240,51 @@ export async function loginByEmail<ED extends EntityDict>(
}
};
const closeRootMode = context.openRootMode();
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 as EmailConfig;
const emailConfig = system?.config.Emails?.find((ele) => ele.account === config.account);
assert(emailConfig);
const emailSuffix = config.emailSuffix;
// 检查邮箱后缀是否满足配置
if (emailSuffix?.length! > 0) {
let isValid = false;
for (const suffix of emailSuffix!) {
if (email.endsWith(suffix)) {
isValid = true;
break;
}
}
if (!isValid) {
throw new OakUserException('邮箱后缀不符合要求')
}
}
if (disableRegister) {
const [existEmail] = await context.select(
'email',
@ -1217,7 +1357,7 @@ export async function bindByMobile<ED extends EntityDict>(
}
// 到这里说明验证码已经通过
//检查当前user是否已绑定mobile
// 检查当前user是否已绑定mobile
const [boundMobile] = await context.select(
'mobile',
{
@ -1423,6 +1563,52 @@ export async function bindByEmail<ED extends EntityDict>(
const closeRootMode = context.openRootMode();
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 as EmailConfig;
const emailConfig = system?.config.Emails?.find((ele) => ele.account === config.account);
assert(emailConfig);
const emailSuffix = config.emailSuffix;
// 检查邮箱后缀是否满足配置
if (emailSuffix?.length! > 0) {
let isValid = false;
for (const suffix of emailSuffix!) {
if (email.endsWith(suffix)) {
isValid = true;
break;
}
}
if (!isValid) {
throw new OakUserException('邮箱后缀不符合要求')
}
}
const [otherUserEmail] = await context.select(
'email',
{
@ -2714,14 +2900,31 @@ export async function sendCaptchaByEmail<ED extends EntityDict>(
const duration = config.codeDuration || 5;
const digit = config.digit || 4;
const mockSend = config.mockSend;
const emailSuffix = config.emailSuffix;
// 检查邮箱后缀是否满足配置
if (emailSuffix?.length! > 0) {
let isValid = false;
for (const suffix of emailSuffix!) {
if (email.endsWith(suffix)) {
isValid = true;
break;
}
}
if (!isValid) {
throw new OakUserException('邮箱后缀不符合要求')
}
}
let emailOptions: EmailOptions = {
host: emailConfig.host,
port: emailConfig.port,
account: emailConfig.account,
password: emailConfig.password,
subject: config.subject,
// host: emailConfig.host,
// port: emailConfig.port,
// secure: emailConfig.secure,
// account: emailConfig.account,
// password: emailConfig.password,
from: emailConfig.name ? `"${emailConfig.name}" <${emailConfig.account}>` : emailConfig.account,
subject: config.subject,
to: email,
text: config.text,
html: config.html,

View File

@ -95,6 +95,21 @@ export default function Email(props: {
}}
/>
</Form.Item>
<Form.Item
label="启用SSL加密"
tooltip="当连接使用的是SSL端口请启用SSL加密功能以保障数据传输安全"
>
<>
<Switch
checkedChildren="是"
unCheckedChildren="否"
checked={ele?.secure}
onChange={(checked) =>
setValue(`${idx}.secure`, checked)
}
/>
</>
</Form.Item>
<Form.Item
label="账号"
>

View File

@ -1,15 +1,101 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useRef } from "react";
import { EmailConfig, MfwConfig, PfwConfig, SmsConfig } from "../../../entities/Passport";
import { EntityDict } from "../../../oak-app-domain";
import { Space, Switch, Alert, Typography, Form, Input, Radio, Tag, Select, Tooltip } from 'antd';
import { Space, Switch, Alert, Typography, Form, Input, Radio, Tag, Select, Tooltip, InputRef, Flex } from 'antd';
import Styles from './web.module.less';
import '@wangeditor/editor/dist/css/style.css'; // 引入 css
import { Editor, Toolbar } from "@wangeditor/editor-for-react";
import { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor';
import { PlusOutlined, CloseOutlined } from '@ant-design/icons';
const { TextArea } = Input;
const { Text } = Typography;
function RenderEmailSuffix(props: {
emailSuffix?: string[];
onChange: (v: string[]) => void;
t: (k: string) => string;
}) {
const { emailSuffix, onChange, t } = props;
const [inputVisible, setInputVisible] = useState(false);
const [inputValue, setInputValue] = useState('');
const inputRef = useRef<InputRef>(null);
const tagInputStyle: React.CSSProperties = {
width: 100,
height: 22,
marginInlineEnd: 8,
verticalAlign: 'top',
};
const tagPlusStyle: React.CSSProperties = {
height: 22,
borderStyle: 'dashed',
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
const handleInputConfirm = () => {
if (inputValue && !emailSuffix?.includes(inputValue)) {
onChange([...emailSuffix || [], inputValue]);
}
setInputVisible(false);
setInputValue('');
};
const handleClose = (removedTag: string) => {
const emailSuffix2 = emailSuffix!.filter((tag) => tag !== removedTag);
onChange(emailSuffix2);
};
const showInput = () => {
setInputVisible(true);
};
return (
<Flex gap="4px 0" wrap="wrap">
{(emailSuffix || []).map<React.ReactNode>((tag, index) => {
const isLongTag = tag.length > 20;
const tagElem = (
<Tag
closeIcon={<CloseOutlined />}
key={tag}
onClose={() => handleClose(tag)}
>
<span>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</span>
</Tag>
);
return isLongTag ? (
<Tooltip title={tag} key={tag}>
{tagElem}
</Tooltip>
) : (
tagElem
);
})}
{inputVisible ? (
<Input
ref={inputRef}
type="text"
size="small"
style={tagInputStyle}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
/>
) : (
<Tag style={tagPlusStyle} icon={<PlusOutlined />} onClick={showInput}>
{t('common::action.add')}
</Tag>
)}
</Flex>
);
}
export default function Email(props: {
passport: EntityDict['passport']['OpSchema'] & { stateColor: string };
t: (k: string, params?: any) => string;
@ -26,6 +112,7 @@ export default function Email(props: {
const [html, setHtml] = useState(config?.html || '');
const [emailCodeDuration, setEmailCodeDuration] = useState(config?.codeDuration || '');
const [emailDigit, setEmailDigit] = useState(config?.digit || '');
const [emailSuffix, setEmailSuffix] = useState(config?.emailSuffix || []);
// editor 实例
const [editor, setEditor] = useState<IDomEditor | null>(null) // TS 语法
@ -54,6 +141,7 @@ export default function Email(props: {
setHtml(config?.html || '');
setEmailCodeDuration(config?.codeDuration || '');
setEmailDigit(config?.digit || '');
setEmailSuffix(config?.emailSuffix || []);
if (config?.html) {
setEContentType('html');
} else {
@ -255,6 +343,20 @@ export default function Email(props: {
}}
/>
</Form.Item>
<Form.Item
label="邮箱后缀"
tooltip="允许的邮箱后缀(如: @qq.com不填则不校验"
>
<RenderEmailSuffix
t={t}
emailSuffix={emailSuffix}
onChange={(v) => {
if (v !== (config as EmailConfig)?.emailSuffix) {
updateConfig(id, config!, 'emailSuffix', v, 'email');
}
}}
/>
</Form.Item>
</Form>
</div>
}

View File

@ -7,6 +7,40 @@ export default OakComponent({
config: 1,
description: 1,
style: 1,
// system$platform: {
// $entity: 'system',
// data: {
// id: 1,
// name: 1,
// config: 1,
// description: 1,
// super: 1,
// folder: 1,
// platformId: 1,
// style: 1,
// domain$system: {
// $entity: 'domain',
// data: {
// id: 1,
// systemId: 1,
// url: 1,
// },
// },
// application$system: {
// $entity: 'application',
// data: {
// id: 1,
// name: 1,
// config: 1,
// description: 1,
// type: 1,
// systemId: 1,
// domainId: 1,
// style: 1,
// }
// }
// }
// }
},
formData({ data }) {
return data || {};

View File

@ -1,9 +1,7 @@
.container {
padding: 8px;
height: 100%;
.tabLabel {
// writing-mode: vertical-rl;
letter-spacing: .2rem;
}
// .tabLabel {
// letter-spacing: .2rem;
// }
}

View File

@ -64,7 +64,7 @@ export default function render(props: WebComponentProps<EntityDict, 'platform',
key: 'system',
children: (
<PlatformSystem
oakPath={`${oakFullpath}.system$platform`}
oakPath={`${oakFullpath}-PlatformSystem`}
platformId={id}
/>
),

View File

@ -32,6 +32,13 @@ export default OakComponent({
}
}
},
filters: [{
filter() {
return {
platformId: this.props.platformId,
};
},
}],
properties: {
platformId: '',
},

View File

@ -66,6 +66,7 @@ export default function render(props: WebComponentProps<EntityDict, 'system', tr
</Modal>
<Tabs
type="editable-card"
// destroyInactiveTabPane={true}
onEdit={(key, action) => {
if (action === 'add') {
const id = addItem({ platformId, config: { App: {} } });
@ -83,6 +84,7 @@ export default function render(props: WebComponentProps<EntityDict, 'system', tr
return {
label: item.name,
key: `${idx}`,
children: (
<SystemPanel
oakPath={`${oakFullpath}.${item.id}`}

View File

@ -18,18 +18,6 @@ export default OakComponent({
url: 1,
},
},
// application$system: {
// $entity: 'application',
// data: {
// id: 1,
// name: 1,
// config: 1,
// description: 1,
// type: 1,
// systemId: 1,
// domainId: 1,
// }
// }
},
formData({ data }) {
return data || {};

View File

@ -129,7 +129,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'system', fa
destroyInactiveTabPane: true,
children: (
<Passport
oakPath={`$system-passport`}
oakPath={`$system-passport-${id}`}
systemId={id}
systemName={name}
/>

View File

@ -628,7 +628,8 @@ const i18ns: I18n[] = [
"loginWayDisabled": "暂不允许该登录方式",
"hasToSetPassword": "需要设置密码",
"hasToVerifyPassword": "需要验证密码",
"cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息"
"cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息",
"emailSuffixIsInvalid": "邮箱后缀不合法"
},
"distinguishUser": "需要鉴别用户身份",
"mobileUnset": "需要先登记手机号",

View File

@ -20,6 +20,7 @@ export type EmailConfig = {
html?: string;
codeDuration?: number; //验证码有效时间 单位分钟, 不填5分钟
digit?: number; //验证码位数 4~8默认为4位
emailSuffix?: string[] //邮箱后缀,不填则不校验
};
export type PfwConfig = {

View File

@ -11,7 +11,8 @@
"loginWayDisabled": "暂不允许该登录方式",
"hasToSetPassword": "需要设置密码",
"hasToVerifyPassword": "需要验证密码",
"cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息"
"cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息",
"emailSuffixIsInvalid": "邮箱后缀不合法"
},
"distinguishUser": "需要鉴别用户身份",
"mobileUnset": "需要先登记手机号",

View File

@ -143,6 +143,16 @@ export type TencentSmsConfig = {
endpoint: string;
};
export type EmailConfig = {
host: string; //主机名
port: number; //端口
account: string; //账号
password: string; //授权码
passwordExpiredAt?: number; //授权码过期时间
name?: string; //发件人名称
secure?: boolean; //是否ssl
};
export type QrCodeType = 'wechatMpDomainUrl' | 'wechatMpWxaCode' | 'wechatPublic' | 'wechatPublicForMp' | 'webForWechatPublic';
export type Config = {
@ -186,14 +196,7 @@ export type Config = {
needUploadIDCardPhoto?: boolean; // 实名认证是否需要上传身份照片
needManualVerification?: boolean; // 实名认证是否需要人工校验
};
Emails?: {
host: string; //主机名
port: number; //端口
account: string; //账号
password: string; //授权码
passwordExpiredAt?: number; //授权码过期时间
name?: string; //发件人名称
}[];
Emails?: EmailConfig[];
Security?: {
type?: 'password', // 采用密码作为第一安全元素
level?: 'weak' | 'medium' | 'strong'; // 强度

View File

@ -28,13 +28,14 @@ interface Attachment extends AttachmentLike {
}
export type EmailOptions = {
host: string;
port: number;
account: string;
password: string;
subject: string;
host?: string;
port?: number;
secure?: boolean;
account?: string;
password?: string;
from: string;
to: string;
subject: string;
text?: string;
html?: string;
attachments?: Attachment[]

View File

@ -3,19 +3,64 @@ import nodemailer from 'nodemailer';
import { EntityDict } from '../../oak-app-domain';
import { BRC } from '../../types/RuntimeCxt';
import Email, { EmailOptions } from '../../types/Email';
import { assert } from 'oak-domain/lib/utils/assert';
import { BackendRuntimeContext } from '../../context/BackendRuntimeContext';
import { get } from 'oak-domain/lib/utils/lodash';
import { EmailConfig } from '../../types/Config';
export default class Nodemailer implements Email<EntityDict> {
name = 'nodemailer';
async getConfig(context: BackendRuntimeContext<EntityDict>, systemId?: string) {
let system;
if (systemId) {
[system] = await context.select(
'system',
{
data: {
id: 1,
config: 1,
},
filter: {
id: systemId,
}
},
{
dontCollect: true,
}
);
} else {
system = context.getApplication()!.system;
}
const { config: systemConfig } = system as EntityDict['system']['Schema'];
const emailConfig = get(systemConfig, 'Emails.0', {}) as EmailConfig;
const { host, port, password, account } = emailConfig;
assert(host, 'host未配置');
assert(port, 'port未配置');
assert(account, 'account未配置');
assert(password, 'password未配置');
return {
config: emailConfig,
};
}
async sendEmail(options: EmailOptions, context: BRC<EntityDict>) {
const { host, port, account, password, subject, from, text, html, to, attachments } =
options;
const { subject, from, text, html, to, attachments } = options;
const {
config,
} = await this.getConfig(context);
const transport = Object.assign(config, options);
const _from = from || (config.name ? `"${config.name}" <${config.account}>` : config.account);
const transporter = nodemailer.createTransport({
host,
port,
secure: port === 465, //true for 465, false for other ports
host: transport.host,
port: transport.port,
secure: transport.secure,
auth: {
user: account,
pass: password,
user: transport.account,
pass: transport.password,
},
});
@ -37,7 +82,7 @@ export default class Nodemailer implements Email<EntityDict> {
console.log('Server is ready to take our messages');
}
let mailOptions = {
from,
from: _from,
to,
subject,
text,