mirror of https://github.com/fantasticit/think.git
merge main
This commit is contained in:
commit
c6e969097e
|
@ -21,3 +21,4 @@ tsconfig.tsbuildinfo
|
|||
scripts/update.sh
|
||||
|
||||
output
|
||||
runtime
|
||||
|
|
13
Dockerfile
13
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/*
|
||||
|
|
169
README.md
169
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 是一款开源知识管理工具。通过独立的知识库空间,结
|
|||
|
||||
欢迎进群交流。
|
||||
|
||||
<img width="313" alt="image" src="https://user-images.githubusercontent.com/26452939/174938220-5b7301fd-f207-4ff4-a3af-d6b2ab489727.png">
|
||||
|
||||
<img width="300" alt="image" src="https://user-images.githubusercontent.com/26452939/176181151-04b3be2e-86e6-4f9e-81f7-e03d9948294c.PNG">
|
||||
|
||||
## 预览
|
||||
|
||||
![知识库](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)
|
||||
<details>
|
||||
<summary>查看预览图</summary>
|
||||
<img alt="知识库" src="http://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-20/YN67GM4VQMBTZFZ88TYP8X/image.png" width="420" />
|
||||
<img alt="新建文档" src="http://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-20/YN67GM4VQMBTZFZ88TYPQX/image.png" width="420" />
|
||||
<img alt="编辑器" src="http://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-20/YN67GM4VQMBTZFZ88TYPZX/image.png" width="420" />
|
||||
</details>
|
||||
|
||||
## 项目结构
|
||||
## 项目开发
|
||||
|
||||
本项目依赖 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)评论区留言。
|
||||
|
||||
<div style="display: flex;">
|
||||
<img width="300" alt="alipay" src="https://think-1256095494.cos.ap-shanghai.myqcloud.com/think-alipay.jpg" />
|
||||
<img width="300" alt="wechat" src="https://think-1256095494.cos.ap-shanghai.myqcloud.com/think-wechat.jpg" />
|
||||
</div>
|
||||
|
||||
## 贡献者
|
||||
|
||||
## 资料
|
||||
感谢所有为本项目作出贡献的同学!
|
||||
|
||||
- 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/
|
||||
<a href="https://github.com/fantasticit/think/contributors"><img src="https://opencollective.com/think/contributors.svg?width=890" /></a>
|
||||
|
|
|
@ -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} 打包完成"
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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:
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
```
|
|
@ -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/;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IProps> = ({ tab, onNavigate }) => {
|
||||
return (
|
||||
<Tabs lazyRender type="line" activeKey={tab} onChange={onNavigate}>
|
||||
<TabPane tab={TitleMap['base']} itemKey="base">
|
||||
<System />
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab={TitleMap['mail']} itemKey="mail">
|
||||
<Mail />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={error}
|
||||
normalContent={() => (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Banner
|
||||
type="warning"
|
||||
description="配置邮箱服务后,请测试是否正确,否则可能导致无法注册用户,找回密码!"
|
||||
closeIcon={null}
|
||||
/>
|
||||
|
||||
<Form initValues={data} onChange={onFormChange} onSubmit={onFinish}>
|
||||
<Form.Input
|
||||
field="emailServiceHost"
|
||||
label="邮件服务地址"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="输入邮件服务地址"
|
||||
rules={[{ required: true, message: '请输入邮件服务地址' }]}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field="emailServicePort"
|
||||
label="邮件服务端口"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="输入邮件服务端口"
|
||||
rules={[{ required: true, message: '请输入邮件服务端口' }]}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field="emailServicePassword"
|
||||
label="邮件服务密码"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="输入邮件服务密码"
|
||||
rules={[{ required: true, message: '请输入邮件服务密码' }]}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field="emailServiceUser"
|
||||
label="邮件服务用户"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="输入邮件服务密码"
|
||||
rules={[{ required: true, message: '请输入邮件服务密码' }]}
|
||||
/>
|
||||
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
theme="solid"
|
||||
disabled={!changed}
|
||||
loading={loading}
|
||||
style={{ margin: '16px 0' }}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
|
||||
<Button style={{ margin: '16px' }} onClick={sendTestEmail}>
|
||||
测试
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={error}
|
||||
normalContent={() => (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Banner type="warning" description="系统锁定后,除系统管理员外均不可登录,谨慎修改!" closeIcon={null} />
|
||||
<Form labelPosition="left" initValues={data} onChange={onFormChange} onSubmit={onFinish}>
|
||||
<Form.Switch field="isSystemLocked" label="系统锁定" />
|
||||
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
theme="solid"
|
||||
disabled={!changed}
|
||||
loading={loading}
|
||||
style={{ margin: '16px 0' }}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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<IProps> = ({ wikiId, documentId, disabled = false }) => {
|
||||
const { isMobile } = IsOnMobile.useHook();
|
||||
const ref = useRef<HTMLInputElement>();
|
||||
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<IProps> = ({ 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<IProps> = ({ wikiId, documentId, di
|
|||
event.on(JOIN_USER, handler);
|
||||
|
||||
return () => {
|
||||
toastedUsersRef.current = [];
|
||||
event.off(JOIN_USER, handler);
|
||||
};
|
||||
}, [currentUser]);
|
||||
|
|
|
@ -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 <Button icon={<IconSpin />} theme="borderless" type="tertiary" />;
|
||||
|
||||
if (error || !user) {
|
||||
|
@ -32,6 +37,11 @@ export const User: React.FC = () => {
|
|||
<Dropdown.Item onClick={() => toggleVisible(true)}>
|
||||
<Text>账户设置</Text>
|
||||
</Dropdown.Item>
|
||||
{user.isSystemAdmin ? (
|
||||
<Dropdown.Item onClick={toAdmin}>
|
||||
<Text>管理后台</Text>
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item onClick={logout}>
|
||||
<Text>退出登录</Text>
|
||||
|
|
|
@ -65,7 +65,13 @@ export const UserSetting: React.FC<IProps> = ({ visible, toggleVisible }) => {
|
|||
disabled
|
||||
placeholder="请输入账户名称"
|
||||
></Form.Input>
|
||||
<Form.Input label="邮箱" field="email" style={{ width: '100%' }} placeholder="请输入账户邮箱"></Form.Input>
|
||||
<Form.Input
|
||||
disabled
|
||||
label="邮箱"
|
||||
field="email"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请输入账户邮箱"
|
||||
></Form.Input>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -42,7 +42,9 @@ export const WikiCard: React.FC<{ wiki: IWikiWithIsMember; shareMode?: boolean }
|
|||
</header>
|
||||
<main>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>{wiki.name}</Text>
|
||||
<Paragraph ellipsis={{ rows: 1 }} strong>
|
||||
{wiki.name}
|
||||
</Paragraph>
|
||||
<Paragraph ellipsis={{ rows: 1 }}>{wiki.description}</Paragraph>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -7,7 +7,7 @@ import Link from 'next/link';
|
|||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
export const WikiPinCard: React.FC<{ wiki: IWiki }> = ({ wiki }) => {
|
||||
return (
|
||||
|
@ -35,7 +35,9 @@ export const WikiPinCard: React.FC<{ wiki: IWiki }> = ({ wiki }) => {
|
|||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<Text strong>{wiki.name}</Text>
|
||||
<Paragraph ellipsis={{ rows: 1 }} strong>
|
||||
{wiki.name}
|
||||
</Paragraph>
|
||||
</main>
|
||||
<footer>
|
||||
<Text type="tertiary" size="small">
|
||||
|
|
|
@ -9,7 +9,7 @@ import { useWikiDetail, useWikiTocs } from 'data/wiki';
|
|||
import { triggerCreateDocument } from 'event';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
import { Tree } from './tree';
|
||||
|
@ -29,7 +29,7 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
docAsLink = '/wiki/[wikiId]/document/[documentId]',
|
||||
getDocLink = (documentId) => `/wiki/${wikiId}/document/${documentId}`,
|
||||
}) => {
|
||||
const { pathname } = useRouter();
|
||||
const { pathname, query } = useRouter();
|
||||
const { data: wiki, loading: wikiLoading, error: wikiError } = useWikiDetail(wikiId);
|
||||
const { data: tocs, loading: tocsLoading, error: tocsError } = useWikiTocs(wikiId);
|
||||
const { data: starWikis } = useStarWikis();
|
||||
|
@ -39,6 +39,7 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
error: starDocumentsError,
|
||||
} = useWikiStarDocuments(wikiId);
|
||||
const [parentIds, setParentIds] = useState<Array<string>>([]);
|
||||
const otherStarWikis = useMemo(() => (starWikis || []).filter((wiki) => wiki.id !== wikiId), [starWikis, wikiId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tocs || !tocs.length) return;
|
||||
|
@ -73,15 +74,14 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
</div>
|
||||
}
|
||||
error={wikiError}
|
||||
normalContent={() => (
|
||||
<Dropdown
|
||||
trigger={'click'}
|
||||
position="bottomRight"
|
||||
render={
|
||||
<Dropdown.Menu style={{ width: 180 }}>
|
||||
{(starWikis || [])
|
||||
.filter((wiki) => wiki.id !== wikiId)
|
||||
.map((wiki) => {
|
||||
normalContent={() =>
|
||||
otherStarWikis.length ? (
|
||||
<Dropdown
|
||||
trigger={'click'}
|
||||
position="bottomRight"
|
||||
render={
|
||||
<Dropdown.Menu style={{ width: 180 }}>
|
||||
{otherStarWikis.map((wiki) => {
|
||||
return (
|
||||
<Dropdown.Item key={wiki.id}>
|
||||
<Link
|
||||
|
@ -93,35 +93,55 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
<a
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<Avatar
|
||||
shape="square"
|
||||
size="small"
|
||||
src={wiki.avatar}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{wiki.name.charAt(0)}
|
||||
</Avatar>
|
||||
<Text strong ellipsis={{ rows: 1 }}>
|
||||
{wiki.name}
|
||||
</Text>
|
||||
</span>
|
||||
<Avatar
|
||||
shape="square"
|
||||
size="small"
|
||||
src={wiki.avatar}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{wiki.name.charAt(0)}
|
||||
</Avatar>
|
||||
<Text strong style={{ width: 120 }} ellipsis={{ showTooltip: true }}>
|
||||
{wiki.name}
|
||||
</Text>
|
||||
</a>
|
||||
</Link>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
<div className={styles.titleWrap}>
|
||||
<span>
|
||||
<Avatar
|
||||
shape="square"
|
||||
size="small"
|
||||
src={wiki.avatar}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{wiki.name.charAt(0)}
|
||||
</Avatar>
|
||||
<Text strong>{wiki.name}</Text>
|
||||
</span>
|
||||
<IconSmallTriangleDown />
|
||||
</div>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<div className={styles.titleWrap}>
|
||||
<span>
|
||||
<Avatar
|
||||
|
@ -139,10 +159,9 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
</Avatar>
|
||||
<Text strong>{wiki.name}</Text>
|
||||
</span>
|
||||
<IconSmallTriangleDown />
|
||||
</div>
|
||||
</Dropdown>
|
||||
)}
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<DataRender
|
||||
|
@ -170,7 +189,12 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
}
|
||||
error={wikiError}
|
||||
normalContent={() => (
|
||||
<div className={cls(styles.linkWrap, pathname === '/wiki/[wikiId]' && styles.isActive)}>
|
||||
<div
|
||||
className={cls(
|
||||
styles.linkWrap,
|
||||
(pathname === '/wiki/[wikiId]' || query.documentId === wiki.homeDocumentId) && styles.isActive
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
href={{
|
||||
pathname: `/wiki/[wikiId]`,
|
||||
|
@ -179,7 +203,7 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
>
|
||||
<a>
|
||||
<IconOverview style={{ fontSize: '1em' }} />
|
||||
<span>主页</span>
|
||||
<Text>主页</Text>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -220,7 +244,7 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
>
|
||||
<a>
|
||||
<IconSetting style={{ fontSize: '1em' }} />
|
||||
<span>设置</span>
|
||||
<Text>设置</Text>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { ILoginUser, IUser, UserApiDefinition } from '@think/domains';
|
||||
import { Toast } from '@douyinfe/semi-ui';
|
||||
import { ILoginUser, ISystemConfig, IUser, UserApiDefinition } from '@think/domains';
|
||||
import { getStorage, setStorage } from 'helpers/storage';
|
||||
import { useAsyncLoading } from 'hooks/use-async-loading';
|
||||
import Router, { useRouter } from 'next/router';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
@ -16,6 +18,63 @@ export const toLogin = () => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
* @returns
|
||||
*/
|
||||
export const useVerifyCode = () => {
|
||||
const [sendVerifyCode, loading] = useAsyncLoading((params: { email: string }) =>
|
||||
HttpClient.request({
|
||||
method: UserApiDefinition.sendVerifyCode.method,
|
||||
url: UserApiDefinition.sendVerifyCode.client(),
|
||||
params,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
sendVerifyCode,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册
|
||||
* @returns
|
||||
*/
|
||||
export const useRegister = () => {
|
||||
const [registerWithLoading, loading] = useAsyncLoading((data) =>
|
||||
HttpClient.request({
|
||||
method: UserApiDefinition.register.method,
|
||||
url: UserApiDefinition.register.client(),
|
||||
data,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
register: registerWithLoading,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
* @returns
|
||||
*/
|
||||
export const useResetPassword = () => {
|
||||
const [resetPasswordWithLoading, loading] = useAsyncLoading((data) =>
|
||||
HttpClient.request({
|
||||
method: UserApiDefinition.resetPassword.method,
|
||||
url: UserApiDefinition.resetPassword.client(),
|
||||
data,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
reset: resetPasswordWithLoading,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
export const useUser = () => {
|
||||
const router = useRouter();
|
||||
const { data, error, refetch } = useQuery<ILoginUser>('user', () => {
|
||||
|
@ -78,3 +137,40 @@ export const useUser = () => {
|
|||
updateUser,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
* @returns
|
||||
*/
|
||||
export const useSystemConfig = () => {
|
||||
const { data, error, isLoading, refetch } = useQuery(UserApiDefinition.getSystemConfig.client(), () =>
|
||||
HttpClient.request<ISystemConfig>({
|
||||
method: UserApiDefinition.getSystemConfig.method,
|
||||
url: UserApiDefinition.getSystemConfig.client(),
|
||||
})
|
||||
);
|
||||
|
||||
const sendTestEmail = useCallback(async () => {
|
||||
return await HttpClient.request<ISystemConfig>({
|
||||
method: UserApiDefinition.sendTestEmail.method,
|
||||
url: UserApiDefinition.sendTestEmail.client(),
|
||||
}).then(() => {
|
||||
Toast.success('测试邮件发送成功');
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateSystemConfig = useCallback(
|
||||
async (data: Partial<ISystemConfig>) => {
|
||||
const ret = await HttpClient.request<ISystemConfig>({
|
||||
method: UserApiDefinition.updateSystemConfig.method,
|
||||
url: UserApiDefinition.updateSystemConfig.client(),
|
||||
data,
|
||||
});
|
||||
refetch();
|
||||
return ret;
|
||||
},
|
||||
[refetch]
|
||||
);
|
||||
|
||||
return { data, error, loading: isLoading, refresh: refetch, sendTestEmail, updateSystemConfig };
|
||||
};
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export const isEmail = (email) => {
|
||||
return !!String(email)
|
||||
.toLowerCase()
|
||||
.match(
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
);
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useToggle } from './use-toggle';
|
||||
import { useIsomorphicLayoutEffect } from './user-isomorphic-layout-effect';
|
||||
|
||||
export const useInterval = (callback: () => void, delay: number) => {
|
||||
const savedCallback = useRef(callback);
|
||||
const timer = useRef(null);
|
||||
const [canActive, toggleCanActive] = useToggle(false);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canActive) return;
|
||||
|
||||
clearInterval(timer.current);
|
||||
timer.current = setInterval(() => savedCallback.current(), delay);
|
||||
|
||||
return () => clearInterval(timer.current);
|
||||
}, [canActive, delay]);
|
||||
|
||||
const start = useCallback(() => {
|
||||
toggleCanActive(true);
|
||||
}, [toggleCanActive]);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
clearInterval(timer.current);
|
||||
toggleCanActive(false);
|
||||
}, [toggleCanActive]);
|
||||
|
||||
return { start, stop };
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
import { useEffect, useLayoutEffect } from 'react';
|
||||
|
||||
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
@ -0,0 +1,225 @@
|
|||
import React from 'react';
|
||||
|
||||
export const Forbidden = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
data-name="Layer 1"
|
||||
width="20em"
|
||||
height="15em"
|
||||
viewBox="0 0 807.45276 499.98424"
|
||||
>
|
||||
<path
|
||||
id="ad903c08-5677-4dbe-a9c7-05a0eb46801f-129"
|
||||
data-name="Path 461"
|
||||
d="M252.30849,663.16553a22.728,22.728,0,0,0,21.947-3.866c7.687-6.452,10.1-17.081,12.058-26.924l5.8-29.112-12.143,8.362c-8.733,6.013-17.662,12.219-23.709,20.929s-8.686,20.6-3.828,30.024"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#e6e6e6"
|
||||
/>
|
||||
<path
|
||||
id="a94887ac-0642-4b28-b311-c351a0f7f12b-130"
|
||||
data-name="Path 462"
|
||||
d="M253.34651,698.41151c-1.229-8.953-2.493-18.02-1.631-27.069.766-8.036,3.217-15.885,8.209-22.321a37.13141,37.13141,0,0,1,9.527-8.633c.953-.6,1.829.909.881,1.507a35.29989,35.29989,0,0,0-13.963,16.847c-3.04,7.732-3.528,16.161-3,24.374.317,4.967.988,9.9,1.665,14.83a.9.9,0,0,1-.61,1.074.878.878,0,0,1-1.074-.61Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#f2f2f2"
|
||||
/>
|
||||
<path
|
||||
d="M496.87431,505.52556a6.9408,6.9408,0,0,1-2.85071.67077l-91.60708,2.51425a14.3796,14.3796,0,0,1-.62506-28.75241l91.60729-2.51381a7.00744,7.00744,0,0,1,7.15064,6.8456l.32069,14.75586a7.01658,7.01658,0,0,1-3.99577,6.47974Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#6c63ff"
|
||||
/>
|
||||
<path
|
||||
d="M379.332,698.59808H364.57245a7.00786,7.00786,0,0,1-7-7V568.58392a7.00785,7.00785,0,0,1,7-7H379.332a7.00786,7.00786,0,0,1,7,7V691.59808A7.00787,7.00787,0,0,1,379.332,698.59808Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#6c63ff"
|
||||
/>
|
||||
<path
|
||||
d="M418.52435,698.59808H403.76459a7.00786,7.00786,0,0,1-7-7V568.58392a7.00785,7.00785,0,0,1,7-7h14.75976a7.00786,7.00786,0,0,1,7,7V691.59808A7.00787,7.00787,0,0,1,418.52435,698.59808Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#6c63ff"
|
||||
/>
|
||||
<circle cx="196.71571" cy="182.69717" r="51" fill="#6c63ff" />
|
||||
<path
|
||||
d="M410.30072,605.205H373.61127a43.27708,43.27708,0,0,1-37.56043-65.05664l51.30933-88.87012a6.5,6.5,0,0,1,11.2583,0l50.27612,87.08057A44.56442,44.56442,0,0,1,410.30072,605.205Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
<path
|
||||
d="M405.02686,404.114c3.30591-.0918,7.42029-.20655,10.59-2.522a8.13274,8.13274,0,0,0,3.20007-6.07275,5.47084,5.47084,0,0,0-1.86035-4.49315c-1.65552-1.39894-4.073-1.72706-6.67823-.96144l2.69922-19.72558-1.98144-.27149-3.17322,23.18994,1.65466-.75928c1.91834-.87988,4.55164-1.32763,6.188.05518a3.51514,3.51514,0,0,1,1.15271,2.89551,6.14686,6.14686,0,0,1-2.38122,4.52783c-2.46668,1.80176-5.74622,2.03418-9.46582,2.13818Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
<rect x="226.50312" y="172.03238" width="10.77161" height="2" fill="#2f2e41" />
|
||||
<rect x="192.50312" y="172.03238" width="10.77161" height="2" fill="#2f2e41" />
|
||||
<path
|
||||
d="M380.99359,593.79839a6.94088,6.94088,0,0,1-.67077-2.85072l-2.51425-91.60708a14.3796,14.3796,0,0,1,28.75241-.62506l2.51381,91.60729a7.00744,7.00744,0,0,1-6.8456,7.15064l-14.75586.32069a7.01655,7.01655,0,0,1-6.47974-3.99576Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#6c63ff"
|
||||
/>
|
||||
<path
|
||||
d="M388.25747,345.00549c6.19637,8.10336,16.033,13.53931,26.42938,12.25223,9.90031-1.22567,18.06785-8.12619,20.117-18.0055a29.66978,29.66978,0,0,0-7.79665-26.1905c-7.00748-7.37032-17.03634-11.335-26.96311-12.69456-18.80446-2.57537-38.1172,4.04852-52.33518,16.4023a64.1102,64.1102,0,0,0-16.69251,22.37513,62.72346,62.72346,0,0,0-5.175,27.07767c.54633,18.375,8.595,36.71479,22.48271,48.90083a63.37666,63.37666,0,0,0,5.40808,4.23578c1.58387,1.11112,3.08464-1.48868,1.51415-2.59042-14.222-9.977-23.29362-26.21093-25.78338-43.26844a59.92391,59.92391,0,0,1,14.05278-48.33971c11.48411-13.058,28.32271-21.54529,45.7628-22.30575,17.54894-.76521,39.47915,7.06943,42.7631,26.60435,1.47191,8.7558-1.801,17.95926-9.82454,22.3428-8.59053,4.69326-19.12416,2.76181-26.50661-3.29945a30.448,30.448,0,0,1-4.86258-5.01092c-1.157-1.51313-3.76387-.02044-2.59041,1.51416Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
<rect
|
||||
id="fc777aff-63b1-4720-84dc-e3a9c20790b9"
|
||||
data-name="ab2e16f2-9798-47da-b25d-769524f3c86f"
|
||||
x="484.20919"
|
||||
y="242.03206"
|
||||
width="437.1948"
|
||||
height="207.45652"
|
||||
transform="translate(-238.48792 -95.97299) rotate(-8.21995)"
|
||||
fill="#f1f1f1"
|
||||
/>
|
||||
<rect
|
||||
id="ecffa418-b240-4504-be04-512edea7ccda"
|
||||
data-name="bf81c03f-68cf-4889-8697-1102f95f97bb"
|
||||
x="496.79745"
|
||||
y="259.81556"
|
||||
width="412.19197"
|
||||
height="173.08746"
|
||||
transform="translate(-238.57266 -95.95442) rotate(-8.21995)"
|
||||
fill="#fff"
|
||||
/>
|
||||
<rect
|
||||
id="b49ce3f1-9d75-4481-986b-3b6beb000c79"
|
||||
data-name="f065dccc-d150-492a-a09f-a7f3f89523f0"
|
||||
x="468.80837"
|
||||
y="231.16611"
|
||||
width="437.19481"
|
||||
height="18.57334"
|
||||
transform="translate(-223.58995 -99.25677) rotate(-8.21995)"
|
||||
fill="#e5e5e5"
|
||||
/>
|
||||
<circle
|
||||
id="a4219562-805a-49cd-8b89-b1f92f7a9e75"
|
||||
data-name="bdbbf39c-df25-4682-8b85-5a6af4a1bd14"
|
||||
cx="288.67474"
|
||||
cy="71.34324"
|
||||
r="3.4425"
|
||||
fill="#fff"
|
||||
/>
|
||||
<circle
|
||||
id="b0f6399c-6944-4f74-a888-473f61f9730c"
|
||||
data-name="abcd4292-0b1f-4102-9b5e-e8bbd87baabc"
|
||||
cx="301.6071"
|
||||
cy="69.47507"
|
||||
r="3.4425"
|
||||
fill="#fff"
|
||||
/>
|
||||
<circle
|
||||
id="b03f93dc-2c99-4323-9b17-02f51b8830c0"
|
||||
data-name="a3fb731e-8b3d-41ca-96f2-91600dc0b434"
|
||||
cx="314.54005"
|
||||
cy="67.6068"
|
||||
r="3.4425"
|
||||
fill="#fff"
|
||||
/>
|
||||
<rect
|
||||
id="a6067cfc-0392-4d68-afe4-e34d11a8f0ac"
|
||||
data-name="ab2e16f2-9798-47da-b25d-769524f3c86f"
|
||||
x="370.25796"
|
||||
y="100.18309"
|
||||
width="437.1948"
|
||||
height="207.45652"
|
||||
fill="#e6e6e6"
|
||||
/>
|
||||
<rect
|
||||
id="ecd65817-7467-4dbd-a435-c0f1d9841c98"
|
||||
data-name="bf81c03f-68cf-4889-8697-1102f95f97bb"
|
||||
x="382.75969"
|
||||
y="117.97286"
|
||||
width="412.19197"
|
||||
height="173.08746"
|
||||
fill="#fff"
|
||||
/>
|
||||
<rect
|
||||
id="eea6c39d-8a45-4eb1-bab9-6120f465de14"
|
||||
data-name="f065dccc-d150-492a-a09f-a7f3f89523f0"
|
||||
x="370.07154"
|
||||
y="88.19711"
|
||||
width="437.19481"
|
||||
height="18.57334"
|
||||
fill="#cbcbcb"
|
||||
/>
|
||||
<circle
|
||||
id="ab9e51f9-7431-4d30-8193-f9435a6bd5c3"
|
||||
data-name="bdbbf39c-df25-4682-8b85-5a6af4a1bd14"
|
||||
cx="383.87383"
|
||||
cy="99.11864"
|
||||
r="3.4425"
|
||||
fill="#fff"
|
||||
/>
|
||||
<circle
|
||||
id="a54ed687-3b0d-413b-b405-af8897a5c032"
|
||||
data-name="abcd4292-0b1f-4102-9b5e-e8bbd87baabc"
|
||||
cx="396.94043"
|
||||
cy="99.11864"
|
||||
r="3.4425"
|
||||
fill="#fff"
|
||||
/>
|
||||
<circle
|
||||
id="fd1d2195-7e97-488f-8f4b-7061a06deb9a"
|
||||
data-name="a3fb731e-8b3d-41ca-96f2-91600dc0b434"
|
||||
cx="410.00762"
|
||||
cy="99.11864"
|
||||
r="3.4425"
|
||||
fill="#fff"
|
||||
/>
|
||||
<rect x="620.27691" y="144.28855" width="58.05212" height="4.36334" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="157.09784" width="89.64514" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="621.20899" y="169.29697" width="73.05881" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="182.68222" width="42.65054" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="195.75677" width="64.37073" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="142.916" width="7.10843" height="7.10842" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="155.72527" width="7.10843" height="7.10841" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="167.92442" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="181.30967" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="194.38423" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="208.91306" width="58.05212" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="221.72236" width="89.64514" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="621.20899" y="233.92149" width="73.05881" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="247.30674" width="42.65054" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="260.38129" width="64.37073" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="207.54051" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="220.34979" width="7.10843" height="7.10841" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="232.54894" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="245.93419" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="259.00875" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="436.63003" y="243.13905" width="58.05213" height="4.36333" fill="#e6e6e6" />
|
||||
<rect x="428.86266" y="254.4769" width="73.05881" height="4.36332" fill="#e6e6e6" />
|
||||
<path
|
||||
d="M699.66075,388.1056a37.91872,37.91872,0,0,1-55.87819,33.382l-.00736-.00737a37.907,37.907,0,1,1,55.88555-33.37461Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#e6e6e6"
|
||||
/>
|
||||
<circle cx="465.67554" cy="175.95347" r="10.30421" fill="#fff" />
|
||||
<path
|
||||
d="M679.54362,407.55657a53.11056,53.11056,0,0,1-35.56788-.13775l-.00738-.0051,7.6766-15.15329h20.60841Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M547.86351,482.19293c-17.96014,0-32.5719-15.52155-32.5719-34.60067,0-19.07858,14.61176-34.60014,32.5719-34.60014s32.5719,15.52156,32.5719,34.60014C580.43541,466.67138,565.82365,482.19293,547.86351,482.19293Zm0-60.4582c-13.13954,0-23.82929,11.59955-23.82929,25.85753s10.68975,25.85806,23.82929,25.85806,23.82928-11.60008,23.82928-25.85806S561.00305,421.73473,547.86351,421.73473Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#6c63ff"
|
||||
/>
|
||||
<path
|
||||
d="M578.70786,542.49212h-61.6887a20.54138,20.54138,0,0,1-20.51852-20.51826V461.46391a14.06356,14.06356,0,0,1,14.04747-14.04774h74.6308a14.06356,14.06356,0,0,1,14.04747,14.04774v60.50995A20.54138,20.54138,0,0,1,578.70786,542.49212Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#3f3d56"
|
||||
/>
|
||||
<path
|
||||
d="M559.88461,481.84022a12.0211,12.0211,0,1,0-17.48524,10.69829v18.808h10.92827v-18.808A12.01088,12.01088,0,0,0,559.88461,481.84022Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M578.27362,699.99212h-381a1,1,0,0,1,0-2h381a1,1,0,0,1,0,2Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#3f3d56"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
|
@ -38,7 +38,7 @@ const menus = [
|
|||
},
|
||||
{
|
||||
itemKey: '/star',
|
||||
text: '收藏',
|
||||
text: '星标',
|
||||
onClick: () => {
|
||||
Router.push({
|
||||
pathname: `/star`,
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
.wikiItemWrap {
|
||||
padding: 12px 16px !important;
|
||||
margin: 8px 2px;
|
||||
cursor: pointer;
|
||||
background-color: var(--semi-color-bg-2);
|
||||
border: 1px solid var(--semi-color-border) !important;
|
||||
}
|
||||
|
||||
.titleWrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { Typography } from '@douyinfe/semi-ui';
|
||||
import { SystemConfig } from 'components/admin/system-config';
|
||||
import { Seo } from 'components/seo';
|
||||
import { useUser } from 'data/user';
|
||||
import { Forbidden } from 'illustrations/forbidden';
|
||||
import { SingleColumnLayout } from 'layouts/single-column';
|
||||
import type { NextPage } from 'next';
|
||||
import Router, { useRouter } from 'next/router';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const Page: NextPage = () => {
|
||||
const { user } = useUser();
|
||||
const { query = {} } = useRouter();
|
||||
const { tab = 'base' } = query as {
|
||||
tab?: string;
|
||||
};
|
||||
|
||||
const navigate = useCallback((tab = 'base') => {
|
||||
Router.push({
|
||||
pathname: `/admin`,
|
||||
query: { tab },
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SingleColumnLayout>
|
||||
<Seo title="管理后台" key={tab} />
|
||||
<div className="container">
|
||||
{user && user.isSystemAdmin ? (
|
||||
<>
|
||||
<div className={styles.titleWrap}>
|
||||
<Title heading={3} style={{ margin: '8px 0' }}>
|
||||
管理后台
|
||||
</Title>
|
||||
</div>
|
||||
<SystemConfig tab={tab} onNavigate={navigate} />
|
||||
</>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Forbidden />
|
||||
<Text strong type="danger">
|
||||
无权限查看
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SingleColumnLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,35 @@
|
|||
.wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--semi-color-bg-0);
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
padding: 10vh 24px;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.form {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 32px 24px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
|
||||
footer {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--semi-color-border);
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
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 { useResetPassword, useVerifyCode } from 'data/user';
|
||||
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, { useCallback, useState } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Content, Footer } = Layout;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const Page = () => {
|
||||
const query = useRouterQuery();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [hasSendVerifyCode, toggleHasSendVerifyCode] = useToggle(false);
|
||||
const [countDown, setCountDown] = useState(0);
|
||||
const { reset, loading } = useResetPassword();
|
||||
const { sendVerifyCode, loading: sendVerifyCodeLoading } = useVerifyCode();
|
||||
|
||||
const onFormChange = useCallback((formState) => {
|
||||
setEmail(formState.values.email);
|
||||
}, []);
|
||||
|
||||
const { start, stop } = useInterval(() => {
|
||||
setCountDown((v) => {
|
||||
if (v - 1 <= 0) {
|
||||
stop();
|
||||
toggleHasSendVerifyCode(false);
|
||||
return 0;
|
||||
}
|
||||
return v - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
const onFinish = useCallback(
|
||||
(values) => {
|
||||
reset(values).then((res) => {
|
||||
Modal.confirm({
|
||||
title: <Title heading={5}>密码修改成功</Title>,
|
||||
content: <Text>是否跳转至登录?</Text>,
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk() {
|
||||
Router.push('/login', { query });
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[reset, query]
|
||||
);
|
||||
|
||||
const getVerifyCode = useCallback(() => {
|
||||
stop();
|
||||
sendVerifyCode({ email })
|
||||
.then(() => {
|
||||
Toast.success('请前往邮箱查收验证码');
|
||||
setCountDown(60);
|
||||
start();
|
||||
toggleHasSendVerifyCode(true);
|
||||
})
|
||||
.catch(() => {
|
||||
toggleHasSendVerifyCode(false);
|
||||
});
|
||||
}, [email, toggleHasSendVerifyCode, sendVerifyCode, start, stop]);
|
||||
|
||||
return (
|
||||
<Layout className={styles.wrap}>
|
||||
<Seo title="重置密码" />
|
||||
<Content className={styles.content}>
|
||||
<Title heading={4} style={{ marginBottom: 16, textAlign: 'center' }}>
|
||||
<Space>
|
||||
<LogoImage></LogoImage>
|
||||
<LogoText></LogoText>
|
||||
</Space>
|
||||
</Title>
|
||||
<Form
|
||||
className={styles.form}
|
||||
initValues={{ name: '', password: '' }}
|
||||
onChange={onFormChange}
|
||||
onSubmit={onFinish}
|
||||
>
|
||||
<Title type="tertiary" heading={5} style={{ marginBottom: 16, textAlign: 'center' }}>
|
||||
重置密码
|
||||
</Title>
|
||||
|
||||
<Form.Input
|
||||
noLabel
|
||||
field="email"
|
||||
placeholder={'请输入邮箱'}
|
||||
rules={[
|
||||
{
|
||||
type: 'email',
|
||||
message: '请输入正确的邮箱地址!',
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
message: '请输入邮箱地址!',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Row gutter={8} style={{ paddingTop: 12 }}>
|
||||
<Col span={16}>
|
||||
<Form.Input
|
||||
noLabel
|
||||
fieldStyle={{ paddingTop: 0 }}
|
||||
placeholder={'请输入验证码'}
|
||||
field="verifyCode"
|
||||
rules={[{ required: true, message: '请输入邮箱收到的验证码!' }]}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button disabled={!email || countDown > 0} loading={sendVerifyCodeLoading} onClick={getVerifyCode} block>
|
||||
{hasSendVerifyCode ? countDown : '获取验证码'}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Input
|
||||
noLabel
|
||||
mode="password"
|
||||
field="password"
|
||||
label="密码"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="输入用户密码"
|
||||
rules={[{ required: true, message: '请输入新密码' }]}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
noLabel
|
||||
mode="password"
|
||||
field="confirmPassword"
|
||||
label="密码"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="确认用户密码"
|
||||
rules={[{ required: true, message: '请再次输入密码' }]}
|
||||
/>
|
||||
|
||||
<Button htmlType="submit" type="primary" theme="solid" block loading={loading} style={{ margin: '16px 0' }}>
|
||||
重置密码
|
||||
</Button>
|
||||
<footer>
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/login',
|
||||
query,
|
||||
}}
|
||||
>
|
||||
<Text link style={{ textAlign: 'center' }}>
|
||||
去登录
|
||||
</Text>
|
||||
</Link>
|
||||
</footer>
|
||||
</Form>
|
||||
</Content>
|
||||
<Footer>
|
||||
<Author></Author>
|
||||
</Footer>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
|
@ -51,9 +51,10 @@ const Page = () => {
|
|||
field="name"
|
||||
label="账户"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="输入账户名称"
|
||||
rules={[{ required: true, message: '请输入账户' }]}
|
||||
placeholder="输入账户名称或邮箱"
|
||||
rules={[{ required: true, message: '请输入账户或邮箱' }]}
|
||||
></Form.Input>
|
||||
|
||||
<Form.Input
|
||||
noLabel
|
||||
mode="password"
|
||||
|
@ -67,16 +68,29 @@ const Page = () => {
|
|||
登录
|
||||
</Button>
|
||||
<footer>
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/register',
|
||||
query,
|
||||
}}
|
||||
>
|
||||
<Text link style={{ textAlign: 'center' }}>
|
||||
注册用户以登录
|
||||
</Text>
|
||||
</Link>
|
||||
<Space>
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/register',
|
||||
query,
|
||||
}}
|
||||
>
|
||||
<Text link style={{ textAlign: 'center' }}>
|
||||
注册用户以登录
|
||||
</Text>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/forgetPassword',
|
||||
query,
|
||||
}}
|
||||
>
|
||||
<a>
|
||||
<Text type="tertiary">忘记密码?</Text>
|
||||
</a>
|
||||
</Link>
|
||||
</Space>
|
||||
</footer>
|
||||
</Form>
|
||||
</Content>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
height: calc(100% - 52px);
|
||||
padding: 10vh 24px;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -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: <Title heading={5}>注册成功</Title>,
|
||||
content: <Text>是否跳转至登录?</Text>,
|
||||
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: <Title heading={5}>注册成功</Title>,
|
||||
content: <Text>是否跳转至登录?</Text>,
|
||||
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 (
|
||||
<Layout className={styles.wrap}>
|
||||
|
@ -42,10 +88,16 @@ const Page = () => {
|
|||
<LogoText></LogoText>
|
||||
</Space>
|
||||
</Title>
|
||||
<Form className={styles.form} initValues={{ name: '', password: '' }} onSubmit={onFinish}>
|
||||
<Form
|
||||
className={styles.form}
|
||||
initValues={{ name: '', password: '' }}
|
||||
onChange={onFormChange}
|
||||
onSubmit={onFinish}
|
||||
>
|
||||
<Title type="tertiary" heading={5} style={{ marginBottom: 16, textAlign: 'center' }}>
|
||||
用户注册
|
||||
</Title>
|
||||
|
||||
<Form.Input
|
||||
noLabel
|
||||
field="name"
|
||||
|
@ -54,6 +106,7 @@ const Page = () => {
|
|||
placeholder="输入账户名称"
|
||||
rules={[{ required: true, message: '请输入账户' }]}
|
||||
></Form.Input>
|
||||
|
||||
<Form.Input
|
||||
noLabel
|
||||
mode="password"
|
||||
|
@ -63,15 +116,40 @@ const Page = () => {
|
|||
placeholder="输入用户密码"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
></Form.Input>
|
||||
|
||||
<Form.Input
|
||||
noLabel
|
||||
mode="password"
|
||||
field="confirmPassword"
|
||||
label="密码"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="确认用户密码"
|
||||
rules={[{ required: true, message: '请再次输入密码' }]}
|
||||
></Form.Input>
|
||||
field="email"
|
||||
placeholder={'请输入邮箱'}
|
||||
rules={[
|
||||
{
|
||||
type: 'email',
|
||||
message: '请输入正确的邮箱地址!',
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
message: '请输入邮箱地址!',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Row gutter={8} style={{ paddingTop: 12 }}>
|
||||
<Col span={16}>
|
||||
<Form.Input
|
||||
noLabel
|
||||
fieldStyle={{ paddingTop: 0 }}
|
||||
placeholder={'请输入验证码'}
|
||||
field="verifyCode"
|
||||
rules={[{ required: true, message: '请输入邮箱收到的验证码!' }]}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button disabled={!email || countDown > 0} loading={sendVerifyCodeLoading} onClick={getVerifyCode} block>
|
||||
{hasSendVerifyCode ? countDown : '获取验证码'}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Button htmlType="submit" type="primary" theme="solid" block loading={loading} style={{ margin: '16px 0' }}>
|
||||
注册
|
||||
</Button>
|
||||
|
|
|
@ -30,7 +30,7 @@ export const Title = Node.create<TitleOptions>({
|
|||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: 'title',
|
||||
class: 'node-title',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -47,7 +47,7 @@ export const Title = Node.create<TitleOptions>({
|
|||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'div[class=title]',
|
||||
tag: 'div[class=node-title]',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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"; }
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,3 +5,4 @@ export * from './message';
|
|||
export * from './template';
|
||||
export * from './comment';
|
||||
export * from './pagination';
|
||||
export * from './system';
|
||||
|
|
|
@ -17,3 +17,4 @@ __exportStar(require("./message"), exports);
|
|||
__exportStar(require("./template"), exports);
|
||||
__exportStar(require("./comment"), exports);
|
||||
__exportStar(require("./pagination"), exports);
|
||||
__exportStar(require("./system"), exports);
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export interface ISystemConfig {
|
||||
isSystemLocked: boolean;
|
||||
emailServiceHost: string;
|
||||
emailServicePassword: string;
|
||||
emailServicePort: string;
|
||||
emailServiceUser: string;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
"use strict";
|
||||
exports.__esModule = true;
|
|
@ -24,6 +24,7 @@ export interface IUser {
|
|||
email?: string;
|
||||
role: UserRole;
|
||||
status: UserStatus;
|
||||
isSystemAdmin?: boolean;
|
||||
}
|
||||
/**
|
||||
* 登录用户数据定义
|
||||
|
|
|
@ -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`,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,3 +5,4 @@ export * from './message';
|
|||
export * from './template';
|
||||
export * from './comment';
|
||||
export * from './pagination';
|
||||
export * from './system';
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export interface ISystemConfig {
|
||||
isSystemLocked: boolean;
|
||||
emailServiceHost: string;
|
||||
emailServicePassword: string;
|
||||
emailServicePort: string;
|
||||
emailServiceUser: string;
|
||||
}
|
|
@ -26,6 +26,7 @@ export interface IUser {
|
|||
email?: string;
|
||||
role: UserRole;
|
||||
status: UserStatus;
|
||||
isSystemAdmin?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)' })
|
||||
|
|
|
@ -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' })
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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: '是否公开' })
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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: '知识库名称' })
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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 {}
|
|
@ -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,
|
||||
],
|
||||
|
|
|
@ -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 {}
|
|
@ -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<SystemEntity>,
|
||||
|
||||
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<SystemEntity>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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<UserEntity, 'comparePassword' | 'encryptPassword' | 'encrypt' | 'password'>;
|
||||
|
||||
@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<OutUser> {
|
||||
if (await this.userRepo.findOne({ name: user.name })) {
|
||||
throw new HttpException('该账户已被注册', HttpStatus.BAD_REQUEST);
|
||||
async createUser(user: RegisterUserDto): Promise<OutUser> {
|
||||
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: `<p>测试邮件</p>`,
|
||||
});
|
||||
return '测试邮件发送成功';
|
||||
} catch (err) {
|
||||
throw new HttpException('测试邮件发送失败!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新系统配置
|
||||
* @param user
|
||||
* @param targetUserId
|
||||
*/
|
||||
async updateSystemConfig(user: UserEntity, systemConfig: Partial<SystemEntity>) {
|
||||
const currentUser = await this.userRepo.findOne(user.id);
|
||||
|
||||
if (!currentUser.isSystemAdmin) {
|
||||
throw new HttpException('您无权操作', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
return await this.systemService.updateConfigInDatabase(systemConfig);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<VerifyEntity>,
|
||||
|
||||
@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: `<p>您的验证码为 ${verifyCode}</p>`,
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
103
pnpm-lock.yaml
103
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==}
|
||||
|
|
Loading…
Reference in New Issue