merge main

This commit is contained in:
wuding 2022-06-30 20:29:00 +08:00
commit c6e969097e
66 changed files with 2140 additions and 428 deletions

1
.gitignore vendored
View File

@ -21,3 +21,4 @@ tsconfig.tsbuildinfo
scripts/update.sh
output
runtime

View File

@ -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
View File

@ -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>

View File

@ -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} 打包完成"

View File

@ -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'

View File

@ -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:

View File

@ -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

View File

@ -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

110
let-us-start.md Normal file
View File

@ -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'
```

View File

@ -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/;
}
}

View File

@ -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>
);
};

View File

@ -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>
)}
/>
);
};

View File

@ -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>
)}
/>
);
};

View File

@ -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]);

View File

@ -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>

View File

@ -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>
);

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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 };
};

View File

@ -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,}))$/
);
};

View File

@ -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 };
};

View File

@ -0,0 +1,3 @@
import { useEffect, useLayoutEffect } from 'react';
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;

View File

@ -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>
);
};

View File

@ -38,7 +38,7 @@ const menus = [
},
{
itemKey: '/star',
text: '收藏',
text: '星标',
onClick: () => {
Router.push({
pathname: `/star`,

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}
}
}
}

View File

@ -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;

View File

@ -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>

View File

@ -9,6 +9,7 @@
position: relative;
z-index: 10;
display: flex;
height: calc(100% - 52px);
padding: 10vh 24px;
flex: 1;
flex-direction: column;

View File

@ -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>

View File

@ -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]',
},
];
},

View File

@ -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;
};
};

View File

@ -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"; }
}
};

View File

@ -5,3 +5,4 @@ export * from './message';
export * from './template';
export * from './comment';
export * from './pagination';
export * from './system';

View File

@ -17,3 +17,4 @@ __exportStar(require("./message"), exports);
__exportStar(require("./template"), exports);
__exportStar(require("./comment"), exports);
__exportStar(require("./pagination"), exports);
__exportStar(require("./system"), exports);

View File

@ -0,0 +1,7 @@
export interface ISystemConfig {
isSystemLocked: boolean;
emailServiceHost: string;
emailServicePassword: string;
emailServicePort: string;
emailServiceUser: string;
}

View File

@ -0,0 +1,2 @@
"use strict";
exports.__esModule = true;

View File

@ -24,6 +24,7 @@ export interface IUser {
email?: string;
role: UserRole;
status: UserStatus;
isSystemAdmin?: boolean;
}
/**
*

View File

@ -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`,
},
};

View File

@ -5,3 +5,4 @@ export * from './message';
export * from './template';
export * from './comment';
export * from './pagination';
export * from './system';

View File

@ -0,0 +1,7 @@
export interface ISystemConfig {
isSystemLocked: boolean;
emailServiceHost: string;
emailServicePassword: string;
emailServicePort: string;
emailServiceUser: string;
}

View File

@ -26,6 +26,7 @@ export interface IUser {
email?: string;
role: UserRole;
status: UserStatus;
isSystemAdmin?: boolean;
}
/**

View File

@ -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"
},

View File

@ -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({

View File

@ -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) {}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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' })

View File

@ -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' })

View File

@ -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;
}

View File

@ -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: '是否公开' })

View File

@ -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,

View File

@ -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;
}

View File

@ -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: '知识库名称' })

View File

@ -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);
};

View File

@ -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 {}

View File

@ -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,
],

View File

@ -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 {}

View File

@ -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);
}
}
);
});
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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==}