diff --git a/.gitignore b/.gitignore index fb929853..44719a16 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ tsconfig.tsbuildinfo scripts/update.sh output +runtime diff --git a/Dockerfile b/Dockerfile index 95f5c87a..6ad5a105 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,13 @@ FROM node:18-alpine as builder -COPY . /app/ +COPY . /app/ WORKDIR /app -ARG EIP=mrdoc.fun RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories RUN npm config set registry https://registry.npmmirror.com RUN npm i -g pm2 @nestjs/cli pnpm -RUN apk --no-cache add bash -RUN sed -i "s/localhost/$EIP/g" /app/docker/prod-sample.yaml -RUN cp -f /app/docker/prod-sample.yaml /app/config/prod.yaml +RUN apk --no-cache add bash RUN bash build-output.sh - FROM node:18-alpine as prod -LABEL maintainer="www.mrdoc.fun" ENV TZ=Asia/Shanghai COPY --from=builder /app/docker/* /app/docker/ COPY --from=builder /app/output/ /app/ @@ -20,9 +15,9 @@ COPY --from=builder /app/output/ /app/ WORKDIR /app RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories RUN npm config set registry https://registry.npmmirror.com -RUN set -x \ +RUN set -x \ && apk update \ - && apk add --no-cache tzdata redis \ + && apk add --no-cache tzdata redis \ && chmod +x /app/docker/start.sh \ && npm i -g pm2 @nestjs/cli pnpm \ && rm -rf /var/cache/apk/* diff --git a/README.md b/README.md index 765cceee..fa4bbdde 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Think 是一款开源知识管理工具。通过独立的知识库空间,结 - `nest.js`:服务端框架 - `tiptap`:编辑器及文档协作 -可访问[云策文档帮助中心](https://think.codingit.cn/share/wiki/eb520cdf-aa4b-4af2-ae4a-7140e21403ab),查看更多功能文档。 +可访问[云策文档帮助中心](https://think.codingit.cn/share/wiki/JtXHW2BjrQ6G),查看更多功能文档。 ## 链接 @@ -19,156 +19,20 @@ Think 是一款开源知识管理工具。通过独立的知识库空间,结 欢迎进群交流。 -image - +image ## 预览 -![知识库](http://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-20/YN67GM4VQMBTZFZ88TYP8X/image.png) -![新建文档](http://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-20/YN67GM4VQMBTZFZ88TYPQX/image.png) -![编辑器](http://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-20/YN67GM4VQMBTZFZ88TYPZX/image.png) +
+ 查看预览图 + 知识库 + 新建文档 + 编辑器 +
-## 项目结构 +## 项目开发 -本项目依赖 pnpm 使用 monorepo 形式进行代码组织,分包如下: - -- `@think/config`: 客户端、服务端、OSS、MySQL、Redis 等配置管理 -- `@think/domains`:领域模型数据定义 -- `@think/constants`:常量配置 -- `@think/server`:服务端 -- `@think/client`:客户端 - -## 项目依赖 - -- nodejs ≥ 16.5 -- pnpm -- pm2 -- mysql ≥ 5.7 -- redis (可选) - -依赖安装命令: `npm i -g pm2 @nestjs/cli pnpm` - - -#### 数据库 - -首先安装 `MySQL`,推荐使用 docker 进行安装。 - -```bash -docker image pull mysql:5.7 -# m1 的 mac 可以用:docker image pull --platform linux/x86_64 mysql:5.7 -docker run -d --restart=always --name think -p 3306:3306 -e MYSQL_DATABASE=think -e MYSQL_ROOT_PASSWORD=root mysql:5.7 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci -``` - -#### 可选:Redis - -如果需要文档版本服务,请在根目录 `yaml` 配置中进行 `db.redis` 的配置。 - -``` -docker pull redis:latest -docker run --name think-redis -p 6379:6379 -d redis --appendonly yes --requirepass "root" -``` - -## Docker-compose 一键构建安装 - -- 实测腾讯轻量云 2C4G 机器构建需 8 分钟左右 - -**请注意修改 `docker-compose.yml` 中的 `EIP` 参数,否则无法正常使用!!!** - - -``` -# 首次安装 -git clone https://github.com/fantasticit/think.git -cd think -docker-compose up -d - -# 二次更新升级 -cd think -git pull -docker-compose build -docker-compose up -d - -# FAQ -如遇二次更新有问题,请更新代码重新构建,然后删除本地配置文件并重启容器. -如果还不能解决,1.有能力可自行解决|2.等待更新|3.去mrdoc.fun站点留言 -``` - -然后访问 `http://ip:5001` 即可. - - - -## 手动安装教程 - -- 前台页面地址:`http://localhost:5001` -- 服务接口地址:`http://localhost:5002` -- 协作接口地址:`http://localhost:5003` - -如需修改配置,开发环境编辑 `config/dev.yaml`。生产环境编辑 `config/prod.yaml` (如没有,可复制开发环境的配置修改即可.) - -### 本地源代码运行(开发环境) - - -```bash -git clone https://github.com/fantasticit/think.git -cd think -pnpm install -pnpm run dev -``` - -然后访问 `http://ip:5001` 即可. - - - -### 本地源代码运行(生产环境) - -生产环境部署的脚本如下: - -```bash -git clone https://github.com/fantasticit/think.git -cd think -pnpm install -pnpm run build -pnpm run pm2 - -pm2 startup -pm2 save -``` - -### nginx 配置参考 - -采用反向代理进行 `nginx` 配置,**同时设置 `proxy_set_header X-Real-IP $remote_addr;` 以便服务端获取到真实 ip 地址**。 - -```bash -upstream wipi_client { - server 127.0.0.1:3000; - keepalive 64; -} - -# http -> https 重定向 -server { - listen 80; - server_name 域名; - rewrite ^(.*)$ https://$host$1 permanent; -} - -server { - listen 443 ssl; - server_name 域名; - ssl_certificate 证书存放路径; - ssl_certificate_key 证书存放路径; - - location / { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Nginx-Proxy true; - proxy_cache_bypass $http_upgrade; - proxy_pass http://wipi_client; #反向代理 - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } -} -``` +[项目开发说明](./let-us-start.md)。 ## 自动化部署 @@ -176,24 +40,17 @@ server { 参考:[webhook](https://github.com/adnanh/webhook/blob/master/docs/Hook-Examples.md#incoming-github-webhook) -## 商用 - -如需商用,请联系作者,取得授权后可商用。 - ## 赞助 如果这个项目对您有帮助,并且您希望支持该项目的开发和维护,请随时扫描一下二维码进行捐赠。非常感谢您的捐款,谢谢! -如果您希望留下您的信息,可以到[感谢信](https://think.codingit.cn/wiki/eb520cdf-aa4b-4af2-ae4a-7140e21403ab/document/230548f5-3220-4c5b-a209-02b1eb0299e7)评论区留言。
alipay wechat
+## 贡献者 -## 资料 +感谢所有为本项目作出贡献的同学! -- next.js 源码:https://github.com/vercel/next.js -- next.js 文档:https://nextjs.org/ -- nest.js 源码:https://github.com/nestjs/nest -- nest.js 文档:https://nestjs.com/ + diff --git a/build-output.sh b/build-output.sh index a2caa46b..ed234320 100755 --- a/build-output.sh +++ b/build-output.sh @@ -1,7 +1,12 @@ #! /bin/bash # 该脚本只保留生产环境运行所需文件到统一目录 +if [ ! -f './config/prod.yaml' ]; then + echo "缺少 config/prod.yaml 文件,可参考 docker-prod-sample.yaml 进行配置" + exit 1 +fi # 构建 +pnpm fetch --prod pnpm install pnpm run build @@ -71,7 +76,7 @@ cd ../../ # @see https://github.com/typicode/husky/issues/914#issuecomment-826768549 cd ${outputDir} npm set-script prepare "" -pnpm install -r --prod +pnpm install -r --offline --prod cd ../ echo "${outputDir} 打包完成" diff --git a/config/dev.yaml b/config/dev.yaml index b38705eb..1b7e3409 100644 --- a/config/dev.yaml +++ b/config/dev.yaml @@ -23,13 +23,22 @@ server: enableRateLimit: true # 是否限流 rateLimitWindowMs: 60000 # 限流时间 rateLimitMax: 1000 # 单位限流时间内单个 ip 最大访问数量 + email: # 邮箱服务,参考 http://help.163.com/09/1223/14/5R7P6CJ600753VB8.html?servCode=6010376 获取 SMTP 配置 + host: '' + port: 465 + user: '' + password: '' + admin: + name: 'admin' # 注意修改 + password: 'admin' # 注意修改 + email: 'admin@think.com' # 注意修改为真实邮箱地址 # 数据库配置 db: mysql: host: '127.0.0.1' - username: 'root' - password: 'root' + username: 'think' + password: 'think' database: 'think' port: 3306 charset: 'utf8mb4' diff --git a/docker/prod-sample.yaml b/config/docker-prod-sample.yaml similarity index 76% rename from docker/prod-sample.yaml rename to config/docker-prod-sample.yaml index 3b445e5f..103f5c18 100644 --- a/docker/prod-sample.yaml +++ b/config/docker-prod-sample.yaml @@ -1,4 +1,4 @@ -# 生产环境docker示例配置 +# 开发环境配置 client: port: 5001 assetPrefix: '/' @@ -7,7 +7,7 @@ client: # 以下为页面 meta 配置 seoAppName: '云策文档' seoDescription: '云策文档是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。' - seoKeywords: '云策文档,协作,文档,前端面试题,fantasticit,https://github.com/fantasticit/think' + seoKeywords: '云策文档,协作,文档,fantasticit,https://github.com/fantasticit/think' # 预先连接的来源,空格分割(比如图片存储服务器) dnsPrefetch: '//wipi.oss-cn-shanghai.aliyuncs.com' # 站点地址(如:http://think.codingit.cn/),一定要设置,否则会出现 cookie、跨域等问题 @@ -23,22 +23,31 @@ server: enableRateLimit: true # 是否限流 rateLimitWindowMs: 60000 # 限流时间 rateLimitMax: 1000 # 单位限流时间内单个 ip 最大访问数量 + email: # 邮箱服务,参考 http://help.163.com/09/1223/14/5R7P6CJ600753VB8.html?servCode=6010376 获取 SMTP 配置 + host: '' + port: 465 + user: '' + password: '' + admin: + name: 'admin' # 注意修改 + password: 'admin' # 注意修改 + email: 'admin@think.com' # 注意修改为真实邮箱地址 # 数据库配置 db: mysql: - host: 'mysql-with-think' - username: 'jonnyan404' - password: 'www.mrdoc.fun' + host: 'mysql-for-think' + username: 'think' + password: 'think' database: 'think' port: 3306 charset: 'utf8mb4' timezone: '+08:00' synchronize: true redis: - host: '127.0.0.1' + host: 'redis-for-think' port: '6379' - password: '' + password: 'root' # oss 文件存储服务 oss: diff --git a/docker-compose.yml b/docker-compose.yml index c776d794..0e4ea897 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,44 +1,58 @@ -version: "3" +version: '3' services: - thinkdoc: + think: build: context: . - args: - EIP: x.x.x.x # api接口IP,必须设置,可以是 IP 或者域名. image: think - container_name: thinkdoc - #restart: always + container_name: think volumes: - - /path/to/you/dir/config:/app/config # 请注意修改 /path/to/you/dir 为云策文档配置文件目录. - - /path/to/you/dir/static:/app/packages/server/static # 请注意修改 /path/to/you/dir 为云策文档附件存储目录. + - ./config:/app/config + - ./runtime/static:/app/packages/server/static environment: - TZ=Asia/Shanghai ports: - - "5001-5003:5001-5003" + - '5001-5003:5001-5003' depends_on: - mysql + - redis + networks: + - think mysql: image: mysql:5.7 - container_name: mysql-with-think - #restart: always + restart: always + container_name: mysql-for-think volumes: - - /path/to/you/dir/mysql:/var/lib/mysql # 请注意修改 /path/to/you/dir 为您要存储mysql数据的目录绝对路径. + - ./runtime/mysql:/var/lib/mysql environment: - TZ=Asia/Shanghai - - MYSQL_ROOT_PASSWORD=Jonnyan404! + - MYSQL_ROOT_PASSWORD=root - MYSQL_DATABASE=think - - MYSQL_USER=jonnyan404 - - MYSQL_PASSWORD=www.mrdoc.fun + - MYSQL_USER=think + - MYSQL_PASSWORD=think expose: - - "3306" + - '3306' ports: - - "63306:3306" # 如果不需要外部连接mysql,可注释此行+上一行. + - '3306:3306' command: - - "--character-set-server=utf8mb4" - - "--collation-server=utf8mb4_unicode_ci" -# Volumes for persisting data, see https://docs.docker.com/engine/admin/volumes/volumes/ -#volumes: -# thinkdoc-data: -# driver: local -# mysql-data: -# driver: local + - '--character-set-server=utf8mb4' + - '--collation-server=utf8mb4_unicode_ci' + networks: + - think + redis: + image: redis:latest + restart: always + container_name: redis-for-think + command: > + --requirepass root + expose: + - '6379' + ports: + - '6379:6379' + volumes: + - ./runtime/redis:/data + privileged: true + networks: + - think +networks: + think: + driver: bridge diff --git a/docker/start.sh b/docker/start.sh index 60c3e2f2..821e6cc4 100644 --- a/docker/start.sh +++ b/docker/start.sh @@ -1,14 +1,8 @@ #!/bin/sh ### Author:jonnyan404 ### date:2022年5月22日 -CONFIG_FILE='/app/config/prod.yaml' -if [ ! -f $CONFIG_FILE ]; then - echo "#####Generating configuration file#####" - cp /app/docker/prod-sample.yaml $CONFIG_FILE -else - echo "#####Configuration file already exists#####" -fi -redis-server --daemonize yes pnpm run pm2 +pm2 startup +pm2 save pm2 logs diff --git a/let-us-start.md b/let-us-start.md new file mode 100644 index 00000000..1fbfbea3 --- /dev/null +++ b/let-us-start.md @@ -0,0 +1,110 @@ +# think + +## 项目结构 + +本项目依赖 pnpm 使用 monorepo 形式进行代码组织,分包如下: + +- `@think/config`: 客户端、服务端、OSS、MySQL、Redis 等配置管理 +- `@think/domains`:领域模型数据定义 +- `@think/constants`:常量配置 +- `@think/server`:服务端 +- `@think/client`:客户端 + +## 项目依赖 + +为了将项目运行起来,至少需要以下依赖。 + +- nodejs >=16.5.0:推荐使用 nvm 安装 +- pnpm:安装 nodejs 后,运行 `npm i -g pnpm` 即可安装 +- pm2:安装 nodejs 后,运行 `npm i -g pm2` 即可安装 +- MySQL 5.7 +- Redis + +## 配置文件 + +项目所有的配置文件都在 `config` 目录下,其中 `dev.yaml` 中各字段均有解释,生产环境打包依赖 `prod.yaml`(需要自行修改为所需配置)。如果运行不起来,请对比 `dev.yaml` 检查配置。 + +**如果部署遇到问题,首先请确认相应配置是否正确!** + +## 项目运行 + +无论是开发环境,还是生产环境,项目运行成功后会在 3 个端口启动相应服务(默认 5001、5002、5003),具体端口号由 `config` 文件夹下的配置文件决定。 + +- 前台页面地址:`http://localhost:5001` +- 服务接口地址:`http://localhost:5002` +- 协作接口地址:`http://localhost:5003` + +### 本地开发 + +1. 安装数据库 + +首先安装 `MySQL` 和 `Redis`,推荐使用 docker 进行安装。 + +```bash +docker image pull mysql:5.7 +# m1 的 mac 可以用:docker image pull --platform linux/x86_64 mysql:5.7 +docker run -d --restart=always --name mysql-for-think-dev -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_USER=think -e MYSQL_PASSWORD=think -e MYSQL_DATABASE=think mysql:5.7 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + +docker pull redis:latest +docker run --name redis-for-think-dev -p 6379:6379 -d redis --appendonly yes --requirepass "root" +``` + +2. 安装依赖并运行 + +```bash +git clone https://github.com/fantasticit/think.git +cd think +pnpm install +pnpm run dev +``` + +### 生产部署 + +首先确认在 `config` 文件夹下新建 `prod.yaml` 配置文件,然后运行以下命令。 + +```bash +git clone https://github.com/fantasticit/think.git +cd think +pnpm install # 安装依赖 +pnpm run build # 项目打包 + +# 以下如果没有安装 pm2,直接 pnpm run start,推荐使用 pm2 +pnpm run pm2 +pm2 startup +pm2 save +``` + +### docker-compose + +也可以使用 docker-compose 进行项目部署。首先,根据需要修改 `docker-compose.yml` 中的数据库、Redis 相关用户名、密码等配置,然后,从 `config/docker-prod-sample.yaml` 复制出 `config/prod.yaml` 并修改其中对应的配置。 + +```bash +# 首次安装 +git clone https://github.com/fantasticit/think.git +cd think +vim docker-compose.yml +docker-compose up -d + +# 二次更新升级 +cd think +git pull +docker-compose build +docker-compose up -d + +# 如果二次更新有问题 +docker-compose kill +docker-compose rm +docker image rm think # 删掉构建的镜像 +docker-compose up -d +``` + +### nginx 配置参考 + +无论以何种方式进行项目部署,项目运行成功后会在 3 个端口启动服务(默认 5001、5002、5003,具体由配置文件决定)。`nginx` 配置参考 <[think/nginx.conf.sample](https://github.com/fantasticit/think/blob/main/nginx.conf.sample)>。 + +特别强调,在 `config` 文件夹的配置中 `client.siteUrl` 一定要配置正确,否则客户端可能无法正常运行。 + +```yaml +# 站点地址(如:http://think.codingit.cn/),一定要设置,否则会出现 cookie、跨域等问题 +siteUrl: 'http://localhost:5001' +``` diff --git a/nginx.conf.bak b/nginx.conf.sample similarity index 81% rename from nginx.conf.bak rename to nginx.conf.sample index f3e36d98..f2b36de7 100644 --- a/nginx.conf.bak +++ b/nginx.conf.sample @@ -1,9 +1,9 @@ -upstream think_server { +upstream think_client { server 127.0.0.1:5001; keepalive 64; } -upstream think_client { +upstream think_server { server 127.0.0.1:5002; keepalive 64; } @@ -29,7 +29,7 @@ server { client_max_body_size 100m; - location /think { + location /api { proxy_pass http://think_server; proxy_read_timeout 300s; proxy_send_timeout 300s; @@ -43,16 +43,23 @@ server { } location /think/wss { - proxy_pass http://think_wss; - proxy_read_timeout 300s; - proxy_send_timeout 300s; + proxy_pass http://think_wss; + proxy_read_timeout 300s; + proxy_send_timeout 300s; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + } + + location /static/ { + gzip_static on; + expires max; + add_header Cache-Control public; + alias /apps/think/packages/server/static/; } } diff --git a/packages/client/src/components/admin/system-config/index.tsx b/packages/client/src/components/admin/system-config/index.tsx new file mode 100644 index 00000000..c1b12c9c --- /dev/null +++ b/packages/client/src/components/admin/system-config/index.tsx @@ -0,0 +1,29 @@ +import { TabPane, Tabs } from '@douyinfe/semi-ui'; +import React from 'react'; + +import { Mail } from './mail'; +import { System } from './system'; + +interface IProps { + tab?: string; + onNavigate: (arg: string) => void; +} + +const TitleMap = { + base: '系统管理', + mail: '邮箱服务', +}; + +export const SystemConfig: React.FC = ({ tab, onNavigate }) => { + return ( + + + + + + + + + + ); +}; diff --git a/packages/client/src/components/admin/system-config/mail/index.tsx b/packages/client/src/components/admin/system-config/mail/index.tsx new file mode 100644 index 00000000..fec4553e --- /dev/null +++ b/packages/client/src/components/admin/system-config/mail/index.tsx @@ -0,0 +1,88 @@ +import { Banner, Button, Form, Toast } from '@douyinfe/semi-ui'; +import { DataRender } from 'components/data-render'; +import { useSystemConfig } from 'data/user'; +import { useToggle } from 'hooks/use-toggle'; +import React, { useCallback } from 'react'; + +export const Mail = () => { + const { data, loading, error, sendTestEmail, updateSystemConfig } = useSystemConfig(); + const [changed, toggleChanged] = useToggle(false); + + const onFormChange = useCallback(() => { + toggleChanged(true); + }, [toggleChanged]); + + const onFinish = useCallback( + (values) => { + updateSystemConfig(values).then(() => { + Toast.success('操作成功'); + }); + }, + [updateSystemConfig] + ); + + return ( + ( +
+ + +
+ + + + + + + + + + + + +
+ )} + /> + ); +}; diff --git a/packages/client/src/components/admin/system-config/system/index.tsx b/packages/client/src/components/admin/system-config/system/index.tsx new file mode 100644 index 00000000..d4c539be --- /dev/null +++ b/packages/client/src/components/admin/system-config/system/index.tsx @@ -0,0 +1,49 @@ +import { Banner, Button, Form, Toast } from '@douyinfe/semi-ui'; +import { DataRender } from 'components/data-render'; +import { useSystemConfig } from 'data/user'; +import { useToggle } from 'hooks/use-toggle'; +import React, { useCallback } from 'react'; + +export const System = () => { + const { data, loading, error, updateSystemConfig } = useSystemConfig(); + const [changed, toggleChanged] = useToggle(false); + + const onFormChange = useCallback(() => { + toggleChanged(true); + }, [toggleChanged]); + + const onFinish = useCallback( + (values) => { + updateSystemConfig(values).then(() => { + Toast.success('操作成功'); + }); + }, + [updateSystemConfig] + ); + + return ( + ( +
+ +
+ + + + +
+ )} + /> + ); +}; diff --git a/packages/client/src/components/document/collaboration/index.tsx b/packages/client/src/components/document/collaboration/index.tsx index 16dd5a5b..39d9e0f1 100644 --- a/packages/client/src/components/document/collaboration/index.tsx +++ b/packages/client/src/components/document/collaboration/index.tsx @@ -16,7 +16,6 @@ import { Tooltip, Typography, } from '@douyinfe/semi-ui'; -import { IUser } from '@think/domains'; import { DataRender } from 'components/data-render'; import { DocumentLinkCopyer } from 'components/document/link'; import { useDoumentMembers } from 'data/document'; @@ -54,6 +53,7 @@ const renderChecked = (onChange, authKey: 'readable' | 'editable') => (checked, export const DocumentCollaboration: React.FC = ({ wikiId, documentId, disabled = false }) => { const { isMobile } = IsOnMobile.useHook(); const ref = useRef(); + const toastedUsersRef = useRef([]); const { user: currentUser } = useUser(); const [visible, toggleVisible] = useToggle(false); const { users, loading, error, addUser, updateUser, deleteUser } = useDoumentMembers(documentId, { @@ -169,7 +169,10 @@ export const DocumentCollaboration: React.FC = ({ wikiId, documentId, di return joinUser.name !== currentUser.name; }) .forEach((joinUser) => { - Toast.info(`${joinUser.name}-${joinUser.clientId}加入文档`); + if (!toastedUsersRef.current.includes(joinUser.clientId)) { + Toast.info(`${joinUser.name}-${joinUser.clientId}加入文档`); + toastedUsersRef.current.push(joinUser.clientId); + } }); setCollaborationUsers(joinUsers); @@ -178,6 +181,7 @@ export const DocumentCollaboration: React.FC = ({ wikiId, documentId, di event.on(JOIN_USER, handler); return () => { + toastedUsersRef.current = []; event.off(JOIN_USER, handler); }; }, [currentUser]); diff --git a/packages/client/src/components/user/index.tsx b/packages/client/src/components/user/index.tsx index d6bb585b..2d4111e0 100644 --- a/packages/client/src/components/user/index.tsx +++ b/packages/client/src/components/user/index.tsx @@ -2,7 +2,8 @@ import { IconSpin } from '@douyinfe/semi-icons'; import { Avatar, Button, Dropdown, Typography } from '@douyinfe/semi-ui'; import { useUser } from 'data/user'; import { useToggle } from 'hooks/use-toggle'; -import React from 'react'; +import Router from 'next/router'; +import React, { useCallback } from 'react'; import { UserSetting } from './setting'; @@ -12,6 +13,10 @@ export const User: React.FC = () => { const { user, loading, error, toLogin, logout } = useUser(); const [visible, toggleVisible] = useToggle(false); + const toAdmin = useCallback(() => { + Router.push('/admin'); + }, []); + if (loading) return + + + + + + + + +
+ + + 去登录 + + +
+ + +
+ +
+ + ); +}; + +export default Page; diff --git a/packages/client/src/pages/login/index.tsx b/packages/client/src/pages/login/index.tsx index 0125d6aa..0559b239 100644 --- a/packages/client/src/pages/login/index.tsx +++ b/packages/client/src/pages/login/index.tsx @@ -51,9 +51,10 @@ const Page = () => { field="name" label="账户" style={{ width: '100%' }} - placeholder="输入账户名称" - rules={[{ required: true, message: '请输入账户' }]} + placeholder="输入账户名称或邮箱" + rules={[{ required: true, message: '请输入账户或邮箱' }]} >
+ { 登录 diff --git a/packages/client/src/pages/register/index.module.scss b/packages/client/src/pages/register/index.module.scss index 3b94d33a..4366dc25 100644 --- a/packages/client/src/pages/register/index.module.scss +++ b/packages/client/src/pages/register/index.module.scss @@ -9,6 +9,7 @@ position: relative; z-index: 10; display: flex; + height: calc(100% - 52px); padding: 10vh 24px; flex: 1; flex-direction: column; diff --git a/packages/client/src/pages/register/index.tsx b/packages/client/src/pages/register/index.tsx index 90651e70..e414e526 100644 --- a/packages/client/src/pages/register/index.tsx +++ b/packages/client/src/pages/register/index.tsx @@ -1,13 +1,16 @@ -import { Button, Form, Layout, Modal, Space, Typography } from '@douyinfe/semi-ui'; +import { Button, Col, Form, Layout, Modal, Row, Space, Toast, Typography } from '@douyinfe/semi-ui'; import { Author } from 'components/author'; import { LogoImage, LogoText } from 'components/logo'; import { Seo } from 'components/seo'; -import { useAsyncLoading } from 'hooks/use-async-loading'; +import { useRegister, useVerifyCode } from 'data/user'; +import { isEmail } from 'helpers/validator'; +import { useInterval } from 'hooks/use-interval'; import { useRouterQuery } from 'hooks/use-router-query'; +import { useToggle } from 'hooks/use-toggle'; import Link from 'next/link'; import Router from 'next/router'; -import React from 'react'; -import { register as registerApi } from 'services/user'; +import { emit } from 'process'; +import React, { useCallback, useState } from 'react'; import styles from './index.module.scss'; @@ -16,21 +19,64 @@ const { Title, Text } = Typography; const Page = () => { const query = useRouterQuery(); - const [registerWithLoading, loading] = useAsyncLoading(registerApi); - const onFinish = (values) => { - registerWithLoading(values).then((res) => { - Modal.confirm({ - title: 注册成功, - content: 是否跳转至登录?, - okText: '确认', - cancelText: '取消', - onOk() { - Router.push('/login'); - }, - }); + const [email, setEmail] = useState(''); + const [hasSendVerifyCode, toggleHasSendVerifyCode] = useToggle(false); + const [countDown, setCountDown] = useState(0); + const { register, loading } = useRegister(); + const { sendVerifyCode, loading: sendVerifyCodeLoading } = useVerifyCode(); + + const onFormChange = useCallback((formState) => { + const email = formState.values.email; + + if (isEmail(email)) { + setEmail(email); + } else { + setEmail(null); + } + }, []); + + const { start, stop } = useInterval(() => { + setCountDown((v) => { + if (v - 1 <= 0) { + stop(); + toggleHasSendVerifyCode(false); + return 0; + } + return v - 1; }); - }; + }, 1000); + + const onFinish = useCallback( + (values) => { + register(values).then((res) => { + Modal.confirm({ + title: 注册成功, + content: 是否跳转至登录?, + okText: '确认', + cancelText: '取消', + onOk() { + Router.push('/login', { query }); + }, + }); + }); + }, + [register, query] + ); + + const getVerifyCode = useCallback(() => { + stop(); + sendVerifyCode({ email }) + .then(() => { + Toast.success('请前往邮箱查收验证码'); + setCountDown(60); + start(); + toggleHasSendVerifyCode(true); + }) + .catch(() => { + toggleHasSendVerifyCode(false); + }); + }, [email, toggleHasSendVerifyCode, sendVerifyCode, start, stop]); return ( @@ -42,10 +88,16 @@ const Page = () => { -
+ 用户注册 + { placeholder="输入账户名称" rules={[{ required: true, message: '请输入账户' }]} > + { placeholder="输入用户密码" rules={[{ required: true, message: '请输入密码' }]} > + + field="email" + placeholder={'请输入邮箱'} + rules={[ + { + type: 'email', + message: '请输入正确的邮箱地址!', + }, + { + required: true, + message: '请输入邮箱地址!', + }, + ]} + /> + + + + + + + + + + diff --git a/packages/client/src/tiptap/core/extensions/title.ts b/packages/client/src/tiptap/core/extensions/title.ts index 1e47cf04..b812c2ae 100644 --- a/packages/client/src/tiptap/core/extensions/title.ts +++ b/packages/client/src/tiptap/core/extensions/title.ts @@ -30,7 +30,7 @@ export const Title = Node.create({ addOptions() { return { HTMLAttributes: { - class: 'title', + class: 'node-title', }, }; }, @@ -47,7 +47,7 @@ export const Title = Node.create({ parseHTML() { return [ { - tag: 'div[class=title]', + tag: 'div[class=node-title]', }, ]; }, diff --git a/packages/domains/lib/api/user.d.ts b/packages/domains/lib/api/user.d.ts index 485502ce..ea48ec9c 100644 --- a/packages/domains/lib/api/user.d.ts +++ b/packages/domains/lib/api/user.d.ts @@ -7,6 +7,14 @@ export declare const UserApiDefinition: { server: "/"; client: () => string; }; + /** + * 获取验证码 + */ + sendVerifyCode: { + method: "get"; + server: "sendVerifyCode"; + client: () => string; + }; /** * 注册 */ @@ -15,6 +23,14 @@ export declare const UserApiDefinition: { server: "register"; client: () => string; }; + /** + * 重置密码 + */ + resetPassword: { + method: "post"; + server: "resetPassword"; + client: () => string; + }; /** * 登录 */ @@ -39,4 +55,36 @@ export declare const UserApiDefinition: { server: "update"; client: () => string; }; + /** + * 锁定用户 + */ + toggleLockUser: { + method: "post"; + server: "lock/user"; + client: () => string; + }; + /** + * 获取系统配置 + */ + getSystemConfig: { + method: "get"; + server: "config/system"; + client: () => string; + }; + /** + * 发送测试邮件 + */ + sendTestEmail: { + method: "get"; + server: "config/system/sendTestEmail"; + client: () => string; + }; + /** + * 发送测试邮件 + */ + updateSystemConfig: { + method: "post"; + server: "config/system/updateSystemConfig"; + client: () => string; + }; }; diff --git a/packages/domains/lib/api/user.js b/packages/domains/lib/api/user.js index 8729c559..1f687f31 100644 --- a/packages/domains/lib/api/user.js +++ b/packages/domains/lib/api/user.js @@ -10,6 +10,14 @@ exports.UserApiDefinition = { server: '/', client: function () { return '/user'; } }, + /** + * 获取验证码 + */ + sendVerifyCode: { + method: 'get', + server: 'sendVerifyCode', + client: function () { return '/verify/sendVerifyCode'; } + }, /** * 注册 */ @@ -18,6 +26,14 @@ exports.UserApiDefinition = { server: 'register', client: function () { return '/user/register'; } }, + /** + * 重置密码 + */ + resetPassword: { + method: 'post', + server: 'resetPassword', + client: function () { return '/user/resetPassword'; } + }, /** * 登录 */ @@ -41,5 +57,37 @@ exports.UserApiDefinition = { method: 'patch', server: 'update', client: function () { return "/user/update"; } + }, + /** + * 锁定用户 + */ + toggleLockUser: { + method: 'post', + server: 'lock/user', + client: function () { return "/user/lock/user"; } + }, + /** + * 获取系统配置 + */ + getSystemConfig: { + method: 'get', + server: 'config/system', + client: function () { return "/user/config/system"; } + }, + /** + * 发送测试邮件 + */ + sendTestEmail: { + method: 'get', + server: 'config/system/sendTestEmail', + client: function () { return "/user/config/system/sendTestEmail"; } + }, + /** + * 发送测试邮件 + */ + updateSystemConfig: { + method: 'post', + server: 'config/system/updateSystemConfig', + client: function () { return "/user/config/system/updateSystemConfig"; } } }; diff --git a/packages/domains/lib/models/index.d.ts b/packages/domains/lib/models/index.d.ts index f9692ca8..88796f1f 100644 --- a/packages/domains/lib/models/index.d.ts +++ b/packages/domains/lib/models/index.d.ts @@ -5,3 +5,4 @@ export * from './message'; export * from './template'; export * from './comment'; export * from './pagination'; +export * from './system'; diff --git a/packages/domains/lib/models/index.js b/packages/domains/lib/models/index.js index d3a790c4..ef200cb9 100644 --- a/packages/domains/lib/models/index.js +++ b/packages/domains/lib/models/index.js @@ -17,3 +17,4 @@ __exportStar(require("./message"), exports); __exportStar(require("./template"), exports); __exportStar(require("./comment"), exports); __exportStar(require("./pagination"), exports); +__exportStar(require("./system"), exports); diff --git a/packages/domains/lib/models/system.d.ts b/packages/domains/lib/models/system.d.ts new file mode 100644 index 00000000..c3951630 --- /dev/null +++ b/packages/domains/lib/models/system.d.ts @@ -0,0 +1,7 @@ +export interface ISystemConfig { + isSystemLocked: boolean; + emailServiceHost: string; + emailServicePassword: string; + emailServicePort: string; + emailServiceUser: string; +} diff --git a/packages/domains/lib/models/system.js b/packages/domains/lib/models/system.js new file mode 100644 index 00000000..0e345787 --- /dev/null +++ b/packages/domains/lib/models/system.js @@ -0,0 +1,2 @@ +"use strict"; +exports.__esModule = true; diff --git a/packages/domains/lib/models/user.d.ts b/packages/domains/lib/models/user.d.ts index d832b43e..7b11ff57 100644 --- a/packages/domains/lib/models/user.d.ts +++ b/packages/domains/lib/models/user.d.ts @@ -24,6 +24,7 @@ export interface IUser { email?: string; role: UserRole; status: UserStatus; + isSystemAdmin?: boolean; } /** * 登录用户数据定义 diff --git a/packages/domains/src/api/user.ts b/packages/domains/src/api/user.ts index 843e4a9b..56b1d26f 100644 --- a/packages/domains/src/api/user.ts +++ b/packages/domains/src/api/user.ts @@ -10,6 +10,15 @@ export const UserApiDefinition = { client: () => '/user', }, + /** + * 获取验证码 + */ + sendVerifyCode: { + method: 'get' as const, + server: 'sendVerifyCode' as const, + client: () => '/verify/sendVerifyCode', + }, + /** * 注册 */ @@ -19,6 +28,15 @@ export const UserApiDefinition = { client: () => '/user/register', }, + /** + * 重置密码 + */ + resetPassword: { + method: 'post' as const, + server: 'resetPassword' as const, + client: () => '/user/resetPassword', + }, + /** * 登录 */ @@ -45,4 +63,40 @@ export const UserApiDefinition = { server: 'update' as const, client: () => `/user/update`, }, + + /** + * 锁定用户 + */ + toggleLockUser: { + method: 'post' as const, + server: 'lock/user' as const, + client: () => `/user/lock/user`, + }, + + /** + * 获取系统配置 + */ + getSystemConfig: { + method: 'get' as const, + server: 'config/system' as const, + client: () => `/user/config/system`, + }, + + /** + * 发送测试邮件 + */ + sendTestEmail: { + method: 'get' as const, + server: 'config/system/sendTestEmail' as const, + client: () => `/user/config/system/sendTestEmail`, + }, + + /** + * 发送测试邮件 + */ + updateSystemConfig: { + method: 'post' as const, + server: 'config/system/updateSystemConfig' as const, + client: () => `/user/config/system/updateSystemConfig`, + }, }; diff --git a/packages/domains/src/models/index.ts b/packages/domains/src/models/index.ts index f9692ca8..88796f1f 100644 --- a/packages/domains/src/models/index.ts +++ b/packages/domains/src/models/index.ts @@ -5,3 +5,4 @@ export * from './message'; export * from './template'; export * from './comment'; export * from './pagination'; +export * from './system'; diff --git a/packages/domains/src/models/system.ts b/packages/domains/src/models/system.ts new file mode 100644 index 00000000..7ec16362 --- /dev/null +++ b/packages/domains/src/models/system.ts @@ -0,0 +1,7 @@ +export interface ISystemConfig { + isSystemLocked: boolean; + emailServiceHost: string; + emailServicePassword: string; + emailServicePort: string; + emailServiceUser: string; +} diff --git a/packages/domains/src/models/user.ts b/packages/domains/src/models/user.ts index 30c4995f..0d52ec96 100644 --- a/packages/domains/src/models/user.ts +++ b/packages/domains/src/models/user.ts @@ -26,6 +26,7 @@ export interface IUser { email?: string; role: UserRole; status: UserStatus; + isSystemAdmin?: boolean; } /** diff --git a/packages/server/package.json b/packages/server/package.json index df1bde33..a93c30e0 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -49,6 +49,7 @@ "lodash": "^4.17.21", "mysql2": "^2.3.3", "nestjs-pino": "^2.5.2", + "nodemailer": "^6.7.5", "nuid": "^1.1.6", "passport": "^0.5.2", "passport-jwt": "^4.0.0", @@ -61,6 +62,7 @@ "rxjs": "^7.2.0", "typeorm": "^0.2.41", "ua-parser-js": "^1.0.2", + "validator": "^13.7.0", "y-prosemirror": "^1.0.14", "yjs": "^13.5.24" }, diff --git a/packages/server/src/app.module.ts b/packages/server/src/app.module.ts index bfc10bc4..ab417971 100644 --- a/packages/server/src/app.module.ts +++ b/packages/server/src/app.module.ts @@ -3,8 +3,10 @@ import { DocumentEntity } from '@entities/document.entity'; import { DocumentAuthorityEntity } from '@entities/document-authority.entity'; import { MessageEntity } from '@entities/message.entity'; import { StarEntity } from '@entities/star.entity'; +import { SystemEntity } from '@entities/system.entity'; import { TemplateEntity } from '@entities/template.entity'; import { UserEntity } from '@entities/user.entity'; +import { VerifyEntity } from '@entities/verify.entity'; import { ViewEntity } from '@entities/view.entity'; import { WikiEntity } from '@entities/wiki.entity'; import { WikiUserEntity } from '@entities/wiki-user.entity'; @@ -15,8 +17,10 @@ import { DocumentModule } from '@modules/document.module'; import { FileModule } from '@modules/file.module'; import { MessageModule } from '@modules/message.module'; import { StarModule } from '@modules/star.module'; +import { SystemModule } from '@modules/system.module'; import { TemplateModule } from '@modules/template.module'; import { UserModule } from '@modules/user.module'; +import { VerifyModule } from '@modules/verify.module'; import { ViewModule } from '@modules/view.module'; import { WikiModule } from '@modules/wiki.module'; import { forwardRef, Inject, Module } from '@nestjs/common'; @@ -40,6 +44,8 @@ const ENTITIES = [ MessageEntity, TemplateEntity, ViewEntity, + VerifyEntity, + SystemEntity, ]; const MODULES = [ @@ -52,6 +58,8 @@ const MODULES = [ MessageModule, TemplateModule, ViewModule, + VerifyModule, + SystemModule, ]; @Module({ diff --git a/packages/server/src/controllers/system.controller.ts b/packages/server/src/controllers/system.controller.ts new file mode 100644 index 00000000..49344c70 --- /dev/null +++ b/packages/server/src/controllers/system.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { SystemService } from '@services/system.service'; + +@Controller('system') +export class SystemController { + constructor(private readonly systemService: SystemService) {} +} diff --git a/packages/server/src/controllers/user.controller.ts b/packages/server/src/controllers/user.controller.ts index dde8b15a..c697e2be 100644 --- a/packages/server/src/controllers/user.controller.ts +++ b/packages/server/src/controllers/user.controller.ts @@ -1,4 +1,4 @@ -import { CreateUserDto } from '@dtos/create-user.dto'; +import { RegisterUserDto, ResetPasswordDto } from '@dtos/create-user.dto'; import { LoginUserDto } from '@dtos/login-user.dto'; import { UpdateUserDto } from '@dtos/update-user.dto'; import { JwtGuard } from '@guard/jwt.guard'; @@ -11,6 +11,7 @@ import { HttpStatus, Patch, Post, + Query, Request, Res, UseGuards, @@ -41,7 +42,7 @@ export class UserController { @UseInterceptors(ClassSerializerInterceptor) @Post(UserApiDefinition.register.server) @HttpCode(HttpStatus.CREATED) - async register(@Body() user: CreateUserDto) { + async register(@Body() user: RegisterUserDto) { return await this.userService.createUser(user); } @@ -62,6 +63,16 @@ export class UserController { return { ...data, token }; } + /** + * 重置密码 + */ + @UseInterceptors(ClassSerializerInterceptor) + @Post(UserApiDefinition.resetPassword.server) + @HttpCode(HttpStatus.OK) + async resetPassword(@Body() user: ResetPasswordDto) { + return await this.userService.resetPassword(user); + } + /** * 登出 */ @@ -88,4 +99,48 @@ export class UserController { async updateUser(@Request() req, @Body() dto: UpdateUserDto) { return await this.userService.updateUser(req.user, dto); } + + /** + * 获取系统配置 + */ + @UseInterceptors(ClassSerializerInterceptor) + @Get(UserApiDefinition.getSystemConfig.server) + @HttpCode(HttpStatus.OK) + @UseGuards(JwtGuard) + async getSystemConfig(@Request() req) { + return await this.userService.getSystemConfig(req.user); + } + + /** + * 发送测试邮件 + */ + @UseInterceptors(ClassSerializerInterceptor) + @Get(UserApiDefinition.sendTestEmail.server) + @HttpCode(HttpStatus.OK) + @UseGuards(JwtGuard) + async sendTestEmail(@Request() req) { + return await this.userService.sendTestEmail(req.user); + } + + /** + * 更新系统配置 + */ + @UseInterceptors(ClassSerializerInterceptor) + @Post(UserApiDefinition.updateSystemConfig.server) + @HttpCode(HttpStatus.OK) + @UseGuards(JwtGuard) + async toggleLockSystem(@Request() req, @Body() systemConfig) { + return await this.userService.updateSystemConfig(req.user, systemConfig); + } + + /** + * 锁定用户 + */ + @UseInterceptors(ClassSerializerInterceptor) + @Post(UserApiDefinition.toggleLockUser.server) + @HttpCode(HttpStatus.OK) + @UseGuards(JwtGuard) + async toggleLockUser(@Request() req, @Query('targetUserId') targetUserId) { + return await this.userService.toggleLockUser(req.user, targetUserId); + } } diff --git a/packages/server/src/controllers/verify.controller.ts b/packages/server/src/controllers/verify.controller.ts new file mode 100644 index 00000000..7d76b72e --- /dev/null +++ b/packages/server/src/controllers/verify.controller.ts @@ -0,0 +1,23 @@ +import { + ClassSerializerInterceptor, + Controller, + Get, + HttpCode, + HttpStatus, + Query, + UseInterceptors, +} from '@nestjs/common'; +import { VerifyService } from '@services/verify.service'; +import { UserApiDefinition } from '@think/domains'; + +@Controller('verify') +export class VerifyController { + constructor(private readonly verifyService: VerifyService) {} + + @UseInterceptors(ClassSerializerInterceptor) + @Get(UserApiDefinition.sendVerifyCode.server) + @HttpCode(HttpStatus.CREATED) + async sendVerifyCode(@Query('email') email) { + return await this.verifyService.sendVerifyCode(email); + } +} diff --git a/packages/server/src/dtos/create-user.dto.ts b/packages/server/src/dtos/create-user.dto.ts index 2b471c2a..d1f76593 100644 --- a/packages/server/src/dtos/create-user.dto.ts +++ b/packages/server/src/dtos/create-user.dto.ts @@ -1,27 +1,52 @@ -import { IsEmail, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; +import { IsEmail, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; -export class CreateUserDto { +/** + * 用户注册 + */ +export class RegisterUserDto { + @MaxLength(20, { message: '用户账号最多20个字符' }) + @MinLength(5, { message: '用户账号至少5个字符' }) @IsString({ message: '用户名称类型错误(正确类型为:String)' }) @IsNotEmpty({ message: '用户账号不能为空' }) - @MinLength(5, { message: '用户账号至少5个字符' }) - @MaxLength(20, { message: '用户账号最多20个字符' }) - readonly name: string; + name: string; + @MinLength(5, { message: '用户密码至少5个字符' }) @IsString({ message: '用户密码类型错误(正确类型为:String)' }) @IsNotEmpty({ message: '用户密码不能为空' }) - @MinLength(5, { message: '用户密码至少5个字符' }) password: string; - @IsString({ message: ' 用户确认密码类型错误(正确类型为:String)' }) - @MinLength(5, { message: '用户密码至少5个字符' }) - readonly confirmPassword: string; + @IsEmail({ message: '请输入正确的邮箱地址' }) + @IsString({ message: '用户邮箱类型错误(正确类型为:String)' }) + @IsNotEmpty({ message: '用户邮箱不能为空' }) + email: string; - @IsString({ message: '用户头像类型错误(正确类型为:String)' }) - @IsOptional() - readonly avatar?: string; - - @IsString({ message: ' 用户邮箱类型错误(正确类型为:String)' }) - @IsEmail() - @IsOptional() - readonly email?: string; + @MinLength(5, { message: '邮箱验证码至少5个字符' }) + @IsString({ message: '邮箱验证码错误(正确类型为:String)' }) + @IsNotEmpty({ message: '邮箱验证码不能为空' }) + verifyCode: string; +} + +/** + * 重置密码 + */ +export class ResetPasswordDto { + @MinLength(5, { message: '用户密码至少5个字符' }) + @IsString({ message: '用户密码类型错误(正确类型为:String)' }) + @IsNotEmpty({ message: '用户密码不能为空' }) + password: string; + + @MinLength(5, { message: '用户二次确认密码至少5个字符' }) + @IsString({ message: '用户二次确认密码类型错误(正确类型为:String)' }) + @IsNotEmpty({ message: '用户二次确认密码不能为空' }) + confirmPassword: string; + + @IsEmail({ message: '请输入正确的邮箱地址' }) + @IsString({ message: '用户邮箱类型错误(正确类型为:String)' }) + @IsNotEmpty({ message: '用户邮箱不能为空' }) + email: string; + + @MinLength(5, { message: '邮箱验证码至少5个字符' }) + @IsString({ message: '邮箱验证码错误(正确类型为:String)' }) + @IsNotEmpty({ message: '邮箱验证码不能为空' }) + verifyCode: string; } diff --git a/packages/server/src/dtos/login-user.dto.ts b/packages/server/src/dtos/login-user.dto.ts index 5366ff10..4d037dc4 100644 --- a/packages/server/src/dtos/login-user.dto.ts +++ b/packages/server/src/dtos/login-user.dto.ts @@ -1,4 +1,4 @@ -import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; +import { IsEmail, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; export class LoginUserDto { @IsString({ message: '用户名称类型错误(正确类型为:String)' }) diff --git a/packages/server/src/entities/document.entity.ts b/packages/server/src/entities/document.entity.ts index 7783e3a3..b1ee38a4 100644 --- a/packages/server/src/entities/document.entity.ts +++ b/packages/server/src/entities/document.entity.ts @@ -1,10 +1,16 @@ +import { getShortId } from '@helpers/shortid.herlper'; import { DocumentStatus } from '@think/domains'; import { Exclude } from 'class-transformer'; -import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import { BeforeInsert, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; @Entity('document') export class DocumentEntity { - @PrimaryGeneratedColumn('uuid') + @BeforeInsert() + getShortId() { + this.id = getShortId(); + } + + @PrimaryColumn() public id: string; @Column({ type: 'varchar', comment: '文档所属知识库 Id' }) diff --git a/packages/server/src/entities/system.entity.ts b/packages/server/src/entities/system.entity.ts new file mode 100644 index 00000000..ba9b89fc --- /dev/null +++ b/packages/server/src/entities/system.entity.ts @@ -0,0 +1,56 @@ +import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('system') +export class SystemEntity { + @PrimaryGeneratedColumn('uuid') + public id: string; + + /** + * 是否锁定系统,锁定后除系统管理员外均不可登录,同时禁止注册 + */ + + @Column({ type: 'boolean', default: false, comment: '是否锁定系统' }) + isSystemLocked: boolean; + + /** + * 邮箱服务地址 + */ + + @Column({ type: 'text', default: null }) + emailServiceHost: string; + + /** + * 邮箱服务端口 + */ + + @Column({ type: 'text', default: null }) + emailServicePort: string; + + /** + * 邮箱服务用户名 + */ + + @Column({ type: 'text', default: null }) + emailServiceUser: string; + + /** + * 邮箱服务授权码 + */ + + @Column({ type: 'text', default: null }) + emailServicePassword: string; + + @CreateDateColumn({ + type: 'timestamp', + name: 'createdAt', + comment: '创建时间', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp', + name: 'updatedAt', + comment: '更新时间', + }) + updatedAt: Date; +} diff --git a/packages/server/src/entities/template.entity.ts b/packages/server/src/entities/template.entity.ts index 77ee93e2..9ac379e0 100644 --- a/packages/server/src/entities/template.entity.ts +++ b/packages/server/src/entities/template.entity.ts @@ -1,8 +1,14 @@ -import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import { getShortId } from '@helpers/shortid.herlper'; +import { BeforeInsert, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; @Entity('template') export class TemplateEntity { - @PrimaryGeneratedColumn('uuid') + @BeforeInsert() + getShortId() { + this.id = getShortId(); + } + + @PrimaryColumn() public id: string; @Column({ type: 'boolean', default: false, comment: '是否公开' }) diff --git a/packages/server/src/entities/user.entity.ts b/packages/server/src/entities/user.entity.ts index 7f56f0f5..aecb8061 100644 --- a/packages/server/src/entities/user.entity.ts +++ b/packages/server/src/entities/user.entity.ts @@ -28,12 +28,15 @@ export class UserEntity { @Column({ type: 'varchar', length: 200, comment: '用户加密密码' }) public password: string; - @Column({ type: 'varchar', comment: '头像地址', default: '' }) + @Column({ type: 'varchar', length: 500, comment: '头像地址', default: '' }) public avatar: string; - @Column({ type: 'varchar', comment: '邮箱地址', default: '' }) + @Column({ type: 'varchar', comment: '邮箱地址' }) public email: string; + @Column({ type: 'boolean', default: false, comment: '是否为系统管理员' }) + isSystemAdmin: boolean; + @Column({ type: 'enum', enum: UserRole, diff --git a/packages/server/src/entities/verify.entity.ts b/packages/server/src/entities/verify.entity.ts new file mode 100644 index 00000000..9fafc61d --- /dev/null +++ b/packages/server/src/entities/verify.entity.ts @@ -0,0 +1,27 @@ +import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('verify') +export class VerifyEntity { + @PrimaryGeneratedColumn('uuid') + public id: string; + + @Column({ type: 'varchar', comment: '邮箱地址' }) + public email: string; + + @Column({ type: 'varchar', comment: '验证码' }) + public verifyCode: string; + + @CreateDateColumn({ + type: 'timestamp', + name: 'createdAt', + comment: '创建时间', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp', + name: 'updatedAt', + comment: '更新时间', + }) + updatedAt: Date; +} diff --git a/packages/server/src/entities/wiki.entity.ts b/packages/server/src/entities/wiki.entity.ts index c3861cb6..988b1b38 100644 --- a/packages/server/src/entities/wiki.entity.ts +++ b/packages/server/src/entities/wiki.entity.ts @@ -1,10 +1,16 @@ +import { getShortId } from '@helpers/shortid.herlper'; import { DEFAULT_WIKI_AVATAR } from '@think/constants'; import { WikiStatus } from '@think/domains'; -import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import { BeforeInsert, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; @Entity('wiki') export class WikiEntity { - @PrimaryGeneratedColumn('uuid') + @BeforeInsert() + getShortId() { + this.id = getShortId(); + } + + @PrimaryColumn() public id: string; @Column({ type: 'varchar', length: 200, comment: '知识库名称' }) diff --git a/packages/server/src/helpers/shortid.herlper.ts b/packages/server/src/helpers/shortid.herlper.ts new file mode 100644 index 00000000..043fc0e7 --- /dev/null +++ b/packages/server/src/helpers/shortid.herlper.ts @@ -0,0 +1,43 @@ +import { randomFillSync } from 'node:crypto'; + +const urlAlphabet = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'; + +// It is best to make fewer, larger requests to the crypto module to +// avoid system call overhead. So, random numbers are generated in a +// pool. The pool is a Buffer that is larger than the initial random +// request size by this multiplier. The pool is enlarged if subsequent +// requests exceed the maximum buffer size. +const POOL_SIZE_MULTIPLIER = 128; +let pool, poolOffset; + +const fillPool = (bytes) => { + if (!pool || pool.length < bytes) { + pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER); + randomFillSync(pool); + poolOffset = 0; + } else if (poolOffset + bytes > pool.length) { + randomFillSync(pool); + poolOffset = 0; + } + poolOffset += bytes; +}; + +const nanoid = (size = 21) => { + // `-=` convert `size` to number to prevent `valueOf` abusing + fillPool((size -= 0)); + let id = ''; + // We are reading directly from the random pool to avoid creating new array + for (let i = poolOffset - size; i < poolOffset; i++) { + // It is incorrect to use bytes exceeding the alphabet size. + // The following mask reduces the random byte in the 0-255 value + // range to the 0-63 value range. Therefore, adding hacks, such + // as empty string fallback or magic numbers, is unneccessary because + // the bitmask trims bytes down to the alphabet size. + id += urlAlphabet[pool[i] & 63]; + } + return id; +}; + +export const getShortId = () => { + return nanoid(12); +}; diff --git a/packages/server/src/modules/system.module.ts b/packages/server/src/modules/system.module.ts new file mode 100644 index 00000000..c4d4f9cb --- /dev/null +++ b/packages/server/src/modules/system.module.ts @@ -0,0 +1,13 @@ +import { SystemController } from '@controllers/system.controller'; +import { SystemEntity } from '@entities/system.entity'; +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SystemService } from '@services/system.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([SystemEntity])], + providers: [SystemService], + exports: [SystemService], + controllers: [SystemController], +}) +export class SystemModule {} diff --git a/packages/server/src/modules/user.module.ts b/packages/server/src/modules/user.module.ts index 6eaf195a..c311a3f1 100644 --- a/packages/server/src/modules/user.module.ts +++ b/packages/server/src/modules/user.module.ts @@ -13,6 +13,9 @@ import { getConfig } from '@think/config'; import { Request as RequestType } from 'express'; import { ExtractJwt, Strategy } from 'passport-jwt'; +import { SystemModule } from './system.module'; +import { VerifyModule } from './verify.module'; + const config = getConfig(); const jwtConfig = config.jwt as { secretkey: string; @@ -61,6 +64,8 @@ const jwtModule = JwtModule.register({ forwardRef(() => WikiModule), forwardRef(() => MessageModule), forwardRef(() => StarModule), + forwardRef(() => VerifyModule), + forwardRef(() => SystemModule), passModule, jwtModule, ], diff --git a/packages/server/src/modules/verify.module.ts b/packages/server/src/modules/verify.module.ts new file mode 100644 index 00000000..cc00c135 --- /dev/null +++ b/packages/server/src/modules/verify.module.ts @@ -0,0 +1,14 @@ +import { VerifyController } from '@controllers/verify.controller'; +import { VerifyEntity } from '@entities/verify.entity'; +import { SystemModule } from '@modules/system.module'; +import { forwardRef, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { VerifyService } from '@services/verify.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([VerifyEntity]), forwardRef(() => SystemModule)], + providers: [VerifyService], + exports: [VerifyService], + controllers: [VerifyController], +}) +export class VerifyModule {} diff --git a/packages/server/src/services/system.service.ts b/packages/server/src/services/system.service.ts new file mode 100644 index 00000000..7031d26e --- /dev/null +++ b/packages/server/src/services/system.service.ts @@ -0,0 +1,121 @@ +import { SystemEntity } from '@entities/system.entity'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { InjectRepository } from '@nestjs/typeorm'; +import * as nodemailer from 'nodemailer'; +import { Repository } from 'typeorm'; + +@Injectable() +export class SystemService { + constructor( + @InjectRepository(SystemEntity) + private readonly systemRepo: Repository, + + private readonly confifgService: ConfigService + ) { + this.loadFromConfigFile(); + } + + /** + * 从数据库获取配置 + * @returns + */ + public async getConfigFromDatabase() { + const data = await this.systemRepo.find(); + return (data && data[0]) || null; + } + + /** + * 更新系统配置 + * @param patch + * @returns + */ + public async updateConfigInDatabase(patch: Partial) { + const current = await this.getConfigFromDatabase(); + return await this.systemRepo.save(await this.systemRepo.merge(current, patch)); + } + + /** + * 从配置文件载入配置 + */ + private async loadFromConfigFile() { + const currentConfig = await this.getConfigFromDatabase(); + const emailConfigKeys = ['emailServiceHost', 'emailServicePort', 'emailServiceUser', 'emailServicePassword']; + + if (currentConfig && emailConfigKeys.every((configKey) => Boolean(currentConfig[configKey]))) { + return; + } + + // 同步邮件服务配置 + const emailConfigFromConfigFile = await this.confifgService.get('server.email'); + let emailConfig = {}; + if (emailConfigFromConfigFile && typeof emailConfigFromConfigFile === 'object') { + emailConfig = { + emailServiceHost: emailConfigFromConfigFile.host, + emailServicePort: emailConfigFromConfigFile.port, + emailServiceUser: emailConfigFromConfigFile.user, + emailServicePassword: emailConfigFromConfigFile.password, + }; + } + + const newConfig = currentConfig + ? await this.systemRepo.merge(currentConfig, emailConfig) + : await this.systemRepo.create(emailConfig); + await this.systemRepo.save(newConfig); + + console.log('[think] 已载入文件配置:', newConfig); + } + + /** + * 发送邮件 + * @param content + */ + public async sendEmail(mail: { to: string; subject: string; text?: string; html?: string }) { + const config = await this.getConfigFromDatabase(); + + if (!config) { + throw new HttpException('系统未配置邮箱服务,请联系系统管理员', HttpStatus.SERVICE_UNAVAILABLE); + } + + const emailConfig = { + host: config.emailServiceHost, + port: +config.emailServicePort, + user: config.emailServiceUser, + pass: config.emailServicePassword, + }; + + if (Object.keys(emailConfig).some((key) => !emailConfig[key])) { + throw new HttpException('系统邮箱服务配置不完善,请联系系统管理员', HttpStatus.SERVICE_UNAVAILABLE); + } + + const transporter = nodemailer.createTransport({ + host: emailConfig.host, + port: emailConfig.port, + secure: emailConfig.port === 465, + auth: { + user: emailConfig.user, + pass: emailConfig.pass, + }, + }); + + return new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error(`发送邮件失败`)); + }, 10 * 1000); + + transporter.sendMail( + { + from: emailConfig.user, + ...mail, + }, + (err, info) => { + if (err) { + reject(err); + } else { + resolve(info); + } + } + ); + }); + } +} diff --git a/packages/server/src/services/user.service.ts b/packages/server/src/services/user.service.ts index b96fbed1..d7099bd8 100644 --- a/packages/server/src/services/user.service.ts +++ b/packages/server/src/services/user.service.ts @@ -1,6 +1,7 @@ -import { CreateUserDto } from '@dtos/create-user.dto'; +import { RegisterUserDto, ResetPasswordDto } from '@dtos/create-user.dto'; import { LoginUserDto } from '@dtos/login-user.dto'; import { UpdateUserDto } from '@dtos/update-user.dto'; +import { SystemEntity } from '@entities/system.entity'; import { UserEntity } from '@entities/user.entity'; import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @@ -8,11 +9,14 @@ import { JwtService } from '@nestjs/jwt'; import { InjectRepository } from '@nestjs/typeorm'; import { MessageService } from '@services/message.service'; import { StarService } from '@services/star.service'; +import { VerifyService } from '@services/verify.service'; import { WikiService } from '@services/wiki.service'; import { UserStatus } from '@think/domains'; import { instanceToPlain } from 'class-transformer'; import { Repository } from 'typeorm'; +import { SystemService } from './system.service'; + export type OutUser = Omit; @Injectable() @@ -33,8 +37,47 @@ export class UserService { private readonly starService: StarService, @Inject(forwardRef(() => WikiService)) - private readonly wikiService: WikiService - ) {} + private readonly wikiService: WikiService, + + @Inject(forwardRef(() => VerifyService)) + private readonly verifyService: VerifyService, + + @Inject(forwardRef(() => SystemService)) + private readonly systemService: SystemService + ) { + this.createDefaultSystemAdminFromConfigFile(); + } + + /** + * 从配置文件创建默认系统管理员 + */ + private async createDefaultSystemAdminFromConfigFile() { + if (await this.userRepo.findOne({ isSystemAdmin: true })) { + return; + } + + const config = await this.confifgService.get('server.admin'); + + if (!config.name || !config.password || !config.email) { + throw new Error(`请指定名称、密码和邮箱`); + } + + if (await this.userRepo.findOne({ name: config.name })) { + return; + } + + try { + await this.userRepo.save( + await this.userRepo.create({ + ...config, + isSystemAdmin: true, + }) + ); + console.log('[think] 已创建默认系统管理员,请尽快登录系统修改密码'); + } catch (e) { + console.error(`[think] 创建默认系统管理员失败:`, e.message); + } + } /** * 根据 id 查询用户 @@ -71,13 +114,15 @@ export class UserService { * @param user CreateUserDto * @returns */ - async createUser(user: CreateUserDto): Promise { - if (await this.userRepo.findOne({ name: user.name })) { - throw new HttpException('该账户已被注册', HttpStatus.BAD_REQUEST); + async createUser(user: RegisterUserDto): Promise { + const currentSystemConfig = await this.systemService.getConfigFromDatabase(); + + if (currentSystemConfig.isSystemLocked) { + throw new HttpException('系统维护中,暂不可注册', HttpStatus.FORBIDDEN); } - if (user.password !== user.confirmPassword) { - throw new HttpException('两次密码不一致,请重试', HttpStatus.BAD_REQUEST); + if (await this.userRepo.findOne({ name: user.name })) { + throw new HttpException('该账户已被注册', HttpStatus.BAD_REQUEST); } if (await this.userRepo.findOne({ name: user.name })) { @@ -88,6 +133,10 @@ export class UserService { throw new HttpException('该邮箱已被注册', HttpStatus.BAD_REQUEST); } + if (!(await this.verifyService.checkVerifyCode(user.email, user.verifyCode))) { + throw new HttpException('验证码不正确,请检查', HttpStatus.BAD_REQUEST); + } + const res = await this.userRepo.create(user); const createdUser = await this.userRepo.save(res); const wiki = await this.wikiService.createWiki(createdUser, { @@ -105,14 +154,60 @@ export class UserService { return instanceToPlain(createdUser) as OutUser; } + /** + * 重置密码 + * @param registerUser + */ + public async resetPassword(resetPasswordDto: ResetPasswordDto) { + const currentSystemConfig = await this.systemService.getConfigFromDatabase(); + + if (currentSystemConfig.isSystemLocked) { + throw new HttpException('系统维护中,暂不可使用', HttpStatus.FORBIDDEN); + } + + const { email, password, confirmPassword, verifyCode } = resetPasswordDto; + + const inDatabaseUser = await this.userRepo.findOne({ email }); + + if (!inDatabaseUser) { + throw new HttpException('该邮箱尚未注册', HttpStatus.BAD_REQUEST); + } + + if (password !== confirmPassword) { + throw new HttpException('两次密码不一致,请重试', HttpStatus.BAD_REQUEST); + } + + if (!(await this.verifyService.checkVerifyCode(email, verifyCode))) { + throw new HttpException('验证码不正确,请检查', HttpStatus.BAD_REQUEST); + } + + const user = await this.userRepo.save( + await this.userRepo.merge(inDatabaseUser, { password: UserEntity.encryptPassword(password) }) + ); + + return instanceToPlain(user); + } + /** * 用户登录 * @param user * @returns */ async login(user: LoginUserDto): Promise<{ user: OutUser; token: string; domain: string; expiresIn: number }> { + const currentSystemConfig = await this.systemService.getConfigFromDatabase(); + const { name, password } = user; - const existUser = await this.userRepo.findOne({ where: { name } }); + let existUser = await this.userRepo.findOne({ where: { name } }); + + if (!existUser) { + existUser = await this.userRepo.findOne({ where: { email: name } }); + } + + const isExistUserSystemAdmin = existUser ? existUser.isSystemAdmin : false; + + if (currentSystemConfig.isSystemLocked && !isExistUserSystemAdmin) { + throw new HttpException('系统维护中,暂不可登录', HttpStatus.FORBIDDEN); + } if (!existUser || !(await UserEntity.comparePassword(password, existUser.password))) { throw new HttpException('用户名或密码错误', HttpStatus.BAD_REQUEST); @@ -168,4 +263,80 @@ export class UserService { const [data] = await query.getManyAndCount(); return data; } + + /** + * 锁定或解锁用户 + * @param user + * @param targetUserId + */ + async toggleLockUser(user: UserEntity, targetUserId) { + const currentUser = await this.userRepo.findOne(user.id); + + if (!currentUser.isSystemAdmin) { + throw new HttpException('您无权操作', HttpStatus.FORBIDDEN); + } + + const targetUser = await this.userRepo.findOne(targetUserId); + + if (!targetUser) { + throw new HttpException('目标用户不存在', HttpStatus.NOT_FOUND); + } + + const nextStatus = targetUser.status === UserStatus.normal ? UserStatus.locked : UserStatus.normal; + return await this.userRepo.save(await this.userRepo.merge(targetUser, { status: nextStatus })); + } + + /** + * 获取系统配置 + * @param user + * @returns + */ + async getSystemConfig(user: UserEntity) { + const currentUser = await this.userRepo.findOne(user.id); + + if (!currentUser.isSystemAdmin) { + throw new HttpException('您无权操作', HttpStatus.FORBIDDEN); + } + + return await this.systemService.getConfigFromDatabase(); + } + + /** + * 发送测试邮件 + * @param user + */ + async sendTestEmail(user: UserEntity) { + const currentUser = await this.userRepo.findOne(user.id); + + if (!currentUser.isSystemAdmin) { + throw new HttpException('您无权操作', HttpStatus.FORBIDDEN); + } + + const currentConfig = await this.systemService.getConfigFromDatabase(); + try { + await this.systemService.sendEmail({ + to: currentConfig.emailServiceUser, + subject: '测试邮件', + html: `

测试邮件

`, + }); + return '测试邮件发送成功'; + } catch (err) { + throw new HttpException('测试邮件发送失败!', HttpStatus.BAD_REQUEST); + } + } + + /** + * 更新系统配置 + * @param user + * @param targetUserId + */ + async updateSystemConfig(user: UserEntity, systemConfig: Partial) { + const currentUser = await this.userRepo.findOne(user.id); + + if (!currentUser.isSystemAdmin) { + throw new HttpException('您无权操作', HttpStatus.FORBIDDEN); + } + + return await this.systemService.updateConfigInDatabase(systemConfig); + } } diff --git a/packages/server/src/services/verify.service.ts b/packages/server/src/services/verify.service.ts new file mode 100644 index 00000000..bf1371c8 --- /dev/null +++ b/packages/server/src/services/verify.service.ts @@ -0,0 +1,70 @@ +import { VerifyEntity } from '@entities/verify.entity'; +import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { SystemService } from '@services/system.service'; +import { randomInt } from 'node:crypto'; +import { Repository } from 'typeorm'; +import { isEmail } from 'validator'; + +@Injectable() +export class VerifyService { + constructor( + @InjectRepository(VerifyEntity) + private readonly verifyRepo: Repository, + + @Inject(forwardRef(() => SystemService)) + private readonly systemService: SystemService + ) {} + + /** + * 删除验证记录 + * @param record + */ + private async deleteVerifyCode(id) { + const record = await this.verifyRepo.findOne(id); + await this.verifyRepo.remove(record); + } + + /** + * 向指定邮箱发送验证码 + * @param email + */ + public async sendVerifyCode(email: string) { + if (!email || !isEmail(email)) { + throw new HttpException('请检查邮箱地址', HttpStatus.BAD_REQUEST); + } + + const verifyCode = randomInt(1000000).toString().padStart(6, '0'); + const record = await this.verifyRepo.save(await this.verifyRepo.create({ email, verifyCode })); + await this.systemService.sendEmail({ + to: email, + subject: '验证码', + html: `

您的验证码为 ${verifyCode}

`, + }); + + const timer = setTimeout(() => { + this.deleteVerifyCode(record.id); + clearTimeout(timer); + }, 5 * 60 * 1000); + } + + /** + * 检验验证码 + * @param email + * @param verifyCode + * @returns + */ + public async checkVerifyCode(email: string, verifyCode: string) { + if (!email || !isEmail(email)) { + throw new HttpException('请检查邮箱地址', HttpStatus.BAD_REQUEST); + } + + const ret = await this.verifyRepo.findOne({ email, verifyCode }); + + if (!ret) { + throw new HttpException('验证码错误', HttpStatus.BAD_REQUEST); + } + + return Boolean(ret); + } +} diff --git a/packages/server/src/services/wiki.service.ts b/packages/server/src/services/wiki.service.ts index 5070c5f2..e5e943d4 100644 --- a/packages/server/src/services/wiki.service.ts +++ b/packages/server/src/services/wiki.service.ts @@ -326,6 +326,10 @@ export class WikiService { const withHomeDocumentIdWiki = await this.wikiRepo.merge(wiki, { homeDocumentId }); await this.wikiRepo.save(withHomeDocumentIdWiki); + await this.starService.toggleStar(user, { + wikiId: wiki.id, + }); + return withHomeDocumentIdWiki; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd5197b4..ea992e6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,6 +331,7 @@ importers: lodash: ^4.17.21 mysql2: ^2.3.3 nestjs-pino: ^2.5.2 + nodemailer: ^6.7.5 nuid: ^1.1.6 passport: ^0.5.2 passport-jwt: ^4.0.0 @@ -351,6 +352,7 @@ importers: typeorm: ^0.2.41 typescript: ^4.3.5 ua-parser-js: ^1.0.2 + validator: ^13.7.0 y-prosemirror: ^1.0.14 yjs: ^13.5.24 dependencies: @@ -385,6 +387,7 @@ importers: lodash: 4.17.21 mysql2: 2.3.3 nestjs-pino: 2.5.2_mhy7oan3ndcdblrodjh6yhl2fu + nodemailer: 6.7.5 nuid: 1.1.6 passport: 0.5.2 passport-jwt: 4.0.0 @@ -397,6 +400,7 @@ importers: rxjs: 7.5.2 typeorm: 0.2.41_ioredis@5.0.1+mysql2@2.3.3 ua-parser-js: 1.0.2 + validator: 13.7.0 y-prosemirror: 1.0.14_r7lszcnoz35zlwdkpf5veb6ziu yjs: 13.5.24 devDependencies: @@ -575,7 +579,7 @@ packages: '@babel/parser': 7.16.12 '@babel/template': 7.16.7 '@babel/traverse': 7.16.10 - '@babel/types': 7.17.12 + '@babel/types': 7.16.8 convert-source-map: 1.8.0 debug: 4.3.4 gensync: 1.0.0-beta.2 @@ -693,7 +697,7 @@ packages: resolution: {integrity: sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.12 + '@babel/types': 7.16.8 /@babel/helper-explode-assignable-expression/7.16.7: resolution: {integrity: sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ==} @@ -708,7 +712,7 @@ packages: dependencies: '@babel/helper-get-function-arity': 7.16.7 '@babel/template': 7.16.7 - '@babel/types': 7.17.12 + '@babel/types': 7.16.8 /@babel/helper-function-name/7.17.9: resolution: {integrity: sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==} @@ -722,13 +726,13 @@ packages: resolution: {integrity: sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.12 + '@babel/types': 7.16.8 /@babel/helper-hoist-variables/7.16.7: resolution: {integrity: sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.12 + '@babel/types': 7.16.8 /@babel/helper-member-expression-to-functions/7.17.7: resolution: {integrity: sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==} @@ -741,7 +745,7 @@ packages: resolution: {integrity: sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.12 + '@babel/types': 7.16.8 /@babel/helper-module-transforms/7.16.7: resolution: {integrity: sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==} @@ -838,7 +842,7 @@ packages: resolution: {integrity: sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.12 + '@babel/types': 7.16.8 /@babel/helper-validator-identifier/7.16.7: resolution: {integrity: sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==} @@ -1725,7 +1729,7 @@ packages: dependencies: '@babel/code-frame': 7.16.7 '@babel/parser': 7.16.12 - '@babel/types': 7.17.12 + '@babel/types': 7.16.8 /@babel/traverse/7.16.10: resolution: {integrity: sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw==} @@ -2313,7 +2317,6 @@ packages: uuid: 8.3.2 transitivePeerDependencies: - debug - dev: false /@nestjs/config/1.1.6_b524vv6dxrk4ofgusl7jksm334: resolution: {integrity: sha512-HYizKt6Dr6gcZl8FmZbTfQxP0MG8oXMh+gVFT0XCwYDAq26BOKyhPsIxrKsryicVeKViRgetCUhlJY9EqaekZA==} @@ -2362,7 +2365,6 @@ packages: uuid: 8.3.2 transitivePeerDependencies: - encoding - dev: false /@nestjs/jwt/8.0.0_@nestjs+common@8.2.6: resolution: {integrity: sha512-fz2LQgYY2zmuD8S+8UE215anwKyXlnB/1FwJQLVR47clNfMeFMK8WCxmn6xdPhF5JKuV1crO6FVabb1qWzDxqQ==} @@ -2399,7 +2401,6 @@ packages: tslib: 2.3.1 transitivePeerDependencies: - supports-color - dev: false /@nestjs/schedule/2.0.1_3bw5czexdkmnohgaaospg37ray: resolution: {integrity: sha512-NqiCk3P7HDMw55kpefNIzAAQEsP+6dDIXUt4/KQANtAZ+opdLzo8rkzI0j8vDqgYeTh+PKq+V6zwSRjR61xPAQ==} @@ -2613,7 +2614,6 @@ packages: node-fetch: 2.6.7 transitivePeerDependencies: - encoding - dev: false /@popperjs/core/2.11.2: resolution: {integrity: sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==} @@ -3707,7 +3707,6 @@ packages: dependencies: mime-types: 2.1.34 negotiator: 0.6.2 - dev: false /acorn-globals/6.0.0: resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} @@ -3944,7 +3943,6 @@ packages: /append-field/1.0.0: resolution: {integrity: sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=} - dev: false /arg/4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -3971,7 +3969,6 @@ packages: /array-flatten/1.1.1: resolution: {integrity: sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=} - dev: false /array-includes/3.1.4: resolution: {integrity: sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==} @@ -4100,7 +4097,6 @@ packages: follow-redirects: 1.14.7 transitivePeerDependencies: - debug - dev: false /axios/0.25.0: resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==} @@ -4299,7 +4295,6 @@ packages: type-is: 1.6.18 transitivePeerDependencies: - supports-color - dev: false /body-parser/1.20.0: resolution: {integrity: sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==} @@ -4439,7 +4434,6 @@ packages: dependencies: dicer: 0.2.5 readable-stream: 1.1.14 - dev: false /bytes/3.0.0: resolution: {integrity: sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=} @@ -4449,7 +4443,6 @@ packages: /bytes/3.1.1: resolution: {integrity: sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==} engines: {node: '>= 0.8'} - dev: false /bytes/3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} @@ -4562,14 +4555,12 @@ packages: /class-transformer/0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} - dev: false /class-validator/0.13.2: resolution: {integrity: sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==} dependencies: libphonenumber-js: 1.9.46 validator: 13.7.0 - dev: false /classnames/2.3.1: resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==} @@ -4793,7 +4784,6 @@ packages: inherits: 2.0.4 readable-stream: 2.3.7 typedarray: 0.0.6 - dev: false /concurrently/7.0.0: resolution: {integrity: sha512-WKM7PUsI8wyXpF80H+zjHP32fsgsHNQfPLw/e70Z5dYkV7hF+rf8q3D+ScWJIEr57CpkO3OWBko6hwhQLPR8Pw==} @@ -4829,19 +4819,16 @@ packages: /consola/2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} - dev: false /content-disposition/0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} dependencies: safe-buffer: 5.2.1 - dev: false /content-type/1.0.4: resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} engines: {node: '>= 0.6'} - dev: false /convert-source-map/1.8.0: resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==} @@ -4858,12 +4845,10 @@ packages: /cookie-signature/1.0.6: resolution: {integrity: sha1-4wOogrNCzD7oylE6eZmXNNqzriw=} - dev: false /cookie/0.4.1: resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} engines: {node: '>= 0.6'} - dev: false /cookiejar/2.1.3: resolution: {integrity: sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==} @@ -4910,7 +4895,6 @@ packages: /core-util-is/1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: false /cors/2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} @@ -4918,7 +4902,6 @@ packages: dependencies: object-assign: 4.1.1 vary: 1.1.2 - dev: false /cos-nodejs-sdk-v5/2.11.9: resolution: {integrity: sha512-szsUw/8hx1RWUfMNwgErzYcdPM3EwcmgbylqQf82HPZALMCAcaa7qCeAxVQHNvCumWYeQLy7EEloZjMUyjg7Ug==} @@ -5238,7 +5221,6 @@ packages: /depd/1.1.2: resolution: {integrity: sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=} engines: {node: '>= 0.6'} - dev: false /depd/2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} @@ -5247,7 +5229,6 @@ packages: /destroy/1.0.4: resolution: {integrity: sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=} - dev: false /destroy/1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} @@ -5276,7 +5257,6 @@ packages: dependencies: readable-stream: 1.1.14 streamsearch: 0.1.2 - dev: false /diff-sequences/27.4.0: resolution: {integrity: sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww==} @@ -5390,7 +5370,6 @@ packages: /ee-first/1.1.1: resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=} - dev: false /ejs/3.1.8: resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} @@ -5427,7 +5406,6 @@ packages: /encodeurl/1.0.2: resolution: {integrity: sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=} engines: {node: '>= 0.8'} - dev: false /end-of-stream/1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -5549,7 +5527,6 @@ packages: /escape-html/1.0.3: resolution: {integrity: sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=} - dev: false /escape-string-regexp/1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} @@ -5867,7 +5844,6 @@ packages: /etag/1.8.1: resolution: {integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=} engines: {node: '>= 0.6'} - dev: false /events/3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} @@ -5971,7 +5947,6 @@ packages: vary: 1.1.2 transitivePeerDependencies: - supports-color - dev: false /extend-shallow/2.0.1: resolution: {integrity: sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=} @@ -6116,7 +6091,6 @@ packages: unpipe: 1.0.0 transitivePeerDependencies: - supports-color - dev: false /find-cache-dir/3.3.2: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} @@ -6168,7 +6142,6 @@ packages: peerDependenciesMeta: debug: optional: true - dev: false /foreach/2.0.5: resolution: {integrity: sha1-C+4AUBiusmDQo6865ljdATbsG5k=} @@ -6262,12 +6235,10 @@ packages: /forwarded/0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - dev: false /fresh/0.5.2: resolution: {integrity: sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=} engines: {node: '>= 0.6'} - dev: false /fs-extra/10.0.0: resolution: {integrity: sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==} @@ -6601,7 +6572,6 @@ packages: setprototypeof: 1.2.0 statuses: 1.5.0 toidentifier: 1.0.1 - dev: false /http-errors/2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} @@ -6832,7 +6802,6 @@ packages: /ipaddr.js/1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - dev: false /is-arguments/1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} @@ -7077,11 +7046,9 @@ packages: /isarray/0.0.1: resolution: {integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=} - dev: false /isarray/1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - dev: false /isarray/2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -7147,7 +7114,6 @@ packages: /iterare/1.2.1: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} - dev: false /jake/10.8.5: resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} @@ -7930,7 +7896,6 @@ packages: /libphonenumber-js/1.9.46: resolution: {integrity: sha512-QqTX4UVsGy24njtCgLRspiKpxfRniRBZE/P+d0vQXuYWQ+hwDS6X0ouo0O/SRyf7bhhMCE71b6vAvLMtY5PfEw==} - dev: false /lie/3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} @@ -8262,7 +8227,6 @@ packages: /media-typer/0.3.0: resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=} engines: {node: '>= 0.6'} - dev: false /memfs/3.4.1: resolution: {integrity: sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==} @@ -8299,7 +8263,6 @@ packages: /merge-descriptors/1.0.1: resolution: {integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=} - dev: false /merge-refs/1.0.0: resolution: {integrity: sha512-WZ4S5wqD9FCR9hxkLgvcHJCBxzXzy3VVE6p8W2OzxRzB+hLRlcadGE2bW9xp2KSzk10rvp4y+pwwKO6JQVguMg==} @@ -8341,7 +8304,6 @@ packages: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} hasBin: true - dev: false /mime/2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} @@ -8399,7 +8361,6 @@ packages: hasBin: true dependencies: minimist: 1.2.6 - dev: false /mkdirp/1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} @@ -8433,7 +8394,6 @@ packages: on-finished: 2.3.0 type-is: 1.6.18 xtend: 4.0.2 - dev: false /mute-stream/0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -8497,7 +8457,6 @@ packages: /negotiator/0.6.2: resolution: {integrity: sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==} engines: {node: '>= 0.6'} - dev: false /neo-async/2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -8599,7 +8558,6 @@ packages: optional: true dependencies: whatwg-url: 5.0.0 - dev: false /node-int64/0.4.0: resolution: {integrity: sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=} @@ -8612,6 +8570,11 @@ packages: resolution: {integrity: sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==} dev: false + /nodemailer/6.7.5: + resolution: {integrity: sha512-6VtMpwhsrixq1HDYSBBHvW0GwiWawE75dS3oal48VqRhUvKJNnKnJo2RI/bCVQubj1vgrgscMNW4DHaD6xtMCg==} + engines: {node: '>=6.0.0'} + dev: false + /normalize-package-data/2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -8666,7 +8629,6 @@ packages: /object-hash/2.2.0: resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} engines: {node: '>= 6'} - dev: false /object-inspect/1.12.0: resolution: {integrity: sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==} @@ -8739,7 +8701,6 @@ packages: engines: {node: '>= 0.8'} dependencies: ee-first: 1.1.1 - dev: false /on-finished/2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -8957,7 +8918,6 @@ packages: /parseurl/1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - dev: false /passport-jwt/4.0.0: resolution: {integrity: sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==} @@ -9004,11 +8964,9 @@ packages: /path-to-regexp/0.1.7: resolution: {integrity: sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=} - dev: false /path-to-regexp/3.2.0: resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==} - dev: false /path-type/4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -9301,7 +9259,6 @@ packages: /process-nextick-args/2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - dev: false /process-warning/1.0.0: resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} @@ -9444,7 +9401,6 @@ packages: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 - dev: false /proxy-agent/5.0.0: resolution: {integrity: sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g==} @@ -9506,7 +9462,6 @@ packages: /qs/6.9.6: resolution: {integrity: sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==} engines: {node: '>=0.6'} - dev: false /queue-microtask/1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -9528,7 +9483,6 @@ packages: /range-parser/1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - dev: false /raw-body/2.4.2: resolution: {integrity: sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==} @@ -9538,7 +9492,6 @@ packages: http-errors: 1.8.1 iconv-lite: 0.4.24 unpipe: 1.0.0 - dev: false /raw-body/2.5.1: resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} @@ -9775,7 +9728,6 @@ packages: inherits: 2.0.4 isarray: 0.0.1 string_decoder: 0.10.31 - dev: false /readable-stream/2.3.7: resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} @@ -9787,7 +9739,6 @@ packages: safe-buffer: 5.1.2 string_decoder: 1.1.1 util-deprecate: 1.0.2 - dev: false /readable-stream/3.6.0: resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} @@ -9837,7 +9788,6 @@ packages: /reflect-metadata/0.1.13: resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} - dev: false /regenerate-unicode-properties/10.0.1: resolution: {integrity: sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==} @@ -10218,7 +10168,6 @@ packages: statuses: 1.5.0 transitivePeerDependencies: - supports-color - dev: false /seq-queue/0.0.5: resolution: {integrity: sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=} @@ -10245,7 +10194,6 @@ packages: send: 0.17.2 transitivePeerDependencies: - supports-color - dev: false /setimmediate/1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} @@ -10253,7 +10201,6 @@ packages: /setprototypeof/1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - dev: false /sha.js/2.4.11: resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} @@ -10482,7 +10429,6 @@ packages: /statuses/1.5.0: resolution: {integrity: sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=} engines: {node: '>= 0.6'} - dev: false /statuses/2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} @@ -10511,7 +10457,6 @@ packages: /streamsearch/0.1.2: resolution: {integrity: sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=} engines: {node: '>=0.8.0'} - dev: false /string-argv/0.3.1: resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} @@ -10569,13 +10514,11 @@ packages: /string_decoder/0.10.31: resolution: {integrity: sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=} - dev: false /string_decoder/1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} dependencies: safe-buffer: 5.1.2 - dev: false /string_decoder/1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -11098,7 +11041,6 @@ packages: /toidentifier/1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - dev: false /tough-cookie/2.5.0: resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} @@ -11119,7 +11061,6 @@ packages: /tr46/0.0.3: resolution: {integrity: sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=} - dev: false /tr46/1.0.1: resolution: {integrity: sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=} @@ -11327,7 +11268,6 @@ packages: dependencies: media-typer: 0.3.0 mime-types: 2.1.34 - dev: false /typedarray-to-buffer/3.1.5: resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} @@ -11337,7 +11277,6 @@ packages: /typedarray/0.0.6: resolution: {integrity: sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=} - dev: false /typeorm/0.2.41_ioredis@5.0.1+mysql2@2.3.3: resolution: {integrity: sha512-/d8CLJJxKPgsnrZWiMyPI0rz2MFZnBQrnQ5XP3Vu3mswv2WPexb58QM6BEtmRmlTMYN5KFWUz8SKluze+wS9xw==} @@ -11495,7 +11434,6 @@ packages: /unpipe/1.0.0: resolution: {integrity: sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=} engines: {node: '>= 0.8'} - dev: false /upath/1.2.0: resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} @@ -11568,7 +11506,6 @@ packages: /utils-merge/1.0.1: resolution: {integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=} engines: {node: '>= 0.4.0'} - dev: false /uuid/3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} @@ -11579,7 +11516,6 @@ packages: /uuid/8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true - dev: false /v8-compile-cache/2.3.0: resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} @@ -11604,12 +11540,10 @@ packages: /validator/13.7.0: resolution: {integrity: sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==} engines: {node: '>= 0.10'} - dev: false /vary/1.1.2: resolution: {integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=} engines: {node: '>= 0.8'} - dev: false /verror/1.10.0: resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=} @@ -11669,7 +11603,6 @@ packages: /webidl-conversions/3.0.1: resolution: {integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=} - dev: false /webidl-conversions/4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -11757,7 +11690,6 @@ packages: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - dev: false /whatwg-url/7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -12102,7 +12034,6 @@ packages: /xtend/4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - dev: false /y-prosemirror/1.0.14_r7lszcnoz35zlwdkpf5veb6ziu: resolution: {integrity: sha512-fJQn/XT+z/gks9sd64eB+Mf1UQP0d5SHQ8RwbrzIwYFW+FgU/nx8/hJm+nrBAoO9azHOY5aDeoffeLmOFXLD9w==}