From 606b9e2023dd1b87d684eda2809ca9f1ab5a5b8d Mon Sep 17 00:00:00 2001 From: wkj <278599135@.com> Date: Tue, 6 May 2025 16:25:33 +0800 Subject: [PATCH] =?UTF-8?q?=E9=82=AE=E7=AE=B1=E6=94=AF=E6=8C=81=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=90=8E=E7=BC=80=EF=BC=8C=E5=BD=93=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E5=90=8E=E7=BC=80=EF=BC=8C=E4=BC=9A=E6=A3=80=E6=9F=A5=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E7=9A=84=E9=82=AE=E7=AE=B1=E6=98=AF=E5=90=A6=E7=AC=A6?= =?UTF-8?q?=E5=90=88=E8=A6=81=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- es/aspects/session.js | 6 +- es/aspects/token.js | 198 +++++++++++++++- es/aspects/wechatQrCode.js | 2 +- es/components/article/detail/index.js | 4 +- es/components/article/preview/index.js | 4 +- es/components/article/treeList/index.js | 2 +- es/components/article/upsert/index.js | 10 +- es/components/article/upsert/web.js | 6 +- es/components/articleMenu/treeCell/index.js | 2 +- es/components/articleMenu/treeList/index.js | 2 +- .../articleMenu/treeManager/index.js | 22 +- es/components/common/errorPage/index.js | 2 +- es/components/common/tabBar/index.js | 2 +- es/components/config/upsert/email/index.js | 7 +- es/components/extraFile/crop/index.js | 90 ++++---- es/components/extraFile/crop/web.js | 2 +- es/components/extraFile/forUrl/index.js | 4 +- es/components/extraFile/gallery/index.js | 8 +- es/components/extraFile/upload/index.js | 28 +-- es/components/passport/email/index.js | 63 ++++- es/components/platform/panel/index.js | 34 +++ es/components/platform/panel/web.pc.js | 2 +- .../platform/panel/web.pc.module.less | 8 +- es/components/platform/system/index.js | 7 + es/components/platform/system/web.pc.js | 4 +- es/components/session/forMessage/index.js | 2 +- es/components/session/list/index.js | 10 +- es/components/sessionMessage/list/index.js | 2 +- es/components/system/panel/index.js | 12 - es/components/system/panel/web.pc.js | 2 +- es/components/user/login/email/index.js | 4 +- es/components/user/login/index.js | 6 +- es/components/user/login/password/index.js | 8 +- es/components/user/login/sms/index.js | 8 +- es/components/userEntityGrant/share/index.js | 2 +- es/components/wechatLogin/qrCode/index.js | 2 +- es/components/wechatQrCode/scan/index.js | 2 +- es/components/wechatQrCode/share/index.js | 2 +- es/data/i18n.js | 3 +- es/entities/Livestream.js | 4 +- es/entities/Passport.d.ts | 1 + es/locales/error/zh-CN.json | 3 +- es/oak-app-domain/Passport/_baseSchema.d.ts | 1 + es/triggers/index.d.ts | 2 +- es/triggers/notification.js | 2 +- es/types/Config.d.ts | 18 +- es/types/Email.d.ts | 11 +- es/utils/email/node-mailer.d.ts | 5 + es/utils/email/node-mailer.js | 48 +++- lib/aspects/session.js | 6 +- lib/aspects/token.js | 198 +++++++++++++++- lib/aspects/wechatQrCode.js | 2 +- lib/data/i18n.js | 3 +- lib/entities/Livestream.js | 4 +- lib/entities/Passport.d.ts | 1 + lib/locales/error/zh-CN.json | 3 +- lib/oak-app-domain/Passport/_baseSchema.d.ts | 1 + lib/triggers/index.d.ts | 2 +- lib/triggers/notification.js | 2 +- lib/types/Config.d.ts | 18 +- lib/types/Email.d.ts | 11 +- lib/utils/email/node-mailer.d.ts | 5 + lib/utils/email/node-mailer.js | 48 +++- src/aspects/token.ts | 215 +++++++++++++++++- src/components/config/upsert/email/index.tsx | 15 ++ src/components/passport/email/index.tsx | 106 ++++++++- src/components/platform/panel/index.ts | 34 +++ .../platform/panel/web.pc.module.less | 8 +- src/components/platform/panel/web.pc.tsx | 2 +- src/components/platform/system/index.ts | 7 + src/components/platform/system/web.pc.tsx | 2 + src/components/system/panel/index.ts | 12 - src/components/system/panel/web.pc.tsx | 2 +- src/data/i18n.ts | 3 +- src/entities/Passport.ts | 1 + src/locales/error/zh-CN.json | 3 +- src/types/Config.ts | 19 +- src/types/Email.ts | 11 +- src/utils/email/node-mailer.ts | 61 ++++- 79 files changed, 1217 insertions(+), 267 deletions(-) diff --git a/es/aspects/session.js b/es/aspects/session.js index 022bb1b58..53276de61 100644 --- a/es/aspects/session.js +++ b/es/aspects/session.js @@ -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, diff --git a/es/aspects/token.js b/es/aspects/token.js index 6f949a6ba..31b16ce14 100644 --- a/es/aspects/token.js +++ b/es/aspects/token.js @@ -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; diff --git a/es/aspects/wechatQrCode.js b/es/aspects/wechatQrCode.js index 6e62e08c7..3da0ebebd 100644 --- a/es/aspects/wechatQrCode.js +++ b/es/aspects/wechatQrCode.js @@ -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, }; // 直接创建 diff --git a/es/components/article/detail/index.js b/es/components/article/detail/index.js index 53db304d0..ad432b5ae 100644 --- a/es/components/article/detail/index.js +++ b/es/components/article/detail/index.js @@ -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, }, diff --git a/es/components/article/preview/index.js b/es/components/article/preview/index.js index f90ebe6ba..f3942cac1 100644 --- a/es/components/article/preview/index.js +++ b/es/components/article/preview/index.js @@ -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, }, diff --git a/es/components/article/treeList/index.js b/es/components/article/treeList/index.js index 4c04d8285..dc05b4dbd 100644 --- a/es/components/article/treeList/index.js +++ b/es/components/article/treeList/index.js @@ -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, diff --git a/es/components/article/upsert/index.js b/es/components/article/upsert/index.js index 66be79471..24efb4321 100644 --- a/es/components/article/upsert/index.js +++ b/es/components/article/upsert/index.js @@ -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: { diff --git a/es/components/article/upsert/web.js b/es/components/article/upsert/web.js index 15e456e4a..7c9685d03 100644 --- a/es/components/article/upsert/web.js +++ b/es/components/article/upsert/web.js @@ -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", diff --git a/es/components/articleMenu/treeCell/index.js b/es/components/articleMenu/treeCell/index.js index 044ee9ff6..9e2269f08 100644 --- a/es/components/articleMenu/treeCell/index.js +++ b/es/components/articleMenu/treeCell/index.js @@ -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, diff --git a/es/components/articleMenu/treeList/index.js b/es/components/articleMenu/treeList/index.js index 302412fd5..84ab67d6a 100644 --- a/es/components/articleMenu/treeList/index.js +++ b/es/components/articleMenu/treeList/index.js @@ -6,7 +6,7 @@ export default OakComponent({ entityId: '', parentId: '', onGrandChildEditArticleChange: (data) => undefined, - show: 'edit', + show: 'edit', // edit为编辑,doc为查看,preview为预览 articleMenuId: '', articleId: '', getBreadcrumbItems: (breadcrumbItems) => undefined, diff --git a/es/components/articleMenu/treeManager/index.js b/es/components/articleMenu/treeManager/index.js index 4a0a41a48..1d64b9e88 100644 --- a/es/components/articleMenu/treeManager/index.js +++ b/es/components/articleMenu/treeManager/index.js @@ -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 }, }); diff --git a/es/components/common/errorPage/index.js b/es/components/common/errorPage/index.js index 45afa922d..8dfa687a8 100644 --- a/es/components/common/errorPage/index.js +++ b/es/components/common/errorPage/index.js @@ -37,7 +37,7 @@ export default OakComponent({ code: '', title: '', desc: '', - icon: '', + icon: '', //web独有 imagePath: '', //小程序独有 }, lifetimes: { diff --git a/es/components/common/tabBar/index.js b/es/components/common/tabBar/index.js index bb5fb6958..ffb8ea41c 100644 --- a/es/components/common/tabBar/index.js +++ b/es/components/common/tabBar/index.js @@ -12,7 +12,7 @@ export default OakComponent({ color: '#666', selectedColor: '', border: false, - selectedIconPath: '', + selectedIconPath: '', //一般在list设置 iconPath: '', }, lifetimes: { diff --git a/es/components/config/upsert/email/index.js b/es/components/config/upsert/email/index.js index b1ff27188..59dff6810 100644 --- a/es/components/config/upsert/email/index.js +++ b/es/components/config/upsert/email/index.js @@ -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)); }}/> + + <> + setValue(`${idx}.secure`, checked)}/> + + { setValue(`${idx}.account`, e.target.value); diff --git a/es/components/extraFile/crop/index.js b/es/components/extraFile/crop/index.js index 125adac90..806ab742a 100644 --- a/es/components/extraFile/crop/index.js +++ b/es/components/extraFile/crop/index.js @@ -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中且文件大小超过此值的文件将转换为 JPEG,Infinity表示禁用该功能 }, features: ['extraFile'], diff --git a/es/components/extraFile/crop/web.js b/es/components/extraFile/crop/web.js index 10b4941c9..09f15740c 100644 --- a/es/components/extraFile/crop/web.js +++ b/es/components/extraFile/crop/web.js @@ -13,7 +13,7 @@ export default function render(props) { }; return (<> {enableCrop ? ( { diff --git a/es/components/extraFile/forUrl/index.js b/es/components/extraFile/forUrl/index.js index 9488a37cf..cae1fb5d7 100644 --- a/es/components/extraFile/forUrl/index.js +++ b/es/components/extraFile/forUrl/index.js @@ -55,9 +55,9 @@ export default OakComponent({ data: { isModalOpen: false, isModalOpen1: false, - renderImgs: [], + renderImgs: [], // 读取的原文图片,在modal使用 methodsType: '', - bridgeUrl: '', + bridgeUrl: '', // 通过桥接方式获得的url selectedId: -1, }, properties: { diff --git a/es/components/extraFile/gallery/index.js b/es/components/extraFile/gallery/index.js index 7e3d08e92..aaa8f4633 100644 --- a/es/components/extraFile/gallery/index.js +++ b/es/components/extraFile/gallery/index.js @@ -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: '', diff --git a/es/components/extraFile/upload/index.js b/es/components/extraFile/upload/index.js index 9993f92db..c61143357 100644 --- a/es/components/extraFile/upload/index.js +++ b/es/components/extraFile/upload/index.js @@ -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, diff --git a/es/components/passport/email/index.js b/es/components/passport/email/index.js index 1713c038b..b076732d0 100644 --- a/es/components/passport/email/index.js +++ b/es/components/passport/email/index.js @@ -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 ( + {(emailSuffix || []).map((tag, index) => { + const isLongTag = tag.length > 20; + const tagElem = (} key={tag} onClose={() => handleClose(tag)}> + + {isLongTag ? `${tag.slice(0, 20)}...` : tag} + + ); + return isLongTag ? ( + {tagElem} + ) : (tagElem); + })} + {inputVisible ? () : (} onClick={showInput}> + {t('common::action.add')} + )} + ); +} 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'); } + }}/> + + + { + if (v !== config?.emailSuffix) { + updateConfig(id, config, 'emailSuffix', v, 'email'); + } }}/> diff --git a/es/components/platform/panel/index.js b/es/components/platform/panel/index.js index c50786c61..532c90565 100644 --- a/es/components/platform/panel/index.js +++ b/es/components/platform/panel/index.js @@ -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 || {}; diff --git a/es/components/platform/panel/web.pc.js b/es/components/platform/panel/web.pc.js index 8c925f084..79a99aa87 100644 --- a/es/components/platform/panel/web.pc.js +++ b/es/components/platform/panel/web.pc.js @@ -29,7 +29,7 @@ export default function render(props) { { label:
{t('system-list')}
, key: 'system', - children: (), + children: (), }, ]}/> ); diff --git a/es/components/platform/panel/web.pc.module.less b/es/components/platform/panel/web.pc.module.less index bb24882e6..caa7d4881 100644 --- a/es/components/platform/panel/web.pc.module.less +++ b/es/components/platform/panel/web.pc.module.less @@ -1,9 +1,7 @@ .container { - padding: 8px; height: 100%; - .tabLabel { - // writing-mode: vertical-rl; - letter-spacing: .2rem; - } + // .tabLabel { + // letter-spacing: .2rem; + // } } \ No newline at end of file diff --git a/es/components/platform/system/index.js b/es/components/platform/system/index.js index ac7fc266d..940c0a56c 100644 --- a/es/components/platform/system/index.js +++ b/es/components/platform/system/index.js @@ -32,6 +32,13 @@ export default OakComponent({ } } }, + filters: [{ + filter() { + return { + platformId: this.props.platformId, + }; + }, + }], properties: { platformId: '', }, diff --git a/es/components/platform/system/web.pc.js b/es/components/platform/system/web.pc.js index 9171e4154..fd342e7d3 100644 --- a/es/components/platform/system/web.pc.js +++ b/es/components/platform/system/web.pc.js @@ -32,7 +32,9 @@ export default function render(props) { }> {t('confirmToRemove')} - { + { if (action === 'add') { const id = addItem({ platformId, config: { App: {} } }); setCreateId(id); diff --git a/es/components/session/forMessage/index.js b/es/components/session/forMessage/index.js index 85aa30acf..e51d10291 100644 --- a/es/components/session/forMessage/index.js +++ b/es/components/session/forMessage/index.js @@ -20,7 +20,7 @@ export default OakComponent({ properties: { sessionId: '', isEntity: false, - entityDisplay: (data) => [], + entityDisplay: (data) => [], // user端,指示如何显示entity对象名称 entityProjection: null, // user端,指示需要取哪些entity的属性来显示entityDisplay }, methods: { diff --git a/es/components/session/list/index.js b/es/components/session/list/index.js index afd4e8a62..c22ab6988 100644 --- a/es/components/session/list/index.js +++ b/es/components/session/list/index.js @@ -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, }, diff --git a/es/components/sessionMessage/list/index.js b/es/components/sessionMessage/list/index.js index c87698fd2..608733d85 100644 --- a/es/components/sessionMessage/list/index.js +++ b/es/components/sessionMessage/list/index.js @@ -102,7 +102,7 @@ export default OakComponent({ dialog: false, entity: '', entityId: '', - entityDisplay: (data) => [], + entityDisplay: (data) => [], // user端,指示如何显示entity对象名称 entityProjection: null, // user端,指示需要取哪些entity的属性来显示entityDisplay }, filters: [ diff --git a/es/components/system/panel/index.js b/es/components/system/panel/index.js index d55eac850..f5414402e 100644 --- a/es/components/system/panel/index.js +++ b/es/components/system/panel/index.js @@ -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 || {}; diff --git a/es/components/system/panel/web.pc.js b/es/components/system/panel/web.pc.js index c12a32143..7154f66e7 100644 --- a/es/components/system/panel/web.pc.js +++ b/es/components/system/panel/web.pc.js @@ -62,7 +62,7 @@ export default function Render(props) { ), key: 'passport-list', destroyInactiveTabPane: true, - children: (), + children: (), }, ]}/> ); diff --git a/es/components/user/login/email/index.js b/es/components/user/login/email/index.js index dd10e9af6..f87f9b258 100644 --- a/es/components/user/login/email/index.js +++ b/es/components/user/login/email/index.js @@ -21,8 +21,8 @@ export default OakComponent({ }, properties: { disabled: '', - url: '', - callback: undefined, + url: '', // 登录系统之后要返回的页面 + callback: undefined, // 登录成功回调,排除微信登录方式 setLoginMode: (value) => undefined, digit: 4, //验证码位数 }, diff --git a/es/components/user/login/index.js b/es/components/user/login/index.js index c01994bc6..ee877f16c 100644 --- a/es/components/user/login/index.js +++ b/es/components/user/login/index.js @@ -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 }) { diff --git a/es/components/user/login/password/index.js b/es/components/user/login/password/index.js index df5ec579b..c841c6228 100644 --- a/es/components/user/login/password/index.js +++ b/es/components/user/login/password/index.js @@ -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: {}, diff --git a/es/components/user/login/sms/index.js b/es/components/user/login/sms/index.js index ce3cbbaac..aa98691ba 100644 --- a/es/components/user/login/sms/index.js +++ b/es/components/user/login/sms/index.js @@ -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 //验证码位数, }, diff --git a/es/components/userEntityGrant/share/index.js b/es/components/userEntityGrant/share/index.js index ef5413e9e..66944b893 100644 --- a/es/components/userEntityGrant/share/index.js +++ b/es/components/userEntityGrant/share/index.js @@ -17,7 +17,7 @@ export default OakComponent({ id: 1, entity: 1, entityId: 1, - type: 1, + type: 1, //类型 ticket: 1, url: 1, buffer: 1, diff --git a/es/components/wechatLogin/qrCode/index.js b/es/components/wechatLogin/qrCode/index.js index 79865982e..3de7a0234 100644 --- a/es/components/wechatLogin/qrCode/index.js +++ b/es/components/wechatLogin/qrCode/index.js @@ -64,7 +64,7 @@ export default OakComponent({ id: 1, entity: 1, entityId: 1, - type: 1, + type: 1, //类型 ticket: 1, url: 1, buffer: 1, diff --git a/es/components/wechatQrCode/scan/index.js b/es/components/wechatQrCode/scan/index.js index 8c8c5e99f..3d25bce61 100644 --- a/es/components/wechatQrCode/scan/index.js +++ b/es/components/wechatQrCode/scan/index.js @@ -5,7 +5,7 @@ export default OakComponent({ id: 1, entity: 1, entityId: 1, - type: 1, + type: 1, //类型 ticket: 1, url: 1, expired: 1, diff --git a/es/components/wechatQrCode/share/index.js b/es/components/wechatQrCode/share/index.js index 7fc6fdc2c..3da7cf99c 100644 --- a/es/components/wechatQrCode/share/index.js +++ b/es/components/wechatQrCode/share/index.js @@ -4,7 +4,7 @@ export default OakComponent({ id: 1, entity: 1, entityId: 1, - type: 1, + type: 1, //类型 ticket: 1, url: 1, buffer: 1, diff --git a/es/data/i18n.js b/es/data/i18n.js index c401580b2..2840b8b8f 100644 --- a/es/data/i18n.js +++ b/es/data/i18n.js @@ -626,7 +626,8 @@ const i18ns = [ "loginWayDisabled": "暂不允许该登录方式", "hasToSetPassword": "需要设置密码", "hasToVerifyPassword": "需要验证密码", - "cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息" + "cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息", + "emailSuffixIsInvalid": "邮箱后缀不合法" }, "distinguishUser": "需要鉴别用户身份", "mobileUnset": "需要先登记手机号", diff --git a/es/entities/Livestream.js b/es/entities/Livestream.js index bc927d8d5..4bd3382f0 100644 --- a/es/entities/Livestream.js +++ b/es/entities/Livestream.js @@ -3,10 +3,10 @@ export const entityDesc = { zh_CN: { name: '直播流', attr: { - title: '名称', + title: '名称', // 用户定义直播间名称, streamTitle: '直播流名称', liveonly: '活跃状态', - hub: '直播空间名称', + hub: '直播空间名称', // 所属直播空间名称 entity: '所属实体', entityId: '所属实体id', rtmpPushUrl: '推流地址', diff --git a/es/entities/Passport.d.ts b/es/entities/Passport.d.ts index 62d54c047..a64415a18 100644 --- a/es/entities/Passport.d.ts +++ b/es/entities/Passport.d.ts @@ -18,6 +18,7 @@ export type EmailConfig = { html?: string; codeDuration?: number; digit?: number; + emailSuffix?: string[]; }; export type PfwConfig = { appId: string; diff --git a/es/locales/error/zh-CN.json b/es/locales/error/zh-CN.json index 7808c978f..ebfcbb5b6 100644 --- a/es/locales/error/zh-CN.json +++ b/es/locales/error/zh-CN.json @@ -11,7 +11,8 @@ "loginWayDisabled": "暂不允许该登录方式", "hasToSetPassword": "需要设置密码", "hasToVerifyPassword": "需要验证密码", - "cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息" + "cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息", + "emailSuffixIsInvalid": "邮箱后缀不合法" }, "distinguishUser": "需要鉴别用户身份", "mobileUnset": "需要先登记手机号", diff --git a/es/oak-app-domain/Passport/_baseSchema.d.ts b/es/oak-app-domain/Passport/_baseSchema.d.ts index ab68caad3..5fb54201c 100644 --- a/es/oak-app-domain/Passport/_baseSchema.d.ts +++ b/es/oak-app-domain/Passport/_baseSchema.d.ts @@ -19,6 +19,7 @@ export type EmailConfig = { html?: string; codeDuration?: number; digit?: number; + emailSuffix?: string[]; }; export type PfwConfig = { appId: string; diff --git a/es/triggers/index.d.ts b/es/triggers/index.d.ts index fc7f50979..1d6a506c0 100644 --- a/es/triggers/index.d.ts +++ b/es/triggers/index.d.ts @@ -1,2 +1,2 @@ -declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; +declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; export default _default; diff --git a/es/triggers/notification.js b/es/triggers/notification.js index 0d40730d2..c5859ffeb 100644 --- a/es/triggers/notification.js +++ b/es/triggers/notification.js @@ -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], }); diff --git a/es/types/Config.d.ts b/es/types/Config.d.ts index 5e8796f90..2c34d1f8f 100644 --- a/es/types/Config.d.ts +++ b/es/types/Config.d.ts @@ -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'; diff --git a/es/types/Email.d.ts b/es/types/Email.d.ts index 7c3b03c93..12c4f5a23 100644 --- a/es/types/Email.d.ts +++ b/es/types/Email.d.ts @@ -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[]; diff --git a/es/utils/email/node-mailer.d.ts b/es/utils/email/node-mailer.d.ts index 503f5a093..0d8d8a97b 100644 --- a/es/utils/email/node-mailer.d.ts +++ b/es/utils/email/node-mailer.d.ts @@ -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 { name: string; + getConfig(context: BackendRuntimeContext, systemId?: string): Promise<{ + config: EmailConfig; + }>; sendEmail(options: EmailOptions, context: BRC): Promise<{ success: boolean; error?: undefined; diff --git a/es/utils/email/node-mailer.js b/es/utils/email/node-mailer.js index 63242f2b7..0f6e512b9 100644 --- a/es/utils/email/node-mailer.js +++ b/es/utils/email/node-mailer.js @@ -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, diff --git a/lib/aspects/session.js b/lib/aspects/session.js index 0f5331d91..6b8e992f5 100644 --- a/lib/aspects/session.js +++ b/lib/aspects/session.js @@ -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, diff --git a/lib/aspects/token.js b/lib/aspects/token.js index 3c959c940..307170803 100644 --- a/lib/aspects/token.js +++ b/lib/aspects/token.js @@ -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; diff --git a/lib/aspects/wechatQrCode.js b/lib/aspects/wechatQrCode.js index 02dcaf68e..ab0fc4df0 100644 --- a/lib/aspects/wechatQrCode.js +++ b/lib/aspects/wechatQrCode.js @@ -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, }; // 直接创建 diff --git a/lib/data/i18n.js b/lib/data/i18n.js index b69070228..f24353d01 100644 --- a/lib/data/i18n.js +++ b/lib/data/i18n.js @@ -628,7 +628,8 @@ const i18ns = [ "loginWayDisabled": "暂不允许该登录方式", "hasToSetPassword": "需要设置密码", "hasToVerifyPassword": "需要验证密码", - "cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息" + "cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息", + "emailSuffixIsInvalid": "邮箱后缀不合法" }, "distinguishUser": "需要鉴别用户身份", "mobileUnset": "需要先登记手机号", diff --git a/lib/entities/Livestream.js b/lib/entities/Livestream.js index b8849e4a3..729547ff8 100644 --- a/lib/entities/Livestream.js +++ b/lib/entities/Livestream.js @@ -6,10 +6,10 @@ exports.entityDesc = { zh_CN: { name: '直播流', attr: { - title: '名称', + title: '名称', // 用户定义直播间名称, streamTitle: '直播流名称', liveonly: '活跃状态', - hub: '直播空间名称', + hub: '直播空间名称', // 所属直播空间名称 entity: '所属实体', entityId: '所属实体id', rtmpPushUrl: '推流地址', diff --git a/lib/entities/Passport.d.ts b/lib/entities/Passport.d.ts index 62d54c047..a64415a18 100644 --- a/lib/entities/Passport.d.ts +++ b/lib/entities/Passport.d.ts @@ -18,6 +18,7 @@ export type EmailConfig = { html?: string; codeDuration?: number; digit?: number; + emailSuffix?: string[]; }; export type PfwConfig = { appId: string; diff --git a/lib/locales/error/zh-CN.json b/lib/locales/error/zh-CN.json index 7808c978f..ebfcbb5b6 100644 --- a/lib/locales/error/zh-CN.json +++ b/lib/locales/error/zh-CN.json @@ -11,7 +11,8 @@ "loginWayDisabled": "暂不允许该登录方式", "hasToSetPassword": "需要设置密码", "hasToVerifyPassword": "需要验证密码", - "cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息" + "cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息", + "emailSuffixIsInvalid": "邮箱后缀不合法" }, "distinguishUser": "需要鉴别用户身份", "mobileUnset": "需要先登记手机号", diff --git a/lib/oak-app-domain/Passport/_baseSchema.d.ts b/lib/oak-app-domain/Passport/_baseSchema.d.ts index ab68caad3..5fb54201c 100644 --- a/lib/oak-app-domain/Passport/_baseSchema.d.ts +++ b/lib/oak-app-domain/Passport/_baseSchema.d.ts @@ -19,6 +19,7 @@ export type EmailConfig = { html?: string; codeDuration?: number; digit?: number; + emailSuffix?: string[]; }; export type PfwConfig = { appId: string; diff --git a/lib/triggers/index.d.ts b/lib/triggers/index.d.ts index fc7f50979..1d6a506c0 100644 --- a/lib/triggers/index.d.ts +++ b/lib/triggers/index.d.ts @@ -1,2 +1,2 @@ -declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; +declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; export default _default; diff --git a/lib/triggers/notification.js b/lib/triggers/notification.js index 594d30e40..ca7df6315 100644 --- a/lib/triggers/notification.js +++ b/lib/triggers/notification.js @@ -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], }); diff --git a/lib/types/Config.d.ts b/lib/types/Config.d.ts index 5e8796f90..2c34d1f8f 100644 --- a/lib/types/Config.d.ts +++ b/lib/types/Config.d.ts @@ -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'; diff --git a/lib/types/Email.d.ts b/lib/types/Email.d.ts index 7c3b03c93..12c4f5a23 100644 --- a/lib/types/Email.d.ts +++ b/lib/types/Email.d.ts @@ -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[]; diff --git a/lib/utils/email/node-mailer.d.ts b/lib/utils/email/node-mailer.d.ts index 503f5a093..0d8d8a97b 100644 --- a/lib/utils/email/node-mailer.d.ts +++ b/lib/utils/email/node-mailer.d.ts @@ -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 { name: string; + getConfig(context: BackendRuntimeContext, systemId?: string): Promise<{ + config: EmailConfig; + }>; sendEmail(options: EmailOptions, context: BRC): Promise<{ success: boolean; error?: undefined; diff --git a/lib/utils/email/node-mailer.js b/lib/utils/email/node-mailer.js index acb5f51eb..62b6babf3 100644 --- a/lib/utils/email/node-mailer.js +++ b/lib/utils/email/node-mailer.js @@ -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, diff --git a/src/aspects/token.ts b/src/aspects/token.ts index f99d6da6d..c79d13edb 100644 --- a/src/aspects/token.ts +++ b/src/aspects/token.ts @@ -650,6 +650,32 @@ export async function loginByMobile( }; 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( 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( } }; 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); @@ -1145,6 +1240,51 @@ export async function loginByEmail( } }; 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( } // 到这里说明验证码已经通过 - //检查当前user是否已绑定mobile + // 检查当前user是否已绑定mobile const [boundMobile] = await context.select( 'mobile', { @@ -1423,6 +1563,52 @@ export async function bindByEmail( 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( 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, diff --git a/src/components/config/upsert/email/index.tsx b/src/components/config/upsert/email/index.tsx index e52b34362..26b4b00bc 100644 --- a/src/components/config/upsert/email/index.tsx +++ b/src/components/config/upsert/email/index.tsx @@ -95,6 +95,21 @@ export default function Email(props: { }} /> + + <> + + setValue(`${idx}.secure`, checked) + } + /> + + diff --git a/src/components/passport/email/index.tsx b/src/components/passport/email/index.tsx index 1e0d3b19b..5965e7b6b 100644 --- a/src/components/passport/email/index.tsx +++ b/src/components/passport/email/index.tsx @@ -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(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) => { + 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 ( + + {(emailSuffix || []).map((tag, index) => { + const isLongTag = tag.length > 20; + const tagElem = ( + } + key={tag} + onClose={() => handleClose(tag)} + > + + {isLongTag ? `${tag.slice(0, 20)}...` : tag} + + + ); + return isLongTag ? ( + + {tagElem} + + ) : ( + tagElem + ); + })} + {inputVisible ? ( + + ) : ( + } onClick={showInput}> + {t('common::action.add')} + + )} + + ); +} + 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(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: { }} /> + + { + if (v !== (config as EmailConfig)?.emailSuffix) { + updateConfig(id, config!, 'emailSuffix', v, 'email'); + } + }} + /> + } diff --git a/src/components/platform/panel/index.ts b/src/components/platform/panel/index.ts index c50786c61..532c90565 100644 --- a/src/components/platform/panel/index.ts +++ b/src/components/platform/panel/index.ts @@ -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 || {}; diff --git a/src/components/platform/panel/web.pc.module.less b/src/components/platform/panel/web.pc.module.less index bb24882e6..caa7d4881 100644 --- a/src/components/platform/panel/web.pc.module.less +++ b/src/components/platform/panel/web.pc.module.less @@ -1,9 +1,7 @@ .container { - padding: 8px; height: 100%; - .tabLabel { - // writing-mode: vertical-rl; - letter-spacing: .2rem; - } + // .tabLabel { + // letter-spacing: .2rem; + // } } \ No newline at end of file diff --git a/src/components/platform/panel/web.pc.tsx b/src/components/platform/panel/web.pc.tsx index dc2705ac6..27f6034fc 100644 --- a/src/components/platform/panel/web.pc.tsx +++ b/src/components/platform/panel/web.pc.tsx @@ -64,7 +64,7 @@ export default function render(props: WebComponentProps ), diff --git a/src/components/platform/system/index.ts b/src/components/platform/system/index.ts index e24a1bd3d..0de3b522c 100644 --- a/src/components/platform/system/index.ts +++ b/src/components/platform/system/index.ts @@ -32,6 +32,13 @@ export default OakComponent({ } } }, + filters: [{ + filter() { + return { + platformId: this.props.platformId, + }; + }, + }], properties: { platformId: '', }, diff --git a/src/components/platform/system/web.pc.tsx b/src/components/platform/system/web.pc.tsx index 42d8d14bd..7fa38ecb2 100644 --- a/src/components/platform/system/web.pc.tsx +++ b/src/components/platform/system/web.pc.tsx @@ -66,6 +66,7 @@ export default function render(props: WebComponentProps { if (action === 'add') { const id = addItem({ platformId, config: { App: {} } }); @@ -83,6 +84,7 @@ export default function render(props: WebComponentProps diff --git a/src/data/i18n.ts b/src/data/i18n.ts index 44ef54a18..ce40bd118 100644 --- a/src/data/i18n.ts +++ b/src/data/i18n.ts @@ -628,7 +628,8 @@ const i18ns: I18n[] = [ "loginWayDisabled": "暂不允许该登录方式", "hasToSetPassword": "需要设置密码", "hasToVerifyPassword": "需要验证密码", - "cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息" + "cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息", + "emailSuffixIsInvalid": "邮箱后缀不合法" }, "distinguishUser": "需要鉴别用户身份", "mobileUnset": "需要先登记手机号", diff --git a/src/entities/Passport.ts b/src/entities/Passport.ts index 899b69168..d61778a2e 100644 --- a/src/entities/Passport.ts +++ b/src/entities/Passport.ts @@ -20,6 +20,7 @@ export type EmailConfig = { html?: string; codeDuration?: number; //验证码有效时间 单位分钟, 不填5分钟 digit?: number; //验证码位数 4~8,默认为4位 + emailSuffix?: string[] //邮箱后缀,不填则不校验 }; export type PfwConfig = { diff --git a/src/locales/error/zh-CN.json b/src/locales/error/zh-CN.json index afc7f3f2b..c3f85bb6c 100644 --- a/src/locales/error/zh-CN.json +++ b/src/locales/error/zh-CN.json @@ -11,7 +11,8 @@ "loginWayDisabled": "暂不允许该登录方式", "hasToSetPassword": "需要设置密码", "hasToVerifyPassword": "需要验证密码", - "cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息" + "cantChangeVerifiedUser": "不能修改已经验证身份的用户的敏感信息", + "emailSuffixIsInvalid": "邮箱后缀不合法" }, "distinguishUser": "需要鉴别用户身份", "mobileUnset": "需要先登记手机号", diff --git a/src/types/Config.ts b/src/types/Config.ts index a50f61dbf..10673c38f 100644 --- a/src/types/Config.ts +++ b/src/types/Config.ts @@ -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'; // 强度 diff --git a/src/types/Email.ts b/src/types/Email.ts index 0abd8af1d..7337c3347 100644 --- a/src/types/Email.ts +++ b/src/types/Email.ts @@ -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[] diff --git a/src/utils/email/node-mailer.ts b/src/utils/email/node-mailer.ts index a4f673558..0b71f0b90 100644 --- a/src/utils/email/node-mailer.ts +++ b/src/utils/email/node-mailer.ts @@ -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 { name = 'nodemailer'; + + async getConfig(context: BackendRuntimeContext, 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) { - 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 { console.log('Server is ready to take our messages'); } let mailOptions = { - from, + from: _from, to, subject, text,