mirror of https://github.com/fantasticit/think.git
commit
11f8c652a4
|
@ -21,3 +21,4 @@ tsconfig.tsbuildinfo
|
|||
scripts/update.sh
|
||||
|
||||
output
|
||||
runtime
|
||||
|
|
11
Dockerfile
11
Dockerfile
|
@ -1,18 +1,13 @@
|
|||
FROM node:18-alpine as builder
|
||||
COPY . /app/
|
||||
COPY . /app/
|
||||
WORKDIR /app
|
||||
ARG EIP=mrdoc.fun
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
|
||||
RUN npm config set registry https://registry.npmmirror.com
|
||||
RUN npm i -g pm2 @nestjs/cli pnpm
|
||||
RUN apk --no-cache add bash
|
||||
RUN sed -i "s/localhost/$EIP/g" /app/docker/prod-sample.yaml
|
||||
RUN cp -f /app/docker/prod-sample.yaml /app/config/prod.yaml
|
||||
RUN apk --no-cache add bash
|
||||
RUN bash build-output.sh
|
||||
|
||||
|
||||
FROM node:18-alpine as prod
|
||||
LABEL maintainer="www.mrdoc.fun"
|
||||
ENV TZ=Asia/Shanghai
|
||||
COPY --from=builder /app/docker/* /app/docker/
|
||||
COPY --from=builder /app/output/ /app/
|
||||
|
@ -22,7 +17,7 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositorie
|
|||
RUN npm config set registry https://registry.npmmirror.com
|
||||
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/*
|
||||
|
|
193
README.md
193
README.md
|
@ -19,182 +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`
|
||||
|
||||
|
||||
|
||||
## Docker-compose 一键构建安装
|
||||
|
||||
- 实测腾讯轻量云 2C4G 机器构建需 8 分钟左右
|
||||
|
||||
**请注意构建前修改 `docker-compose.yml` 中的 `EIP` 参数,否则无法正常使用!!!**
|
||||
|
||||
```
|
||||
# 首次安装
|
||||
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
|
||||
|
||||
# FAQ
|
||||
如遇二次更新有问题,请更新代码重新构建,然后删除本地配置文件并重启容器.
|
||||
如果还不能解决,1.有能力可自行解决|2.等待更新|3.去mrdoc.fun站点留言
|
||||
```
|
||||
|
||||
然后访问 `http://ip:5001` 即可.
|
||||
|
||||
|
||||
|
||||
## 手动安装教程
|
||||
|
||||
- 前台页面地址:`http://localhost:5001`
|
||||
- 服务接口地址:`http://localhost:5002`
|
||||
- 协作接口地址:`http://localhost:5003`
|
||||
|
||||
如需修改配置,开发环境编辑 `config/dev.yaml`。生产环境编辑 `config/prod.yaml` (如没有,可复制开发环境的配置修改即可.)
|
||||
|
||||
- 数据库
|
||||
|
||||
首先安装 `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
|
||||
|
||||
如果需要文档版本服务,请在配置文件中修改 `db.redis` 的配置。
|
||||
|
||||
```
|
||||
docker pull redis:latest
|
||||
docker run --name think-redis -p 6379:6379 -d redis --appendonly yes --requirepass "root"
|
||||
```
|
||||
|
||||
|
||||
### 本地源代码运行(开发环境)
|
||||
|
||||
|
||||
```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` 作为反向代理的配置参考(部分),完整版请见 <[think/nginx.conf.bak](https://github.com/fantasticit/think/blob/main/nginx.conf.bak)>
|
||||
|
||||
```bash
|
||||
upstream wipi_client {
|
||||
server 127.0.0.1:5001;
|
||||
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; #获取客户端真实IP
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### caddy2 配置参考
|
||||
|
||||
采用 caddy v2 作为反向代理的配置文件参考
|
||||
|
||||
```
|
||||
# 例子中的域名,请自行替换.
|
||||
think.mrdoc.fun {
|
||||
encode zstd gzip
|
||||
reverse_proxy localhost:5001
|
||||
}
|
||||
|
||||
|
||||
thinkapi.mrdoc.fun {
|
||||
@websockets {
|
||||
header Connection *Upgrade*
|
||||
header Upgrade websocket
|
||||
path /think/wss/*
|
||||
}
|
||||
encode zstd gzip
|
||||
reverse_proxy /api/* localhost:5002
|
||||
reverse_proxy @websockets localhost:5003
|
||||
}
|
||||
```
|
||||
[项目开发说明](./let-us-start.md)。
|
||||
|
||||
## 自动化部署
|
||||
|
||||
|
@ -202,24 +40,17 @@ thinkapi.mrdoc.fun {
|
|||
|
||||
参考:[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>
|
||||
|
|
|
@ -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: 'sytemadmin' # 注意修改
|
||||
password: 'sytemadmin' # 注意修改
|
||||
email: 'sytemadmin@think.com' # 注意修改为真实邮箱地址
|
||||
|
||||
# 数据库配置
|
||||
db:
|
||||
mysql:
|
||||
host: '127.0.0.1'
|
||||
username: 'root'
|
||||
password: 'root'
|
||||
username: 'think'
|
||||
password: 'think'
|
||||
database: 'think'
|
||||
port: 3306
|
||||
charset: 'utf8mb4'
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
# 生产环境docker示例配置
|
||||
# 开发环境配置
|
||||
client:
|
||||
port: 5001
|
||||
assetPrefix: '/'
|
||||
apiUrl: 'http://localhost:5002/api'
|
||||
collaborationUrl: 'ws://localhost:5003/think/wss'
|
||||
collaborationUrl: 'ws://localhost:5003'
|
||||
# 以下为页面 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: 'sytemadmin' # 注意修改
|
||||
password: 'sytemadmin' # 注意修改
|
||||
email: 'sytemadmin@think.com' # 注意修改为真实邮箱地址
|
||||
|
||||
# 数据库配置
|
||||
db:
|
||||
mysql:
|
||||
host: 'mysql-with-think'
|
||||
username: 'jonnyan404'
|
||||
password: 'www.mrdoc.fun'
|
||||
host: 'mysql-for-think'
|
||||
username: 'think'
|
||||
password: 'think'
|
||||
database: 'think'
|
||||
port: 3306
|
||||
charset: 'utf8mb4'
|
||||
timezone: '+08:00'
|
||||
synchronize: true
|
||||
redis:
|
||||
host: '127.0.0.1'
|
||||
host: 'redis-for-think'
|
||||
port: '6379'
|
||||
password: ''
|
||||
password: 'root'
|
||||
|
||||
# oss 文件存储服务
|
||||
oss:
|
|
@ -1,44 +1,58 @@
|
|||
version: "3"
|
||||
version: '3'
|
||||
services:
|
||||
thinkdoc:
|
||||
think:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
EIP: x.x.x.x # api接口IP,必须设置,可以是 IP 或者域名.
|
||||
image: think
|
||||
container_name: thinkdoc
|
||||
#restart: always
|
||||
container_name: think
|
||||
volumes:
|
||||
- /path/to/you/dir/config:/app/config # 请注意修改 /path/to/you/dir 为云策文档配置文件目录.
|
||||
- /path/to/you/dir/static:/app/packages/server/static # 请注意修改 /path/to/you/dir 为云策文档附件存储目录.
|
||||
- ./config:/app/config
|
||||
- ./runtime/static:/app/packages/server/static
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
ports:
|
||||
- "5001-5003:5001-5003"
|
||||
- '5001-5003:5001-5003'
|
||||
depends_on:
|
||||
- mysql
|
||||
- redis
|
||||
networks:
|
||||
- think
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
container_name: mysql-with-think
|
||||
#restart: always
|
||||
restart: always
|
||||
container_name: mysql-for-think
|
||||
volumes:
|
||||
- /path/to/you/dir/mysql:/var/lib/mysql # 请注意修改 /path/to/you/dir 为您要存储mysql数据的目录绝对路径.
|
||||
- ./runtime/mysql:/var/lib/mysql
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- MYSQL_ROOT_PASSWORD=Jonnyan404!
|
||||
- MYSQL_ROOT_PASSWORD=root
|
||||
- MYSQL_DATABASE=think
|
||||
- MYSQL_USER=jonnyan404
|
||||
- MYSQL_PASSWORD=www.mrdoc.fun
|
||||
- MYSQL_USER=think
|
||||
- MYSQL_PASSWORD=think
|
||||
expose:
|
||||
- "3306"
|
||||
- '3306'
|
||||
ports:
|
||||
- "63306:3306" # 如果不需要外部连接mysql,可注释此行+上一行.
|
||||
- '3306:3306'
|
||||
command:
|
||||
- "--character-set-server=utf8mb4"
|
||||
- "--collation-server=utf8mb4_unicode_ci"
|
||||
# Volumes for persisting data, see https://docs.docker.com/engine/admin/volumes/volumes/
|
||||
#volumes:
|
||||
# thinkdoc-data:
|
||||
# driver: local
|
||||
# mysql-data:
|
||||
# driver: local
|
||||
- '--character-set-server=utf8mb4'
|
||||
- '--collation-server=utf8mb4_unicode_ci'
|
||||
networks:
|
||||
- think
|
||||
redis:
|
||||
image: redis:latest
|
||||
restart: always
|
||||
container_name: redis-for-think
|
||||
command: >
|
||||
--requirepass root
|
||||
expose:
|
||||
- '6379'
|
||||
ports:
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
- ./runtime/redis:/data
|
||||
privileged: true
|
||||
networks:
|
||||
- think
|
||||
networks:
|
||||
think:
|
||||
driver: bridge
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
#!/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
|
||||
cp -f /app/config/docker-prod-sample.yaml $CONFIG_FILE
|
||||
else
|
||||
echo "#####Configuration file already exists#####"
|
||||
echo ""
|
||||
fi
|
||||
redis-server --daemonize yes
|
||||
|
||||
pnpm run pm2
|
||||
pm2 logs
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
# think
|
||||
|
||||
## 项目结构
|
||||
|
||||
本项目依赖 pnpm 使用 monorepo 形式进行代码组织,分包如下:
|
||||
|
||||
- `@think/config`: 客户端、服务端、OSS、MySQL、Redis 等配置管理
|
||||
- `@think/domains`:领域模型数据定义
|
||||
- `@think/constants`:常量配置
|
||||
- `@think/server`:服务端
|
||||
- `@think/client`:客户端
|
||||
|
||||
## 项目依赖
|
||||
|
||||
为了将项目运行起来,至少需要以下依赖。
|
||||
|
||||
- nodejs >=16.5.0:推荐使用 nvm 安装
|
||||
- pnpm:安装 nodejs 后,运行 `npm i -g pnpm` 即可安装
|
||||
- pm2:安装 nodejs 后,运行 `npm i -g pm2` 即可安装
|
||||
- MySQL 5.7
|
||||
- Redis
|
||||
|
||||
## 配置文件
|
||||
|
||||
项目所有的配置文件都在 `config` 目录下,其中 `dev.yaml` 中各字段均有解释,生产环境打包依赖 `prod.yaml`(需要自行修改为所需配置)。如果运行不起来,请对比 `dev.yaml` 检查配置。
|
||||
|
||||
**如果部署遇到问题,首先请确认相应配置是否正确!**
|
||||
|
||||
## 项目运行
|
||||
|
||||
无论是开发环境,还是生产环境,项目运行成功后会在 3 个端口启动相应服务(默认 5001、5002、5003),具体端口号由 `config` 文件夹下的配置文件决定。
|
||||
|
||||
- 前台页面地址:`http://localhost:5001`
|
||||
- 服务接口地址:`http://localhost:5002`
|
||||
- 协作接口地址:`http://localhost:5003`
|
||||
|
||||
### 本地开发
|
||||
|
||||
1. 安装数据库
|
||||
|
||||
首先安装 `MySQL` 和 `Redis`,推荐使用 docker 进行安装。
|
||||
|
||||
```bash
|
||||
docker image pull mysql:5.7
|
||||
# m1 的 mac 可以用:docker image pull --platform linux/x86_64 mysql:5.7
|
||||
docker run -d --restart=always --name mysql-for-think-dev -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_USER=think -e MYSQL_PASSWORD=think -e MYSQL_DATABASE=think mysql:5.7 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
|
||||
docker pull redis:latest
|
||||
docker run --name redis-for-think-dev -p 6379:6379 -d redis --appendonly yes --requirepass "root"
|
||||
```
|
||||
|
||||
2. 安装依赖并运行
|
||||
|
||||
```bash
|
||||
git clone https://github.com/fantasticit/think.git
|
||||
cd think
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
### 生产部署
|
||||
|
||||
首先确认在 `config` 文件夹下新建 `prod.yaml` 配置文件,然后运行以下命令。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/fantasticit/think.git
|
||||
cd think
|
||||
pnpm install # 安装依赖
|
||||
pnpm run build # 项目打包
|
||||
|
||||
# 以下如果没有安装 pm2,直接 pnpm run start,推荐使用 pm2
|
||||
pnpm run pm2
|
||||
pm2 startup
|
||||
pm2 save
|
||||
```
|
||||
|
||||
### docker-compose
|
||||
|
||||
也可以使用 docker-compose 进行项目部署。首先,根据需要修改 `docker-compose.yml` 中的数据库、Redis 相关用户名、密码等配置,然后,修改 `config/docker-prod-sample.yaml` 中对应的配置。
|
||||
|
||||
```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'
|
||||
```
|
|
@ -0,0 +1,29 @@
|
|||
import { TabPane, Tabs } from '@douyinfe/semi-ui';
|
||||
import React from 'react';
|
||||
|
||||
import { Mail } from './mail';
|
||||
import { System } from './system';
|
||||
|
||||
interface IProps {
|
||||
tab?: string;
|
||||
onNavigate: (arg: string) => void;
|
||||
}
|
||||
|
||||
const TitleMap = {
|
||||
base: '系统管理',
|
||||
mail: '邮箱服务',
|
||||
};
|
||||
|
||||
export const SystemConfig: React.FC<IProps> = ({ tab, onNavigate }) => {
|
||||
return (
|
||||
<Tabs lazyRender type="line" activeKey={tab} onChange={onNavigate}>
|
||||
<TabPane tab={TitleMap['base']} itemKey="base">
|
||||
<System />
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab={TitleMap['mail']} itemKey="mail">
|
||||
<Mail />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,88 @@
|
|||
import { Banner, Button, Form, Toast } from '@douyinfe/semi-ui';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { useSystemConfig } from 'data/user';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
export const Mail = () => {
|
||||
const { data, loading, error, sendTestEmail, updateSystemConfig } = useSystemConfig();
|
||||
const [changed, toggleChanged] = useToggle(false);
|
||||
|
||||
const onFormChange = useCallback(() => {
|
||||
toggleChanged(true);
|
||||
}, [toggleChanged]);
|
||||
|
||||
const onFinish = useCallback(
|
||||
(values) => {
|
||||
updateSystemConfig(values).then(() => {
|
||||
Toast.success('操作成功');
|
||||
});
|
||||
},
|
||||
[updateSystemConfig]
|
||||
);
|
||||
|
||||
return (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={error}
|
||||
normalContent={() => (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Banner
|
||||
type="warning"
|
||||
description="配置邮箱服务后,请测试是否正确,否则可能导致无法注册用户,找回密码!"
|
||||
closeIcon={null}
|
||||
/>
|
||||
|
||||
<Form initValues={data} onChange={onFormChange} onSubmit={onFinish}>
|
||||
<Form.Input
|
||||
field="emailServiceHost"
|
||||
label="邮件服务地址"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="输入邮件服务地址"
|
||||
rules={[{ required: true, message: '请输入邮件服务地址' }]}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field="emailServicePort"
|
||||
label="邮件服务端口"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="输入邮件服务端口"
|
||||
rules={[{ required: true, message: '请输入邮件服务端口' }]}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field="emailServicePassword"
|
||||
label="邮件服务密码"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="输入邮件服务密码"
|
||||
rules={[{ required: true, message: '请输入邮件服务密码' }]}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field="emailServiceUser"
|
||||
label="邮件服务用户"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="输入邮件服务密码"
|
||||
rules={[{ required: true, message: '请输入邮件服务密码' }]}
|
||||
/>
|
||||
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
theme="solid"
|
||||
disabled={!changed}
|
||||
loading={loading}
|
||||
style={{ margin: '16px 0' }}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
|
||||
<Button style={{ margin: '16px' }} onClick={sendTestEmail}>
|
||||
测试
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
import { Banner, Button, Form, Toast } from '@douyinfe/semi-ui';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { useSystemConfig } from 'data/user';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
export const System = () => {
|
||||
const { data, loading, error, updateSystemConfig } = useSystemConfig();
|
||||
const [changed, toggleChanged] = useToggle(false);
|
||||
|
||||
const onFormChange = useCallback(() => {
|
||||
toggleChanged(true);
|
||||
}, [toggleChanged]);
|
||||
|
||||
const onFinish = useCallback(
|
||||
(values) => {
|
||||
updateSystemConfig(values).then(() => {
|
||||
Toast.success('操作成功');
|
||||
});
|
||||
},
|
||||
[updateSystemConfig]
|
||||
);
|
||||
|
||||
return (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={error}
|
||||
normalContent={() => (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Banner type="warning" description="系统锁定后,除系统管理员外均不可登录,谨慎修改!" closeIcon={null} />
|
||||
<Form labelPosition="left" initValues={data} onChange={onFormChange} onSubmit={onFinish}>
|
||||
<Form.Switch field="isSystemLocked" label="系统锁定" />
|
||||
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
theme="solid"
|
||||
disabled={!changed}
|
||||
loading={loading}
|
||||
style={{ margin: '16px 0' }}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -16,7 +16,6 @@ import {
|
|||
Tooltip,
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IUser } from '@think/domains';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { DocumentLinkCopyer } from 'components/document/link';
|
||||
import { useDoumentMembers } from 'data/document';
|
||||
|
@ -52,6 +51,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, {
|
||||
|
@ -162,7 +162,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);
|
||||
|
@ -171,6 +174,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
|
|||
event.on(JOIN_USER, handler);
|
||||
|
||||
return () => {
|
||||
toastedUsersRef.current = [];
|
||||
event.off(JOIN_USER, handler);
|
||||
};
|
||||
}, [currentUser]);
|
||||
|
|
|
@ -2,7 +2,8 @@ import { IconSpin } from '@douyinfe/semi-icons';
|
|||
import { Avatar, Button, Dropdown, Typography } from '@douyinfe/semi-ui';
|
||||
import { useUser } from 'data/user';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React from 'react';
|
||||
import Router from 'next/router';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { UserSetting } from './setting';
|
||||
|
||||
|
@ -12,6 +13,10 @@ export const User: React.FC = () => {
|
|||
const { user, loading, error, toLogin, logout } = useUser();
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
|
||||
const toAdmin = useCallback(() => {
|
||||
Router.push('/admin');
|
||||
}, []);
|
||||
|
||||
if (loading) return <Button icon={<IconSpin />} theme="borderless" type="tertiary" />;
|
||||
|
||||
if (error || !user) {
|
||||
|
@ -32,6 +37,11 @@ export const User: React.FC = () => {
|
|||
<Dropdown.Item onClick={() => toggleVisible(true)}>
|
||||
<Text>账户设置</Text>
|
||||
</Dropdown.Item>
|
||||
{user.isSystemAdmin ? (
|
||||
<Dropdown.Item onClick={toAdmin}>
|
||||
<Text>管理后台</Text>
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item onClick={logout}>
|
||||
<Text>退出登录</Text>
|
||||
|
|
|
@ -65,7 +65,13 @@ export const UserSetting: React.FC<IProps> = ({ visible, toggleVisible }) => {
|
|||
disabled
|
||||
placeholder="请输入账户名称"
|
||||
></Form.Input>
|
||||
<Form.Input label="邮箱" field="email" style={{ width: '100%' }} placeholder="请输入账户邮箱"></Form.Input>
|
||||
<Form.Input
|
||||
disabled
|
||||
label="邮箱"
|
||||
field="email"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请输入账户邮箱"
|
||||
></Form.Input>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -42,7 +42,9 @@ export const WikiCard: React.FC<{ wiki: IWikiWithIsMember; shareMode?: boolean }
|
|||
</header>
|
||||
<main>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>{wiki.name}</Text>
|
||||
<Paragraph ellipsis={{ rows: 1 }} strong>
|
||||
{wiki.name}
|
||||
</Paragraph>
|
||||
<Paragraph ellipsis={{ rows: 1 }}>{wiki.description}</Paragraph>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -7,7 +7,7 @@ import Link from 'next/link';
|
|||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
export const WikiPinCard: React.FC<{ wiki: IWiki }> = ({ wiki }) => {
|
||||
return (
|
||||
|
@ -35,7 +35,9 @@ export const WikiPinCard: React.FC<{ wiki: IWiki }> = ({ wiki }) => {
|
|||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<Text strong>{wiki.name}</Text>
|
||||
<Paragraph ellipsis={{ rows: 1 }} strong>
|
||||
{wiki.name}
|
||||
</Paragraph>
|
||||
</main>
|
||||
<footer>
|
||||
<Text type="tertiary" size="small">
|
||||
|
|
|
@ -9,7 +9,7 @@ import { useWikiDetail, useWikiTocs } from 'data/wiki';
|
|||
import { triggerCreateDocument } from 'event';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
import { Tree } from './tree';
|
||||
|
@ -29,7 +29,7 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
docAsLink = '/wiki/[wikiId]/document/[documentId]',
|
||||
getDocLink = (documentId) => `/wiki/${wikiId}/document/${documentId}`,
|
||||
}) => {
|
||||
const { pathname } = useRouter();
|
||||
const { pathname, query } = useRouter();
|
||||
const { data: wiki, loading: wikiLoading, error: wikiError } = useWikiDetail(wikiId);
|
||||
const { data: tocs, loading: tocsLoading, error: tocsError } = useWikiTocs(wikiId);
|
||||
const { data: starWikis } = useStarWikis();
|
||||
|
@ -39,6 +39,7 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
error: starDocumentsError,
|
||||
} = useWikiStarDocuments(wikiId);
|
||||
const [parentIds, setParentIds] = useState<Array<string>>([]);
|
||||
const otherStarWikis = useMemo(() => (starWikis || []).filter((wiki) => wiki.id !== wikiId), [starWikis, wikiId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tocs || !tocs.length) return;
|
||||
|
@ -73,15 +74,14 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
</div>
|
||||
}
|
||||
error={wikiError}
|
||||
normalContent={() => (
|
||||
<Dropdown
|
||||
trigger={'click'}
|
||||
position="bottomRight"
|
||||
render={
|
||||
<Dropdown.Menu style={{ width: 180 }}>
|
||||
{(starWikis || [])
|
||||
.filter((wiki) => wiki.id !== wikiId)
|
||||
.map((wiki) => {
|
||||
normalContent={() =>
|
||||
otherStarWikis.length ? (
|
||||
<Dropdown
|
||||
trigger={'click'}
|
||||
position="bottomRight"
|
||||
render={
|
||||
<Dropdown.Menu style={{ width: 180 }}>
|
||||
{otherStarWikis.map((wiki) => {
|
||||
return (
|
||||
<Dropdown.Item key={wiki.id}>
|
||||
<Link
|
||||
|
@ -93,35 +93,55 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
<a
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<Avatar
|
||||
shape="square"
|
||||
size="small"
|
||||
src={wiki.avatar}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{wiki.name.charAt(0)}
|
||||
</Avatar>
|
||||
<Text strong ellipsis={{ rows: 1 }}>
|
||||
{wiki.name}
|
||||
</Text>
|
||||
</span>
|
||||
<Avatar
|
||||
shape="square"
|
||||
size="small"
|
||||
src={wiki.avatar}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{wiki.name.charAt(0)}
|
||||
</Avatar>
|
||||
<Text strong style={{ width: 120 }} ellipsis={{ showTooltip: true }}>
|
||||
{wiki.name}
|
||||
</Text>
|
||||
</a>
|
||||
</Link>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
<div className={styles.titleWrap}>
|
||||
<span>
|
||||
<Avatar
|
||||
shape="square"
|
||||
size="small"
|
||||
src={wiki.avatar}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{wiki.name.charAt(0)}
|
||||
</Avatar>
|
||||
<Text strong>{wiki.name}</Text>
|
||||
</span>
|
||||
<IconSmallTriangleDown />
|
||||
</div>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<div className={styles.titleWrap}>
|
||||
<span>
|
||||
<Avatar
|
||||
|
@ -139,10 +159,9 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
</Avatar>
|
||||
<Text strong>{wiki.name}</Text>
|
||||
</span>
|
||||
<IconSmallTriangleDown />
|
||||
</div>
|
||||
</Dropdown>
|
||||
)}
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<DataRender
|
||||
|
@ -170,7 +189,12 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
}
|
||||
error={wikiError}
|
||||
normalContent={() => (
|
||||
<div className={cls(styles.linkWrap, pathname === '/wiki/[wikiId]' && styles.isActive)}>
|
||||
<div
|
||||
className={cls(
|
||||
styles.linkWrap,
|
||||
(pathname === '/wiki/[wikiId]' || query.documentId === wiki.homeDocumentId) && styles.isActive
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
href={{
|
||||
pathname: `/wiki/[wikiId]`,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { ILoginUser, IUser, UserApiDefinition } from '@think/domains';
|
||||
import { Toast } from '@douyinfe/semi-ui';
|
||||
import { ILoginUser, ISystemConfig, IUser, UserApiDefinition } from '@think/domains';
|
||||
import { getStorage, setStorage } from 'helpers/storage';
|
||||
import { useAsyncLoading } from 'hooks/use-async-loading';
|
||||
import Router, { useRouter } from 'next/router';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
@ -16,6 +18,63 @@ export const toLogin = () => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
* @returns
|
||||
*/
|
||||
export const useVerifyCode = () => {
|
||||
const [sendVerifyCode, loading] = useAsyncLoading((params: { email: string }) =>
|
||||
HttpClient.request({
|
||||
method: UserApiDefinition.sendVerifyCode.method,
|
||||
url: UserApiDefinition.sendVerifyCode.client(),
|
||||
params,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
sendVerifyCode,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册
|
||||
* @returns
|
||||
*/
|
||||
export const useRegister = () => {
|
||||
const [registerWithLoading, loading] = useAsyncLoading((data) =>
|
||||
HttpClient.request({
|
||||
method: UserApiDefinition.register.method,
|
||||
url: UserApiDefinition.register.client(),
|
||||
data,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
register: registerWithLoading,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
* @returns
|
||||
*/
|
||||
export const useResetPassword = () => {
|
||||
const [resetPasswordWithLoading, loading] = useAsyncLoading((data) =>
|
||||
HttpClient.request({
|
||||
method: UserApiDefinition.resetPassword.method,
|
||||
url: UserApiDefinition.resetPassword.client(),
|
||||
data,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
reset: resetPasswordWithLoading,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
export const useUser = () => {
|
||||
const router = useRouter();
|
||||
const { data, error, refetch } = useQuery<ILoginUser>('user', () => {
|
||||
|
@ -78,3 +137,40 @@ export const useUser = () => {
|
|||
updateUser,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
* @returns
|
||||
*/
|
||||
export const useSystemConfig = () => {
|
||||
const { data, error, isLoading, refetch } = useQuery(UserApiDefinition.getSystemConfig.client(), () =>
|
||||
HttpClient.request<ISystemConfig>({
|
||||
method: UserApiDefinition.getSystemConfig.method,
|
||||
url: UserApiDefinition.getSystemConfig.client(),
|
||||
})
|
||||
);
|
||||
|
||||
const sendTestEmail = useCallback(async () => {
|
||||
return await HttpClient.request<ISystemConfig>({
|
||||
method: UserApiDefinition.sendTestEmail.method,
|
||||
url: UserApiDefinition.sendTestEmail.client(),
|
||||
}).then(() => {
|
||||
Toast.success('测试邮件发送成功');
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateSystemConfig = useCallback(
|
||||
async (data: Partial<ISystemConfig>) => {
|
||||
const ret = await HttpClient.request<ISystemConfig>({
|
||||
method: UserApiDefinition.updateSystemConfig.method,
|
||||
url: UserApiDefinition.updateSystemConfig.client(),
|
||||
data,
|
||||
});
|
||||
refetch();
|
||||
return ret;
|
||||
},
|
||||
[refetch]
|
||||
);
|
||||
|
||||
return { data, error, loading: isLoading, refresh: refetch, sendTestEmail, updateSystemConfig };
|
||||
};
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useToggle } from './use-toggle';
|
||||
import { useIsomorphicLayoutEffect } from './user-isomorphic-layout-effect';
|
||||
|
||||
export const useInterval = (callback: () => void, delay: number) => {
|
||||
const savedCallback = useRef(callback);
|
||||
const timer = useRef(null);
|
||||
const [canActive, toggleCanActive] = useToggle(false);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canActive) return;
|
||||
|
||||
clearInterval(timer.current);
|
||||
timer.current = setInterval(() => savedCallback.current(), delay);
|
||||
|
||||
return () => clearInterval(timer.current);
|
||||
}, [canActive, delay]);
|
||||
|
||||
const start = useCallback(() => {
|
||||
toggleCanActive(true);
|
||||
}, [toggleCanActive]);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
clearInterval(timer.current);
|
||||
toggleCanActive(false);
|
||||
}, [toggleCanActive]);
|
||||
|
||||
return { start, stop };
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
import { useEffect, useLayoutEffect } from 'react';
|
||||
|
||||
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
@ -0,0 +1,225 @@
|
|||
import React from 'react';
|
||||
|
||||
export const Forbidden = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
data-name="Layer 1"
|
||||
width="20em"
|
||||
height="15em"
|
||||
viewBox="0 0 807.45276 499.98424"
|
||||
>
|
||||
<path
|
||||
id="ad903c08-5677-4dbe-a9c7-05a0eb46801f-129"
|
||||
data-name="Path 461"
|
||||
d="M252.30849,663.16553a22.728,22.728,0,0,0,21.947-3.866c7.687-6.452,10.1-17.081,12.058-26.924l5.8-29.112-12.143,8.362c-8.733,6.013-17.662,12.219-23.709,20.929s-8.686,20.6-3.828,30.024"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#e6e6e6"
|
||||
/>
|
||||
<path
|
||||
id="a94887ac-0642-4b28-b311-c351a0f7f12b-130"
|
||||
data-name="Path 462"
|
||||
d="M253.34651,698.41151c-1.229-8.953-2.493-18.02-1.631-27.069.766-8.036,3.217-15.885,8.209-22.321a37.13141,37.13141,0,0,1,9.527-8.633c.953-.6,1.829.909.881,1.507a35.29989,35.29989,0,0,0-13.963,16.847c-3.04,7.732-3.528,16.161-3,24.374.317,4.967.988,9.9,1.665,14.83a.9.9,0,0,1-.61,1.074.878.878,0,0,1-1.074-.61Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#f2f2f2"
|
||||
/>
|
||||
<path
|
||||
d="M496.87431,505.52556a6.9408,6.9408,0,0,1-2.85071.67077l-91.60708,2.51425a14.3796,14.3796,0,0,1-.62506-28.75241l91.60729-2.51381a7.00744,7.00744,0,0,1,7.15064,6.8456l.32069,14.75586a7.01658,7.01658,0,0,1-3.99577,6.47974Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#6c63ff"
|
||||
/>
|
||||
<path
|
||||
d="M379.332,698.59808H364.57245a7.00786,7.00786,0,0,1-7-7V568.58392a7.00785,7.00785,0,0,1,7-7H379.332a7.00786,7.00786,0,0,1,7,7V691.59808A7.00787,7.00787,0,0,1,379.332,698.59808Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#6c63ff"
|
||||
/>
|
||||
<path
|
||||
d="M418.52435,698.59808H403.76459a7.00786,7.00786,0,0,1-7-7V568.58392a7.00785,7.00785,0,0,1,7-7h14.75976a7.00786,7.00786,0,0,1,7,7V691.59808A7.00787,7.00787,0,0,1,418.52435,698.59808Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#6c63ff"
|
||||
/>
|
||||
<circle cx="196.71571" cy="182.69717" r="51" fill="#6c63ff" />
|
||||
<path
|
||||
d="M410.30072,605.205H373.61127a43.27708,43.27708,0,0,1-37.56043-65.05664l51.30933-88.87012a6.5,6.5,0,0,1,11.2583,0l50.27612,87.08057A44.56442,44.56442,0,0,1,410.30072,605.205Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
<path
|
||||
d="M405.02686,404.114c3.30591-.0918,7.42029-.20655,10.59-2.522a8.13274,8.13274,0,0,0,3.20007-6.07275,5.47084,5.47084,0,0,0-1.86035-4.49315c-1.65552-1.39894-4.073-1.72706-6.67823-.96144l2.69922-19.72558-1.98144-.27149-3.17322,23.18994,1.65466-.75928c1.91834-.87988,4.55164-1.32763,6.188.05518a3.51514,3.51514,0,0,1,1.15271,2.89551,6.14686,6.14686,0,0,1-2.38122,4.52783c-2.46668,1.80176-5.74622,2.03418-9.46582,2.13818Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
<rect x="226.50312" y="172.03238" width="10.77161" height="2" fill="#2f2e41" />
|
||||
<rect x="192.50312" y="172.03238" width="10.77161" height="2" fill="#2f2e41" />
|
||||
<path
|
||||
d="M380.99359,593.79839a6.94088,6.94088,0,0,1-.67077-2.85072l-2.51425-91.60708a14.3796,14.3796,0,0,1,28.75241-.62506l2.51381,91.60729a7.00744,7.00744,0,0,1-6.8456,7.15064l-14.75586.32069a7.01655,7.01655,0,0,1-6.47974-3.99576Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#6c63ff"
|
||||
/>
|
||||
<path
|
||||
d="M388.25747,345.00549c6.19637,8.10336,16.033,13.53931,26.42938,12.25223,9.90031-1.22567,18.06785-8.12619,20.117-18.0055a29.66978,29.66978,0,0,0-7.79665-26.1905c-7.00748-7.37032-17.03634-11.335-26.96311-12.69456-18.80446-2.57537-38.1172,4.04852-52.33518,16.4023a64.1102,64.1102,0,0,0-16.69251,22.37513,62.72346,62.72346,0,0,0-5.175,27.07767c.54633,18.375,8.595,36.71479,22.48271,48.90083a63.37666,63.37666,0,0,0,5.40808,4.23578c1.58387,1.11112,3.08464-1.48868,1.51415-2.59042-14.222-9.977-23.29362-26.21093-25.78338-43.26844a59.92391,59.92391,0,0,1,14.05278-48.33971c11.48411-13.058,28.32271-21.54529,45.7628-22.30575,17.54894-.76521,39.47915,7.06943,42.7631,26.60435,1.47191,8.7558-1.801,17.95926-9.82454,22.3428-8.59053,4.69326-19.12416,2.76181-26.50661-3.29945a30.448,30.448,0,0,1-4.86258-5.01092c-1.157-1.51313-3.76387-.02044-2.59041,1.51416Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
<rect
|
||||
id="fc777aff-63b1-4720-84dc-e3a9c20790b9"
|
||||
data-name="ab2e16f2-9798-47da-b25d-769524f3c86f"
|
||||
x="484.20919"
|
||||
y="242.03206"
|
||||
width="437.1948"
|
||||
height="207.45652"
|
||||
transform="translate(-238.48792 -95.97299) rotate(-8.21995)"
|
||||
fill="#f1f1f1"
|
||||
/>
|
||||
<rect
|
||||
id="ecffa418-b240-4504-be04-512edea7ccda"
|
||||
data-name="bf81c03f-68cf-4889-8697-1102f95f97bb"
|
||||
x="496.79745"
|
||||
y="259.81556"
|
||||
width="412.19197"
|
||||
height="173.08746"
|
||||
transform="translate(-238.57266 -95.95442) rotate(-8.21995)"
|
||||
fill="#fff"
|
||||
/>
|
||||
<rect
|
||||
id="b49ce3f1-9d75-4481-986b-3b6beb000c79"
|
||||
data-name="f065dccc-d150-492a-a09f-a7f3f89523f0"
|
||||
x="468.80837"
|
||||
y="231.16611"
|
||||
width="437.19481"
|
||||
height="18.57334"
|
||||
transform="translate(-223.58995 -99.25677) rotate(-8.21995)"
|
||||
fill="#e5e5e5"
|
||||
/>
|
||||
<circle
|
||||
id="a4219562-805a-49cd-8b89-b1f92f7a9e75"
|
||||
data-name="bdbbf39c-df25-4682-8b85-5a6af4a1bd14"
|
||||
cx="288.67474"
|
||||
cy="71.34324"
|
||||
r="3.4425"
|
||||
fill="#fff"
|
||||
/>
|
||||
<circle
|
||||
id="b0f6399c-6944-4f74-a888-473f61f9730c"
|
||||
data-name="abcd4292-0b1f-4102-9b5e-e8bbd87baabc"
|
||||
cx="301.6071"
|
||||
cy="69.47507"
|
||||
r="3.4425"
|
||||
fill="#fff"
|
||||
/>
|
||||
<circle
|
||||
id="b03f93dc-2c99-4323-9b17-02f51b8830c0"
|
||||
data-name="a3fb731e-8b3d-41ca-96f2-91600dc0b434"
|
||||
cx="314.54005"
|
||||
cy="67.6068"
|
||||
r="3.4425"
|
||||
fill="#fff"
|
||||
/>
|
||||
<rect
|
||||
id="a6067cfc-0392-4d68-afe4-e34d11a8f0ac"
|
||||
data-name="ab2e16f2-9798-47da-b25d-769524f3c86f"
|
||||
x="370.25796"
|
||||
y="100.18309"
|
||||
width="437.1948"
|
||||
height="207.45652"
|
||||
fill="#e6e6e6"
|
||||
/>
|
||||
<rect
|
||||
id="ecd65817-7467-4dbd-a435-c0f1d9841c98"
|
||||
data-name="bf81c03f-68cf-4889-8697-1102f95f97bb"
|
||||
x="382.75969"
|
||||
y="117.97286"
|
||||
width="412.19197"
|
||||
height="173.08746"
|
||||
fill="#fff"
|
||||
/>
|
||||
<rect
|
||||
id="eea6c39d-8a45-4eb1-bab9-6120f465de14"
|
||||
data-name="f065dccc-d150-492a-a09f-a7f3f89523f0"
|
||||
x="370.07154"
|
||||
y="88.19711"
|
||||
width="437.19481"
|
||||
height="18.57334"
|
||||
fill="#cbcbcb"
|
||||
/>
|
||||
<circle
|
||||
id="ab9e51f9-7431-4d30-8193-f9435a6bd5c3"
|
||||
data-name="bdbbf39c-df25-4682-8b85-5a6af4a1bd14"
|
||||
cx="383.87383"
|
||||
cy="99.11864"
|
||||
r="3.4425"
|
||||
fill="#fff"
|
||||
/>
|
||||
<circle
|
||||
id="a54ed687-3b0d-413b-b405-af8897a5c032"
|
||||
data-name="abcd4292-0b1f-4102-9b5e-e8bbd87baabc"
|
||||
cx="396.94043"
|
||||
cy="99.11864"
|
||||
r="3.4425"
|
||||
fill="#fff"
|
||||
/>
|
||||
<circle
|
||||
id="fd1d2195-7e97-488f-8f4b-7061a06deb9a"
|
||||
data-name="a3fb731e-8b3d-41ca-96f2-91600dc0b434"
|
||||
cx="410.00762"
|
||||
cy="99.11864"
|
||||
r="3.4425"
|
||||
fill="#fff"
|
||||
/>
|
||||
<rect x="620.27691" y="144.28855" width="58.05212" height="4.36334" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="157.09784" width="89.64514" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="621.20899" y="169.29697" width="73.05881" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="182.68222" width="42.65054" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="195.75677" width="64.37073" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="142.916" width="7.10843" height="7.10842" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="155.72527" width="7.10843" height="7.10841" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="167.92442" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="181.30967" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="194.38423" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="208.91306" width="58.05212" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="221.72236" width="89.64514" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="621.20899" y="233.92149" width="73.05881" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="247.30674" width="42.65054" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="620.27691" y="260.38129" width="64.37073" height="4.36332" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="207.54051" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="220.34979" width="7.10843" height="7.10841" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="232.54894" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="245.93419" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="593.81776" y="259.00875" width="7.10843" height="7.10843" fill="#e6e6e6" />
|
||||
<rect x="436.63003" y="243.13905" width="58.05213" height="4.36333" fill="#e6e6e6" />
|
||||
<rect x="428.86266" y="254.4769" width="73.05881" height="4.36332" fill="#e6e6e6" />
|
||||
<path
|
||||
d="M699.66075,388.1056a37.91872,37.91872,0,0,1-55.87819,33.382l-.00736-.00737a37.907,37.907,0,1,1,55.88555-33.37461Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#e6e6e6"
|
||||
/>
|
||||
<circle cx="465.67554" cy="175.95347" r="10.30421" fill="#fff" />
|
||||
<path
|
||||
d="M679.54362,407.55657a53.11056,53.11056,0,0,1-35.56788-.13775l-.00738-.0051,7.6766-15.15329h20.60841Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M547.86351,482.19293c-17.96014,0-32.5719-15.52155-32.5719-34.60067,0-19.07858,14.61176-34.60014,32.5719-34.60014s32.5719,15.52156,32.5719,34.60014C580.43541,466.67138,565.82365,482.19293,547.86351,482.19293Zm0-60.4582c-13.13954,0-23.82929,11.59955-23.82929,25.85753s10.68975,25.85806,23.82929,25.85806,23.82928-11.60008,23.82928-25.85806S561.00305,421.73473,547.86351,421.73473Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#6c63ff"
|
||||
/>
|
||||
<path
|
||||
d="M578.70786,542.49212h-61.6887a20.54138,20.54138,0,0,1-20.51852-20.51826V461.46391a14.06356,14.06356,0,0,1,14.04747-14.04774h74.6308a14.06356,14.06356,0,0,1,14.04747,14.04774v60.50995A20.54138,20.54138,0,0,1,578.70786,542.49212Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#3f3d56"
|
||||
/>
|
||||
<path
|
||||
d="M559.88461,481.84022a12.0211,12.0211,0,1,0-17.48524,10.69829v18.808h10.92827v-18.808A12.01088,12.01088,0,0,0,559.88461,481.84022Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M578.27362,699.99212h-381a1,1,0,0,1,0-2h381a1,1,0,0,1,0,2Z"
|
||||
transform="translate(-196.27362 -200.00788)"
|
||||
fill="#3f3d56"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
|
@ -38,7 +38,7 @@ const menus = [
|
|||
},
|
||||
{
|
||||
itemKey: '/star',
|
||||
text: '收藏',
|
||||
text: '星标',
|
||||
onClick: () => {
|
||||
Router.push({
|
||||
pathname: `/star`,
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
.wikiItemWrap {
|
||||
padding: 12px 16px !important;
|
||||
margin: 8px 2px;
|
||||
cursor: pointer;
|
||||
background-color: var(--semi-color-bg-2);
|
||||
border: 1px solid var(--semi-color-border) !important;
|
||||
}
|
||||
|
||||
.titleWrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { Typography } from '@douyinfe/semi-ui';
|
||||
import { SystemConfig } from 'components/admin/system-config';
|
||||
import { Seo } from 'components/seo';
|
||||
import { useUser } from 'data/user';
|
||||
import { Forbidden } from 'illustrations/forbidden';
|
||||
import { SingleColumnLayout } from 'layouts/single-column';
|
||||
import type { NextPage } from 'next';
|
||||
import Router, { useRouter } from 'next/router';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const Page: NextPage = () => {
|
||||
const { user } = useUser();
|
||||
const { query = {} } = useRouter();
|
||||
const { tab = 'base' } = query as {
|
||||
tab?: string;
|
||||
};
|
||||
|
||||
const navigate = useCallback((tab = 'base') => {
|
||||
Router.push({
|
||||
pathname: `/admin`,
|
||||
query: { tab },
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SingleColumnLayout>
|
||||
<Seo title="管理后台" />
|
||||
<div className="container">
|
||||
{user && user.isSystemAdmin ? (
|
||||
<>
|
||||
<div className={styles.titleWrap}>
|
||||
<Title heading={3} style={{ margin: '8px 0' }}>
|
||||
管理后台
|
||||
</Title>
|
||||
</div>
|
||||
<SystemConfig tab={tab} onNavigate={navigate} />
|
||||
</>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Forbidden />
|
||||
<Text strong type="danger">
|
||||
无权限查看
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SingleColumnLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,35 @@
|
|||
.wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--semi-color-bg-0);
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
padding: 10vh 24px;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.form {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 32px 24px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
|
||||
footer {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--semi-color-border);
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
import { Button, Col, Form, Layout, Modal, Row, Space, Toast, Typography } from '@douyinfe/semi-ui';
|
||||
import { Author } from 'components/author';
|
||||
import { LogoImage, LogoText } from 'components/logo';
|
||||
import { Seo } from 'components/seo';
|
||||
import { useResetPassword, useVerifyCode } from 'data/user';
|
||||
import { useInterval } from 'hooks/use-interval';
|
||||
import { useRouterQuery } from 'hooks/use-router-query';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import Link from 'next/link';
|
||||
import Router from 'next/router';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Content, Footer } = Layout;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const Page = () => {
|
||||
const query = useRouterQuery();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [hasSendVerifyCode, toggleHasSendVerifyCode] = useToggle(false);
|
||||
const [countDown, setCountDown] = useState(0);
|
||||
const { reset, loading } = useResetPassword();
|
||||
const { sendVerifyCode, loading: sendVerifyCodeLoading } = useVerifyCode();
|
||||
|
||||
const onFormChange = useCallback((formState) => {
|
||||
setEmail(formState.values.email);
|
||||
}, []);
|
||||
|
||||
const { start, stop } = useInterval(() => {
|
||||
setCountDown((v) => {
|
||||
if (v - 1 <= 0) {
|
||||
stop();
|
||||
toggleHasSendVerifyCode(false);
|
||||
return 0;
|
||||
}
|
||||
return v - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
const onFinish = useCallback(
|
||||
(values) => {
|
||||
reset(values).then((res) => {
|
||||
Modal.confirm({
|
||||
title: <Title heading={5}>密码修改成功</Title>,
|
||||
content: <Text>是否跳转至登录?</Text>,
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk() {
|
||||
Router.push('/login', { query });
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[reset, query]
|
||||
);
|
||||
|
||||
const getVerifyCode = useCallback(() => {
|
||||
stop();
|
||||
sendVerifyCode({ email })
|
||||
.then(() => {
|
||||
Toast.success('请前往邮箱查收验证码');
|
||||
setCountDown(60);
|
||||
start();
|
||||
toggleHasSendVerifyCode(true);
|
||||
})
|
||||
.catch(() => {
|
||||
toggleHasSendVerifyCode(false);
|
||||
});
|
||||
}, [email, toggleHasSendVerifyCode, sendVerifyCode, start, stop]);
|
||||
|
||||
return (
|
||||
<Layout className={styles.wrap}>
|
||||
<Seo title="重置密码" />
|
||||
<Content className={styles.content}>
|
||||
<Title heading={4} style={{ marginBottom: 16, textAlign: 'center' }}>
|
||||
<Space>
|
||||
<LogoImage></LogoImage>
|
||||
<LogoText></LogoText>
|
||||
</Space>
|
||||
</Title>
|
||||
<Form
|
||||
className={styles.form}
|
||||
initValues={{ name: '', password: '' }}
|
||||
onChange={onFormChange}
|
||||
onSubmit={onFinish}
|
||||
>
|
||||
<Title type="tertiary" heading={5} style={{ marginBottom: 16, textAlign: 'center' }}>
|
||||
重置密码
|
||||
</Title>
|
||||
|
||||
<Form.Input
|
||||
noLabel
|
||||
field="email"
|
||||
placeholder={'请输入邮箱'}
|
||||
rules={[
|
||||
{
|
||||
type: 'email',
|
||||
message: '请输入正确的邮箱地址!',
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
message: '请输入邮箱地址!',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Row gutter={8} style={{ paddingTop: 12 }}>
|
||||
<Col span={16}>
|
||||
<Form.Input
|
||||
noLabel
|
||||
fieldStyle={{ paddingTop: 0 }}
|
||||
placeholder={'请输入验证码'}
|
||||
field="verifyCode"
|
||||
rules={[{ required: true, message: '请输入邮箱收到的验证码!' }]}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button disabled={!email || countDown > 0} loading={sendVerifyCodeLoading} onClick={getVerifyCode} block>
|
||||
{hasSendVerifyCode ? countDown : '获取验证码'}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Input
|
||||
noLabel
|
||||
mode="password"
|
||||
field="password"
|
||||
label="密码"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="输入用户密码"
|
||||
rules={[{ required: true, message: '请输入新密码' }]}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
noLabel
|
||||
mode="password"
|
||||
field="confirmPassword"
|
||||
label="密码"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="确认用户密码"
|
||||
rules={[{ required: true, message: '请再次输入密码' }]}
|
||||
/>
|
||||
|
||||
<Button htmlType="submit" type="primary" theme="solid" block loading={loading} style={{ margin: '16px 0' }}>
|
||||
重置密码
|
||||
</Button>
|
||||
<footer>
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/login',
|
||||
query,
|
||||
}}
|
||||
>
|
||||
<Text link style={{ textAlign: 'center' }}>
|
||||
去登录
|
||||
</Text>
|
||||
</Link>
|
||||
</footer>
|
||||
</Form>
|
||||
</Content>
|
||||
<Footer>
|
||||
<Author></Author>
|
||||
</Footer>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
|
@ -51,9 +51,10 @@ const Page = () => {
|
|||
field="name"
|
||||
label="账户"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="输入账户名称"
|
||||
rules={[{ required: true, message: '请输入账户' }]}
|
||||
placeholder="输入账户名称或邮箱"
|
||||
rules={[{ required: true, message: '请输入账户或邮箱' }]}
|
||||
></Form.Input>
|
||||
|
||||
<Form.Input
|
||||
noLabel
|
||||
mode="password"
|
||||
|
@ -67,16 +68,29 @@ const Page = () => {
|
|||
登录
|
||||
</Button>
|
||||
<footer>
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/register',
|
||||
query,
|
||||
}}
|
||||
>
|
||||
<Text link style={{ textAlign: 'center' }}>
|
||||
注册用户以登录
|
||||
</Text>
|
||||
</Link>
|
||||
<Space>
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/register',
|
||||
query,
|
||||
}}
|
||||
>
|
||||
<Text link style={{ textAlign: 'center' }}>
|
||||
注册用户以登录
|
||||
</Text>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/forgetPassword',
|
||||
query,
|
||||
}}
|
||||
>
|
||||
<a>
|
||||
<Text type="tertiary">忘记密码?</Text>
|
||||
</a>
|
||||
</Link>
|
||||
</Space>
|
||||
</footer>
|
||||
</Form>
|
||||
</Content>
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
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 { 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 React, { useCallback, useState } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
@ -16,21 +17,58 @@ 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) => {
|
||||
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) => {
|
||||
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,7 +80,12 @@ 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>
|
||||
|
@ -72,6 +115,40 @@ const Page = () => {
|
|||
placeholder="确认用户密码"
|
||||
rules={[{ required: true, message: '请再次输入密码' }]}
|
||||
></Form.Input>
|
||||
|
||||
<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>
|
||||
|
||||
<Button htmlType="submit" type="primary" theme="solid" block loading={loading} style={{ margin: '16px 0' }}>
|
||||
注册
|
||||
</Button>
|
||||
|
|
|
@ -7,6 +7,14 @@ export declare const UserApiDefinition: {
|
|||
server: "/";
|
||||
client: () => string;
|
||||
};
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
sendVerifyCode: {
|
||||
method: "get";
|
||||
server: "sendVerifyCode";
|
||||
client: () => string;
|
||||
};
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
|
@ -15,6 +23,14 @@ export declare const UserApiDefinition: {
|
|||
server: "register";
|
||||
client: () => string;
|
||||
};
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
resetPassword: {
|
||||
method: "post";
|
||||
server: "resetPassword";
|
||||
client: () => string;
|
||||
};
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
|
@ -39,4 +55,36 @@ export declare const UserApiDefinition: {
|
|||
server: "update";
|
||||
client: () => string;
|
||||
};
|
||||
/**
|
||||
* 锁定用户
|
||||
*/
|
||||
toggleLockUser: {
|
||||
method: "post";
|
||||
server: "lock/user";
|
||||
client: () => string;
|
||||
};
|
||||
/**
|
||||
* 获取系统配置
|
||||
*/
|
||||
getSystemConfig: {
|
||||
method: "get";
|
||||
server: "config/system";
|
||||
client: () => string;
|
||||
};
|
||||
/**
|
||||
* 发送测试邮件
|
||||
*/
|
||||
sendTestEmail: {
|
||||
method: "get";
|
||||
server: "config/system/sendTestEmail";
|
||||
client: () => string;
|
||||
};
|
||||
/**
|
||||
* 发送测试邮件
|
||||
*/
|
||||
updateSystemConfig: {
|
||||
method: "post";
|
||||
server: "config/system/updateSystemConfig";
|
||||
client: () => string;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -10,6 +10,14 @@ exports.UserApiDefinition = {
|
|||
server: '/',
|
||||
client: function () { return '/user'; }
|
||||
},
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
sendVerifyCode: {
|
||||
method: 'get',
|
||||
server: 'sendVerifyCode',
|
||||
client: function () { return '/verify/sendVerifyCode'; }
|
||||
},
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
|
@ -18,6 +26,14 @@ exports.UserApiDefinition = {
|
|||
server: 'register',
|
||||
client: function () { return '/user/register'; }
|
||||
},
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
resetPassword: {
|
||||
method: 'post',
|
||||
server: 'resetPassword',
|
||||
client: function () { return '/user/resetPassword'; }
|
||||
},
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
|
@ -41,5 +57,37 @@ exports.UserApiDefinition = {
|
|||
method: 'patch',
|
||||
server: 'update',
|
||||
client: function () { return "/user/update"; }
|
||||
},
|
||||
/**
|
||||
* 锁定用户
|
||||
*/
|
||||
toggleLockUser: {
|
||||
method: 'post',
|
||||
server: 'lock/user',
|
||||
client: function () { return "/user/lock/user"; }
|
||||
},
|
||||
/**
|
||||
* 获取系统配置
|
||||
*/
|
||||
getSystemConfig: {
|
||||
method: 'get',
|
||||
server: 'config/system',
|
||||
client: function () { return "/user/config/system"; }
|
||||
},
|
||||
/**
|
||||
* 发送测试邮件
|
||||
*/
|
||||
sendTestEmail: {
|
||||
method: 'get',
|
||||
server: 'config/system/sendTestEmail',
|
||||
client: function () { return "/user/config/system/sendTestEmail"; }
|
||||
},
|
||||
/**
|
||||
* 发送测试邮件
|
||||
*/
|
||||
updateSystemConfig: {
|
||||
method: 'post',
|
||||
server: 'config/system/updateSystemConfig',
|
||||
client: function () { return "/user/config/system/updateSystemConfig"; }
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,3 +5,4 @@ export * from './message';
|
|||
export * from './template';
|
||||
export * from './comment';
|
||||
export * from './pagination';
|
||||
export * from './system';
|
||||
|
|
|
@ -17,3 +17,4 @@ __exportStar(require("./message"), exports);
|
|||
__exportStar(require("./template"), exports);
|
||||
__exportStar(require("./comment"), exports);
|
||||
__exportStar(require("./pagination"), exports);
|
||||
__exportStar(require("./system"), exports);
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export interface ISystemConfig {
|
||||
isSystemLocked: boolean;
|
||||
emailServiceHost: string;
|
||||
emailServicePassword: string;
|
||||
emailServicePort: string;
|
||||
emailServiceUser: string;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
"use strict";
|
||||
exports.__esModule = true;
|
|
@ -24,6 +24,7 @@ export interface IUser {
|
|||
email?: string;
|
||||
role: UserRole;
|
||||
status: UserStatus;
|
||||
isSystemAdmin?: boolean;
|
||||
}
|
||||
/**
|
||||
* 登录用户数据定义
|
||||
|
|
|
@ -10,6 +10,15 @@ export const UserApiDefinition = {
|
|||
client: () => '/user',
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
sendVerifyCode: {
|
||||
method: 'get' as const,
|
||||
server: 'sendVerifyCode' as const,
|
||||
client: () => '/verify/sendVerifyCode',
|
||||
},
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
|
@ -19,6 +28,15 @@ export const UserApiDefinition = {
|
|||
client: () => '/user/register',
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
resetPassword: {
|
||||
method: 'post' as const,
|
||||
server: 'resetPassword' as const,
|
||||
client: () => '/user/resetPassword',
|
||||
},
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
|
@ -45,4 +63,40 @@ export const UserApiDefinition = {
|
|||
server: 'update' as const,
|
||||
client: () => `/user/update`,
|
||||
},
|
||||
|
||||
/**
|
||||
* 锁定用户
|
||||
*/
|
||||
toggleLockUser: {
|
||||
method: 'post' as const,
|
||||
server: 'lock/user' as const,
|
||||
client: () => `/user/lock/user`,
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取系统配置
|
||||
*/
|
||||
getSystemConfig: {
|
||||
method: 'get' as const,
|
||||
server: 'config/system' as const,
|
||||
client: () => `/user/config/system`,
|
||||
},
|
||||
|
||||
/**
|
||||
* 发送测试邮件
|
||||
*/
|
||||
sendTestEmail: {
|
||||
method: 'get' as const,
|
||||
server: 'config/system/sendTestEmail' as const,
|
||||
client: () => `/user/config/system/sendTestEmail`,
|
||||
},
|
||||
|
||||
/**
|
||||
* 发送测试邮件
|
||||
*/
|
||||
updateSystemConfig: {
|
||||
method: 'post' as const,
|
||||
server: 'config/system/updateSystemConfig' as const,
|
||||
client: () => `/user/config/system/updateSystemConfig`,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,3 +5,4 @@ export * from './message';
|
|||
export * from './template';
|
||||
export * from './comment';
|
||||
export * from './pagination';
|
||||
export * from './system';
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export interface ISystemConfig {
|
||||
isSystemLocked: boolean;
|
||||
emailServiceHost: string;
|
||||
emailServicePassword: string;
|
||||
emailServicePort: string;
|
||||
emailServiceUser: string;
|
||||
}
|
|
@ -26,6 +26,7 @@ export interface IUser {
|
|||
email?: string;
|
||||
role: UserRole;
|
||||
status: UserStatus;
|
||||
isSystemAdmin?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
"lodash": "^4.17.21",
|
||||
"mysql2": "^2.3.3",
|
||||
"nestjs-pino": "^2.5.2",
|
||||
"nodemailer": "^6.7.5",
|
||||
"nuid": "^1.1.6",
|
||||
"passport": "^0.5.2",
|
||||
"passport-jwt": "^4.0.0",
|
||||
|
@ -61,6 +62,7 @@
|
|||
"rxjs": "^7.2.0",
|
||||
"typeorm": "^0.2.41",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"validator": "^13.7.0",
|
||||
"y-prosemirror": "^1.0.14",
|
||||
"yjs": "^13.5.24"
|
||||
},
|
||||
|
|
|
@ -3,8 +3,10 @@ import { DocumentEntity } from '@entities/document.entity';
|
|||
import { DocumentAuthorityEntity } from '@entities/document-authority.entity';
|
||||
import { MessageEntity } from '@entities/message.entity';
|
||||
import { StarEntity } from '@entities/star.entity';
|
||||
import { SystemEntity } from '@entities/system.entity';
|
||||
import { TemplateEntity } from '@entities/template.entity';
|
||||
import { UserEntity } from '@entities/user.entity';
|
||||
import { VerifyEntity } from '@entities/verify.entity';
|
||||
import { ViewEntity } from '@entities/view.entity';
|
||||
import { WikiEntity } from '@entities/wiki.entity';
|
||||
import { WikiUserEntity } from '@entities/wiki-user.entity';
|
||||
|
@ -15,8 +17,10 @@ import { DocumentModule } from '@modules/document.module';
|
|||
import { FileModule } from '@modules/file.module';
|
||||
import { MessageModule } from '@modules/message.module';
|
||||
import { StarModule } from '@modules/star.module';
|
||||
import { SystemModule } from '@modules/system.module';
|
||||
import { TemplateModule } from '@modules/template.module';
|
||||
import { UserModule } from '@modules/user.module';
|
||||
import { VerifyModule } from '@modules/verify.module';
|
||||
import { ViewModule } from '@modules/view.module';
|
||||
import { WikiModule } from '@modules/wiki.module';
|
||||
import { forwardRef, Inject, Module } from '@nestjs/common';
|
||||
|
@ -40,6 +44,8 @@ const ENTITIES = [
|
|||
MessageEntity,
|
||||
TemplateEntity,
|
||||
ViewEntity,
|
||||
VerifyEntity,
|
||||
SystemEntity,
|
||||
];
|
||||
|
||||
const MODULES = [
|
||||
|
@ -52,6 +58,8 @@ const MODULES = [
|
|||
MessageModule,
|
||||
TemplateModule,
|
||||
ViewModule,
|
||||
VerifyModule,
|
||||
SystemModule,
|
||||
];
|
||||
|
||||
@Module({
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { Controller } from '@nestjs/common';
|
||||
import { SystemService } from '@services/system.service';
|
||||
|
||||
@Controller('system')
|
||||
export class SystemController {
|
||||
constructor(private readonly systemService: SystemService) {}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { CreateUserDto } from '@dtos/create-user.dto';
|
||||
import { RegisterUserDto, ResetPasswordDto } from '@dtos/create-user.dto';
|
||||
import { LoginUserDto } from '@dtos/login-user.dto';
|
||||
import { UpdateUserDto } from '@dtos/update-user.dto';
|
||||
import { JwtGuard } from '@guard/jwt.guard';
|
||||
|
@ -11,6 +11,7 @@ import {
|
|||
HttpStatus,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
Request,
|
||||
Res,
|
||||
UseGuards,
|
||||
|
@ -41,7 +42,7 @@ export class UserController {
|
|||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@Post(UserApiDefinition.register.server)
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
async register(@Body() user: CreateUserDto) {
|
||||
async register(@Body() user: RegisterUserDto) {
|
||||
return await this.userService.createUser(user);
|
||||
}
|
||||
|
||||
|
@ -62,6 +63,16 @@ export class UserController {
|
|||
return { ...data, token };
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@Post(UserApiDefinition.resetPassword.server)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async resetPassword(@Body() user: ResetPasswordDto) {
|
||||
return await this.userService.resetPassword(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
|
@ -88,4 +99,48 @@ export class UserController {
|
|||
async updateUser(@Request() req, @Body() dto: UpdateUserDto) {
|
||||
return await this.userService.updateUser(req.user, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统配置
|
||||
*/
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@Get(UserApiDefinition.getSystemConfig.server)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(JwtGuard)
|
||||
async getSystemConfig(@Request() req) {
|
||||
return await this.userService.getSystemConfig(req.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测试邮件
|
||||
*/
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@Get(UserApiDefinition.sendTestEmail.server)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(JwtGuard)
|
||||
async sendTestEmail(@Request() req) {
|
||||
return await this.userService.sendTestEmail(req.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新系统配置
|
||||
*/
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@Post(UserApiDefinition.updateSystemConfig.server)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(JwtGuard)
|
||||
async toggleLockSystem(@Request() req, @Body() systemConfig) {
|
||||
return await this.userService.updateSystemConfig(req.user, systemConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定用户
|
||||
*/
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@Post(UserApiDefinition.toggleLockUser.server)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(JwtGuard)
|
||||
async toggleLockUser(@Request() req, @Query('targetUserId') targetUserId) {
|
||||
return await this.userService.toggleLockUser(req.user, targetUserId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import {
|
||||
ClassSerializerInterceptor,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Query,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { VerifyService } from '@services/verify.service';
|
||||
import { UserApiDefinition } from '@think/domains';
|
||||
|
||||
@Controller('verify')
|
||||
export class VerifyController {
|
||||
constructor(private readonly verifyService: VerifyService) {}
|
||||
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@Get(UserApiDefinition.sendVerifyCode.server)
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
async sendVerifyCode(@Query('email') email) {
|
||||
return await this.verifyService.sendVerifyCode(email);
|
||||
}
|
||||
}
|
|
@ -1,27 +1,57 @@
|
|||
import { IsEmail, IsNotEmpty, IsOptional, 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;
|
||||
@MinLength(5, { message: '用户二次确认密码至少5个字符' })
|
||||
@IsString({ message: '用户二次确认密码类型错误(正确类型为:String)' })
|
||||
@IsNotEmpty({ message: '用户二次确认密码不能为空' })
|
||||
confirmPassword: string;
|
||||
|
||||
@IsString({ message: '用户头像类型错误(正确类型为:String)' })
|
||||
@IsOptional()
|
||||
readonly avatar?: string;
|
||||
@IsEmail({ message: '请输入正确的邮箱地址' })
|
||||
@IsString({ message: '用户邮箱类型错误(正确类型为:String)' })
|
||||
@IsNotEmpty({ message: '用户邮箱不能为空' })
|
||||
email: string;
|
||||
|
||||
@IsString({ message: ' 用户邮箱类型错误(正确类型为:String)' })
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
readonly email?: string;
|
||||
@MinLength(5, { message: '邮箱验证码至少5个字符' })
|
||||
@IsString({ message: '邮箱验证码错误(正确类型为:String)' })
|
||||
@IsNotEmpty({ message: '邮箱验证码不能为空' })
|
||||
verifyCode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
export class ResetPasswordDto {
|
||||
@MinLength(5, { message: '用户密码至少5个字符' })
|
||||
@IsString({ message: '用户密码类型错误(正确类型为:String)' })
|
||||
@IsNotEmpty({ message: '用户密码不能为空' })
|
||||
password: string;
|
||||
|
||||
@MinLength(5, { message: '用户二次确认密码至少5个字符' })
|
||||
@IsString({ message: '用户二次确认密码类型错误(正确类型为:String)' })
|
||||
@IsNotEmpty({ message: '用户二次确认密码不能为空' })
|
||||
confirmPassword: string;
|
||||
|
||||
@IsEmail({ message: '请输入正确的邮箱地址' })
|
||||
@IsString({ message: '用户邮箱类型错误(正确类型为:String)' })
|
||||
@IsNotEmpty({ message: '用户邮箱不能为空' })
|
||||
email: string;
|
||||
|
||||
@MinLength(5, { message: '邮箱验证码至少5个字符' })
|
||||
@IsString({ message: '邮箱验证码错误(正确类型为:String)' })
|
||||
@IsNotEmpty({ message: '邮箱验证码不能为空' })
|
||||
verifyCode: string;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
|
||||
import { IsEmail, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
|
||||
|
||||
export class LoginUserDto {
|
||||
@IsString({ message: '用户名称类型错误(正确类型为:String)' })
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -28,12 +28,15 @@ export class UserEntity {
|
|||
@Column({ type: 'varchar', length: 200, comment: '用户加密密码' })
|
||||
public password: string;
|
||||
|
||||
@Column({ type: 'varchar', comment: '头像地址', default: '' })
|
||||
@Column({ type: 'varchar', length: 500, comment: '头像地址', default: '' })
|
||||
public avatar: string;
|
||||
|
||||
@Column({ type: 'varchar', comment: '邮箱地址', default: '' })
|
||||
@Column({ type: 'varchar', comment: '邮箱地址' })
|
||||
public email: string;
|
||||
|
||||
@Column({ type: 'boolean', default: false, comment: '是否为系统管理员' })
|
||||
isSystemAdmin: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: UserRole,
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('verify')
|
||||
export class VerifyEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
public id: string;
|
||||
|
||||
@Column({ type: 'varchar', comment: '邮箱地址' })
|
||||
public email: string;
|
||||
|
||||
@Column({ type: 'varchar', comment: '验证码' })
|
||||
public verifyCode: string;
|
||||
|
||||
@CreateDateColumn({
|
||||
type: 'timestamp',
|
||||
name: 'createdAt',
|
||||
comment: '创建时间',
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({
|
||||
type: 'timestamp',
|
||||
name: 'updatedAt',
|
||||
comment: '更新时间',
|
||||
})
|
||||
updatedAt: Date;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { SystemController } from '@controllers/system.controller';
|
||||
import { SystemEntity } from '@entities/system.entity';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { SystemService } from '@services/system.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([SystemEntity])],
|
||||
providers: [SystemService],
|
||||
exports: [SystemService],
|
||||
controllers: [SystemController],
|
||||
})
|
||||
export class SystemModule {}
|
|
@ -13,6 +13,9 @@ import { getConfig } from '@think/config';
|
|||
import { Request as RequestType } from 'express';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
|
||||
import { SystemModule } from './system.module';
|
||||
import { VerifyModule } from './verify.module';
|
||||
|
||||
const config = getConfig();
|
||||
const jwtConfig = config.jwt as {
|
||||
secretkey: string;
|
||||
|
@ -61,6 +64,8 @@ const jwtModule = JwtModule.register({
|
|||
forwardRef(() => WikiModule),
|
||||
forwardRef(() => MessageModule),
|
||||
forwardRef(() => StarModule),
|
||||
forwardRef(() => VerifyModule),
|
||||
forwardRef(() => SystemModule),
|
||||
passModule,
|
||||
jwtModule,
|
||||
],
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { VerifyController } from '@controllers/verify.controller';
|
||||
import { VerifyEntity } from '@entities/verify.entity';
|
||||
import { SystemModule } from '@modules/system.module';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { VerifyService } from '@services/verify.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([VerifyEntity]), forwardRef(() => SystemModule)],
|
||||
providers: [VerifyService],
|
||||
exports: [VerifyService],
|
||||
controllers: [VerifyController],
|
||||
})
|
||||
export class VerifyModule {}
|
|
@ -0,0 +1,121 @@
|
|||
import { SystemEntity } from '@entities/system.entity';
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class SystemService {
|
||||
constructor(
|
||||
@InjectRepository(SystemEntity)
|
||||
private readonly systemRepo: Repository<SystemEntity>,
|
||||
|
||||
private readonly confifgService: ConfigService
|
||||
) {
|
||||
this.loadFromConfigFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库获取配置
|
||||
* @returns
|
||||
*/
|
||||
public async getConfigFromDatabase() {
|
||||
const data = await this.systemRepo.find();
|
||||
return (data && data[0]) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新系统配置
|
||||
* @param patch
|
||||
* @returns
|
||||
*/
|
||||
public async updateConfigInDatabase(patch: Partial<SystemEntity>) {
|
||||
const current = await this.getConfigFromDatabase();
|
||||
return await this.systemRepo.save(await this.systemRepo.merge(current, patch));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从配置文件载入配置
|
||||
*/
|
||||
private async loadFromConfigFile() {
|
||||
const currentConfig = await this.getConfigFromDatabase();
|
||||
const emailConfigKeys = ['emailServiceHost', 'emailServicePort', 'emailServiceUser', 'emailServicePassword'];
|
||||
|
||||
if (currentConfig && emailConfigKeys.every((configKey) => Boolean(currentConfig[configKey]))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 同步邮件服务配置
|
||||
const emailConfigFromConfigFile = await this.confifgService.get('server.email');
|
||||
let emailConfig = {};
|
||||
if (emailConfigFromConfigFile && typeof emailConfigFromConfigFile === 'object') {
|
||||
emailConfig = {
|
||||
emailServiceHost: emailConfigFromConfigFile.host,
|
||||
emailServicePort: emailConfigFromConfigFile.port,
|
||||
emailServiceUser: emailConfigFromConfigFile.user,
|
||||
emailServicePassword: emailConfigFromConfigFile.password,
|
||||
};
|
||||
}
|
||||
|
||||
const newConfig = currentConfig
|
||||
? await this.systemRepo.merge(currentConfig, emailConfig)
|
||||
: await this.systemRepo.create(emailConfig);
|
||||
await this.systemRepo.save(newConfig);
|
||||
|
||||
console.log('[think] 已载入文件配置:', newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮件
|
||||
* @param content
|
||||
*/
|
||||
public async sendEmail(mail: { to: string; subject: string; text?: string; html?: string }) {
|
||||
const config = await this.getConfigFromDatabase();
|
||||
|
||||
if (!config) {
|
||||
throw new HttpException('系统未配置邮箱服务,请联系系统管理员', HttpStatus.SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
const emailConfig = {
|
||||
host: config.emailServiceHost,
|
||||
port: +config.emailServicePort,
|
||||
user: config.emailServiceUser,
|
||||
pass: config.emailServicePassword,
|
||||
};
|
||||
|
||||
if (Object.keys(emailConfig).some((key) => !emailConfig[key])) {
|
||||
throw new HttpException('系统邮箱服务配置不完善,请联系系统管理员', HttpStatus.SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: emailConfig.host,
|
||||
port: emailConfig.port,
|
||||
secure: emailConfig.port === 465,
|
||||
auth: {
|
||||
user: emailConfig.user,
|
||||
pass: emailConfig.pass,
|
||||
},
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error(`发送邮件失败`));
|
||||
}, 10 * 1000);
|
||||
|
||||
transporter.sendMail(
|
||||
{
|
||||
from: emailConfig.user,
|
||||
...mail,
|
||||
},
|
||||
(err, info) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(info);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { CreateUserDto } from '@dtos/create-user.dto';
|
||||
import { RegisterUserDto, ResetPasswordDto } from '@dtos/create-user.dto';
|
||||
import { LoginUserDto } from '@dtos/login-user.dto';
|
||||
import { UpdateUserDto } from '@dtos/update-user.dto';
|
||||
import { SystemEntity } from '@entities/system.entity';
|
||||
import { UserEntity } from '@entities/user.entity';
|
||||
import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
@ -8,11 +9,14 @@ import { JwtService } from '@nestjs/jwt';
|
|||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MessageService } from '@services/message.service';
|
||||
import { StarService } from '@services/star.service';
|
||||
import { VerifyService } from '@services/verify.service';
|
||||
import { WikiService } from '@services/wiki.service';
|
||||
import { UserStatus } from '@think/domains';
|
||||
import { instanceToPlain } from 'class-transformer';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { SystemService } from './system.service';
|
||||
|
||||
export type OutUser = Omit<UserEntity, 'comparePassword' | 'encryptPassword' | 'encrypt' | 'password'>;
|
||||
|
||||
@Injectable()
|
||||
|
@ -33,8 +37,47 @@ export class UserService {
|
|||
private readonly starService: StarService,
|
||||
|
||||
@Inject(forwardRef(() => WikiService))
|
||||
private readonly wikiService: WikiService
|
||||
) {}
|
||||
private readonly wikiService: WikiService,
|
||||
|
||||
@Inject(forwardRef(() => VerifyService))
|
||||
private readonly verifyService: VerifyService,
|
||||
|
||||
@Inject(forwardRef(() => SystemService))
|
||||
private readonly systemService: SystemService
|
||||
) {
|
||||
this.createDefaultSystemAdminFromConfigFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从配置文件创建默认系统管理员
|
||||
*/
|
||||
private async createDefaultSystemAdminFromConfigFile() {
|
||||
if (await this.userRepo.findOne({ isSystemAdmin: true })) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await this.confifgService.get('server.admin');
|
||||
|
||||
if (!config.name || !config.password || !config.email) {
|
||||
throw new Error(`请指定名称、密码和邮箱`);
|
||||
}
|
||||
|
||||
if (await this.userRepo.findOne({ name: config.name })) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.userRepo.save(
|
||||
await this.userRepo.create({
|
||||
...config,
|
||||
isSystemAdmin: true,
|
||||
})
|
||||
);
|
||||
console.log('[think] 已创建默认系统管理员,请尽快登录系统修改密码');
|
||||
} catch (e) {
|
||||
console.error(`[think] 创建默认系统管理员失败:`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 id 查询用户
|
||||
|
@ -71,7 +114,13 @@ export class UserService {
|
|||
* @param user CreateUserDto
|
||||
* @returns
|
||||
*/
|
||||
async createUser(user: CreateUserDto): Promise<OutUser> {
|
||||
async createUser(user: RegisterUserDto): Promise<OutUser> {
|
||||
const currentSystemConfig = await this.systemService.getConfigFromDatabase();
|
||||
|
||||
if (currentSystemConfig.isSystemLocked) {
|
||||
throw new HttpException('系统维护中,暂不可注册', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
if (await this.userRepo.findOne({ name: user.name })) {
|
||||
throw new HttpException('该账户已被注册', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
@ -88,6 +137,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 +158,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 +267,80 @@ export class UserService {
|
|||
const [data] = await query.getManyAndCount();
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定或解锁用户
|
||||
* @param user
|
||||
* @param targetUserId
|
||||
*/
|
||||
async toggleLockUser(user: UserEntity, targetUserId) {
|
||||
const currentUser = await this.userRepo.findOne(user.id);
|
||||
|
||||
if (!currentUser.isSystemAdmin) {
|
||||
throw new HttpException('您无权操作', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const targetUser = await this.userRepo.findOne(targetUserId);
|
||||
|
||||
if (!targetUser) {
|
||||
throw new HttpException('目标用户不存在', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
const nextStatus = targetUser.status === UserStatus.normal ? UserStatus.locked : UserStatus.normal;
|
||||
return await this.userRepo.save(await this.userRepo.merge(targetUser, { status: nextStatus }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统配置
|
||||
* @param user
|
||||
* @returns
|
||||
*/
|
||||
async getSystemConfig(user: UserEntity) {
|
||||
const currentUser = await this.userRepo.findOne(user.id);
|
||||
|
||||
if (!currentUser.isSystemAdmin) {
|
||||
throw new HttpException('您无权操作', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
return await this.systemService.getConfigFromDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测试邮件
|
||||
* @param user
|
||||
*/
|
||||
async sendTestEmail(user: UserEntity) {
|
||||
const currentUser = await this.userRepo.findOne(user.id);
|
||||
|
||||
if (!currentUser.isSystemAdmin) {
|
||||
throw new HttpException('您无权操作', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const currentConfig = await this.systemService.getConfigFromDatabase();
|
||||
try {
|
||||
await this.systemService.sendEmail({
|
||||
to: currentConfig.emailServiceUser,
|
||||
subject: '测试邮件',
|
||||
html: `<p>测试邮件</p>`,
|
||||
});
|
||||
return '测试邮件发送成功';
|
||||
} catch (err) {
|
||||
throw new HttpException('测试邮件发送失败!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新系统配置
|
||||
* @param user
|
||||
* @param targetUserId
|
||||
*/
|
||||
async updateSystemConfig(user: UserEntity, systemConfig: Partial<SystemEntity>) {
|
||||
const currentUser = await this.userRepo.findOne(user.id);
|
||||
|
||||
if (!currentUser.isSystemAdmin) {
|
||||
throw new HttpException('您无权操作', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
return await this.systemService.updateConfigInDatabase(systemConfig);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
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) {
|
||||
await this.verifyRepo.remove(await this.verifyRepo.find(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定邮箱发送验证码
|
||||
* @param email
|
||||
*/
|
||||
public async sendVerifyCode(email: string) {
|
||||
if (!email || !isEmail(email)) {
|
||||
throw new HttpException('请检查邮箱地址', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const verifyCode = randomInt(1000000).toString().padStart(6, '0');
|
||||
const record = await this.verifyRepo.save(await this.verifyRepo.create({ email, verifyCode }));
|
||||
await this.systemService.sendEmail({
|
||||
to: email,
|
||||
subject: '验证码',
|
||||
html: `<p>您的验证码为 ${verifyCode}</p>`,
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.deleteVerifyCode(record.id);
|
||||
clearTimeout(timer);
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检验验证码
|
||||
* @param email
|
||||
* @param verifyCode
|
||||
* @returns
|
||||
*/
|
||||
public async checkVerifyCode(email: string, verifyCode: string) {
|
||||
if (!email || !isEmail(email)) {
|
||||
throw new HttpException('请检查邮箱地址', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const ret = await this.verifyRepo.findOne({ email, verifyCode });
|
||||
|
||||
if (!ret) {
|
||||
throw new HttpException('验证码错误', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
return Boolean(ret);
|
||||
}
|
||||
}
|
|
@ -326,6 +326,10 @@ export class WikiService {
|
|||
const withHomeDocumentIdWiki = await this.wikiRepo.merge(wiki, { homeDocumentId });
|
||||
await this.wikiRepo.save(withHomeDocumentIdWiki);
|
||||
|
||||
await this.starService.toggleStar(user, {
|
||||
wikiId: wiki.id,
|
||||
});
|
||||
|
||||
return withHomeDocumentIdWiki;
|
||||
}
|
||||
|
||||
|
|
574
pnpm-lock.yaml
574
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue